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:
Darko Gjorgjijoski
2026-06-05 14:57:06 +02:00
committed by GitHub
parent ac2a8ca939
commit b73dcb5278
28 changed files with 695 additions and 14 deletions

View File

@@ -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) {}
}

View 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 {}
}

View 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'],
],
],
],
],
]);
}
}

View 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'));
}
}

View 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/"
}
}
}

View File

@@ -0,0 +1,5 @@
<?php
return [
'name' => 'HelloWorld',
];

View File

@@ -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([]);
}
}

View File

@@ -0,0 +1,5 @@
<?php
return [
'title' => 'Hello World',
];

View 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)',
];

View File

@@ -0,0 +1,11 @@
{
"name": "HelloWorld",
"alias": "helloworld",
"description": "",
"keywords": [],
"priority": 0,
"providers": [
"Modules\\HelloWorld\\Providers\\HelloWorldServiceProvider"
],
"files": []
}

View 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"
}
}

View File

View File

View 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

View 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,
},
})
})

View 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>

View 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

View File

@@ -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>

View File

@@ -0,0 +1,5 @@
<x-helloworld::layouts.master>
<h1>Hello World</h1>
<p>Module: {!! config('helloworld.name') !!}</p>
</x-helloworld::layouts.master>

View 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');
});

View 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
View 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'),
},
},
})

View File

@@ -3,7 +3,6 @@
namespace App\Services\Storage;
use App\Models\FileDisk;
use App\Services\Storage\FileDiskService;
use Exception;
use Spatie\Backup\Config\Config;

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -2,3 +2,4 @@
!public/
!templates/
!.gitignore
!modules_statuses.json

View File

@@ -0,0 +1,3 @@
{
"HelloWorld": true
}