v1 LiVue v1 is here — server-driven reactivity for Laravel using Vue.js Get Started →

Development

Tools, configuration, and workflows for developing with LiVue: config file, Artisan commands, testing, HMR, DevTools, and asset management.

Configuration

Required vs Optional — For first setup, you can start with only composer require livue/livue, php artisan make:livue Counter, and rendering @livue('counter'). Most settings in this page are optional and scenario-specific.

If you publish the config file with php artisan vendor:publish --tag=livue-config, you will find it at config/livue.php. This file controls component discovery, asset injection, routing, layouts, and developer tools.

config/livue.php
return [

    // Auto-inject CSS and JS when components are detected on the page
    'inject_assets' => true,

    // URL prefix for LiVue's internal AJAX endpoints
    'route_prefix' => 'livue',

    // Middleware applied to AJAX endpoints
    'middleware' => ['web'],

    // Namespace for auto-discovery of component classes
    'component_namespace' => 'App\\LiVue',

    // Filesystem path where component classes live
    'component_path' => app_path('LiVue'),

    // Default Blade layout for page components
    'layout' => 'components.layouts.app',

    // HMR (Hot Module Replacement) settings
    'hmr' => [
        'enabled' => env('LIVUE_HMR', true),
        'indicator' => true,
        'preserve_state' => true,
    ],

    // Enable the in-page DevTools panel (dev environment only)
    'devtools' => true,

    // Version string for cache busting of published assets
    'asset_version' => env('LIVUE_ASSET_VERSION', null),

    // Public path for published assets
    'assets_path' => 'vendor/livue',

    // Enable per-phase timing in AJAX responses (local only)
    'benchmark_responses' => env('LIVUE_BENCHMARK', false),

];

Key Options

inject_assets

When true (the default), LiVue automatically detects components on the page and injects the required JavaScript and CSS before </body>. Set to false if you prefer to include assets manually via Blade directives or Vite.

component_namespace & component_path

Define where LiVue discovers your primary component classes. By default, it scans app/LiVue/ under the App\LiVue namespace.

For additional namespaces, register them during application boot:

use LiVue\Facades\LiVue;

public function boot(): void
{
    LiVue::registerNamespace([
        'App\\Admin\\LiVue',
        'App\\Billing\\LiVue',
    ]);
}

layout

The default Blade layout used by page components. Generate one with php artisan livue:layout or override it per-component.

devtools

Enables the in-page debug panel. Only active in development environments. In production, the DevTools API returns no-op functions and adds zero overhead.

benchmark_responses

When enabled, every AJAX response includes a benchmark key with per-phase lifecycle timing in microseconds. Opt-in via LIVUE_BENCHMARK=true in your .env file. Only active in local environments.

Publishable Assets

LiVue offers several publish tags for customization:

Tag Publishes
livue-config Configuration file to config/livue.php
livue-assets Optional static bundle (livue.js, livue.js.map) to public/vendor/livue/
livue-stubs Generator stubs to stubs/livue/
livue-layout Base layout to resources/views/components/layouts/app.blade.php

Artisan Commands

LiVue provides several Artisan commands to scaffold components, forms, composables, and more. Every make:livue-* command has an equivalent livue:* alias.

make:livue

Generates a new component class and its Blade template.

# Standard component (class + Blade template)
php artisan make:livue Counter

# Equivalent alias
php artisan livue:component Counter

# Single File Component
php artisan make:livue Counter --single

# Multi File Component
php artisan make:livue Counter --multi

Creates app/LiVue/Counter.php and resources/views/livue/counter.blade.php.

make:livue-form

Generates a Form Object class with validation support. The Form suffix is added automatically.

php artisan make:livue-form Contact

# Equivalent alias
php artisan livue:form Contact

Creates app/LiVue/Forms/ContactForm.php.

make:livue-composable

Generates a Composable class. The Use prefix is added automatically.

php artisan make:livue-composable Cart

# Equivalent alias
php artisan livue:composable Cart

Creates app/LiVue/Composables/UseCart.php with a useCart() method.

