mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-16 13: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
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\HelloWorld\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HelloWorldController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
return view('helloworld::index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('helloworld::create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*/
|
||||
public function store(Request $request) {}
|
||||
|
||||
/**
|
||||
* Show the specified resource.
|
||||
*/
|
||||
public function show($id)
|
||||
{
|
||||
return view('helloworld::show');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
return view('helloworld::edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*/
|
||||
public function update(Request $request, $id) {}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*/
|
||||
public function destroy($id) {}
|
||||
}
|
||||
27
Modules/HelloWorld/app/Providers/EventServiceProvider.php
Normal file
27
Modules/HelloWorld/app/Providers/EventServiceProvider.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\HelloWorld\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* The event handler mappings for the application.
|
||||
*
|
||||
* @var array<string, array<int, string>>
|
||||
*/
|
||||
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 {}
|
||||
}
|
||||
122
Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php
Normal file
122
Modules/HelloWorld/app/Providers/HelloWorldServiceProvider.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\HelloWorld\Providers;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use InvoiceShelf\Modules\Registry as ModuleRegistry;
|
||||
use InvoiceShelf\Modules\Support\ModuleServiceProvider;
|
||||
|
||||
/**
|
||||
* Reference module that exercises every InvoiceShelf module-system surface.
|
||||
*
|
||||
* This file started life as the output of `php artisan module:make HelloWorld`
|
||||
* — which, thanks to the custom stubs shipped from the invoiceshelf/modules
|
||||
* package, already includes the Registry::registerMenu/registerSettings
|
||||
* skeleton. The schema below has been expanded beyond the stub default to
|
||||
* exercise more field types for test coverage.
|
||||
*/
|
||||
class HelloWorldServiceProvider extends ModuleServiceProvider
|
||||
{
|
||||
protected string $name = 'HelloWorld';
|
||||
|
||||
protected string $nameLower = 'helloworld';
|
||||
|
||||
protected array $providers = [
|
||||
EventServiceProvider::class,
|
||||
RouteServiceProvider::class,
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
$slug = Str::kebab($this->name);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Module script (Vue page registration)
|
||||
// ----------------------------------------------------------------
|
||||
// Registers a compiled JS file that the host app injects as a
|
||||
// <script type="module"> tag. The script calls
|
||||
// window.InvoiceShelf.booting() to add Vue routes before mount.
|
||||
ModuleRegistry::registerScript(
|
||||
$slug,
|
||||
module_path($this->name, 'resources/dist/init.js')
|
||||
);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Sidebar menu item
|
||||
// ----------------------------------------------------------------
|
||||
// Adds a link in the company sidebar under the "Modules" section.
|
||||
// The `title` uses a Laravel translation key (namespace::file.key)
|
||||
// resolved server-side before being sent to the Vue frontend.
|
||||
// The `link` points to the module's own Vue page (registered by init.js).
|
||||
// The `icon` is any Heroicon name available in BaseIcon.
|
||||
ModuleRegistry::registerMenu($slug, [
|
||||
'title' => $this->nameLower.'::menu.title',
|
||||
'link' => '/admin/modules/'.$slug.'/dashboard',
|
||||
'icon' => 'HandRaisedIcon',
|
||||
]);
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Schema-driven settings
|
||||
// ----------------------------------------------------------------
|
||||
// Registers a settings form that appears in the company modules
|
||||
// page. Each company configures settings independently. Values are
|
||||
// persisted as CompanySetting keys: module.{slug}.{field_key}.
|
||||
//
|
||||
// Supported field types: text, textarea, select, switch, number
|
||||
// Labels use Laravel translation keys, resolved server-side.
|
||||
ModuleRegistry::registerSettings($slug, [
|
||||
'sections' => [
|
||||
[
|
||||
'title' => $this->nameLower.'::settings.greeting_section',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'greeting',
|
||||
'type' => 'text',
|
||||
'label' => $this->nameLower.'::settings.greeting',
|
||||
'rules' => ['required', 'max:120'],
|
||||
'default' => 'Hello, world!',
|
||||
],
|
||||
[
|
||||
'key' => 'recipient',
|
||||
'type' => 'text',
|
||||
'label' => $this->nameLower.'::settings.recipient',
|
||||
'rules' => ['max:60'],
|
||||
'default' => 'friend',
|
||||
],
|
||||
[
|
||||
'key' => 'show_emoji',
|
||||
'type' => 'switch',
|
||||
'label' => $this->nameLower.'::settings.show_emoji',
|
||||
'default' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'title' => $this->nameLower.'::settings.style_section',
|
||||
'fields' => [
|
||||
[
|
||||
'key' => 'tone',
|
||||
'type' => 'select',
|
||||
'label' => $this->nameLower.'::settings.tone',
|
||||
'rules' => ['required'],
|
||||
'default' => 'friendly',
|
||||
'options' => [
|
||||
'friendly' => 'Friendly',
|
||||
'formal' => 'Formal',
|
||||
'enthusiastic' => 'Enthusiastic',
|
||||
],
|
||||
],
|
||||
[
|
||||
'key' => 'note',
|
||||
'type' => 'textarea',
|
||||
'label' => $this->nameLower.'::settings.note',
|
||||
'rules' => ['max:500'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
50
Modules/HelloWorld/app/Providers/RouteServiceProvider.php
Normal file
50
Modules/HelloWorld/app/Providers/RouteServiceProvider.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\HelloWorld\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected string $name = 'HelloWorld';
|
||||
|
||||
/**
|
||||
* Called before routes are registered.
|
||||
*
|
||||
* Register any model bindings or pattern based filters.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the routes for the application.
|
||||
*/
|
||||
public function map(): void
|
||||
{
|
||||
$this->mapApiRoutes();
|
||||
$this->mapWebRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "web" routes for the application.
|
||||
*
|
||||
* These routes all receive session state, CSRF protection, etc.
|
||||
*/
|
||||
protected function mapWebRoutes(): void
|
||||
{
|
||||
Route::middleware('web')->group(module_path($this->name, '/routes/web.php'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the "api" routes for the application.
|
||||
*
|
||||
* These routes are typically stateless.
|
||||
*/
|
||||
protected function mapApiRoutes(): void
|
||||
{
|
||||
Route::middleware('api')->prefix('api')->name('api.')->group(module_path($this->name, '/routes/api.php'));
|
||||
}
|
||||
}
|
||||
33
Modules/HelloWorld/composer.json
Normal file
33
Modules/HelloWorld/composer.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "invoiceshelf/helloworld",
|
||||
"description": "",
|
||||
"authors": [
|
||||
{
|
||||
"name": "InvoiceShelf",
|
||||
"email": "hello@invoiceshelf.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"invoiceshelf/modules": "^3.0"
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [],
|
||||
"aliases": {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Modules\\HelloWorld\\": "app/",
|
||||
"Modules\\HelloWorld\\Database\\Factories\\": "database/factories/",
|
||||
"Modules\\HelloWorld\\Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Modules\\HelloWorld\\Tests\\": "tests/"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
Modules/HelloWorld/config/config.php
Normal file
5
Modules/HelloWorld/config/config.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'name' => 'HelloWorld',
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\HelloWorld\Database\Seeders;
|
||||
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class HelloWorldDatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// $this->call([]);
|
||||
}
|
||||
}
|
||||
5
Modules/HelloWorld/lang/en/menu.php
Normal file
5
Modules/HelloWorld/lang/en/menu.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'title' => 'Hello World',
|
||||
];
|
||||
12
Modules/HelloWorld/lang/en/settings.php
Normal file
12
Modules/HelloWorld/lang/en/settings.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'greeting_section' => 'Greeting',
|
||||
'style_section' => 'Style',
|
||||
|
||||
'greeting' => 'Greeting message',
|
||||
'recipient' => 'Recipient name',
|
||||
'show_emoji' => 'Show emoji',
|
||||
'tone' => 'Tone',
|
||||
'note' => 'Additional note (optional)',
|
||||
];
|
||||
11
Modules/HelloWorld/module.json
Normal file
11
Modules/HelloWorld/module.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "HelloWorld",
|
||||
"alias": "helloworld",
|
||||
"description": "",
|
||||
"keywords": [],
|
||||
"priority": 0,
|
||||
"providers": [
|
||||
"Modules\\HelloWorld\\Providers\\HelloWorldServiceProvider"
|
||||
],
|
||||
"files": []
|
||||
}
|
||||
12
Modules/HelloWorld/package.json
Normal file
12
Modules/HelloWorld/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
}
|
||||
0
Modules/HelloWorld/resources/assets/js/app.js
vendored
Normal file
0
Modules/HelloWorld/resources/assets/js/app.js
vendored
Normal file
0
Modules/HelloWorld/resources/assets/sass/app.scss
vendored
Normal file
0
Modules/HelloWorld/resources/assets/sass/app.scss
vendored
Normal file
83
Modules/HelloWorld/resources/dist/init.js
vendored
Normal file
83
Modules/HelloWorld/resources/dist/init.js
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
const { createBlock: e, createElementVNode: t, createTextVNode: n, createVNode: r, openBlock: i, ref: a, resolveComponent: o, toDisplayString: s, withCtx: c } = window.__invoiceshelf_vue;
|
||||
//#region resources/js/pages/DashboardPage.vue
|
||||
var l = { class: "p-6" }, u = { class: "flex items-center gap-3" }, d = { class: "text-xl font-semibold text-heading" }, f = { class: "mt-6 rounded-lg bg-surface-secondary p-4" }, p = { class: "space-y-1.5 text-sm text-muted" }, m = { class: "flex items-start gap-2" }, h = { class: "flex items-start gap-2" }, g = { class: "flex items-start gap-2" }, _ = {
|
||||
__name: "DashboardPage",
|
||||
setup(_) {
|
||||
let v = a("Hello, world!");
|
||||
return (a, _) => {
|
||||
let y = o("BaseBreadcrumbItem"), b = o("BaseBreadcrumb"), x = o("BasePageHeader"), S = o("BaseIcon"), C = o("BaseCard"), w = o("BasePage");
|
||||
return i(), e(w, null, {
|
||||
default: c(() => [r(x, { title: "Hello World" }, {
|
||||
default: c(() => [r(b, null, {
|
||||
default: c(() => [r(y, {
|
||||
title: "Home",
|
||||
to: "dashboard"
|
||||
}), r(y, {
|
||||
title: "Hello World",
|
||||
to: "#",
|
||||
active: ""
|
||||
})]),
|
||||
_: 1
|
||||
})]),
|
||||
_: 1
|
||||
}), r(C, { class: "mt-6" }, {
|
||||
default: c(() => [t("div", l, [
|
||||
t("div", u, [r(S, {
|
||||
name: "HandRaisedIcon",
|
||||
class: "h-8 w-8 text-primary-500"
|
||||
}), t("h2", d, s(v.value), 1)]),
|
||||
_[12] ||= t("p", { class: "mt-3 text-sm text-muted leading-relaxed" }, [
|
||||
n(" This page is provided by the "),
|
||||
t("strong", null, "HelloWorld"),
|
||||
n(" module. It demonstrates how modules can ship their own Vue pages that render inside the InvoiceShelf SPA using globally registered Base components. ")
|
||||
], -1),
|
||||
t("div", f, [_[11] ||= t("h3", { class: "text-sm font-semibold text-heading mb-2" }, "How it works", -1), t("ul", p, [
|
||||
t("li", m, [
|
||||
r(S, {
|
||||
name: "CheckIcon",
|
||||
class: "h-4 w-4 text-green-500 shrink-0 mt-0.5"
|
||||
}),
|
||||
_[0] ||= n(" Module ships a compiled ", -1),
|
||||
_[1] ||= t("code", { class: "text-primary-600" }, "init.js", -1),
|
||||
_[2] ||= n(" that calls ", -1),
|
||||
_[3] ||= t("code", { class: "text-primary-600" }, "window.InvoiceShelf.booting()", -1)
|
||||
]),
|
||||
t("li", h, [
|
||||
r(S, {
|
||||
name: "CheckIcon",
|
||||
class: "h-4 w-4 text-green-500 shrink-0 mt-0.5"
|
||||
}),
|
||||
_[4] ||= n(" The callback receives ", -1),
|
||||
_[5] ||= t("code", { class: "text-primary-600" }, "(app, router)", -1),
|
||||
_[6] ||= n(" and adds routes via ", -1),
|
||||
_[7] ||= t("code", { class: "text-primary-600" }, "router.addRoute()", -1)
|
||||
]),
|
||||
t("li", g, [
|
||||
r(S, {
|
||||
name: "CheckIcon",
|
||||
class: "h-4 w-4 text-green-500 shrink-0 mt-0.5"
|
||||
}),
|
||||
_[8] ||= n(" Vue pages use globally registered ", -1),
|
||||
_[9] ||= t("code", { class: "text-primary-600" }, "Base*", -1),
|
||||
_[10] ||= n(" components — no imports needed ", -1)
|
||||
])
|
||||
])])
|
||||
])]),
|
||||
_: 1
|
||||
})]),
|
||||
_: 1
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
//#endregion
|
||||
//#region resources/js/init.ts
|
||||
window.InvoiceShelf.booting((e, t) => {
|
||||
t.addRoute("admin", {
|
||||
path: "modules/hello-world/dashboard",
|
||||
name: "modules.hello-world.dashboard",
|
||||
component: _,
|
||||
meta: { requiresAuth: !0 }
|
||||
});
|
||||
});
|
||||
//#endregion
|
||||
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
|
||||
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
|
||||
<title>HelloWorld Module - {{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<meta name="description" content="{{ $description ?? '' }}">
|
||||
<meta name="keywords" content="{{ $keywords ?? '' }}">
|
||||
<meta name="author" content="{{ $author ?? '' }}">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
{{-- Vite CSS --}}
|
||||
{{-- {{ module_vite('build-helloworld', 'resources/assets/sass/app.scss') }} --}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{ $slot }}
|
||||
|
||||
{{-- Vite JS --}}
|
||||
{{-- {{ module_vite('build-helloworld', 'resources/assets/js/app.js') }} --}}
|
||||
</body>
|
||||
</html>
|
||||
5
Modules/HelloWorld/resources/views/index.blade.php
Normal file
5
Modules/HelloWorld/resources/views/index.blade.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<x-helloworld::layouts.master>
|
||||
<h1>Hello World</h1>
|
||||
|
||||
<p>Module: {!! config('helloworld.name') !!}</p>
|
||||
</x-helloworld::layouts.master>
|
||||
8
Modules/HelloWorld/routes/api.php
Normal file
8
Modules/HelloWorld/routes/api.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\HelloWorld\Http\Controllers\HelloWorldController;
|
||||
|
||||
Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () {
|
||||
Route::apiResource('helloworlds', HelloWorldController::class)->names('helloworld');
|
||||
});
|
||||
8
Modules/HelloWorld/routes/web.php
Normal file
8
Modules/HelloWorld/routes/web.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\HelloWorld\Http\Controllers\HelloWorldController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::resource('helloworlds', HelloWorldController::class)->names('helloworld');
|
||||
});
|
||||
69
Modules/HelloWorld/vite.config.js
vendored
Normal file
69
Modules/HelloWorld/vite.config.js
vendored
Normal file
@@ -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 <script type="module">. Vue is externalized and resolved from
|
||||
* the host's `window.__invoiceshelf_vue` global at runtime.
|
||||
*
|
||||
* The vueGlobalPlugin rewrites `import { ref, ... } from "vue"` into
|
||||
* destructuring from the host global — no separate Vue bundle, no import
|
||||
* map, and the module shares the host's Vue instance so globally
|
||||
* registered Base* components are accessible via resolveComponent().
|
||||
*/
|
||||
|
||||
/**
|
||||
* Vite plugin that replaces Vue imports with the host's global at runtime.
|
||||
* Works by marking 'vue' as external and then rewriting the import in the
|
||||
* output via renderChunk.
|
||||
*/
|
||||
function vueGlobalPlugin() {
|
||||
return {
|
||||
name: 'invoiceshelf-vue-global',
|
||||
enforce: 'pre',
|
||||
resolveId(source) {
|
||||
if (source === 'vue') {
|
||||
return { id: 'vue', external: true }
|
||||
}
|
||||
},
|
||||
renderChunk(code) {
|
||||
// Replace: import { ref, computed, ... } from "vue";
|
||||
// With: const { ref, computed, ... } = window.__invoiceshelf_vue;
|
||||
return code.replace(
|
||||
/import\s*(\{[^}]+\})\s*from\s*"vue"\s*;?/g,
|
||||
'const $1 = window.__invoiceshelf_vue;'
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: resolve(__dirname, 'resources/dist'),
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'resources/js/init.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'init.js',
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vueGlobalPlugin(),
|
||||
vue({
|
||||
template: {
|
||||
transformAssetUrls: {
|
||||
base: null,
|
||||
includeAbsolute: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'resources/js'),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Services\Storage;
|
||||
|
||||
use App\Models\FileDisk;
|
||||
use App\Services\Storage\FileDiskService;
|
||||
use Exception;
|
||||
use Spatie\Backup\Config\Config;
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Services\Storage;
|
||||
|
||||
use App\Models\FileDisk;
|
||||
use App\Models\Setting;
|
||||
use App\Services\Storage\FileDiskService;
|
||||
use Spatie\Backup\BackupDestination\BackupDestination;
|
||||
|
||||
class BackupService
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"gotenberg/gotenberg-php": "^2.8",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"hashids/hashids": "^5.0",
|
||||
"invoiceshelf/modules": "^3.0",
|
||||
"invoiceshelf/modules": "^3.0.3",
|
||||
"laravel/framework": "^13.0",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.0",
|
||||
@@ -68,8 +68,7 @@
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force",
|
||||
"@php artisan boost:update --ansi"
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
@@ -116,5 +115,11 @@
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
"prefer-stable": true,
|
||||
"repositories": {
|
||||
"invoiceshelf-modules": {
|
||||
"type": "vcs",
|
||||
"url": "https://github.com/InvoiceShelf/modules.git"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
29
composer.lock
generated
29
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "df856db1b42a7e244e298e6c129c2cba",
|
||||
"content-hash": "bc1914fedd271f614d922d716ab0e3be",
|
||||
"packages": [
|
||||
{
|
||||
"name": "aws/aws-crt-php",
|
||||
@@ -1730,16 +1730,16 @@
|
||||
},
|
||||
{
|
||||
"name": "invoiceshelf/modules",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/InvoiceShelf/modules.git",
|
||||
"reference": "f8184d400cdeb1bcc2ea5e6ed926f4e7b8433570"
|
||||
"reference": "9e508914ca64839ef1a1c7d749cc677342dae648"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/InvoiceShelf/modules/zipball/f8184d400cdeb1bcc2ea5e6ed926f4e7b8433570",
|
||||
"reference": "f8184d400cdeb1bcc2ea5e6ed926f4e7b8433570",
|
||||
"url": "https://api.github.com/repos/InvoiceShelf/modules/zipball/9e508914ca64839ef1a1c7d749cc677342dae648",
|
||||
"reference": "9e508914ca64839ef1a1c7d749cc677342dae648",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -1764,7 +1764,19 @@
|
||||
"InvoiceShelf\\Modules\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"InvoiceShelf\\Modules\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": [
|
||||
"vendor/bin/phpunit"
|
||||
],
|
||||
"lint": [
|
||||
"vendor/bin/pint"
|
||||
]
|
||||
},
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
@@ -1775,9 +1787,10 @@
|
||||
"modules"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/InvoiceShelf/modules/tree/3.0.1"
|
||||
"source": "https://github.com/InvoiceShelf/modules/tree/3.0.3",
|
||||
"issues": "https://github.com/InvoiceShelf/modules/issues"
|
||||
},
|
||||
"time": "2026-04-08T22:07:27+00:00"
|
||||
"time": "2026-06-05T00:06:47+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
|
||||
1
storage/app/.gitignore
vendored
1
storage/app/.gitignore
vendored
@@ -2,3 +2,4 @@
|
||||
!public/
|
||||
!templates/
|
||||
!.gitignore
|
||||
!modules_statuses.json
|
||||
|
||||
3
storage/app/modules_statuses.json
Normal file
3
storage/app/modules_statuses.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"HelloWorld": true
|
||||
}
|
||||
Reference in New Issue
Block a user