Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
82 lines
2.7 KiB
PHP
82 lines
2.7 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
|
|
/**
|
|
* Builds the runtime page root by symlinking module pages into storage/runtime/pages/.
|
|
*
|
|
* Usage: php bin/module-build.php
|
|
*
|
|
* The runtime page root mirrors the core pages/ directory and adds symlinks for
|
|
* each enabled module's pages. MintyPHP Router::$pageRoot points to this directory.
|
|
*
|
|
* This script is idempotent and uses a lock file to prevent concurrent builds.
|
|
*
|
|
* Exit codes:
|
|
* 0 — build successful (or nothing to do)
|
|
* 1 — error during build
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use MintyPHP\App\Module\ModuleRegistry;
|
|
use MintyPHP\App\Module\ModuleRuntimePageBuilder;
|
|
|
|
require_once __DIR__ . '/module-cli-bootstrap.php';
|
|
|
|
function moduleBuildRun(): int
|
|
{
|
|
$warningsBefore = moduleCliVendorWarningsIgnoredTotal();
|
|
moduleCliBootstrapApp();
|
|
|
|
$exitCode = 0;
|
|
$runtimeDir = __DIR__ . '/../storage/runtime/pages';
|
|
$lockFile = __DIR__ . '/../storage/runtime/.module-build.lock';
|
|
|
|
try {
|
|
// Ensure runtime directory exists
|
|
$runtimeParent = dirname($runtimeDir);
|
|
if (!is_dir($runtimeParent) && !mkdir($runtimeParent, 0775, true) && !is_dir($runtimeParent)) {
|
|
throw new \RuntimeException("Failed to create runtime directory: {$runtimeParent}");
|
|
}
|
|
|
|
// Lock to prevent concurrent builds
|
|
$lock = fopen($lockFile, 'c');
|
|
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
|
|
fwrite(STDERR, "module-build: another build is in progress, skipping.\n");
|
|
return 0;
|
|
}
|
|
|
|
$registry = app(ModuleRegistry::class);
|
|
$modules = $registry->getModules();
|
|
$builder = new ModuleRuntimePageBuilder();
|
|
$result = $builder->build($runtimeDir, __DIR__ . '/../pages', $modules);
|
|
|
|
if (count($modules) === 0) {
|
|
fwrite(STDOUT, "module-build: no modules, runtime → core pages (symlink).\n");
|
|
} else {
|
|
fwrite(STDOUT, sprintf(
|
|
"module-build: runtime pages built — %d core entries + %d module entries.\n",
|
|
(int) ($result['core_entries'] ?? 0),
|
|
(int) ($result['module_entries'] ?? 0)
|
|
));
|
|
}
|
|
} catch (Throwable $exception) {
|
|
fwrite(STDERR, sprintf("module-build: FAILED: %s\n", $exception->getMessage()));
|
|
$exitCode = 1;
|
|
} finally {
|
|
if (isset($lock) && is_resource($lock)) {
|
|
flock($lock, LOCK_UN);
|
|
fclose($lock);
|
|
}
|
|
}
|
|
|
|
$vendorWarningsIgnored = moduleCliVendorWarningsIgnoredSince($warningsBefore);
|
|
fwrite(STDOUT, sprintf("module-build: vendor_warnings_ignored=%d\n", $vendorWarningsIgnored));
|
|
|
|
return $exitCode;
|
|
}
|
|
|
|
if (PHP_SAPI === 'cli' && realpath((string) ($_SERVER['SCRIPT_FILENAME'] ?? '')) === __FILE__) {
|
|
exit(moduleBuildRun());
|
|
}
|