livue:layout

Generates the base Blade layout for page components.

# Default layout (layouts/app)
php artisan livue:layout

# Named layout
php artisan livue:layout dashboard

# Overwrite existing
php artisan livue:layout app --force

Creates resources/views/components/layouts/{name}.blade.php.

livue:benchmark

Profiles a component's lifecycle performance across multiple iterations, reporting per-phase timing with min, max, average and median values plus peak memory usage. Only available in local environments.

# Benchmark mount lifecycle
php artisan livue:benchmark Counter

# Benchmark a method call (update lifecycle)
php artisan livue:benchmark Counter --method=increment

# More iterations, JSON output
php artisan livue:benchmark Counter --iterations=50 --format=json

# With context and custom setup class
php artisan livue:benchmark ListProducts --setup="App\Support\TenantSetup" --context='{"tenant_id": 1}'

See Features → Benchmarking for the full options reference and details on measured phases.

Other Useful Commands

# Clear compiled Blade views
php artisan view:clear

# Clear route cache
php artisan route:clear

# Regenerate autoloader after new classes
composer dump-autoload

# Build production assets
npx vite build

# Optional: publish LiVue assets to public directory
php artisan vendor:publish --tag=livue-assets

Testing

LiVue includes a built-in testing system that lets you test components entirely server-side, without a browser or JavaScript. Tests simulate the full component lifecycle -- mount, render, state changes, and AJAX updates -- using the same code paths as production.

Creating a Test

Use LiVue::test() to instantiate and mount a component. The method returns a Testable instance with chainable interaction methods and assertions.

tests/Feature/CounterTest.php
use App\LiVue\Counter;
use LiVue\Facades\LiVue;

it('starts at zero', function () {
    LiVue::test(Counter::class)
        ->assertSet('count', 0)
        ->assertSee('0');
});

it('increments the counter', function () {
    LiVue::test(Counter::class)
        ->call('increment')
        ->assertSet('count', 1);
});

it('can set properties', function () {
    LiVue::test(Counter::class)
        ->set('count', 10)
        ->call('increment')
        ->assertSet('count', 11);
});

You can also pass mount parameters:

LiVue::test(UserProfile::class, ['userId' => 1])
    ->assertSet('user.name', 'John Doe')
    ->assertSee('John Doe');

Interaction Methods

Method Description
set($key, $value) Set a property (simulates v-model)
call($method, ...$params) Call a component method
toggle($property) Toggle a boolean property
refresh() Re-render without changes
dispatch($event, ...$data) Send an event to the component

Assertions

Method Description
assertSet($prop, $value) Assert a property equals a value
assertNotSet($prop, $value) Assert a property does not equal a value
assertCount($prop, $count) Assert array/collection has N elements
assertSee($text) Assert text appears in rendered HTML
assertDontSee($text) Assert text does not appear in HTML
assertSeeHtml($html) Assert exact HTML fragment is present
assertHasErrors($keys) Assert validation errors exist
assertHasNoErrors() Assert no validation errors
assertRedirect($url) Assert a redirect was triggered
assertDispatched($event) Assert an event was dispatched
assertNavigate($url) Assert SPA navigation was triggered

Complete Test Example

Here is a full test for a contact form component with validation:

tests/Feature/ContactFormTest.php
use App\LiVue\ContactForm;
use LiVue\Facades\LiVue;

describe('ContactForm', function () {

    it('validates required fields', function () {
        LiVue::test(ContactForm::class)
            ->call('submit')
            ->assertHasErrors(['name', 'email', 'message']);
    });

    it('validates email format', function () {
        LiVue::test(ContactForm::class)
            ->set('name', 'John')
            ->set('email', 'not-an-email')
            ->set('message', 'Hello')
            ->call('submit')
            ->assertHasErrors('email')
            ->assertHasNoErrors(['name', 'message']);
    });

    it('submits a valid form', function () {
        LiVue::test(ContactForm::class)
            ->set([
                'name' => 'John Doe',
                'email' => 'john@example.com',
                'message' => 'Hello World',
            ])
            ->call('submit')
            ->assertHasNoErrors()
            ->assertDispatched('form-submitted')
            ->assertSee('Thank you for your message!');
    });

    it('redirects after login', function () {
        LiVue::test(Login::class)
            ->set('email', 'admin@example.com')
            ->set('password', 'password')
            ->call('login')
            ->assertRedirect('/dashboard');
    });

});

