@wavelengthusaf/client-router
v0.2.2
Published
Native TypeScript Router
Downloads
1,224
Keywords
Readme
Wavelength Router
A lightweight, native TypeScript web components router for modern web applications.
Table of Contents
- Getting Started
- Use in Your Application
- Usage
- API Reference
- Best Practices
- Contributing and Local Development
Getting Started
Prerequisites
- Node.js 24+ and npm
- A modern browser with native web components support
Installation
Install the package in your application:
npm install @wavelengthusaf/client-routerUse in Your Application
Option 1: npm Package (Published)
Once published to npm, install the package:
npm install @wavelengthusaf/client-routerThen import the elements in your entry point:
// main.ts or main.js
import '@wavelengthusaf/client-router';Usage
You can implement routing using either the HTML template approach or pure TypeScript.
HTML Template Approach
Use the custom elements directly in your HTML. This is ideal for simpler applications or when you want declarative routing.
Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<!-- Router Container - owns navigation and renders matched pages -->
<wavelength-router>
<nav aria-label="Main navigation">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/users/123">User Profile</a>
</nav>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/about" component="about-page"></wavelength-route>
<wavelength-route
path="/users/:userId"
component="user-page"
></wavelength-route>
<wavelength-route path="*" component="not-found-page"></wavelength-route>
</wavelength-router>
<script type="module">
import '@wavelengthusaf/client-router';
// Define your page components as custom elements
customElements.define(
'home-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>Welcome Home</h1>';
}
},
);
customElements.define(
'about-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>About Us</h1><p>We are awesome.</p>';
}
},
);
customElements.define(
'user-page',
class extends HTMLElement {
connectedCallback() {
// Access route parameters via routeContext
const userId = this.routeContext?.routeParams?.userId;
this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
}
},
);
customElements.define(
'not-found-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>404 - Page Not Found</h1>';
}
},
);
</script>
</body>
</html><wavelength-router> delegates links from its own Light DOM and open Shadow
DOM descendants. Keep declarative navigation inside the router, outside the
rendered page outlet. Links outside the router intentionally retain native
browser navigation; external controls can call router.navigate(path).
How It Works
<wavelength-router>collects route declarations from its Light DOM and open Shadow DOM descendants.- It preserves those declarations and mounts the matching component in a separate internal Light DOM outlet.
- Routes are matched in declaration order, so place a wildcard route last.
TypeScript Approach
For more control, you can use the Router class directly and define routes programmatically.
Basic Example
import { Router } from '@wavelengthusaf/client-router';
import type {
RouteContext,
RouteDefinition,
} from '@wavelengthusaf/client-router';
const mountElement =
(tag: string) =>
(container: HTMLElement, context: RouteContext): void => {
const page = document.createElement(tag) as HTMLElement & {
routeContext?: RouteContext;
};
page.routeContext = context;
container.appendChild(page);
};
const routes: RouteDefinition[] = [
{
path: '/',
routeComponent: {
mount: mountElement('home-page'),
},
},
{
path: '/users/:userId',
meta: {
title: 'User Profile',
requiresAuth: true,
breadcrumbs: [{ label: 'Users', href: '/users' }],
},
routeComponent: {
mount: mountElement('user-page'),
},
},
];
const outlet = document.querySelector<HTMLElement>('#app')!;
const router = new Router(routes, {
outlet,
hashMode: false, // History API mode; use true for #/path URLs.
scrollRestoration: 'top', // Use 'preserve' or a callback for custom behavior.
});
router.start();Use Router directly when the host application owns its outlet, route
definitions, and lifecycle. It is also useful in tests because browser globals
can be supplied through the optional third constructor argument.
RouteDefinition Structure
Each route requires:
path: The URL path pattern (supports:paramsyntax for dynamic segments)routeComponent: An object with amountfunction (and optionalunmount)beforeEach: Optional route-specific navigation guardmeta: Optional route metadata for titles, analytics, auth requirements, breadcrumbs, or any application-specific data
interface RouteDefinition {
path: string; // e.g., '/users/:id' or '/products/:category/:id'
meta?: RouteMeta;
routeComponent: {
mount: (
container: HTMLElement,
context: RouteContext,
) => void | Promise<void>;
unmount?(): void; // Optional cleanup function
};
beforeEach?: RouteGuard;
}Router Lifecycle
Call start() after the outlet exists. Call destroy() when the host is
removed or replaced; it removes navigation listeners and runs the active
route's optional unmount() hook.
const router = new Router(routes, { outlet });
router.start();
window.addEventListener('pagehide', () => router.destroy(), { once: true });Each navigation clears the outlet, runs the prior route's unmount(), and then
calls the next route's mount(outlet, context).
mount() may return a promise. The router waits to mark that route complete
until the promise resolves. If a newer navigation begins first, the older mount
is treated as cancelled: it cannot become active or emit a completion event,
and its optional unmount() runs when its promise settles. Route components
should use their own AbortController for work that must be stopped before a
promise settles.
Direct Router Scoping
By default, a direct Router listens on its injected document. For a nested
router or a router inside Shadow DOM, eventTarget and navigationRoot are
required: without both, multiple document-level routers can handle the same
composed navigation event. Scope them to the containing host:
const host = document.querySelector<HTMLElement>('#account-router')!;
const outlet = host.querySelector<HTMLElement>('[data-outlet]')!;
const router = new Router(routes, {
outlet,
eventTarget: host,
navigationRoot: host,
});
router.start();Using the same containing element for eventTarget and navigationRoot
prevents an outer router from acting on links owned by a nested router. A
top-level direct router may omit these options when document-wide navigation is
intended.
API Reference
<wavelength-router> Attributes
| Attribute | Type | Description |
| -------------------- | --------------------------- | -------------------------------------------------------------------------- |
| base-path | string | Base URL pathname stripped before matching and prepended during navigation |
| error-component | string | Custom element tag rendered for guard or mount failures |
| hash-mode | flag | Enable hash-based routing (#/path) instead of history |
| guard | string | Name of a global guard function on window |
| focus-strategy | none, outlet, heading | Focus target after navigation; defaults to none |
| not-found-component | string | Custom element tag rendered when no route matches |
| scroll-restoration | preserve, top | Scroll behavior after navigation; defaults to preserve |
Example with Hash Mode
<wavelength-router hash-mode>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/profile" component="profile-page"></wavelength-route>
</wavelength-router>URL becomes: https://example.com/#/profile
Hash-mode query strings are supported. For #/profile?tab=security, the route
matches /profile and routeContext.query.get('tab') is security.
Example with a Base Path
Use base-path when the app is served from a subpath. Routes still match their
application-relative paths, and programmatic navigation prepends the base path
before updating history:
<wavelength-router base-path="/apps/crucible" focus-strategy="heading">
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/simple" component="simple-page"></wavelength-route>
</wavelength-router>routerElement.navigate('/simple'); // pushes /apps/crucible/simpleFor direct Router usage, pass the same value as basePath in the router
options.
External route-hash changes are observed automatically. Hash routes use the
#/path form, so ordinary fragments such as href="#details" retain native
in-page scrolling behavior. Same-origin absolute links ending in #/path are
also normalized as hash routes.
<wavelength-route> Attributes
| Attribute | Type | Description |
| ----------- | ------ | -------------------------------------------------- |
| path | string | URL path pattern (supports :param syntax) |
| component | string | Custom element tag name to render for this route |
| guard | string | Optional route-specific guard function on window |
| meta | JSON | Optional route metadata object |
Changing any route attribute on a connected route automatically rebuilds the router's definitions.
<wavelength-route
path="/account"
component="account-page"
meta='{"title":"Account","requiresAuth":true,"breadcrumbs":[{"label":"Account","href":"/account"}]}'
></wavelength-route>Example with Error Pages
Use not-found-component for unmatched paths and error-component for guard or
mount failures. The rendered component receives the normal routeContext.
Not-found contexts include routeContext.meta.status === 404; error contexts
include routeContext.meta.status === 500 and
routeContext.meta.errorPhase.
<wavelength-router
error-component="error-page"
not-found-component="error-page"
>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/reports" component="reports-page"></wavelength-route>
</wavelength-router>Path Patterns
- Static:
/about,/dashboard - Dynamic:
/users/:userId,/posts/:category/:postId - Wildcard:
*(catches all unmatched routes)
RouteContext
Passed to your component's routeContext property and the mount function:
interface RouteContext {
path: string; // The matched path, e.g., '/users/123'
routeParams: {
// Extracted URL parameters
[key: string]: string;
};
query: URLSearchParams; // Query string parameters
meta?: RouteMeta; // Metadata from the matched route definition
}meta is passed through unchanged from the matched RouteDefinition. It is
useful for document titles, analytics labels, auth requirements, breadcrumbs,
and other application-owned route data.
Accessing RouteContext in Components
class MyPage extends HTMLElement {
public routeContext?: RouteContext;
public connectedCallback(): void {
const ctx = this.routeContext;
if (ctx) {
console.log('Current path:', ctx.path);
console.log('User ID:', ctx.routeParams.userId);
console.log('Search query:', ctx.query.get('search'));
console.log('Route title:', ctx.meta?.title);
}
}
}RouteGuard
Navigation guards allow you to intercept and redirect navigation. Define a
guard function and reference it via the guard attribute on
<wavelength-router> or a specific <wavelength-route>.
// Define guard on window (required for HTML approach)
type NextCallback = (redirect?: string) => void;
(window as unknown as Record<string, unknown>).authGuard = (
to: RouteContext,
next: NextCallback,
) => {
const isAuthenticated = checkAuth(); // Your auth logic
if (to.path.startsWith('/protected') && !isAuthenticated) {
next('/login'); // Redirect to login
} else {
next(); // Allow navigation
}
};
(window as unknown as Record<string, unknown>).permissionGuard = (
to: RouteContext,
next: NextCallback,
) => {
const user = getCurrentUser();
if (!user?.permissions.includes('billing:read')) {
next('/forbidden');
return;
}
next();
};<wavelength-router guard="authGuard">
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route
path="/billing"
component="billing-page"
guard="permissionGuard"
></wavelength-route>
<wavelength-route path="/login" component="login-page"></wavelength-route>
<wavelength-route
path="/forbidden"
component="forbidden-page"
></wavelength-route>
</wavelength-router>The router-level guard runs before a matched route's guard. If either guard redirects, later guards and the matched component mount are skipped for that navigation.
Use a route-level guard when only one route or route group needs a specific permission check:
<wavelength-router>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route
path="/reports"
component="reports-page"
guard="reportsGuard"
></wavelength-route>
</wavelength-router>Guard Flow:
- Guard receives
RouteContextwith destination path - Guard calls
next()to allow navigation ornext('/redirect-path')to redirect
Guards may call next() asynchronously. If another navigation starts first,
the older continuation is ignored so it cannot replace the newer route. If a
route's guard attribute changes while connected, <wavelength-router>
rebuilds its route definitions automatically.
For a direct Router, pass a global guard as beforeEach, or attach a
route-specific guard to an individual route definition:
const routes: RouteDefinition[] = [
{
path: '/billing',
routeComponent: { mount: mountElement('billing-page') },
beforeEach: permissionGuard,
},
];
const router = new Router(routes, {
outlet,
beforeEach: (context, next) => {
if (context.path.startsWith('/protected') && !getCurrentUser()) {
next('/login');
return;
}
next();
},
});Route-Level Error Handling
Pass notFoundComponent to a direct Router to render a route component when
no route matches. Pass errorComponent to render a route component for guard
and mount failures. The not-found component receives
routeContext.meta.status === 404. The error component receives
routeContext.meta.status === 500 and routeContext.meta.errorPhase.
Pass onError to report route matching, guard, mount, unmount, and scroll
failures in one place. The hook receives the thrown value, its phase, and the
matched RouteContext when one exists. It works in both Light and Shadow DOM
because it is independent of event retargeting.
import type { RouteErrorDetail } from '@wavelengthusaf/client-router';
const router = new Router(routes, {
outlet,
errorComponent: {
mount: (container, routeContext) => {
const page = document.createElement('error-page');
page.routeContext = routeContext;
container.replaceChildren(page);
},
},
onError: ({ error, phase, context }: RouteErrorDetail) => {
reportRouteError(error, { phase, path: context?.path });
},
});The phases are match, guard, mount, unmount, and scroll. Supplying
onError handles synchronous guard and mount errors so the router remains
started. Unmount and scroll errors are reported and do not prevent the next
route from mounting. Without onError or errorComponent, synchronous guard
and mount errors retain their normal thrown behavior.
wavelength-route-error is dispatched in all cases.
Navigation Lifecycle Events
The router emits composed, bubbling events for each navigation. Listen on the
<wavelength-router> itself, an ancestor, or the configured eventTarget of a
direct Router. They are useful for loading indicators, telemetry, and error
reporting without coupling page components to router internals.
| Event | When it fires | detail |
| --------------------------- | -------------------------------------------------------- | ---------------------------------- |
| wavelength-route-start | A route is matched, before its guard or component mounts | { path, context } |
| wavelength-route-complete | The matched component has mounted successfully | { path, context } |
| wavelength-route-error | Route matching, a guard, mount, or unmount fails | { path, context?, error, phase } |
context is the normal RouteContext. A redirect starts a new navigation for
its destination and therefore emits completion only for the destination route.
import type { RouteLifecycleEventDetail } from '@wavelengthusaf/client-router';
document.addEventListener('wavelength-route-start', (event: Event) => {
const { context } = (event as CustomEvent<RouteLifecycleEventDetail>).detail;
loadingIndicator.show(context?.path);
});
document.addEventListener('wavelength-route-complete', () => {
loadingIndicator.hide();
});
document.addEventListener('wavelength-route-error', (event: Event) => {
const { error, path } = (event as CustomEvent<RouteLifecycleEventDetail>)
.detail;
reportNavigationError(path, error);
});Programmatic Navigation
The router provides navigation methods:
import type { RouterElement } from '@wavelengthusaf/client-router';
const routerElement =
document.querySelector<RouterElement>('wavelength-router')!;
// Navigate to a new path (adds to history)
routerElement.navigate('/about');
// Navigate and replace current entry (no history)
routerElement.replace('/new-page');
// Go back in history
routerElement.back();
// Rescan routes after dynamically attaching an open shadow root
routerElement.refresh();The same methods are available on a Router instance. Prefer replace() for
post-login or canonicalization redirects when the previous URL should not
remain in browser history. Programmatic navigation accepts only valid
same-origin HTTP(S) paths; external and malformed targets are ignored.
Scroll Restoration
By default, navigation preserves the current scroll position. Set
scrollRestoration on a direct Router, or scroll-restoration on
<wavelength-router>, to reset to the top after the matched route mounts:
<wavelength-router scroll-restoration="top">
<wavelength-route path="/" component="home-page"></wavelength-route>
</wavelength-router>For custom behavior, pass a callback to Router or assign the
scrollRestoration property on RouterElement:
const router = new Router(routes, {
outlet,
scrollRestoration: (context, navigationType) => {
if (navigationType === 'pop') return;
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
analytics.page(context.meta?.title ?? context.path);
},
});navigationType is initial, push, replace, or pop. In hash mode,
hashchange route navigations are reported as pop.
Route Preloading
Use the application-controlled preload hook to prepare resources for likely
next pages. The router calls it for eligible same-origin route links on hover
and keyboard focus, and it receives the matched RouteContext. Preloading
never delays navigation.
const router = new Router(routes, {
outlet,
preload: async (context) => {
await pageResourceCache.load(context.path, context.query);
},
});router.preload('/reports?range=month') starts the same work explicitly.
router.getPreloadStatus(path) returns idle, preloading, ready, or
failed. Pending and successful paths are deduplicated; a failed preload is
retried by the next explicit call, hover, or focus. A preload failure does not
affect route navigation or onError handling.
For declarative routing, configure the property before or after connection:
import type { RouterElement } from '@wavelengthusaf/client-router';
const routerElement =
document.querySelector<RouterElement>('wavelength-router')!;
routerElement.preloader = async (context) => {
await pageResourceCache.load(context.path, context.query);
};
// Start the same preload work explicitly.
routerElement.preload('/reports?range=month');
// Read the current status: idle, preloading, ready, or failed.
const status = routerElement.getPreloadStatus('/reports?range=month');The preload machine is intentionally scoped per Router. It retains useful
pending work across ordinary navigations; route components should use their own
AbortController for resource work that must be cancelled.
Focus Management
Routing removes the previous page from the outlet. Configure focus management when a page change should be announced to keyboard and screen-reader users.
<wavelength-router focus-strategy="heading">
<wavelength-route path="/" component="home-page"></wavelength-route>
</wavelength-router>heading focuses the first h1 or level-one ARIA heading in the newly mounted
page, adding tabindex="-1" when needed. outlet focuses the render outlet
itself. If a Light DOM or open Shadow DOM heading is rendered asynchronously,
the router waits up to five seconds before focusing. Both strategies work
across Light DOM and open Shadow DOM. For targets that become ready later,
dispatch wavelength-focus-ready as the component-owned focus handoff.
An open root attached after navigation cannot be discovered by mutation
observation alone. When its focus target is ready, dispatch a composed
wavelength-focus-ready event from that target:
heading.dispatchEvent(
new CustomEvent('wavelength-focus-ready', {
bubbles: true,
composed: true,
}),
);For a closed root, include the target in the event detail; this is an explicit component-to-router handoff and does not require exposing the root:
heading.dispatchEvent(
new CustomEvent('wavelength-focus-ready', {
bubbles: true,
composed: true,
detail: { target: heading },
}),
);Direct Router users can also supply a callback for application-specific
targets:
const router = new Router(routes, {
outlet,
focusStrategy: (currentOutlet) =>
currentOutlet.querySelector<HTMLElement>('[data-route-focus]'),
});The callback should return a focusable element. Use focusStrategy: 'none' to
retain the default behavior.
Closed Shadow DOM is intentionally private. Components that render their focus target asynchronously in a closed root should focus that target themselves when it is ready.
Shadow DOM Navigation
Links inside the router's Light DOM and open Shadow DOM descendants are intercepted automatically. Route declarations may also live in open Shadow DOM descendants of the router. Each nested router owns navigation from its own DOM subtree.
Modified clicks, downloads, and links targeting another browsing context keep
their native browser behavior. Same-document fragment links such as
href="#details" also retain native scrolling behavior. The router handles
only valid same-origin HTTP(S) destinations.
const shell = document.querySelector('app-shell')!;
const shadowRoot = shell.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<a href="/settings">Settings</a>';
// The router resolves this link through the event's composed path.If an open root is attached after the router connects, add its route
declarations and then call refresh() so the router can scan and observe it:
import type { RouterElement } from '@wavelengthusaf/client-router';
const router = document.querySelector<RouterElement>('wavelength-router')!;
const routeHost = document.querySelector('route-declarations')!;
const shadowRoot = routeHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML =
'<wavelength-route path="/reports" component="reports-page"></wavelength-route>';
router.refresh();Closed Shadow DOM intentionally hides its internal links from event delegation.
Dispatch a composed wavelength-navigate event from the closed-root component
instead:
button.addEventListener('click', () => {
button.dispatchEvent(
new CustomEvent('wavelength-navigate', {
bubbles: true,
cancelable: true,
composed: true,
detail: { path: '/account' },
}),
);
});Set detail.replace to true to replace the current history entry. Nested
routers handle navigation only for their own DOM subtree.
Closed roots also cannot be scanned for <wavelength-route> declarations.
Their owning component can register definitions explicitly, then unregister
them when it disconnects:
import { RouteElement, RouterElement } from '@wavelengthusaf/client-router';
import type { RouteDefinition } from '@wavelengthusaf/client-router';
const router = this.closest<RouterElement>('wavelength-router')!;
const definitions: RouteDefinition[] = Array.from(
this.#closedRoot.querySelectorAll<RouteElement>('wavelength-route'),
).flatMap((route) => {
const definition = route.toDefinition({ document });
return definition ? [definition] : [];
});
this.#unregisterRoutes = router.registerRoutes(definitions);
// In disconnectedCallback():
this.#unregisterRoutes?.();Registered definitions preserve their registration order and are inserted before
the first declarative wildcard (path="*"). This keeps a declarative fallback
from making closed-root routes unreachable. Registration captures a snapshot;
to change closed-root definitions, unregister and register the new array.
Injected Browser Environment
Direct Router instances accept optional window and document dependencies
as their third constructor argument. This is useful for an iframe, a test DOM,
or an application that needs to avoid implicit global browser access.
const frame = document.querySelector('iframe')!;
const router = new Router(
routes,
{ outlet },
{
window: frame.contentWindow!,
document: frame.contentDocument!,
},
);Matching Utilities
compileRoute() and matchRoute() are exported for applications that need to
inspect the same route patterns without mounting a router.
import { compileRoute, matchRoute } from '@wavelengthusaf/client-router';
const compiledRoutes = routes.map(compileRoute);
const match = matchRoute(compiledRoutes, '/users/42');
if (match) {
console.log(match.routeParams.userId); // "42"
}Best Practices
1. Provide a Not-Found Route
Use not-found-component when all unmatched paths should render one fallback
component:
<wavelength-router not-found-component="not-found-page">
<wavelength-route path="/" component="home-page"></wavelength-route>
</wavelength-router>Use a catch-all route when unmatched paths need route-specific guards or metadata:
<wavelength-route path="*" component="not-found-page"></wavelength-route>2. Access RouteContext Correctly
The routeContext is injected into your custom element. Always check for its existence:
class UserPage extends HTMLElement {
connectedCallback() {
const ctx = (this as unknown as { routeContext?: RouteContext })
.routeContext;
if (ctx) {
this.innerHTML = `<p>User: ${ctx.routeParams.userId}</p>`;
}
}
}3. Use Hash Mode for Static Hosts
If deploying to static hosting without server configuration, use hash mode:
<wavelength-router hash-mode> ... </wavelength-router>This avoids the need for server-side routing rules.
4. Clean Up in Route Components
Implement cleanup logic in the unmount function if needed:
const routes: RouteDefinition[] = [
{
path: '/chat',
routeComponent: {
mount(container, context) {
const chat = document.createElement('chat-widget');
container.appendChild(chat);
},
unmount() {
// Close WebSocket connections, remove event listeners, etc.
},
},
},
];5. Route Guards for Authentication
Always protect authenticated routes:
(window as unknown as Record<string, unknown>).requireAuth = (ctx, next) => {
if (isProtectedPath(ctx.path) && !getCurrentUser()) {
next('/login');
} else {
next();
}
};6. URL Encoding
Route parameters are automatically URL-decoded. If you pass special characters, they will be properly handled.
7. Order Routes by Specificity
More specific routes should be defined before general ones:
<!-- Correct order -->
<wavelength-route path="/users/:id" component="user-page"></wavelength-route>
<wavelength-route path="/users" component="users-list"></wavelength-route>
<wavelength-route path="*" component="not-found"></wavelength-route>8. Avoid Memory Leaks
Always clean up when routes unmount:
routeComponent: {
let interval: number;
mount(container) {
const page = document.createElement('my-page');
container.appendChild(page);
// Store reference for cleanup
interval = setInterval(() => updateTime(), 1000);
},
unmount() {
clearInterval(interval);
},
}Contributing and Local Development
This section is for contributors working in this repository. Application users only need the package-installation steps above.
Repository Setup
git clone [email protected]:common-components/client-router.git
cd wavelength-router
npm installContributors should use Node.js 24 or newer. The GitLab pipeline runs on Node
24 because it is the current LTS baseline for this project; Node 26 is still a
Current release. This is documented project policy rather than an install-time
constraint, because package.json does not currently declare an engines
field.
Husky Setup
Husky runs the repository's tests and lint checks before commits. It runs during
npm install; if hooks need to be restored manually, run:
npm run prepareDevelopment Commands
# Type check
npm run type-check
# Run tests
npm run test
# Run tests with coverage
npm run test -- --coverage
# Run native-browser integration tests (install Chromium once with
# `npx playwright install chromium`)
npm run test:browser
# Lint and format
npm run lint
npm run prettier
# Build for production
npm run build
# Build Storybook
npm run build-storybook
# Run Storybook for component development
npm run storybookCI/CD Expectations
GitLab CI installs dependencies, builds the package, runs TypeScript checking, linting, unit tests, native-browser integration tests, and security checks. Before opening a merge request, run the same local regression set when feasible:
npm run type-check
npm run lint
npm run test
npm run test:browser
npm run buildPublishing to npm is tag-only. The publish job runs for semantic version tags,
verifies the package build, skips versions already present on npm, and requires
the NPM_AUTH_TOKEN CI variable.
License
ISC - Wavelength
