@askrjs/askr 0.0.64 → 0.0.65

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-B71c2eio.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-C93To416.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-OnYuVVfg.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.65",
67
+ buildLabel: "0.0.65-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-eRoaj11J.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-OnYuVVfg.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, w as assertExecutionModel, y as pageRenderEnvelope } from "./deferred-B71c2eio.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-C93To416.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-DTZ2SM8_.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,11 @@ 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
+ const resolved = source?.registry ? await resolveRouteRequest(href, {
412
+ manifest: source.registry.manifest,
413
413
  auth,
414
414
  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 });
415
+ }) : await resolveRouteRequest(href, { auth });
426
416
  if (typeof window === "undefined" || !resolved || resolved.kind !== "redirect") return {
427
417
  path,
428
418
  href,
@@ -473,47 +463,34 @@ function createIslands(config) {
473
463
  });
474
464
  }
475
465
  /**
476
- * createSPA: Initializes router and mounts the app with the provided route manifest or route table.
477
- *
478
- * Preferred usage with manifest:
466
+ * createSPA: Initializes the router and mounts the app with an explicit route registry.
479
467
  * ```ts
480
- * import { getManifest } from '@askrjs/askr/router';
481
- * await createSPA({ root: '#app', manifest: getManifest() });
468
+ * import { createRouteRegistry } from '@askrjs/askr/router';
469
+ * const registry = createRouteRegistry(() => { ... });
470
+ * await createSPA({ root: '#app', registry });
482
471
  * ```
483
472
  *
484
- * Legacy usage with plain routes array (still supported):
485
- * ```ts
486
- * await createSPA({ root: '#app', routes: getRoutes() });
487
- * ```
488
473
  */