Browser Testing with Dusk

For end-to-end tests in a real browser, LiVue provides the WithLiVueDusk trait with helper methods for waiting on LiVue responses and asserting component state:

tests/Browser/CounterBrowserTest.php
use LiVue\Features\SupportTesting\WithLiVueDusk;
use Tests\DuskTestCase;

class CounterBrowserTest extends DuskTestCase
{
    use WithLiVueDusk;

    public function test_counter_increments_in_browser()
    {
        $this->browse(function ($browser) {
            $browser->visit('/counter');

            $this->waitForLiVue($browser)
                ->assertLiVueState($browser, 'count', 0);

            $browser->press('Increment');

            $this->waitForLiVueResponse($browser)
                ->assertLiVueState($browser, 'count', 1);
        });
    }
}
Dusk Helper Description
waitForLiVue($browser) Wait for LiVue to initialize
waitForLiVueResponse($browser) Wait for an AJAX request to finish
assertLiVueState($browser, $prop, $value) Assert component state in browser
clickAndWaitForLiVue($browser, $selector) Click an element and wait for response

Hot Module Replacement

LiVue integrates with Vite to provide Hot Module Replacement during development. When you save a Blade template or PHP component file, the component updates instantly in the browser without a full page refresh -- and your state is preserved.

How It Works

  1. A custom Vite plugin watches your component files
  2. On file change, it sends an HMR event to the browser
  3. LiVue saves the current state of all affected components
  4. Components are reloaded from the server with the new templates
  5. The saved state is restored automatically

No special configuration needed -- if you are using the standard Vite dev server (npm run dev), HMR works out of the box.

What Gets Hot-Reloaded

Instant Update

  • Blade template changes
  • @script block changes
  • CSS / style changes

Requires Page Reload

  • PHP class changes (new properties, methods)
  • Route changes
  • Config changes

State Preservation

Preserved

  • Public properties ($count, $name, etc.)
  • Dirty state (unsaved changes)
  • Form input values

Not Preserved

  • Functions and callbacks
  • DOM element references
  • Vue internal state (computed cache)

Visual Indicator

A small toast notification appears during HMR updates:

"Updating..." -- Update in progress
"Updated" -- Success
"Update failed" -- Error (check terminal for details)

Configuration

vite.config.js
import livueHmr from 'livue/vite-plugin';

export default {
    plugins: [
        livueHmr({
            watchPaths: [
                'resources/views/livue',
                'app/LiVue',
            ],
            verbose: true,
        }),
    ],
};
config/livue.php
'hmr' => [
    // Enable/disable HMR
    'enabled' => env('LIVUE_HMR', true),

    // Show visual toast indicator
    'indicator' => true,

    // Preserve state during reload
    'preserve_state' => true,
],

JavaScript API

// Check availability
LiVue.hmr.isAvailable();

// Enable/disable at runtime
LiVue.hmr.enable();
LiVue.hmr.disable();

// Hook into update events
const unsub = LiVue.hmr.onUpdate((data) => {
    console.log('File changed:', data.fileName);
});

// Manual trigger (useful for testing)
LiVue.hmr.trigger();

Note: HMR is automatically disabled in production. When there is no Vite dev server, import.meta.hot is undefined and HMR adds zero performance overhead.

DevTools

LiVue includes a built-in in-page debug panel with 7 tabs for inspecting every aspect of your components at runtime. The panel is only available in development environments and is completely tree-shaken in production builds.

Enable: Set devtools => true in config/livue.php. DevTools are only active when your app runs in a local/dev environment. Open the panel with Ctrl+Shift+L or call LiVue.devtools.open().

