Generate TypeScript $wire interface files from PHP classes annotated with #[WireExposed].

Maintainers

Package info

gitlab.encoredigitalgroup.com/oss/wirescript

pkg:composer/encoredigitalgroup/wirescript

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

v0.1.2 2026-06-22 12:35 UTC

This package is auto-updated.

Last update: 2026-06-22 12:36:16 UTC


README

Generate TypeScript $wire interface files from PHP classes annotated with #[WireExposed].

WireScript generates the entire mechanical frontend surface a Livewire/Alpine module needs, so a developer hand-writes only component behavior. PHP is the single source of truth: annotate the public properties and methods the frontend calls, run the generator, and commit the emitted files. A --check mode fails CI when any committed file drifts from what the generator would produce.

From each registered module, wirescript:generate emits — all carrying a @generated header and all byte-stable:

ArtifactPurpose
types/wire/<Name>.tsOne export interface per annotated class (the $wire public surface).
types/wire/index.tsThe WireRegistry, the canonical Livewire WireBase, and Wire<T>.
register.tsalpine.data() wiring for every factory under components/.
index.tsThe alpine:init entry point that calls the generated register function.
lib/persisted.tsTyped Alpine.$persist wrapper with the module's storage-key prefix.
lib/keymap.tsGeneric typed keyboard-shortcut dispatcher.
types/globals.d.tsAmbient window.Alpine / window.Livewire declarations the wiring uses.

Component factories themselves are never generated or overwritten — only scaffolded on demand (see Scaffolding components). The only manual step that remains is registering the module's interface group from its service provider.

  • Package: encoredigitalgroup/wirescript
  • Namespace: EncoreDigitalGroup\WireScript
  • Requires: PHP 8.4+, Laravel 12/13

Installation

composer require encoredigitalgroup/wirescript

The service provider (EncoreDigitalGroup\WireScript\Providers\WireScriptServiceProvider) is auto-discovered. It binds the WireTypeRegistry singleton and registers the wirescript:generate and wirescript:scaffold console commands.

WireScript derives every output path from one registered directory. A module is expected to follow the conventional layout (resources/js/types/wire, resources/js/components, resources/js/lib); the module JS root is the directory two levels above the wire-types directory.

The same convention serves both project shapes, and WireScript detects which one you have automatically — nothing to configure:

  • Flat Laravel app — one frontend, registered under the app's own resources/js (e.g. resource_path('js/types/wire')).
  • Multi-module app — many modules, each under a container directory (app_modules/ or app-modules/), e.g. app_modules/Projects/resources/js/types/wire.

Usage

1. Annotate the PHP class

Mark each public property or method the frontend reads or calls with #[WireExposed]:

use EncoreDigitalGroup\WireScript\WireExposed;

class BacklogList extends Component
{
    #[WireExposed(type: 'number[]')]
    public array $selectedTicketIds = [];

    #[WireExposed]
    public function clearFilters(): void { /* ... */ }

    #[WireExposed(params: ['ticketIds' => 'unknown[]'])]
    public function bulkAssign(array $ticketIds): void { /* ... */ }
}

Override arguments handle shapes reflection cannot infer:

ArgumentTargetEffect
typepropertyExplicit TS type, overrides reflection.
returnsmethodExplicit TS return type, without the Promise<> wrapper.
paramsmethodPer-parameter TS overrides, keyed by parameter name.

Reflection defaults: int/floatnumber, boolboolean, stringstring, arrayunknown[], ?TT | null, Collectionunknown[], Carbonstring, models/DTOs → Record<string, unknown>, void return → Promise<void>.

2. Register the interfaces

Each consuming package registers a group from its own service provider's boot(). This co-locates the registry with the package that owns the classes:

use EncoreDigitalGroup\WireScript\WireTypeRegistry;

public function boot(): void
{
    app(WireTypeRegistry::class)->register(
        outputDir: __DIR__ . '/../../resources/js/types/wire',
        interfaces: [
            'BacklogList' => BacklogList::class,
            'SprintPanel' => SprintPanel::class,
        ],
    );
}

WireTypeRegistry is a singleton, so register() calls accumulate across every booting provider. The command iterates every registered group, so a single wirescript:generate serves the whole application.

The optional moduleKey argument sets the module identity used for the storage-key prefix, the register-function name, and the doc-header labels. When omitted it is inferred from the layout: a flat app registering under the host's own resources/js becomes app (prefix app., function registerAppComponents); otherwise it is the directory segment above resources (e.g. Projects → prefix projects., function registerProjectsComponents). Pass it explicitly only when the directory name does not match the desired identity:

