mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-18 14:55:21 +00:00
fix: boot app + AI driver registration on invoiceshelf/modules 3.0.3 (via VCS) (#655)
* 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.
This commit is contained in:
committed by
GitHub
parent
ac2a8ca939
commit
b73dcb5278
23
Modules/HelloWorld/resources/js/init.ts
Normal file
23
Modules/HelloWorld/resources/js/init.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* HelloWorld module entry point.
|
||||
*
|
||||
* This file is loaded by the host app via a <script type="module"> tag
|
||||
* injected by Registry::allScripts(). It runs BEFORE the Vue app mounts,
|
||||
* so router.addRoute() works reliably.
|
||||
*
|
||||
* All Base* components (BaseCard, BaseIcon, BasePage, etc.) are globally
|
||||
* registered by the host — no imports needed in your Vue templates.
|
||||
*/
|
||||
import DashboardPage from './pages/DashboardPage.vue'
|
||||
|
||||
window.InvoiceShelf.booting((_app, router) => {
|
||||
// Register a Vue page route
|
||||
router.addRoute('admin', {
|
||||
path: 'modules/hello-world/dashboard',
|
||||
name: 'modules.hello-world.dashboard',
|
||||
component: DashboardPage,
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
49
Modules/HelloWorld/resources/js/pages/DashboardPage.vue
Normal file
49
Modules/HelloWorld/resources/js/pages/DashboardPage.vue
Normal file
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<BasePage>
|
||||
<BasePageHeader title="Hello World">
|
||||
<BaseBreadcrumb>
|
||||
<BaseBreadcrumbItem title="Home" to="dashboard" />
|
||||
<BaseBreadcrumbItem title="Hello World" to="#" active />
|
||||
</BaseBreadcrumb>
|
||||
</BasePageHeader>
|
||||
|
||||
<BaseCard class="mt-6">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<BaseIcon name="HandRaisedIcon" class="h-8 w-8 text-primary-500" />
|
||||
<h2 class="text-xl font-semibold text-heading">{{ greeting }}</h2>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted leading-relaxed">
|
||||
This page is provided by the <strong>HelloWorld</strong> module.
|
||||
It demonstrates how modules can ship their own Vue pages that
|
||||
render inside the InvoiceShelf SPA using globally registered
|
||||
Base components.
|
||||
</p>
|
||||
|
||||
<div class="mt-6 rounded-lg bg-surface-secondary p-4">
|
||||
<h3 class="text-sm font-semibold text-heading mb-2">How it works</h3>
|
||||
<ul class="space-y-1.5 text-sm text-muted">
|
||||
<li class="flex items-start gap-2">
|
||||
<BaseIcon name="CheckIcon" class="h-4 w-4 text-green-500 shrink-0 mt-0.5" />
|
||||
Module ships a compiled <code class="text-primary-600">init.js</code> that calls <code class="text-primary-600">window.InvoiceShelf.booting()</code>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<BaseIcon name="CheckIcon" class="h-4 w-4 text-green-500 shrink-0 mt-0.5" />
|
||||
The callback receives <code class="text-primary-600">(app, router)</code> and adds routes via <code class="text-primary-600">router.addRoute()</code>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<BaseIcon name="CheckIcon" class="h-4 w-4 text-green-500 shrink-0 mt-0.5" />
|
||||
Vue pages use globally registered <code class="text-primary-600">Base*</code> components — no imports needed
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</BaseCard>
|
||||
</BasePage>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const greeting = ref('Hello, world!')
|
||||
</script>
|
||||
37
Modules/HelloWorld/resources/js/vue-shim.js
vendored
Normal file
37
Modules/HelloWorld/resources/js/vue-shim.js
vendored
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user