Panel Tabs

Components

Tree view of all mounted components with live state inspection. Click a component to see its properties, dirty fields, and validation errors.

Timeline

Chronological log of all AJAX requests with timing, status codes, payload preview, and color-coded duration (green <100ms, yellow <500ms, red >500ms).

Events

Track dispatched events with source component, dispatch mode (broadcast, self, to), and event data. Includes a search filter.

Stores

Inspect registered Pinia stores, plugins, global Vue components, and custom directives.

Echo

Monitor Laravel Echo status and active WebSocket channel subscriptions (public, private, presence).

Performance

Aggregated metrics: total/successful/failed requests, average/fastest/slowest timing, template swap counts, and component totals. When benchmark_responses is enabled, a "Server Lifecycle Timing" section shows per-phase server-side benchmarks for each request.

Settings

Configure panel position (right, left, bottom, top), view keyboard shortcuts, and manage panel behavior.

State Inspector

When you select a component in the Components tab, a side panel opens with three views:

View Description
State Current reactive state with type-colored values. Dirty fields marked with an orange asterisk.
Diff Compare server state vs client state to spot unsynced changes at a glance.
Info Component metadata: name, attributes, composables, upload and streaming status.

JavaScript API

// Panel control
LiVue.devtools.open();
LiVue.devtools.close();
LiVue.devtools.toggle();

// Debug mode (verbose lifecycle logging in console)
LiVue.debug(true);

// Access collected data programmatically
LiVue.devtools.getComponents();  // Component tree
LiVue.devtools.getTimeline();    // AJAX request history
LiVue.devtools.getEvents();      // Dispatched events
LiVue.devtools.getPerf();        // Performance metrics

// Clear collected data
LiVue.devtools.clear();
LiVue.devtools.clearTimeline();
LiVue.devtools.clearEvents();

// Background data collection (without opening the panel)
LiVue.devtools.startCollecting();
LiVue.devtools.stopCollecting();

Features

4 Panel Positions

Right (default), left, bottom, or top. Resizable by dragging the panel edge.

State Persistence

Panel position, active tab, and open/closed state are saved in localStorage and restored on page reload.

Keyboard Shortcut

Toggle the panel with Ctrl+Shift+L from anywhere in your application.

Component Icons

Visual indicators in the tree: square (root component), circle (child), diamond (island / separate Vue app).

Asset Management

LiVue provides an asset system for registering CSS and JavaScript assets from your components or packages. Assets are automatically deduplicated, versioned for cache busting, and rendered in the correct order.

Registering Assets

Use the LiVueAsset facade to register typed asset objects from a service provider:

app/Providers/AppServiceProvider.php
use LiVue\Facades\LiVueAsset;
use LiVue\Features\SupportAssets\Js;
use LiVue\Features\SupportAssets\Css;

public function boot(): void
{
    $this->app->booted(function () {
        LiVueAsset::register([
            Css::make('my-styles', url('css/custom.css'))->version('1.2.3'),
            Js::make('my-script', url('js/custom.js'))->module()->version('1.2.3'),
        ], 'my-package');
    });
}

Scoped / On-Request Assets

To avoid loading package assets globally, mark them with ->onRequest(). On-request assets are registered in the asset manager, but excluded from global @livueStyles and @livueScripts output.

LiVueAsset::register([
    Css::make('table-css', url('primix/tables/table.css'))
        ->version('2.4.0')
        ->onRequest(),
    Js::make('table-js', url('primix/tables/table.js'))
        ->module()
        ->version('2.4.0')
        ->onRequest(),
], 'primix/tables');

Then load the assets only inside the Blade that needs them:

@livueLoadStyle('table-css', 'primix/tables')
@livueLoadScript('table-js', 'primix/tables')

// Optional script options:
@livueLoadScript('table-js', 'primix/tables', [
    'type' => 'module',
    'defer' => false,
    'async' => true,
])

On-request loaders are deduplicated client-side, so the same asset is appended only once even across repeated renders or SPA navigation.

