@askrjs/askr 0.0.64 → 0.0.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -54,16 +54,16 @@ Startup belongs in `@askrjs/askr/boot`. Routing helpers live in
54
54
 
55
55
  ```ts
56
56
  import { createSPA } from '@askrjs/askr/boot';
57
- import { getManifest, registerRoutes, route } from '@askrjs/askr/router';
57
+ import { createRouteRegistry, route } from '@askrjs/askr/router';
58
58
 
59
- registerRoutes(() => {
60
- route('/', () => <Home />);
61
- route('/about', () => <About />);
59
+ const registry = createRouteRegistry(() => {
60
+ route('/', Home);
61
+ route('/about', About);
62
62
  });
63
63
 
64
64
  createSPA({
65
65
  root: document.body,
66
- manifest: getManifest(),
66
+ registry,
67
67
  });
68
68
  ```
69
69
 
@@ -0,0 +1,200 @@
1
+ import { f as getCurrentComponentInstance } from "./component-scope-CC3cojX9.js";
2
+ import { T as getExecutionModel, x as createRouteHandler } from "./deferred-Eh9bCVGH.js";
3
+ import { A as insertRecordSorted, C as getCurrentPathPrefix, I as computeRank, L as normalizeRouteSegmentName, M as pushRegistrationScope, O as hasActivePageScope, R as parseSegments, S as getCurrentPageScope, _ as getCurrentInheritedAuthRequirements, b as getCurrentLayoutChain, f as addRouteToStores, p as assertRouteRegistrationUnlocked, v as getCurrentInheritedMeta, w as getCurrentScopeKind, x as getCurrentPageChain, y as getCurrentInheritedPolicies } from "./route-matching-DLBWDeXX.js";
4
+ import { allOf } from "@askrjs/auth";
5
+ //#region src/router/access.ts
6
+ function compileNodePolicies(node) {
7
+ return node.policies ? [...node.policies] : [];
8
+ }
9
+ //#endregion
10
+ //#region src/router/authoring.ts
11
+ function validateRoutePath(path) {
12
+ if (!path.startsWith("/")) throw new Error(`Route path must begin with "/". Got: "${path}"`);
13
+ if (/\/{2,}/.test(path)) throw new Error("Route path cannot contain consecutive slashes.");
14
+ if (/:([^/{}]+)/.test(path)) {
15
+ const suggested = path.replace(/:([^/{}]+)/g, "{$1}");
16
+ throw new Error(`Route parameter syntax uses {name} interpolation, not :name. Use "${suggested}" instead of "${path}".`);
17
+ }
18
+ const segments = path.split("/").filter(Boolean);
19
+ const seenParamNames = /* @__PURE__ */ new Set();
20
+ for (let index = 0; index < segments.length; index++) {
21
+ const segment = segments[index];
22
+ if (segment === "*") continue;
23
+ const hasOpenBrace = segment.includes("{");
24
+ const hasCloseBrace = segment.includes("}");
25
+ if (!hasOpenBrace && !hasCloseBrace) continue;
26
+ if (!(segment.startsWith("{") && segment.endsWith("}"))) throw new Error("Route parameter segments must use complete {name} interpolation.");
27
+ const rawParamName = normalizeRouteSegmentName(segment.slice(1, -1));
28
+ const isSplat = rawParamName.startsWith("*");
29
+ const paramName = isSplat ? normalizeRouteSegmentName(rawParamName.slice(1)) : rawParamName;
30
+ if (!paramName) throw new Error(isSplat ? "Route splat parameter name cannot be empty." : "Route parameter name cannot be empty.");
31
+ if (isSplat && paramName === "*") throw new Error("Route named splat parameter name cannot be \"*\".");
32
+ if (isSplat && index !== segments.length - 1) throw new Error("Route named splat parameters must be the final segment.");
33
+ if (seenParamNames.has(paramName)) throw new Error(`Route path cannot reuse duplicate parameter name "${paramName}".`);
34
+ seenParamNames.add(paramName);
35
+ }
36
+ }
37
+ function normalizeAbsoluteRoutePath(path) {
38
+ if (!path || path === "/") return "/";
39
+ return (path.endsWith("/") ? path.slice(0, -1) : path) || "/";
40
+ }
41
+ function joinRoutePaths(prefix, path) {
42
+ const normalizedPrefix = normalizeAbsoluteRoutePath(prefix || "/");
43
+ const normalizedPath = path.replace(/^\/+|\/+$/g, "");
44
+ if (!normalizedPath) return normalizedPrefix;
45
+ return normalizedPrefix === "/" ? `/${normalizedPath}` : `${normalizedPrefix}/${normalizedPath}`;
46
+ }
47
+ function resolvePageScopePath(path) {
48
+ if (!path) throw new Error("page(path, Component, fn) requires a non-empty path.");
49
+ if (path.startsWith("/")) {
50
+ validateRoutePath(path);
51
+ return normalizeAbsoluteRoutePath(path);
52
+ }
53
+ return joinRoutePaths(getCurrentPathPrefix(), path);
54
+ }
55
+ function resolveIndexPath() {
56
+ return normalizeAbsoluteRoutePath(getCurrentPathPrefix() || "/");
57
+ }
58
+ function resolveRouteRegistrationPath(path) {
59
+ if (path.startsWith("/")) {
60
+ if (hasActivePageScope()) throw new Error(`Child route paths inside page() must be relative. Use "${path.slice(1)}" instead of "${path}".`);
61
+ validateRoutePath(path);
62
+ return normalizeAbsoluteRoutePath(path);
63
+ }
64
+ const prefix = getCurrentPathPrefix();
65
+ if (!prefix) throw new Error(`Route path must begin with "/". Got: "${path}"`);
66
+ return joinRoutePaths(prefix, path);
67
+ }
68
+ function pushGroupScope(options, fn) {
69
+ const policies = compileNodePolicies(options);
70
+ pushRegistrationScope({
71
+ kind: "group",
72
+ pathPrefix: getCurrentPathPrefix(),
73
+ layout: options.layout,
74
+ auth: options.auth,
75
+ policies,
76
+ meta: options.meta
77
+ }, fn);
78
+ }
79
+ function pushPageScope(path, Component, options, fn) {
80
+ if (hasActivePageScope()) throw new Error("page() cannot be nested inside another page(). Use route() for child leaves or group() for inherited behavior inside the existing page scope.");
81
+ const policies = compileNodePolicies(options);
82
+ pushRegistrationScope({
83
+ kind: "page",
84
+ pathPrefix: resolvePageScopePath(path),
85
+ page: Component,
86
+ hasIndex: false,
87
+ auth: options.auth,
88
+ policies,
89
+ meta: options.meta
90
+ }, fn);
91
+ }
92
+ function normalizeRouteOptions(options) {
93
+ if (!options) return;
94
+ const loader = options.loader;
95
+ const preload = options.preload;
96
+ const policies = compileNodePolicies(options);
97
+ if (!loader && !preload && !options.entries && policies.length === 0 && !options.title && !options.namespace && !options.search && !options.meta && !options.actions && options.auth === void 0) return;
98
+ return {
99
+ ...loader ? { loader } : {},
100
+ ...preload ? { preload } : {},
101
+ ...options.entries ? { entries: options.entries } : {},
102
+ ...options.auth !== void 0 ? { auth: options.auth } : {},
103
+ ...policies.length > 0 ? { policies } : {},
104
+ ...options.title ? { title: options.title } : {},
105
+ ...options.namespace ? { namespace: options.namespace } : {},
106
+ ...options.search ? { search: options.search } : {},
107
+ ...options.meta ? { meta: options.meta } : {},
108
+ ...options.actions ? { actions: options.actions } : {}
109
+ };
110
+ }
111
+ function registerRouteAtResolvedPath(path, Component, options, metadata) {
112
+ validateRoutePath(path);
113
+ const chain = getCurrentLayoutChain();
114
+ const pageChain = getCurrentPageChain();
115
+ const segments = parseSegments(path);
116
+ const rank = computeRank(segments);
117
+ const isFallback = metadata?.isFallback ?? path === "/*";
118
+ const comp = Component;
119
+ const normalizedOptions = normalizeRouteOptions(options);
120
+ const policies = [...getCurrentInheritedPolicies(), ...normalizedOptions?.policies ?? []];
121
+ const authRequirements = [...getCurrentInheritedAuthRequirements(), ...normalizedOptions?.auth ? [normalizedOptions.auth] : []];
122
+ const auth = authRequirements.length === 0 ? void 0 : authRequirements.length === 1 ? authRequirements[0] : allOf(...authRequirements);
123
+ const metaChain = [...getCurrentInheritedMeta(), ...normalizedOptions?.meta ? [normalizedOptions.meta] : []];
124
+ const handler = createRouteHandler(comp, pageChain, chain);
125
+ const renderHandler = createRouteHandler(comp, pageChain, chain, true);
126
+ insertRecordSorted({
127
+ path,
128
+ component: comp,
129
+ segments,
130
+ rank,
131
+ layoutChain: chain,
132
+ pageChain,
133
+ options: normalizedOptions ? {
134
+ ...normalizedOptions,
135
+ ...auth ? { auth } : {},
136
+ ...policies.length > 0 ? { policies } : {}
137
+ } : policies.length > 0 ? {
138
+ policies,
139
+ ...auth ? { auth } : {}
140
+ } : auth ? { auth } : {},
141
+ ...metaChain.length > 0 ? { metaChain } : {},
142
+ isFallback,
143
+ handler,
144
+ renderHandler,
145
+ ...metadata?.fallbackPrefix ? { fallbackPrefix: metadata.fallbackPrefix } : {}
146
+ });
147
+ addRouteToStores({
148
+ path,
149
+ handler,
150
+ namespace: normalizedOptions?.namespace ?? options?.namespace,
151
+ ...metadata?.fallbackPrefix ? { fallbackPrefix: metadata.fallbackPrefix } : {}
152
+ });
153
+ }
154
+ function group(options, fn) {
155
+ pushGroupScope(options, fn);
156
+ }
157
+ function page(path, Component, optionsOrFn, maybeFn) {
158
+ const options = typeof optionsOrFn === "function" ? {} : optionsOrFn;
159
+ const fn = typeof optionsOrFn === "function" ? optionsOrFn : maybeFn;
160
+ if (typeof Component !== "function") throw new Error("page(path, Component, fn) requires a component function as the second argument.");
161
+ if (typeof fn !== "function") throw new Error("page(path, Component, fn) requires a route definition callback as the final argument.");
162
+ pushPageScope(path, Component, options, fn);
163
+ }
164
+ function index(Component, options) {
165
+ const pageScope = getCurrentPageScope();
166
+ if (pageScope?.hasIndex) throw new Error("page() cannot declare multiple index routes.");
167
+ if (pageScope) pageScope.hasIndex = true;
168
+ registerRouteAtResolvedPath(resolveIndexPath(), Component, options);
169
+ }
170
+ function fallback(Component) {
171
+ if (hasActivePageScope()) {
172
+ if (getCurrentScopeKind() !== "page") throw new Error("fallback() inside page() must be declared directly in the page scope, not inside nested group().");
173
+ registerRouteAtResolvedPath(`${getCurrentPathPrefix()}/*`, Component, void 0, {
174
+ isFallback: true,
175
+ fallbackPrefix: getCurrentPathPrefix()
176
+ });
177
+ return;
178
+ }
179
+ if (!(getCurrentInheritedPolicies().length === 0 && getCurrentInheritedAuthRequirements().length === 0)) throw new Error("fallback() can only be registered at the root scope. Use route(\"/*\", Component) if you need compatibility behavior.");
180
+ registerRouteAtResolvedPath("/*", Component, void 0, {
181
+ isFallback: true,
182
+ fallbackPrefix: "/"
183
+ });
184
+ }
185
+ function route(path, Component, options) {
186
+ if (typeof path === "undefined") throw new Error("route() is only for route registration. Use currentRoute() inside components.");
187
+ if (getExecutionModel() === "islands") throw new Error("Routes are not supported with islands. Use createSPA (client) or createSSR (server) instead.");
188
+ const currentInst = getCurrentComponentInstance();
189
+ if (currentInst && currentInst.ssr) throw new Error("route() cannot be called during SSR rendering. Register routes at module load time instead.");
190
+ assertRouteRegistrationUnlocked();
191
+ if (typeof Component !== "function") throw new Error("route(path, Component) requires a component function as the second argument. Passing JSX elements or VNodes directly is not supported.");
192
+ if (options?.search && options.search.kind !== "object") throw new Error("route search must be an object schema.");
193
+ registerRouteAtResolvedPath(resolveRouteRegistrationPath(path), Component, options);
194
+ return Object.freeze({
195
+ path: resolveRouteRegistrationPath(path),
196
+ ...options?.search ? { searchSchema: options.search } : {}
197
+ });
198
+ }
199
+ //#endregion
200
+ export { route as a, page as i, group as n, index as r, fallback as t };
package/dist/benchmark.js CHANGED
@@ -4,7 +4,7 @@ import { t as state } from "./state-Cc5HumQ0.js";
4
4
  import { t as selector } from "./selector-pApF_lGd.js";
