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.
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.
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:
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:
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
- A custom Vite plugin watches your component files
- On file change, it sends an HMR event to the browser
- LiVue saves the current state of all affected components
- Components are reloaded from the server with the new templates
- 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:
Configuration
import livueHmr from 'livue/vite-plugin';
export default {
plugins: [
livueHmr({
watchPaths: [
'resources/views/livue',
'app/LiVue',
],
verbose: true,
}),
],
};
'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:
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
// 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')
// 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:
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:
<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.