feat(core): add SourceResolver for backwards-compatible src/ → source/ migration

Introduces SourceResolver utility class with source/ → src/ → htdocs/
fallback chain, replacing hardcoded src/ references across 28 files.
This enables renaming root-level src/ to source/ in all repos while
maintaining backwards compatibility during the transition.

Phase 1: New lib/Enterprise/SourceResolver.php with resolve(),
resolveAbsolute(), globSource(), findUnderSource(), warnIfLegacy()
Phase 2: Updated 19 CLI/deploy tools to use SourceResolver
Phase 3: Updated 7 validator/lib files (McpServerPlugin,
PackageBuilder, RepositorySynchronizer, auto_detect_platform,
check_dolibarr_module, check_client_theme, check_structure)

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-06-06 08:58:52 -05:00
parent 9526d006c4
commit ca55e5d2d2
28 changed files with 387 additions and 184 deletions
+30 -20
View File
@@ -19,7 +19,7 @@ declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\{CliFramework, SourceResolver};
/**
* Validates client theme packages that deliver CSS, JS, and images
@@ -44,17 +44,17 @@ class CheckClientTheme extends CliFramework
/** Recommended XML elements. */
private const RECOMMENDED_ELEMENTS = ['updateservers', 'scriptfile', 'description', 'fileset'];
/** Required theme CSS files relative to repo root. */
/** Required theme CSS files relative to the source directory. */
private const REQUIRED_THEME_FILES = [
'src/media/templates/site/mokoonyx/css/theme/light.custom.css',
'src/media/templates/site/mokoonyx/css/theme/dark.custom.css',
'media/templates/site/mokoonyx/css/theme/light.custom.css',
'media/templates/site/mokoonyx/css/theme/dark.custom.css',
];
/** Optional but expected files. */
/** Optional but expected files (paths prefixed with ~ are relative to source dir). */
private const EXPECTED_FILES = [
'src/media/templates/site/mokoonyx/css/user.css',
'src/media/templates/site/mokoonyx/js/user.js',
'src/script.php',
'~media/templates/site/mokoonyx/css/user.css',
'~media/templates/site/mokoonyx/js/user.js',
'~script.php',
'updates.xml',
];
@@ -81,10 +81,12 @@ class CheckClientTheme extends CliFramework
// ── Manifest ──────────────────────────────────────────
$this->section('Manifest validation');
$manifest = $path . '/src/templateDetails.xml';
$srcName = SourceResolver::resolve($path);
SourceResolver::warnIfLegacy($path);
$manifest = $path . "/{$srcName}/templateDetails.xml";
if (!is_file($manifest)) {
$this->status(false, 'Missing src/templateDetails.xml');
$this->status(false, "Missing {$srcName}/templateDetails.xml");
$this->printSummary(0, 1, $this->elapsed());
return 1;
}
@@ -144,28 +146,36 @@ class CheckClientTheme extends CliFramework
// ── Required files ────────────────────────────────────
$this->section('Required files');
foreach (self::REQUIRED_THEME_FILES as $file) {
$full = $path . '/' . $file;
$full = "{$path}/{$srcName}/{$file}";
if (is_file($full)) {
$this->status(true, basename($file));
} else {
$this->status(false, "Missing: {$file}");
$this->status(false, "Missing: {$srcName}/{$file}");
$errors++;
}
}
foreach (self::EXPECTED_FILES as $file) {
$full = $path . '/' . $file;
// Paths prefixed with ~ are relative to source dir
if (str_starts_with($file, '~')) {
$relFile = substr($file, 1);
$full = "{$path}/{$srcName}/{$relFile}";
$display = "{$srcName}/{$relFile}";
} else {
$full = "{$path}/{$file}";
$display = $file;
}
if (is_file($full)) {
$this->status(true, basename($file));
} else {
$this->warning("Missing: {$file}");
$this->warning("Missing: {$display}");
$warns++;
}
}
// ── PHP syntax ────────────────────────────────────────
$this->section('PHP syntax');
$phpFiles = glob($path . '/src/*.php') ?: [];
$phpFiles = glob("{$path}/{$srcName}/*.php") ?: [];
foreach ($phpFiles as $phpFile) {
$output = [];
$ret = 0;
@@ -179,20 +189,20 @@ class CheckClientTheme extends CliFramework
}
}
if (empty($phpFiles)) {
$this->warning('No PHP files in src/');
$this->warning("No PHP files in {$srcName}/");
}
// ── CSS validation ────────────────────────────────────
$this->section('CSS validation');
$cssFiles = array_merge(
glob($path . '/src/media/templates/site/mokoonyx/css/theme/*.css') ?: [],
glob($path . '/src/media/templates/site/mokoonyx/css/*.css') ?: [],
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/theme/*.css") ?: [],
glob("{$path}/{$srcName}/media/templates/site/mokoonyx/css/*.css") ?: [],
);
foreach ($cssFiles as $cssFile) {
$css = (string) file_get_contents($cssFile);
$open = substr_count($css, '{');
$close = substr_count($css, '}');
$name = str_replace($path . '/src/', '', $cssFile);
$name = str_replace("{$path}/{$srcName}/", '', $cssFile);
if ($open !== $close) {
$this->status(false, "Unbalanced braces in {$name} (open: {$open}, close: {$close})");
@@ -241,7 +251,7 @@ class CheckClientTheme extends CliFramework
// ── Image sizes ───────────────────────────────────────
$this->section('Image optimization');
$largeImages = 0;
$imageDir = $path . '/src/images';
$imageDir = "{$path}/{$srcName}/images";
if (is_dir($imageDir)) {
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($imageDir, \FilesystemIterator::SKIP_DOTS)