489
474
  async function createSPA(config) {
490
475
  assertExecutionModel("spa");
491
476
  if (!config || typeof config !== "object") throw new Error("createSPA requires a config object");
492
477
  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.");
478
+ if (!config.registry) throw new Error("createSPA requires a route registry.");
479
+ const { manifest } = config.registry;
480
+ 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
481
  const rootElement = resolveRootElement(config.root);
499
482
  if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
500
483
  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;
484
+ clearRouteState();
485
+ _applyManifest(manifest);
486
+ const routeAuth = config.auth ?? manifest.auth;
506
487
  const appRouteSource = {
507
- manifest: activeManifest,
508
- routes: hasManifest ? void 0 : routeTable,
488
+ registry: config.registry,
509
489
  auth: routeAuth
510
490
  };
511
491
  _setActiveRouteAuthOptions(routeAuth);
512
492
  if (isProductionEnvironment()) lockRouteRegistration();
513
- const { path, resolved } = await resolveInitialRoute(routeAuth, {
514
- manifest: activeManifest,
515
- routes: hasManifest ? void 0 : routeTable
516
- });
493
+ const { path, resolved } = await resolveInitialRoute(routeAuth, { registry: config.registry });
517
494
  const appRuntime = createAppRenderRuntime(resolved?.kind === "render" && hasRouteRenderData(resolved) ? {
518
495
  route: getRouteRenderData(resolved),
519
496
  hasRoute: true
@@ -554,18 +531,15 @@ async function createSPA(config) {
554
531
  await registerAppNavigation(rootElement, path, { ...appRouteSource });
555
532
  }
556
533
  /**
557
- * hydrateSPA: Hydrate server-rendered HTML.
558
- * Accepts either a `manifest` (preferred) or a legacy `routes` array.
534
+ * hydrateSPA: Hydrate server-rendered HTML with an explicit route registry.
559
535
  */
560
536
  async function hydrateSPA(config) {
561
537
  assertExecutionModel("spa");
562
538
  if (!config || typeof config !== "object") throw new Error("hydrateSPA requires a config object");
563
539
  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.");
540
+ if (!config.registry) throw new Error("hydrateSPA requires a route registry.");
541
+ const { manifest } = config.registry;
542
+ 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
543
  const rootElement = resolveRootElement(config.root);
570
544
  if (!rootElement) throw new Error(`Root element not found: ${config.root}`);
571
545
  applyDeferredStreamPatches(rootElement);
@@ -574,14 +548,11 @@ async function hydrateSPA(config) {
574
548
  if (hydrationQueryCache) hydrateDataRuntime(config.dataRuntime ?? getDefaultDataRuntime(), hydrationQueryCache);
575
549
  const hydrationRenderDataForApp = hydrationRenderData;
576
550
  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;
551
+ clearRouteState();
552
+ _applyManifest(manifest);
553
+ const routeAuth = config.auth ?? manifest.auth;
582
554
  const appRouteSource = {
583
- manifest: activeManifest,
584
- routes: hasManifest ? void 0 : routeTable,
555
+ registry: config.registry,
585
556
  auth: routeAuth,
586
557
  runtime: createAppRenderRuntime({
587
558
  framework: hydrationRenderData?.framework,
@@ -591,8 +562,7 @@ async function hydrateSPA(config) {
591
562
  };
592
563
  _setActiveRouteAuthOptions(routeAuth);
593
564
  const { path, href: currentUrl, resolved } = await resolveInitialRoute(routeAuth, {
594
- manifest: activeManifest,
595
- routes: hasManifest ? void 0 : routeTable,
565
+ registry: config.registry,
596
566
  load: false
597
567
  });
598
568
  setServerLocation(currentUrl);
@@ -612,17 +582,11 @@ async function hydrateSPA(config) {
612
582
  cspNonce: config.cspNonce
613
583
  }));
614
584
  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");
585
+ const { verifyHydrationSyncForUrl } = await import("./verify-hydration-BdKE9Hi8.js");
622
586
  if (!verifyHydrationSyncForUrl({
623
587
  root: rootElement,
624
588
  url: currentUrl,
625
- routes: legacyRouteTable,
589
+ registry: config.registry,
626
590
  resolved: hydrationResolved,
627
591
  options: {
628
592
  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-C93To416.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);
@@ -321,6 +326,42 @@ function resolveRouteRequest(target, options = {}) {
321
326
  });
322
327
  }
323
328
  //#endregion
329
+ //#region src/router/manifest.ts
330
+ function createRouteManifest() {
331
+ const auth = getDefaultRouteAuthOptions();
332
+ return {
333
+ records: [...getRouteRecords()],
334
+ ...auth ? { auth } : {}
335
+ };
336
+ }
337
+ function _applyManifest(manifest) {
338
+ setDefaultRouteAuthOptions(manifest.auth);
339
+ for (const record of manifest.records) {
340
+ insertRecordSorted(record);
341
+ addRouteToStores({
342
+ path: record.path,
343
+ handler: record.handler,
344
+ namespace: record.options.namespace,
345
+ ..."fallbackPrefix" in record && typeof record.fallbackPrefix === "string" ? { fallbackPrefix: record.fallbackPrefix } : {}
346
+ });
347
+ }
348
+ }
349
+ function createRouteRegistry(definition, options = {}) {
350
+ const previous = snapshotRouteState();
351
+ clearRouteState();
352
+ try {
353
+ setDefaultRouteAuthOptions(options.auth);
354
+ definition();
355
+ const manifest = createRouteManifest();
356
+ return Object.freeze({
357
+ manifest,
358
+ routes: getRouteList()
359
+ });
360
+ } finally {
361
+ restoreRouteState(previous);
362
+ }
363
+ }
364
+ //#endregion
324
365
  //#region src/router/deferred.tsx
325
366
  const DEFERRED_VALUE = Symbol.for("@askrjs/askr/deferred-value");
326
367
  const DEFERRED_BOUNDARY = Symbol.for("@askrjs/askr/deferred-boundary");
@@ -450,4 +491,4 @@ function Resolve(props) {
450
491
  return props.children(result.value);
451
492
  }
452
493
  //#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 };
494
+ export { lazy as C, getRenderHandler as S, getExecutionModel as T, isEmptyPageRenderEnvelope as _, resolveDeferredValues as a, Outlet as b, _applyManifest as c, getRouteRenderContext as d, getRouteRenderData as f, createPageRenderEnvelope as g, currentAuth 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, isPageRenderEnvelope as v, assertExecutionModel as w, createRouteHandler as x, pageRenderEnvelope as y };