When an on-request module registers Vue plugins/components via LiVue.setup(), LiVue applies that callback to mounted roots too. Package authors can keep setup registration simple without custom late-bootstrap wrappers.

Dynamic Package Resolution

If you are inside a package service provider, you can resolve the package name from the nearest composer.json instead of hardcoding it.

use LiVue\Facades\LiVueAsset;

$resolvedPackage = LiVueAsset::registerForPackage([
    Css::make('tables-css', url('primix/tables.css')),
    Js::make('tables-js', url('primix/tables.js'))->onRequest(),
], __DIR__);

$packageName = LiVueAsset::resolvePackageName(__DIR__, 'app');

If no valid composer.json is found, LiVue uses the fallback package (default: app).

Versioning Strategy

LiVue appends ?v=... to local assets. Priority order: explicit ->version(), package version from Composer, then app config for app. The core runtime /livue/livue.js uses package version first, then config, then a file hash fallback.

Css::make('tables-css', url('primix/tables.css'))
    ->version('2.4.0');

Js::make('tables-js', url('primix/tables.js'))
    ->version('2.4.0')
    ->onRequest();

// Example output:
// /livue/livue.js?v=3.1.0

Asset Types

JavaScript Assets
// Standard script
Js::make('id', '/path/to/script.js')

// ES Module
Js::make('id', '/path/to/module.js')
    ->module()

// Inline script
Js::make('id', '')
    ->inline('console.log("hi")')

// Async loading
Js::make('id', '/path.js')
    ->async()

// Explicit version
Js::make('id', '/path.js')
    ->version('1.2.3')
CSS Assets
// Standard stylesheet
Css::make('id', '/path/to/styles.css')

// With media query
Css::make('id', '/path/to/print.css')
    ->media('print')

// Inline styles
Css::make('id', '')
    ->inline('.my-class { color: red; }')

// Explicit version
Css::make('id', '/path/to/styles.css')
    ->version('1.2.3')

Per-Component Attributes

You can also attach assets directly to a component using PHP attributes:

app/LiVue/Chart.php
use LiVue\Component;
use LiVue\Attributes\Js;
use LiVue\Attributes\Css;

#[Js('https://cdn.example.com/chart.js')]
#[Css('/css/chart-styles.css')]
class Chart extends Component
{
    // Assets are loaded only when this component is on the page
}

CSS Variables

Register CSS custom properties that are rendered as :root blocks:

LiVueAsset::registerCssVariables([
    'primary-color' => '#3b82f6',
    'border-radius' => '0.5rem',
    'font-family' => 'Inter, sans-serif',
], 'my-package');

Script Data

Pass server-side data to JavaScript via window.LiVueData:

LiVueAsset::registerScriptData([
    'locale' => app()->getLocale(),
    'csrfToken' => csrf_token(),
    'user' => auth()->user()?->only(['id', 'name']),
], 'my-package');

// Access in JavaScript:
// const locale = window.LiVueData.locale;

Rendering

Assets are rendered automatically when inject_assets is enabled. For manual control, use Blade directives in your layout:

resources/views/components/layouts/app.blade.php
<head>
    @livueStyles    <!-- CSS variables + stylesheets -->
</head>
<body>
    {{ $slot }}

    @livueScripts   <!-- Script data + import maps + JS bundles -->
</body>

Key Features

Auto-Deduplication

The same asset is never loaded twice, even when registered by multiple components or packages.

Cache Busting

Local assets automatically include a version query string based on your package version or config.

Package Isolation

Assets are grouped by package name, so each package manages its own scripts and styles independently.

Scoped Loading

Mark assets with onRequest() and load them only where needed via @livueLoadStyle / @livueLoadScript.

Inline & External

Register both external URLs and inline code. Inline assets are rendered as embedded script/style tags.

Import Maps

Register ES module import mappings for browser-native module resolution.

Ordered Rendering

Assets render in the correct order: CSS variables first, then stylesheets, then script data, import maps, and finally scripts.