5
5
  import { jsx, jsxs } from "./jsx-runtime.js";
6
6
  import { i as For } from "./control-KVP6LMEQ.js";
7
- import { t as createIsland } from "./boot-BwqMApN4.js";
7
+ import { t as createIsland } from "./boot-B4UZumvB.js";
8
8
  //#region src/bench/components/benchmark-row.tsx
9
9
  function BenchmarkRow({ item, isSelected, onSelect, onRemove }) {
10
10
  return /* @__PURE__ */ jsxs("tr", {
@@ -63,8 +63,8 @@ function BenchmarkTable({ rows, isSelected, onSelect, onRemove }) {
63
63
  installRendererBridge();
64
64
  const benchmarkMetadata = {
65
65
  packageName: "@askrjs/askr",
66
- packageVersion: "0.0.64",
67
- buildLabel: "0.0.64-local"
66
+ packageVersion: "0.0.66",
67
+ buildLabel: "0.0.66-local"
68
68
  };
69
69
  function getBenchmarkMetadata() {
70
70
  return { ...benchmarkMetadata };
@@ -1,6 +1,6 @@
1
1
  import { s as ComponentFunction } from "../control-CMzFPMjr.js";
2
2
  import { v as DataRuntime } from "../index-Bki2rqcr.js";
3
- import { P as RouteRegistry, m as RouteAuthOptions, p as Route, x as RouteManifest } from "../router-Dz7ZoLja.js";
3
+ import { N as RouteRegistry, p as RouteAuthOptions } from "../router-4EhVGcma.js";
4
4
  import { d as ScrollRestorationOptions } from "../navigate-BVDCzOuB.js";
5
5
 
6
6
  //#region src/boot/types.d.ts
@@ -17,28 +17,10 @@ type IslandsConfig = {
17
17
  };
18
18
  type BootRouteSource = {
19
19
  registry: RouteRegistry;
20
- manifest?: RouteManifest;
21
- routes?: Route[];
22
- } | {
23
- manifest: RouteManifest;
24
- registry?: RouteRegistry;
25
- routes?: Route[];
26
- } | {
27
- manifest?: RouteManifest;
28
- registry?: RouteRegistry;
29
- routes: Route[];
30
20
  };
31
21
  type SPAConfig = BootRouteSource & {
32
22
  root: Element | string;
33
- cspNonce?: string;
34
- /**
35
- * Preferred: pass a route registry built via `createRouteRegistry(() => { ... })`.
36
- * ```ts
37
- * import { createRouteRegistry } from '@askrjs/askr/router';
38
- * const registry = createRouteRegistry(() => { ... });
39
- * await createSPA({ root: '#app', registry });
40
- * ```
41
- */
23
+ cspNonce?: string; /** Pass a route registry built via `createRouteRegistry(() => { ... })`. */
42
24
  auth?: RouteAuthOptions;
43
25
  scrollRestoration?: boolean | ScrollRestorationOptions;
44
26
  cleanupStrict?: boolean;
@@ -47,7 +29,7 @@ type SPAConfig = BootRouteSource & {
47
29
  type HydrateSPAConfig = BootRouteSource & {
48
30
  root: Element | string;
49
31
  cspNonce?: string;
50
- dataRuntime?: DataRuntime; /** Preferred route input - see `SPAConfig`. */
32
+ dataRuntime?: DataRuntime; /** Pass the same explicit route registry used for the server render. */
51
33
  auth?: RouteAuthOptions;
52
34
  scrollRestoration?: boolean | ScrollRestorationOptions;
53
35
  cleanupStrict?: boolean;
@@ -75,23 +57,17 @@ declare function createIsland(config: IslandConfig): void;
75
57
  */
76
58
  declare function createIslands(config: IslandsConfig): void;
77
59
  /**
78
- * createSPA: Initializes router and mounts the app with the provided route manifest or route table.
79
- *
80
- * Preferred usage with manifest:
60
+ * createSPA: Initializes the router and mounts the app with an explicit route registry.
81
61
  * ```ts
82
- * import { getManifest } from '@askrjs/askr/router';
83
- * await createSPA({ root: '#app', manifest: getManifest() });
62
+ * import { createRouteRegistry } from '@askrjs/askr/router';
63
+ * const registry = createRouteRegistry(() => { ... });
64
+ * await createSPA({ root: '#app', registry });
84
65
  * ```
85
66
  *
86
- * Legacy usage with plain routes array (still supported):
87
- * ```ts
88
- * await createSPA({ root: '#app', routes: getRoutes() });
89
- * ```
90
67
  */
91
68
  declare function createSPA(config: SPAConfig): Promise<void>;
92
69
  /**
93
- * hydrateSPA: Hydrate server-rendered HTML.
94
- * Accepts either a `manifest` (preferred) or a legacy `routes` array.
70
+ * hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
95
71
  */
96
72
  declare function hydrateSPA(config: HydrateSPAConfig): Promise<void>;
97
73
  //#endregion
@@ -1,2 +1,2 @@
1
- import { a as cleanupApp, i as hydrateSPA, n as createIslands, o as hasApp, r as createSPA, t as createIsland } from "../boot-BwqMApN4.js";
1
+ import { a as cleanupApp, i as hydrateSPA, n as createIslands, o as hasApp, r as createSPA, t as createIsland } from "../boot-B4UZumvB.js";
2
2
  export { cleanupApp, createIsland, createIslands, createSPA, hasApp, hydrateSPA };
@@ -4,14 +4,14 @@ import { n as createComponentInstance, r as executeComponent } from "./component
4
4
  import { h as cleanupComponent, m as isPromiseLike } from "./component-lifecycle-DEMPihoZ.js";
5
5
  import { a as withIntrinsicHydrationAdoption, i as registerDeferredHydrationBoundary, n as activateHydrationBoundary$1, o as setStaticChildSlotsCacheEnabled, r as clearDeferredHydrationBoundaries, t as installRendererBridge } from "./renderer-aUD-acDe.js";
6
6
  import { _ as teardownNodeSubtree, d as removeAllListeners } from "./cleanup-B6KovDZZ.js";
7
- import { C as assertExecutionModel, S as route, a as resolveRouteMeta, c as registerAppInstance, d as clearRoutes, g as createAppRenderRuntime, h as setServerLocation, i as reconcileRouteMeta, l as unregisterAppInstance, s as configureScrollRestoration, t as initializeNavigation, u as _applyManifest } from "./navigate-CaFA0DdW.js";
7
+ import { c as _applyManifest, d as getRouteRenderContext, f as getRouteRenderData, m as resolveRouteRequest, o as reviveDeferredValue, p as hasRouteRenderData, v as pageRenderEnvelope, w as assertExecutionModel } from "./deferred-Eh9bCVGH.js";
8
8
  import { a as startHydrationRenderPhase, s as stopHydrationRenderPhase } from "./render-context-Clw3rFiO.js";
9
9
  import { a as disposeDefaultPortalScope, t as DefaultPortal } from "./portal-D2tPWNrC.js";
10
10
  import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-C-8E85El.js";
11
11
  import { i as getDefaultDataRuntime } from "./data-runtime-SJ1BSUvk.js";
12
12
  import { a as hydrateDataRuntime } from "./query-registry-CQIT4mXr.js";
13
- import { a as resolveRouteFromRoutes, d as _setActiveRouteAuthOptions, j as lockRouteRegistration, k as hasRegisteredRoutes } from "./route-matching-vJpvLVbX.js";
14
- import { _ as pageRenderEnvelope, d as hasRouteRenderData, f as resolveRouteRequest, l as getRouteRenderContext, o as reviveDeferredValue, u as getRouteRenderData, x as _preloadRouteHandler } from "./deferred-vBQJwHl5.js";
13
+ import { d as _setActiveRouteAuthOptions, j as lockRouteRegistration, k as hasRegisteredRoutes, m as clearRouteState } from "./route-matching-DLBWDeXX.js";
14
+ import { a as resolveRouteMeta, c as registerAppInstance, d as setServerLocation, f as createAppRenderRuntime, i as reconcileRouteMeta, l as unregisterAppInstance, s as configureScrollRestoration, t as initializeNavigation } from "./navigate-QMRcR581.js";
15
15
  import "./ssr-CbQO5xd3.js";
16
16
  //#region src/boot/hydration.ts
17
17
  const DEFERRED_PAYLOAD = "__askr_deferred__";
@@ -364,7 +364,7 @@ function cleanupApp(root) {
364
364
  const wasRoutedRoot = routedRoots.delete(rootElement);
365
365
  instancesByRoot.delete(rootElement);
366
366
  clearRootCleanupCallbacks(rootElement);
367
- if (wasRoutedRoot && routedRoots.size === 0) clearRoutes();
367
+ if (wasRoutedRoot && routedRoots.size === 0) clearRouteState();
368
368
  try {
369
369
  delete rootElement[CLEANUP_SYMBOL];
370
370
  } catch {}
@@ -408,21 +408,12 @@ async function resolveInitialRoute(auth, source) {
408
408
  for (let redirects = 0; redirects <= MAX_INITIAL_ROUTE_REDIRECTS; redirects++) {
409
409
  if (visited.has(href)) throw new Error(`[Askr] Route redirect cycle detected at ${href}.`);
410
410
  visited.add(href);
411
- const resolved = source?.manifest ? await resolveRouteRequest(href, {
412
- manifest: source.manifest,
411
+ if (!source?.registry) throw new TypeError("resolveInitialRoute requires a route registry.");
412
+ const resolved = await resolveRouteRequest(href, {
413
+ registry: source.registry,
413
414
  auth,
414
415
  load: source.load
415
- }) : source?.routes ? await (() => {
416
- const match = resolveRouteFromRoutes(path, source.routes);
417
- if (!match) return null;
418
- const lazyImport = _preloadRouteHandler(match.handler);
419
- const result = {
420
- kind: "render",
421
- handler: match.handler,
422
- params: match.params
423
- };
424
- return lazyImport ? lazyImport.then(() => result) : result;
425
- })() : await resolveRouteRequest(href, { auth });
416
+ });
426
417
  if (typeof window === "undefined" || !resolved || resolved.kind !== "redirect") return {
427
418
  path,
428
419
  href,
@@ -473,47 +464,34 @@ function createIslands(config) {
473
464
  });
474
465
  }
475
466
  /**
476
- * createSPA: Initializes router and mounts the app with the provided route manifest or route table.
477
- *
478
- * Preferred usage with manifest:
467
+ * createSPA: Initializes the router and mounts the app with an explicit route registry.
479
468
  * ```ts
480
- * import { getManifest } from '@askrjs/askr/router';
481
- * await createSPA({ root: '#app', manifest: getManifest() });
469
+ * import { createRouteRegistry } from '@askrjs/askr/router';
470
+ * const registry = createRouteRegistry(() => { ... });
471
+ * await createSPA({ root: '#app', registry });
482
472
  * ```
483
473
  *
484
- * Legacy usage with plain routes array (still supported):
485
- * ```ts
486
- * await createSPA({ root: '#app', routes: getRoutes() });
487
- * ```
488
474
  */
489
475
  async function createSPA(config) {
490
476
  assertExecutionModel("spa");
491
477
  if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
492
478
  validateCspNonce(config.cspNonce);
493
- const manifest = config.manifest ?? config.registry?.manifest;
494
- const routeTable = config.routes ?? config.registry?.routes;
495
- const hasManifest = manifest != null && manifest.records.length > 0;
496
- const hasRoutes = Array.isArray(routeTable) && routeTable.length > 0;
497
- if (!hasManifest && !hasRoutes) throw new Error("createSPA requires a route manifest or route table. Pass `manifest: getManifest()` or `routes: getRoutes()`. If you are enhancing existing HTML, use createIsland instead.");
479
+ if (!config.registry) throw new Error("createSPA requires a route registry.");
480
+ const { manifest } = config.registry;
481
+ if (manifest.records.length === 0) throw new Error("createSPA requires a route registry with at least one route. If you are enhancing existing HTML, use createIsland instead.");
498
482
  const rootElement = resolveRootElement(config.root);
499
483
  if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
500
484
  configureScrollRestoration(config.scrollRestoration);
501
- clearRoutes();
502
- if (hasManifest) _applyManifest(manifest);
503
- else for (const r of routeTable) route(r.path, r.handler);
504
- const routeAuth = config.auth ?? manifest?.auth;
505
- const activeManifest = hasManifest ? manifest : void 0;
485
+ clearRouteState();
486
+ _applyManifest(manifest);
487
+ const routeAuth = config.auth ?? manifest.auth;
506
488
  const appRouteSource = {
507
- manifest: activeManifest,
508
- routes: hasManifest ? void 0 : routeTable,
489
+ registry: config.registry,
509
490
  auth: routeAuth
510
491
  };
511
492
  _setActiveRouteAuthOptions(routeAuth);
512
493
  if (isProductionEnvironment()) lockRouteRegistration();
513
- const { path, resolved } = await resolveInitialRoute(routeAuth, {
514
- manifest: activeManifest,
515
- routes: hasManifest ? void 0 : routeTable
516
- });
494
+ const { path, resolved } = await resolveInitialRoute(routeAuth, { registry: config.registry });
517
495
  const appRuntime = createAppRenderRuntime(resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
518
496
  route: getRouteRenderData(resolved),
519
497
  hasRoute: true
@@ -554,18 +532,15 @@ async function createSPA(config) {
554
532
  await registerAppNavigation(rootElement, path, { ...appRouteSource });
555
533
  }
556
534
  /**
557
- * hydrateSPA: Hydrate server-rendered HTML.
558
- * Accepts either a `manifest` (preferred) or a legacy `routes` array.
535
+ * hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
559
536
  */
560
537
  async function hydrateSPA(config) {
561
538
  assertExecutionModel("spa");
562
539
  if (!config || typeof config !== "object") throw new Error("hydrateSPA requires a config object");
563
540
  validateCspNonce(config.cspNonce);
564
- const manifest = config.manifest ?? config.registry?.manifest;
565
- const routeTable = config.routes ?? config.registry?.routes;
566
- const hasManifest = manifest != null && manifest.records.length > 0;
567
- const hasRoutes = Array.isArray(routeTable) && routeTable.length > 0;
568
- if (!hasManifest && !hasRoutes) throw new Error("hydrateSPA requires a route manifest or route table. Pass `manifest: getManifest()` or `routes: getRoutes()`. If you are enhancing existing HTML, use createIsland instead.");
541
+ if (!config.registry) throw new Error("hydrateSPA requires a route registry.");
542
+ const { manifest } = config.registry;
543
+ if (manifest.records.length === 0) throw new Error("hydrateSPA requires a route registry with at least one route. If you are enhancing existing HTML, use createIsland instead.");
569
544
  const rootElement = resolveRootElement(config.root);
570
545
  if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
571
546
  applyDeferredStreamPatches(rootElement);
@@ -574,14 +549,11 @@ async function hydrateSPA(config) {
574
549
  if (hydrationQueryCache) hydrateDataRuntime(config.dataRuntime ?? getDefaultDataRuntime(), hydrationQueryCache);
575
550
  const hydrationRenderDataForApp = hydrationRenderData;
576
551
  configureScrollRestoration(config.scrollRestoration);
577
- clearRoutes();
578
- if (hasManifest) _applyManifest(manifest);
579
- else for (const r of routeTable) route(r.path, r.handler);
580
- const routeAuth = config.auth ?? manifest?.auth;
581
- const activeManifest = hasManifest ? manifest : void 0;
552
+ clearRouteState();
553
+ _applyManifest(manifest);
554
+ const routeAuth = config.auth ?? manifest.auth;
582
555
  const appRouteSource = {
583
- manifest: activeManifest,
584
- routes: hasManifest ? void 0 : routeTable,
556
+ registry: config.registry,
585
557
  auth: routeAuth,
586
558
  runtime: createAppRenderRuntime({
587
559
  framework: hydrationRenderData?.framework,
@@ -591,8 +563,7 @@ async function hydrateSPA(config) {
591
563
  };
592
564
  _setActiveRouteAuthOptions(routeAuth);
593
565
  const { path, href: currentUrl, resolved } = await resolveInitialRoute(routeAuth, {
594
- manifest: activeManifest,
595
- routes: hasManifest ? void 0 : routeTable,
566
+ registry: config.registry,
596
567
  load: false
597
568
  });
598
569
  setServerLocation(currentUrl);
@@ -612,17 +583,11 @@ async function hydrateSPA(config) {
612
583
  cspNonce: config.cspNonce
613
584
  }));
614
585
  if (shouldVerifyHydrationMarkup(config)) {
615
- const legacyRouteTable = hasManifest ? manifest.records.map((r) => ({
616
- ...r,
617
- path: r.path,
618
- handler: r.handler,
619
- namespace: r.options.namespace
620
- })) : routeTable;
621
- const { verifyHydrationSyncForUrl } = await import("./verify-hydration-DfsBcGSf.js");
586
+ const { verifyHydrationSyncForUrl } = await import("./verify-hydration-BcZXRU-q.js");
622
587
  if (!verifyHydrationSyncForUrl({
623
588
  root: rootElement,
624
589
  url: currentUrl,
625
- routes: legacyRouteTable,
590
+ registry: config.registry,
626
591
  resolved: hydrationResolved,
627
592
  options: {
628
593
  data: hydrationRenderDataForApp?.resources,
@@ -6,7 +6,19 @@ import { n as getActiveRenderContext, r as getCurrentRenderData } from "./render
6
6
  import { t as resource } from "./resource-operation-Dz2Nxi1q.js";
7
7
  import { i as ROUTE_ROOT_COMPONENT } from "./csp-nonce-C-8E85El.js";
8
8
  import { c as withTelemetry, t as createQueryPrefetchContext } from "./query-registry-CQIT4mXr.js";
9
- import { E as getRouteRecords, h as getActiveRouteAuthOptions, i as getMatchingRouteRecord, o as buildRouteContext, s as buildRouteContextBase } from "./route-matching-vJpvLVbX.js";
9
+ import { A as insertRecordSorted, D as getRouteRecords, E as getRouteList, F as snapshotRouteState, N as restoreRouteState, P as setDefaultRouteAuthOptions, T as getDefaultRouteAuthOptions, f as addRouteToStores, h as getActiveRouteAuthOptions, i as getMatchingRouteRecord, m as clearRouteState, o as buildRouteContext, s as buildRouteContextBase } from "./route-matching-DLBWDeXX.js";
10
+ //#region src/runtime/execution-model.ts
11
+ const EXECUTION_MODEL_KEY = Symbol.for("__ASKR_EXECUTION_MODEL__");
12
+ function getExecutionModel() {
13
+ return globalThis[EXECUTION_MODEL_KEY];
14
+ }
15
+ function assertExecutionModel(model) {
16
+ const g = globalThis;
17
+ const cur = g[EXECUTION_MODEL_KEY];
18
+ if (cur && cur !== model) throw new Error(`[Askr] mixing execution models is not allowed (current: ${cur}, attempted: ${model}). Choose exactly one: createSPA, createSSR, or createIslands.`);
19
+ if (!cur) g[EXECUTION_MODEL_KEY] = model;
20
+ }
21
+ //#endregion
10
22
  //#region src/router/lazy.ts
11
23
  const pendingLazy = /* @__PURE__ */ new Set();
12
24
  const lazyStates = /* @__PURE__ */ new WeakMap();
@@ -55,13 +67,6 @@ function _preloadRouteRecord(record) {
55
67
  function _associateLazyHandler(handler, components) {
56
68
  handlerLazyComponents.set(handler, components);
57
69
  }
58
- function _preloadRouteHandler(handler) {
59
- const imports = (handlerLazyComponents.get(handler) ?? []).flatMap((component) => {
60
- const state = lazyStates.get(component);
61
- return state ? [startLazyLoad(state)] : [];
62
- });
63
- return imports.length > 0 ? Promise.all(imports).then(() => void 0) : null;
64
- }
65
70
  //#endregion
66
71
  //#region src/router/rendering.ts
67
72
  const outletScope = defineScope(null);
@@ -111,6 +116,24 @@ function getRenderHandler(record) {
111
116
  return record.renderHandler ?? createRouteHandler(record.component, record.pageChain, record.layoutChain, true);
112
117
  }
113
118
  //#endregion
119
+ //#region src/router/auth.ts
120
+ let clientAuth = Object.freeze({
121
+ authenticated: false,
122
+ principal: null,
123
+ session: null,
124
+ tenant: null
125
+ });
126
+ /** Return the identity resolved for the route currently being rendered. */
127
+ function currentAuth() {
128
+ return getActiveRenderContext()?.authContext ?? clientAuth;
129
+ }
130
+ /** @internal Updated atomically with route resolution. */
131
+ function setCurrentAuth(context) {
132
+ clientAuth = context;
133
+ const render = getActiveRenderContext();
134
+ if (render) render.authContext = context;
135
+ }
136
+ //#endregion
114
137
  //#region src/common/page-render-envelope.ts
115
138
  const PAGE_RENDER_ENVELOPE_VERSION = 1;
116
139
  function ownedRecord(value) {
@@ -148,24 +171,6 @@ function isEmptyPageRenderEnvelope(value) {
148
171
  return value.route === void 0 && Object.keys(value.resources).length === 0 && Object.keys(value.framework).length === 0;
149
172
  }
150
173
  //#endregion
151
- //#region src/router/auth.ts
152
- let clientAuth = Object.freeze({
153
- authenticated: false,
154
- principal: null,
155
- session: null,
156
- tenant: null
157
- });
158
- /** Return the identity resolved for the route currently being rendered. */
159
- function currentAuth() {
160
- return getActiveRenderContext()?.authContext ?? clientAuth;
161
- }
162
- /** @internal Updated atomically with route resolution. */
163
- function setCurrentAuth(context) {
164
- clientAuth = context;
165
- const render = getActiveRenderContext();
166
- if (render) render.authContext = context;
167
- }
168
- //#endregion
169
174
  //#region src/router/resolution.ts
170
175
  const anonymous = () => ({
171
176
  authenticated: false,
@@ -295,13 +300,15 @@ function resolveMatchedRoute(record, params, context, authOptions, request, tele
295
300
  const decision = record.options.auth(context.auth);
296
301
  return isPromiseLike(decision) ? Promise.resolve(decision).then(continueResolution) : continueResolution(decision);
297
302
  }
298
- function resolveRouteRequest(target, options = {}) {
303
+ function resolveRouteRequest(target, options) {
304
+ if (!options?.registry) throw new TypeError("resolveRouteRequest requires options.registry.");
299
305
  return withTelemetry(options.telemetry?.routeMatch, {}, () => {
300
- const match = getMatchingRouteRecord(target, options.manifest?.records ?? getRouteRecords());
306
+ const records = options.registry.manifest.records;
307
+ const match = getMatchingRouteRecord(target, records);
301
308
  if (!match) return null;
302
309
  const mode = options.mode ?? getDefaultRouteMode();
303
310
  const signal = options.signal ?? getActiveRenderContext()?.signal ?? new AbortController().signal;
304
- const authOptions = getActiveRouteAuthOptions(options.auth ?? options.manifest?.auth);
311
+ const authOptions = getActiveRouteAuthOptions(options.auth ?? options.registry.manifest.auth);
305
312
  const base = buildRouteContextBase(target, match.params, {
306
313
  mode,
307
314
  signal
@@ -321,6 +328,42 @@ function resolveRouteRequest(target, options = {}) {
321
328
  });
322
329
  }
323
330
  //#endregion
331
+ //#region src/router/manifest.ts
332
+ function createRouteManifest() {
333
+ const auth = getDefaultRouteAuthOptions();
334
+ return {
335
+ records: [...getRouteRecords()],
336
+ ...auth ? { auth } : {}
337
+ };
338
+ }
339
+ function _applyManifest(manifest) {
340
+ setDefaultRouteAuthOptions(manifest.auth);
341
+ for (const record of manifest.records) {
342
+ insertRecordSorted(record);
343
+ addRouteToStores({
344
+ path: record.path,
345
+ handler: record.handler,
346
+ namespace: record.options.namespace,
347
+ ..."fallbackPrefix" in record && typeof record.fallbackPrefix === "string" ? { fallbackPrefix: record.fallbackPrefix } : {}
348
+ });
349
+ }
350
+ }
351
+ function createRouteRegistry(definition, options = {}) {
352
+ const previous = snapshotRouteState();
353
+ clearRouteState();
354
+ try {
355
+ setDefaultRouteAuthOptions(options.auth);
356
+ definition();
357
+ const manifest = createRouteManifest();
358
+ return Object.freeze({
359
+ manifest,
360
+ routes: getRouteList()
361
+ });
362
+ } finally {
363
+ restoreRouteState(previous);
364
+ }
365
+ }
366
+ //#endregion
324
367
  //#region src/router/deferred.tsx
325
368
  const DEFERRED_VALUE = Symbol.for("@askrjs/askr/deferred-value");
326
369
  const DEFERRED_BOUNDARY = Symbol.for("@askrjs/askr/deferred-boundary");
@@ -450,4 +493,4 @@ function Resolve(props) {
450
493
  return props.children(result.value);
451
494
  }
452
495
  //#endregion
453
- export { lazy as S, pageRenderEnvelope as _, resolveDeferredValues as a, getRenderHandler as b, bindResolvedRouteData as c, hasRouteRenderData as d, resolveRouteRequest as f, isPageRenderEnvelope as g, isEmptyPageRenderEnvelope as h, isDeferred as i, getRouteRenderContext as l, createPageRenderEnvelope as m, Resolve as n, reviveDeferredValue as o, currentAuth as p, defer as r, routeData as s, DEFERRED_BOUNDARY as t, getRouteRenderData as u, Outlet as v, _preloadRouteHandler as x, createRouteHandler as y };
496
+ export { lazy as C, getRenderHandler as S, getExecutionModel as T, isPageRenderEnvelope as _, resolveDeferredValues as a, Outlet as b, _applyManifest as c, getRouteRenderContext as d, getRouteRenderData as f, isEmptyPageRenderEnvelope as g, createPageRenderEnvelope as h, isDeferred as i, createRouteRegistry as l, resolveRouteRequest as m, Resolve as n, reviveDeferredValue as o, hasRouteRenderData as p, defer as r, routeData as s, DEFERRED_BOUNDARY as t, bindResolvedRouteData as u, pageRenderEnvelope as v, assertExecutionModel as w, createRouteHandler as x, currentAuth as y };