Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to `@escalated-dev/escalated` will be documented in this file.

## [Unreleased]

### Fixed
- Install a `window.route` safety stub in `EscalatedPlugin.install()` on hosts that haven't provided their own. Non-Laravel hosts previously saw bare `ReferenceError: route is not defined` errors when any of the 77 components that call Ziggy's `route(name, params)` helper rendered. The stub throws a descriptive error naming the missing dependency instead. Laravel hosts with Ziggy loaded are untouched.

## [0.7.0] - 2026-04-05

### Added
Expand Down
25 changes: 25 additions & 0 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { markRaw } from 'vue';
*/
export const EscalatedPlugin = {
install(app, options = {}) {
installRouteShim();

if (options.layout) {
app.provide('escalated-layout', markRaw(options.layout));
}
Expand All @@ -38,6 +40,29 @@ export const EscalatedPlugin = {
},
};

// 77 components in this package call the Ziggy `route()` helper (Laravel's
// named-route URL generator). Laravel hosts ship Ziggy and get `window.route`
// for free; other host frameworks (Rails, Django, NestJS, Phoenix, Symfony,
// Adonis, Go, .NET, Spring, WordPress) don't, and the call sites would
// otherwise throw a bare ReferenceError deep inside a component render with
// no hint at the cause.
//
// Install a stub that throws an informative error instead, so the host app's
// first failing request points at the actual missing dependency. Laravel hosts
// that already have Ziggy loaded are left alone.
function installRouteShim() {
if (typeof window === 'undefined') return;
if (typeof window.route === 'function') return;

window.route = function escalatedRouteShimMissing(name) {
throw new Error(
`[escalated] window.route('${name}') called, but no \`route()\` helper is installed on this host. ` +
`The escalated agent/admin UI depends on Laravel's Ziggy-style \`route(name, params)\` helper. ` +
`Install Ziggy (on Laravel hosts) or register a compatible shim on window.route before mounting the app.`,
);
};
}

const themeDefaults = {
primary: '#4f46e5',
primaryHover: null,
Expand Down
45 changes: 44 additions & 1 deletion tests/plugin.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { createApp } from 'vue';
import { EscalatedPlugin } from '../src/plugin.js';

Expand Down Expand Up @@ -159,6 +159,49 @@ describe('EscalatedPlugin', () => {
});
});

describe('route() helper shim', () => {
let originalRoute;

beforeEach(() => {
originalRoute = window.route;
delete window.route;
});

afterEach(() => {
if (originalRoute === undefined) {
delete window.route;
} else {
window.route = originalRoute;
}
});

it('installs a window.route stub on non-Laravel hosts', () => {
expect(typeof window.route).toBe('undefined');
installPlugin();
expect(typeof window.route).toBe('function');
});

it('the stub throws a descriptive error when called', () => {
installPlugin();

expect(() => window.route('escalated.admin.saved-views.update', 1)).toThrow(
/window\.route\('escalated\.admin\.saved-views\.update'\)/,
);
expect(() => window.route('x')).toThrow(/Install Ziggy/);
});

it('does not overwrite an existing window.route from a Laravel host', () => {
const existing = vi.fn(() => '/from-ziggy');
window.route = existing;

installPlugin();

expect(window.route).toBe(existing);
expect(window.route('some.name')).toBe('/from-ziggy');
expect(existing).toHaveBeenCalledWith('some.name');
});
});

describe('darken() utility (tested indirectly via theme)', () => {
beforeEach(() => {
document.documentElement.style.removeProperty('--esc-primary-hover');
Expand Down
Loading