From b73dcb5278dc252cc1c4615efa79690bbbb4fab3 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski <5760249+gdarko@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:57:06 +0200 Subject: [PATCH] fix: boot app + AI driver registration on invoiceshelf/modules 3.0.3 (via VCS) (#655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): require invoiceshelf/modules ^3.0.2 (adds registerExchangeRateDriver) DriverRegistryProvider::registerExchangeRateDrivers() calls Registry::registerExchangeRateDriver(), which only exists in invoiceshelf/modules >= 3.0.2. The constraint (^3.0) and the committed lock (3.0.1) allowed/pinned versions without it, so a fresh `composer install` (CI, Docker, new clones) boots into: Call to undefined method InvoiceShelf\Modules\Registry::registerExchangeRateDriver() Pin to ^3.0.2 and update the lock so every install gets a version that has the method. `php artisan package:discover` verified clean. * fix(ai): register the AI driver via the generic Registry::registerDriver('ai', ...) registerAiDriver() is a convenience wrapper that is NOT present in any published invoiceshelf/modules release (only the generic registerDriver() and registerExchangeRateDriver() ship), so DriverRegistryProvider crashed app boot on a clean composer install: Call to undefined method InvoiceShelf\Modules\Registry::registerAiDriver() Use registerDriver('ai', 'openrouter', ...) instead -- it stores under the 'ai' type exactly like the wrapper would, and AiConfigurationService / AiDriverFactory read it back via allDrivers('ai') / driverMeta('ai', ...). Verified by clean-reinstalling invoiceshelf/modules (no local patch) and running `php artisan package:discover` -> boots clean. * style: fix pre-existing Pint violations in backup services BackupService.php and BackupConfigurationFactory.php (untouched by this PR's boot fix) carried style violations from an earlier domain-reorg refactor (6d1816bd). `pint --test` checks the whole tree and runs on any PR that touches PHP, so these failed CI here. Auto-fixed with Pint (braces_position, no_unused_imports, single_line_empty_body) so the check goes green. * build(deps): pull invoiceshelf/modules ^3.0.3 via VCS, restore registerAiDriver() The 3.0.3 release adds Registry::registerAiDriver() (the method DriverRegistryProvider and AiDriverFactoryTest call). Packagist has the package frozen, so resolve it directly from the canonical GitHub repo via a composer VCS repository and require ^3.0.3 (the tag exists; the freeze is Packagist-side only). Now that the method ships, restore DriverRegistryProvider to Registry::registerAiDriver() — reverting the temporary generic registerDriver('ai', ...) workaround — so it matches the package's intended API and the existing tests. The provider is now net-identical to 3.x. Verified: php artisan package:discover boots clean; the AI suite (incl. the previously failing AiDriverFactoryTest) passes. * test: provision modules_statuses.json in the test bootstrap The Modules/HelloWorld integration test needs the module enabled at the nwidart level, read from storage/app/modules_statuses.json at app boot. That file is gitignored (created locally by `module:make`), so it's absent on CI and fresh clones — HelloWorld stays disabled and the 5 integration tests fail with 404s. Provision it (only if missing) in tests/Pest.php before any test boots the app, so CI matches a local dev environment. Full suite: 462 pass. * fix(test): enable HelloWorld via a committed modules_statuses.json The Modules/HelloWorld integration test needs the module enabled at the nwidart level — read from storage/app/modules_statuses.json by FileActivator. That file is gitignored, so it's absent in CI / fresh clones, leaving HelloWorld disabled and the 5 integration tests failing with 404s. The previous tests/Pest.php provisioning (60a7f0d6) only worked under `./vendor/bin/pest`. CI runs `php artisan test`, which boots the console app first; FileActivator reads and caches the (absent) statuses at construction BEFORE Pest.php runs, so test-runtime provisioning is too late. The file must exist before any boot. Commit the file via a storage/app/.gitignore negation, and revert the ineffective Pest.php hack. Prod-safe: Modules/ is gitignored and not copied by release.yaml, so a phantom "HelloWorld: true" status is ignored by nwidart (no such module on disk). bootstrap/cache/modules.php is gitignored (absent in CI), so nothing overrides the committed file. Verified with `php artisan test --filter=HelloWorld` (the CI command) and the full suite: 462 pass; pint clean. * fix(test): commit the Modules/HelloWorld sample module HelloWorldIntegrationTest exercises Modules/HelloWorld end-to-end, but Modules/ was gitignored (/Modules), so the module was absent from the repo and from CI — the 5 integration tests 404'd, and the committed modules_statuses.json merely enabled a module that wasn't there. Track Modules/HelloWorld (the test fixture) via a `/Modules/*` + `!HelloWorld` negation. Not shipped to prod (release.yaml omits Modules/). Now the module is present, autoloaded (merge-plugin), and enabled (statuses file), so its provider boots and the menu/settings/routes register. * build: drop the boost:update post-update-cmd hook (breaks CI) laravel/boost gates its commands to the local environment, so `php artisan boost:update` fails in CI ("There are no commands defined in the boost namespace"), making composer's post-update-cmd return exit 1. It only began failing the build once Modules/HelloWorld/composer.json was committed: the wikimedia merge-plugin then runs composer's update path on a plain `composer install`, triggering post-update-cmd. Drop the auto-update hook (run `boost:update` manually when needed); vendor:publish stays. --- .../Http/Controllers/HelloWorldController.php | 56 ++++++++ .../app/Providers/EventServiceProvider.php | 27 ++++ .../Providers/HelloWorldServiceProvider.php | 122 ++++++++++++++++++ .../app/Providers/RouteServiceProvider.php | 50 +++++++ Modules/HelloWorld/composer.json | 33 +++++ Modules/HelloWorld/config/config.php | 5 + .../seeders/HelloWorldDatabaseSeeder.php | 16 +++ Modules/HelloWorld/lang/en/menu.php | 5 + Modules/HelloWorld/lang/en/settings.php | 12 ++ Modules/HelloWorld/module.json | 11 ++ Modules/HelloWorld/package.json | 12 ++ Modules/HelloWorld/resources/assets/js/app.js | 0 .../HelloWorld/resources/assets/sass/app.scss | 0 Modules/HelloWorld/resources/dist/init.js | 83 ++++++++++++ Modules/HelloWorld/resources/js/init.ts | 23 ++++ .../resources/js/pages/DashboardPage.vue | 49 +++++++ Modules/HelloWorld/resources/js/vue-shim.js | 37 ++++++ .../views/components/layouts/master.blade.php | 30 +++++ .../resources/views/index.blade.php | 5 + Modules/HelloWorld/routes/api.php | 8 ++ Modules/HelloWorld/routes/web.php | 8 ++ Modules/HelloWorld/vite.config.js | 69 ++++++++++ .../Storage/BackupConfigurationFactory.php | 1 - app/Services/Storage/BackupService.php | 1 - composer.json | 13 +- composer.lock | 29 +++-- storage/app/.gitignore | 1 + storage/app/modules_statuses.json | 3 + 28 files changed, 695 insertions(+), 14 deletions(-) create mode 100644 Modules/HelloWorld/app/Http/Controllers/HelloWorldController.php create mode 100644 Modules/HelloWorld/app/Providers/EventServiceProvider.php create mode 100644 Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php create mode 100644 Modules/HelloWorld/app/Providers/RouteServiceProvider.php create mode 100644 Modules/HelloWorld/composer.json create mode 100644 Modules/HelloWorld/config/config.php create mode 100644 Modules/HelloWorld/database/seeders/HelloWorldDatabaseSeeder.php create mode 100644 Modules/HelloWorld/lang/en/menu.php create mode 100644 Modules/HelloWorld/lang/en/settings.php create mode 100644 Modules/HelloWorld/module.json create mode 100644 Modules/HelloWorld/package.json create mode 100644 Modules/HelloWorld/resources/assets/js/app.js create mode 100644 Modules/HelloWorld/resources/assets/sass/app.scss create mode 100644 Modules/HelloWorld/resources/dist/init.js create mode 100644 Modules/HelloWorld/resources/js/init.ts create mode 100644 Modules/HelloWorld/resources/js/pages/DashboardPage.vue create mode 100644 Modules/HelloWorld/resources/js/vue-shim.js create mode 100644 Modules/HelloWorld/resources/views/components/layouts/master.blade.php create mode 100644 Modules/HelloWorld/resources/views/index.blade.php create mode 100644 Modules/HelloWorld/routes/api.php create mode 100644 Modules/HelloWorld/routes/web.php create mode 100644 Modules/HelloWorld/vite.config.js create mode 100644 storage/app/modules_statuses.json diff --git a/Modules/HelloWorld/app/Http/Controllers/HelloWorldController.php b/Modules/HelloWorld/app/Http/Controllers/HelloWorldController.php new file mode 100644 index 00000000..f83a554e --- /dev/null +++ b/Modules/HelloWorld/app/Http/Controllers/HelloWorldController.php @@ -0,0 +1,56 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php b/Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php new file mode 100644 index 00000000..d318ef84 --- /dev/null +++ b/Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php @@ -0,0 +1,122 @@ +name); + + // ---------------------------------------------------------------- + // Module script (Vue page registration) + // ---------------------------------------------------------------- + // Registers a compiled JS file that the host app injects as a + // diff --git a/Modules/HelloWorld/resources/js/vue-shim.js b/Modules/HelloWorld/resources/js/vue-shim.js new file mode 100644 index 00000000..e5fbabaa --- /dev/null +++ b/Modules/HelloWorld/resources/js/vue-shim.js @@ -0,0 +1,37 @@ +/** + * Vue runtime shim for InvoiceShelf modules. + * + * The host app exposes its Vue instance on window.__invoiceshelf_vue. + * This shim provides a Proxy-based default export that lazily resolves + * Vue APIs on first access — avoiding the crash that happens when the + * module script evaluates before the host has set the global. + * + * For named exports (used by SFC compiled templates), we use a Proxy + * as the module namespace. Vite's lib mode with a default export + * from a Proxy works because the compiled SFC template accesses + * the APIs at render time (long after the host has initialized), + * not at module evaluation time. + */ + +function getVue() { + const vue = window.__invoiceshelf_vue + if (!vue) { + throw new Error( + '[InvoiceShelf Module] Host Vue runtime not available. ' + + 'Ensure the module script loads after the host app.' + ) + } + return vue +} + +// Proxy that forwards all property access to the host's Vue at call time +const vueProxy = new Proxy({}, { + get(_, key) { + return getVue()[key] + }, + has(_, key) { + return key in getVue() + }, +}) + +export default vueProxy diff --git a/Modules/HelloWorld/resources/views/components/layouts/master.blade.php b/Modules/HelloWorld/resources/views/components/layouts/master.blade.php new file mode 100644 index 00000000..8f45477f --- /dev/null +++ b/Modules/HelloWorld/resources/views/components/layouts/master.blade.php @@ -0,0 +1,30 @@ + + + + + + + + + + HelloWorld Module - {{ config('app.name', 'Laravel') }} + + + + + + + + + + {{-- Vite CSS --}} + {{-- {{ module_vite('build-helloworld', 'resources/assets/sass/app.scss') }} --}} + + + + {{ $slot }} + + {{-- Vite JS --}} + {{-- {{ module_vite('build-helloworld', 'resources/assets/js/app.js') }} --}} + + diff --git a/Modules/HelloWorld/resources/views/index.blade.php b/Modules/HelloWorld/resources/views/index.blade.php new file mode 100644 index 00000000..6c350d07 --- /dev/null +++ b/Modules/HelloWorld/resources/views/index.blade.php @@ -0,0 +1,5 @@ + +

Hello World

+ +

Module: {!! config('helloworld.name') !!}

+
diff --git a/Modules/HelloWorld/routes/api.php b/Modules/HelloWorld/routes/api.php new file mode 100644 index 00000000..ad8f2410 --- /dev/null +++ b/Modules/HelloWorld/routes/api.php @@ -0,0 +1,8 @@ +prefix('v1')->group(function () { + Route::apiResource('helloworlds', HelloWorldController::class)->names('helloworld'); +}); diff --git a/Modules/HelloWorld/routes/web.php b/Modules/HelloWorld/routes/web.php new file mode 100644 index 00000000..208babb4 --- /dev/null +++ b/Modules/HelloWorld/routes/web.php @@ -0,0 +1,8 @@ +group(function () { + Route::resource('helloworlds', HelloWorldController::class)->names('helloworld'); +}); diff --git a/Modules/HelloWorld/vite.config.js b/Modules/HelloWorld/vite.config.js new file mode 100644 index 00000000..f1d593f0 --- /dev/null +++ b/Modules/HelloWorld/vite.config.js @@ -0,0 +1,69 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +/** + * Module build config. + * + * Produces a single `resources/dist/init.js` ES module that the host app + * loads via