app(WireTypeRegistry::class)->register(
    outputDir: __DIR__ . '/../../resources/js/types/wire',
    interfaces: ['BacklogList' => BacklogList::class],
    moduleKey: 'Projects',
);

3. Generate and verify

php artisan wirescript:generate          # write every generated artifact
php artisan wirescript:generate --check  # exit 1 on drift, write nothing (use in CI)

Every artifact in the table above is written with a @generated header. Commit the output. --check regenerates each file in memory and compares it byte-for-byte against what is on disk — so adding a #[WireExposed] member, or adding/removing a component file, makes --check fail until you regenerate. Component scaffolds hold developer behavior and are deliberately excluded from the gate. Wire a test or CI step to the --check mode to catch drift:

expect(Artisan::call('wirescript:generate', ['--check' => true]))->toBe(0);

Scaffolding components

wirescript:scaffold writes a typed Alpine component factory under the module's components/ directory. It is create-if-absent: it never overwrites an existing file and is not part of the --check gate. The generated half (imports, $wire typing, the factory shell) keeps the boilerplate correct; you fill only the behavior body.

php artisan wirescript:scaffold backlogList --wire=BacklogList   # typed this.$wire: Wire<'BacklogList'>
php artisan wirescript:scaffold backlogFilterPopover            # no $wire (pure UI state)

Pass --group=<Module> only when more than one module is registered. After scaffolding, run wirescript:generate to wire the new factory into register.ts.

Vite integration

The package ships a Vite plugin at resources/vite/index.js. Its default export probes the filesystem for whichever layout the host uses and adds each frontend entry (resources/js/index.ts) as a build input — no host configuration:

  • a flat app's root resources/js/index.ts (override with {appEntry}, or pass a falsy value to disable);
  • every {Module}/resources/js/index.ts under a module container — app_modules/ or app-modules/ (pin one with {modulesDir}).

Both can coexist; inputs are de-duplicated. Import it by its Composer vendor path:

import wirescript from './vendor/encoredigitalgroup/wirescript/resources/vite';

export default defineConfig({
    plugins: [
        // ...
        ...wirescript(),
    ],
});

Because discovery is convention-driven, a module ships frontend code just by adding resources/js/index.ts — it needs no per-module Vite plugin. The generated $wire types (resources/js/types/wire/*.ts) are not registered as build inputs: they are type-only export interface files, so the module's index.ts pulls them in via import type instead. wirescriptInputs(moduleDir) is exported for building the input list for a single module explicitly.

Loading the bundles on the page

The Vite plugin only registers each module's index.ts as a build input — the compiled bundle lands in the manifest, but a build step cannot place a <script> tag on a server-rendered page. WireScript closes that gap at runtime: it reads every module registered with WireTypeRegistry, resolves each entry, and emits the tags through Laravel's Vite service (the hot-file URL under npm run dev, the manifest URL in production). Two delivery surfaces, both wired by the service provider:

  • Filament — a panels::scripts.before render hook is auto-registered when Filament is installed, so every panel page loads its modules with no host configuration. The hook fires before Filament boots Alpine, so each module's generated alpine:init listener attaches in time.
  • Plain Livewire / Blade — add the @wirescripts directive to your layout, before @livewireScripts:

    <head>
        {{-- ... --}}
        @wirescripts
    </head>
    

Both render the same tags. A module that registers $wire types but ships no resources/js/index.ts is skipped, and nothing is emitted until the assets are built (a dev server is running, or public/build/manifest.json exists).

Public surface

SymbolRole
EncoreDigitalGroup\WireScript\WireExposedAttribute marking a property/method for TS interface generation.
EncoreDigitalGroup\WireScript\WireTypeRegistrySingleton collecting per-package interface groups.
php artisan wirescript:generateGenerates (or, with --check, verifies) the full wiring surface.
php artisan wirescript:scaffoldCreates a typed component factory stub (never overwrites).
wirescript() / wirescriptInputs() (resources/vite)Vite input discovery for module frontend entries.
WireScriptManifest / @wirescripts directiveRuntime <script> injection of each module's built entry.

The canonical templates for every generated artifact live in resources/stubs/ — editing the built-in Livewire WireBase surface, or any boilerplate, is a single change there.