@askrjs/askr 0.3.0 → 0.4.0

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +874 -0
  2. package/README.md +61 -29
  3. package/capabilities.json +19 -9
  4. package/dist/actions/index.js +56 -12
  5. package/dist/benchmark.js +10 -15
  6. package/dist/boot/index.d.ts +3 -2
  7. package/dist/boot/index.js +1 -1
  8. package/dist/boot-DaoTYoCw.js +3989 -0
  9. package/dist/browser-activity-CB8e-V4f.js +220 -0
  10. package/dist/components/index.d.ts +8 -1
  11. package/dist/components/index.js +1 -43
  12. package/dist/{aria-hKjThHcp.js → compose-ref-DWzdbxsD.js} +41 -4
  13. package/dist/control/index.d.ts +2 -0
  14. package/dist/control/index.js +1 -1
  15. package/dist/control-nfpT2fbK.js +209 -0
  16. package/dist/core.d.ts +5 -9
  17. package/dist/csp-nonce-DwGw1A6Q.js +35 -0
  18. package/dist/data/index.js +3 -3
  19. package/dist/{data-2edW0jhF.js → data-BCwClJiF.js} +283 -94
  20. package/dist/data-runtime-C1bFAlba.js +242 -0
  21. package/dist/{deferred-CbgNhf67.js → deferred-bEEHHvic.js} +42 -20
  22. package/dist/domains/actions.d.ts +8 -3
  23. package/dist/domains/component.d.ts +1 -107
  24. package/dist/domains/context.d.ts +0 -15
  25. package/dist/domains/control.d.ts +36 -249
  26. package/dist/domains/data.d.ts +31 -13
  27. package/dist/domains/diagnostics.d.ts +1 -2
  28. package/dist/domains/lifecycle.d.ts +2 -5
  29. package/dist/domains/refs.d.ts +0 -2
  30. package/dist/domains/routing.d.ts +43 -8
  31. package/dist/domains/scheduler.d.ts +1 -76
  32. package/dist/domains/server.d.ts +5 -2
  33. package/dist/domains/state.d.ts +2 -5
  34. package/dist/domains/telemetry.d.ts +0 -2
  35. package/dist/elements.d.ts +214 -51
  36. package/dist/error-boundary-Bpwofwps.js +293 -0
  37. package/dist/foundations/icon/index.d.ts +0 -58
  38. package/dist/foundations/icon/index.js +2 -2
  39. package/dist/foundations/index.js +2 -1
  40. package/dist/foundations/interactions/index.js +1 -1
  41. package/dist/foundations/state/index.d.ts +3 -3
  42. package/dist/foundations/state/index.js +5 -3
  43. package/dist/foundations/structures/index.js +2 -1
  44. package/dist/foundations/utilities/index.d.ts +2 -0
  45. package/dist/foundations/utilities/index.js +1 -2
  46. package/dist/fx/index.d.ts +31 -58
  47. package/dist/fx/index.js +313 -195
  48. package/dist/hooks-BimKStr4.js +202 -0
  49. package/dist/index.d.ts +11 -74
  50. package/dist/index.js +8 -447
  51. package/dist/instance-BAnfmXOR.js +988 -0
  52. package/dist/{interactions-CTeACdT_.js → interactions-D2BoREb-.js} +3 -2
  53. package/dist/jsx-dev-runtime.d.ts +41 -18
  54. package/dist/jsx-dev-runtime.js +1 -6
  55. package/dist/{jsx-runtime-CPJXAOBF.js → jsx-runtime-C4idqHY0.js} +18 -7
  56. package/dist/jsx-runtime.js +1 -1
  57. package/dist/jsx.d.ts +61 -37
  58. package/dist/{manifest-D1P3UNWV.js → manifest-CW9gyNBY.js} +3 -3
  59. package/dist/{navigate-prC5V8XV.js → navigate-CP_0Zy5U.js} +454 -87
  60. package/dist/navigation.d.ts +12 -3
  61. package/dist/peer-types.d.ts +8 -0
  62. package/dist/portal-DuOKfnLa.js +349 -0
  63. package/dist/promise-Dz8iIHAm.js +16 -0
  64. package/dist/query-registry-WMCN0BJL.js +320 -0
  65. package/dist/readable-DFWOsTac.js +21 -0
  66. package/dist/{render-context-B7ohlh6t.js → render-context-DGHnEveC.js} +27 -11
  67. package/dist/render-sync-IC5lMFpq.js +1240 -0
  68. package/dist/resolution-Cx82-xjW.js +1244 -0
  69. package/dist/resource-CLjB7RBQ.js +377 -0
  70. package/dist/resources/index.d.ts +21 -11
  71. package/dist/resources/index.js +45 -244
  72. package/dist/router/index.d.ts +14 -5
  73. package/dist/router/index.js +22 -53
  74. package/dist/scope-CwpVSHVf.js +55 -0
  75. package/dist/{shared-kU8yvryp.js → shared-O2ZnmYAf.js} +1 -7
  76. package/dist/{snapshot-source-An4iKajG.js → snapshot-Cjx4N988.js} +6 -1
  77. package/dist/ssg/index.d.ts +1 -2
  78. package/dist/ssg/index.js +5 -13
  79. package/dist/ssr/index.d.ts +26 -28
  80. package/dist/ssr/index.js +4 -4
  81. package/dist/{ssr-CiNm6Vzl.js → ssr-BJ8ug7zH.js} +133 -122
  82. package/dist/ssr-DoV-vJ-5.js +229 -0
  83. package/dist/state-DWO1KzkX.js +232 -0
  84. package/dist/structures-D4dDMqHX.js +231 -0
  85. package/dist/testing/index.d.ts +8 -4
  86. package/dist/testing/index.js +29 -34
  87. package/dist/url-CnbfpX0K.js +185 -0
  88. package/dist/utilities.d.ts +48 -2
  89. package/dist/verify-hydration-DpVaDnqz.js +152 -0
  90. package/package.json +19 -9
  91. package/dist/access-CEA2pOry.js +0 -1046
  92. package/dist/activity-oy2YFTFF.js +0 -201
  93. package/dist/boot-BMBMWUGI.js +0 -1084
  94. package/dist/capabilities-Bbf0mlp9.js +0 -42
  95. package/dist/compose-ref-v0qGlHoN.js +0 -26
  96. package/dist/control-BR_mrX__.js +0 -173
  97. package/dist/control-C-ApnFs1.js +0 -20
  98. package/dist/csp-nonce-BfFWcd0w.js +0 -173
  99. package/dist/data-runtime-CxoY98lX.js +0 -149
  100. package/dist/default-portal-runtime-DvMvWJVp.js +0 -20
  101. package/dist/dom-renderer.d.ts +0 -77
  102. package/dist/domains/renderer.d.ts +0 -127
  103. package/dist/eager-control.d.ts +0 -8
  104. package/dist/effect-C3Zsc1k6.js +0 -292
  105. package/dist/for-state-BDykeOJL.js +0 -1753
  106. package/dist/instance-VAFYVJOa.js +0 -1027
  107. package/dist/notify-DxxuDPeY.js +0 -266
  108. package/dist/portal-w3oQ6oQU.js +0 -29
  109. package/dist/query-registry-LrYpQsDJ.js +0 -83
  110. package/dist/render-resolved-BC0W7_5b.js +0 -1123
  111. package/dist/renderer-CT0HeY58.js +0 -8623
  112. package/dist/resolution-3DskUkjG.js +0 -577
  113. package/dist/resource-18-sFRl9.js +0 -344
  114. package/dist/route-matching-CUzZ31gT.js +0 -551
  115. package/dist/router-internal-7nuNEHfG.js +0 -36
  116. package/dist/scope-Cq_iYbTR.js +0 -403
  117. package/dist/selector-BSPVkjKP.js +0 -311
  118. package/dist/ssr-EPjwJH5R.js +0 -29
  119. package/dist/state-BXJfudq1.js +0 -91
  120. package/dist/structures-CjEzfj4h.js +0 -654
  121. package/dist/timer-C7uqnJVt.js +0 -85
  122. package/dist/verify-hydration-ikaxotf7.js +0 -27
  123. package/dist/vnode-D24p6IW2.js +0 -7
package/dist/ssg/index.js CHANGED
@@ -1,10 +1,8 @@
1
- import "../access-CEA2pOry.js";
2
- import { n as createRouteRegistry } from "../manifest-D1P3UNWV.js";
3
- import { _ as prepareRouteHydrationData, d as _preloadRouteHandler, m as RouteDataLoadError, t as bindResolvedRouteData, u as getRenderHandler } from "../resolution-3DskUkjG.js";
4
- import { o as addRouteBasePath } from "../route-matching-CUzZ31gT.js";
5
- import { c as route, n as resolveDeferredValues } from "../deferred-CbgNhf67.js";
6
- import { n as renderDocument } from "../ssr-EPjwJH5R.js";
7
- import { n as renderToString } from "../ssr-CiNm6Vzl.js";
1
+ import { mt as prepareRouteHydrationData, nt as getRenderHandler, ot as RouteDataLoadError, rt as _preloadRouteHandler, t as bindResolvedRouteData, x as addRouteBasePath } from "../resolution-Cx82-xjW.js";
2
+ import { n as createRouteRegistry } from "../manifest-CW9gyNBY.js";
3
+ import { c as route, n as resolveDeferredValues } from "../deferred-bEEHHvic.js";
4
+ import { n as renderDocument } from "../ssr-DoV-vJ-5.js";
5
+ import { n as renderToString } from "../ssr-BJ8ug7zH.js";
8
6
  import * as fs$1 from "node:fs/promises";
9
7
  import * as os from "node:os";
10
8
  import * as pathModule from "node:path";
@@ -771,8 +769,6 @@ function routeRegistryToRouteConfigs(registry) {
771
769
  routeConfigs.push({
772
770
  path: record.path,
773
771
  handler: getRenderHandler(record),
774
- params: options.params,
775
- props: options.props,
776
772
  invalidationKeys: options.invalidationKeys ? [...options.invalidationKeys] : void 0,
777
773
  namespace: options.namespace,
778
774
  auth: options.auth,
@@ -873,9 +869,7 @@ function assembleRouteResults({ selected, renderedByRouteId, routeResultsById, d
873
869
  async function publishGeneration({ routeResults, nextManifestRoutes, effectiveMode, seed, concurrency, outputDir, assets, metadata }) {
874
870
  const targetOutputDir = effectiveMode === "full" ? await createStagingDirectory(outputDir) : outputDir;
875
871
  try {
876
- const writeStartTime = performance.now();
877
872
  await writeStaticFiles(routeResults, targetOutputDir, { concurrency });
878
- performance.now() - writeStartTime;
879
873
  await writeMetadata(metadata, targetOutputDir);
880
874
  await writeIncrementalManifest({
881
875
  schemaVersion: 1,
@@ -936,7 +930,6 @@ function createStaticGen(options) {
936
930
  selected.push(entry);
937
931
  if (entry.reason !== "unchanged") routesToRender.push(entry);
938
932
  }
939
- const renderStartTime = performance.now();
940
933
  const renderedResults = routesToRender.length > 0 ? await batchRenderRoutes(getRoutesToRender(routesToRender), {
941
934
  seed,
942
935
  dataMap,
@@ -944,7 +937,6 @@ function createStaticGen(options) {
944
937
  document: options.document,
945
938
  styleRegistrationValidation: options.styleRegistrationValidation
946
939
  }) : [];
947
- performance.now() - renderStartTime;
948
940
  const renderedByRouteId = /* @__PURE__ */ new Map();
949
941
  for (let index = 0; index < renderedResults.length; index += 1) renderedByRouteId.set(routesToRender[index].descriptor.routeId, renderedResults[index]);
950
942
  const { cacheHits, nextManifestRoutes } = assembleRouteResults({
@@ -11,7 +11,6 @@ import {
11
11
  DocumentRenderArgs,
12
12
  Route,
13
13
  RouteRecord,
14
- RouteRequestResult,
15
14
  ServerQueryRegistry,
16
15
  CoreTelemetry,
17
16
  AccessDenyDecision,
@@ -21,7 +20,7 @@ import {
21
20
  AccessRedirectDecision,
22
21
  } from '../core.js';
23
22
  import { JSXElement, Props } from '../elements.js';
24
- import { AuthContext } from '@askrjs/auth';
23
+ import { AuthContext } from '../peer-types.js';
25
24
  declare const PAGE_RENDER_ENVELOPE_VERSION: 1;
26
25
  interface PageRenderEnvelope {
27
26
  readonly version: typeof PAGE_RENDER_ENVELOPE_VERSION;
@@ -36,6 +35,17 @@ declare class SSRDataMissingError extends Error {
36
35
  readonly code = 'SSR_DATA_MISSING';
37
36
  constructor(message?: string);
38
37
  }
38
+ /**
39
+ * Thrown by synchronous route SSR (`renderToString()`/`renderToStream()`) when
40
+ * route auth or a policy redirects or denies the request. Nothing is rendered;
41
+ * send `decision` as the HTTP response (for example a 302 to `decision.to`, or
42
+ * `decision.status`).
43
+ */
44
+ declare class SSRAccessDecisionError extends Error {
45
+ readonly code = 'SSR_ACCESS_DECISION';
46
+ readonly decision: AccessRedirectDecision | AccessDenyDecision;
47
+ constructor(decision: AccessRedirectDecision | AccessDenyDecision);
48
+ }
39
49
  interface RenderContext {
40
50
  url: string;
41
51
  seed: number;
@@ -126,15 +136,6 @@ type RouteStreamOptions = RouteRenderOptions & {
126
136
  onChunk(html: string): void;
127
137
  onComplete(): void;
128
138
  };
129
- /** Resolve a URL against a route registry for SSR, applying auth/policies before render. */
130
- declare function resolveRequest(opts: {
131
- url: string;
132
- registry: RouteRegistry;
133
- auth?: RouteAuthOptions;
134
- authContext?: AuthContext;
135
- request?: Request;
136
- signal?: AbortSignal;
137
- }): Promise<RouteRequestResult>;
138
139
  /** VNode representation for SSR rendering */
139
140
  type VNode = {
140
141
  type: string | SSRComponent | symbol;
@@ -166,25 +167,11 @@ declare function renderToStringSync(
166
167
  envelope?: PageRenderEnvelope;
167
168
  cspNonce?: string;
168
169
  /** @internal Request-local authentication for deferred SSR passes. */
169
- authContext?: import('@askrjs/auth').AuthContext;
170
+ authContext?: AuthContext;
170
171
  /** @internal Capture request-local registrations produced by this pass. */
171
172
  onContext?: (ctx: RenderContext) => void;
172
173
  }
173
174
  ): string;
174
- /** Synchronously render an already-resolved route handler to an HTML string. */
175
- declare function renderResolvedToStringSync(opts: {
176
- url: string;
177
- registry: RouteRegistry;
178
- handler: RouteHandler;
179
- params?: Record<string, string>;
180
- options?: {
181
- seed?: number;
182
- data?: SSRData;
183
- dataRuntime?: DataRuntime;
184
- envelope?: PageRenderEnvelope;
185
- cspNonce?: string;
186
- };
187
- }): string;
188
175
  /** Options for {@link renderRouteRequest} and {@link renderRouteRequestToString}. */
189
176
  interface RenderRouteRequestOptions {
190
177
  url: string;
@@ -235,6 +222,17 @@ declare function renderToString(
235
222
  ) => VNode | JSXElement | string | number | null
236
223
  ): string;
237
224
  declare function renderToString(opts: RouteRenderOptions): string;
225
+ /**
226
+ * Escape a string for interpolation into a hand-written HTML document template,
227
+ * either as text content or inside a quoted attribute value. Replaces `&`, `<`,
228
+ * `>`, `"` and `'` with character references. Use it for every request-derived
229
+ * value a `document` renderer interpolates, such as `context.pathname`,
230
+ * `context.params` or loader data. Do not use it for `appHtml`, which is
231
+ * already rendered markup, or inside `<script>`/`<style>` raw text.
232
+ * `null` and `undefined` render as an empty string; other values are
233
+ * converted with `String()`.
234
+ */
235
+ declare function escapeHtml(value: unknown): string;
238
236
  /** Stream a route request's rendered HTML to the response sink described by `opts`. */
239
237
  declare function renderToStream(opts: RouteStreamOptions): void;
240
238
  export {
@@ -243,6 +241,7 @@ export {
243
241
  type DocumentRenderer,
244
242
  type RenderRouteRequestOptions,
245
243
  type RenderRouteRequestResult,
244
+ SSRAccessDecisionError,
246
245
  type SSRComponent,
247
246
  SSRDataMissingError,
248
247
  type SSRRoute,
@@ -250,14 +249,13 @@ export {
250
249
  type SSRStyleRegistrationValidation,
251
250
  type VNode,
252
251
  createRenderContext,
252
+ escapeHtml,
253
253
  getRenderContext,
254
- renderResolvedToStringSync,
255
254
  renderRouteRequest,
256
255
  renderRouteRequestToString,
257
256
  renderToStream,
258
257
  renderToString,
259
258
  renderToStringSync,
260
- resolveRequest,
261
259
  withRenderContext,
262
260
  withRenderContextAsync,
263
261
  };
package/dist/ssr/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { C as SSRDataMissingError } from "../render-context-B7ohlh6t.js";
2
- import { c as createRenderContext, d as withRenderContext, f as withRenderContextAsync, i as renderToStringSync, l as getRenderContext, n as renderResolvedToStringSync } from "../render-resolved-BC0W7_5b.js";
3
- import { a as resolveRequest, i as renderRouteRequestToString, n as renderToString, r as renderRouteRequest, t as renderToStream } from "../ssr-CiNm6Vzl.js";
4
- export { SSRDataMissingError, createRenderContext, getRenderContext, renderResolvedToStringSync, renderRouteRequest, renderRouteRequestToString, renderToStream, renderToString, renderToStringSync, resolveRequest, withRenderContext, withRenderContextAsync };
1
+ import { T as SSRDataMissingError } from "../render-context-DGHnEveC.js";
2
+ import { a as SSRAccessDecisionError, i as renderRouteRequestToString, n as renderToString, r as renderRouteRequest, t as renderToStream } from "../ssr-BJ8ug7zH.js";
3
+ import { c as getRenderContext, d as escapeHtml, l as withRenderContext, n as renderToStringSync, s as createRenderContext, u as withRenderContextAsync } from "../render-sync-IC5lMFpq.js";
4
+ export { SSRAccessDecisionError, SSRDataMissingError, createRenderContext, escapeHtml, getRenderContext, renderRouteRequest, renderRouteRequestToString, renderToStream, renderToString, renderToStringSync, withRenderContext, withRenderContextAsync };
@@ -1,87 +1,64 @@
1
- import { P as isPromiseLike } from "./instance-VAFYVJOa.js";
2
- import { r as validateCspNonce } from "./csp-nonce-BfFWcd0w.js";
3
- import { t as createDataRuntime } from "./data-runtime-CxoY98lX.js";
4
- import { c as withTelemetry, t as createQueryPrefetchContext } from "./query-registry-LrYpQsDJ.js";
5
- import { a as resolveRouteRequest, n as getRouteRenderContext, o as resolvedRouteHasLoader } from "./resolution-3DskUkjG.js";
6
- import { c as removeRouteBasePath, s as normalizeRouteBasePath, t as _resolveRouteMatchFromRoutes } from "./route-matching-CUzZ31gT.js";
7
- import { n as renderDocument } from "./ssr-EPjwJH5R.js";
8
- import { a as serializeHydrationRenderData, c as createRenderContext, f as withRenderContextAsync, i as renderToStringSync, o as StreamSink, r as renderSSRRouteAppToSink, s as StringSink, u as throwSSRDataMissing } from "./render-resolved-BC0W7_5b.js";
1
+ import { r as validateCspNonce } from "./csp-nonce-DwGw1A6Q.js";
2
+ import { T as SSRDataMissingError, n as getActiveRenderContext, u as setDeferredBoundaryParent } from "./render-context-DGHnEveC.js";
3
+ import { n as isPromiseLike } from "./promise-Dz8iIHAm.js";
4
+ import { c as withTelemetry, i as dehydrateDataRuntime, t as createQueryPrefetchContext } from "./query-registry-WMCN0BJL.js";
5
+ import { C as removeRouteBasePath, S as normalizeRouteBasePath, _ as getMatchingRouteRecord, a as resolveRouteRequest, n as getRouteRenderContext } from "./resolution-Cx82-xjW.js";
6
+ import { t as createDataRuntime } from "./data-runtime-C1bFAlba.js";
7
+ import { n as renderDocument } from "./ssr-DoV-vJ-5.js";
8
+ import { a as serializeHydrationRenderData, i as StringSink, n as renderToStringSync, o as withHydratedAuth, r as StreamSink, s as createRenderContext, t as renderSSRRouteAppToSink, u as withRenderContextAsync } from "./render-sync-IC5lMFpq.js";
9
+ //#region src/ssr/errors.ts
10
+ /**
11
+ * Thrown by synchronous route SSR (`renderToString()`/`renderToStream()`) when
12
+ * route auth or a policy redirects or denies the request. Nothing is rendered;
13
+ * send `decision` as the HTTP response (for example a 302 to `decision.to`, or
14
+ * `decision.status`).
15
+ */
16
+ var SSRAccessDecisionError = class SSRAccessDecisionError extends Error {
17
+ constructor(decision) {
18
+ super(decision.kind === "redirect" ? `SSR: route access redirected to ${decision.to}.` : `SSR: route access denied with status ${decision.status}.`);
19
+ this.code = "SSR_ACCESS_DECISION";
20
+ this.name = "SSRAccessDecisionError";
21
+ this.decision = decision;
22
+ Object.setPrototypeOf(this, SSRAccessDecisionError.prototype);
23
+ }
24
+ };
25
+ //#endregion
9
26
  //#region src/ssr/route-policy-resolution.ts
10
- const MAX_SSR_REDIRECTS = 20;
11
- function getRouteRequestResultSync(result) {
12
- if (isPromiseLike(result)) throwSSRDataMissing();
27
+ function getRouteRequestResultSync(result, href) {
28
+ if (isPromiseLike(result)) {
29
+ Promise.resolve(result).catch(() => void 0);
30
+ throw new SSRDataMissingError(`SSR: route resolution for ${href} is asynchronous (async auth, policy, lazy route, or preload). renderToString()/renderToStream() only resolve routes synchronously; use renderRouteRequest() to await route resolution.`);
31
+ }
13
32
  return result;
14
33
  }
15
- function buildDeniedRoute(route, status) {
34
+ function resolvePolicyAwareSSRRoute(opts, routeTable) {
35
+ const href = opts.url;
36
+ const logicalHref = removeRouteBasePath(href, normalizeRouteBasePath(opts.registry.manifest.basePath));
37
+ if (logicalHref === void 0) throw new Error(`SSR: no route found for url: ${href}`);
38
+ const matched = getMatchingRouteRecord(logicalHref, opts.registry.manifest.records);
39
+ const route = matched && routeTable.find((entry) => entry.handler === matched.record.handler);
40
+ if (!matched || !route) throw new Error(`SSR: no route found for url: ${href}`);
41
+ const { record } = matched;
42
+ if (typeof record.options.loader === "function") throw new SSRDataMissingError(`SSR: route ${record.path} declares a loader, which renderToString()/renderToStream() do not run. Use renderRouteRequest() to await route loaders before rendering.`);
43
+ const resolved = getRouteRequestResultSync(resolveRouteRequest(href, {
44
+ registry: opts.registry,
45
+ mode: "ssr",
46
+ auth: opts.auth,
47
+ authContext: opts.authContext,
48
+ request: opts.request,
49
+ signal: opts.signal,
50
+ load: false
51
+ }), href);
52
+ if (resolved === null) throw new Error(`SSR: no route found for url: ${href}`);
53
+ if (resolved.kind === "redirect" || resolved.kind === "deny") throw new SSRAccessDecisionError(resolved);
16
54
  return {
17
- ...route,
18
- handler: () => ({
19
- type: "div",
20
- props: { "data-route-denied": String(status) },
21
- children: [String(status)]
22
- })
55
+ route,
56
+ params: matched.params,
57
+ authContext: getRouteRenderContext(resolved)?.auth
23
58
  };
24
59
  }
25
- function resolvePolicyAwareSSRRoute(opts, routeTable) {
26
- const manifest = opts.registry.manifest;
27
- let href = opts.url;
28
- const visited = /* @__PURE__ */ new Set();
29
- for (let redirects = 0; redirects <= MAX_SSR_REDIRECTS; redirects += 1) {
30
- const logicalHref = removeRouteBasePath(href, normalizeRouteBasePath(manifest.basePath));
31
- if (logicalHref === void 0) throw new Error(`SSR: no route found for url: ${href}`);
32
- const logicalUrl = new URL(logicalHref, "http://localhost");
33
- const matched = _resolveRouteMatchFromRoutes(logicalUrl.pathname, routeTable);
34
- if (!matched) throw new Error(`SSR: no route found for url: ${href}`);
35
- const resolved = getRouteRequestResultSync(resolveRouteRequest(href, {
36
- registry: opts.registry,
37
- mode: "ssr",
38
- auth: opts.auth,
39
- authContext: opts.authContext,
40
- request: opts.request,
41
- signal: opts.signal
42
- }));
43
- if (resolved === null) throw new Error(`SSR: no route found for url: ${href}`);
44
- if (resolved.kind === "redirect") {
45
- const redirectTarget = new URL(resolved.to, "http://localhost");
46
- const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;
47
- if (redirectHref === href || visited.has(redirectHref)) throw new Error(`[Askr] SSR redirect cycle detected at ${redirectHref}.`);
48
- if (redirects >= MAX_SSR_REDIRECTS) throw new Error(`[Askr] SSR redirect limit exceeded (${MAX_SSR_REDIRECTS}).`);
49
- visited.add(redirectHref);
50
- href = redirectHref;
51
- continue;
52
- }
53
- if (resolved.kind === "deny") return {
54
- url: href,
55
- route: buildDeniedRoute(matched.route, resolved.status),
56
- params: matched.params
57
- };
58
- return {
59
- url: href,
60
- route: {
61
- ...matched.route,
62
- handler: resolvedRouteHasLoader(resolved) ? resolved.handler : matched.route.handler
63
- },
64
- params: matched.params,
65
- authContext: getRouteRenderContext(resolved)?.auth
66
- };
67
- }
68
- throw new Error(`[Askr] SSR redirect limit exceeded (${MAX_SSR_REDIRECTS}).`);
69
- }
70
60
  //#endregion
71
61
  //#region src/ssr/route-render.ts
72
- /** Resolve a URL against a route registry for SSR, applying auth/policies before render. */
73
- async function resolveRequest(opts) {
74
- if (!opts?.registry) throw new TypeError("resolveRequest requires opts.registry.");
75
- const { url, auth, authContext, request, signal } = opts;
76
- return await resolveRouteRequest(url, {
77
- registry: opts.registry,
78
- mode: "ssr",
79
- auth,
80
- authContext,
81
- request,
82
- signal
83
- });
84
- }
85
62
  function resolveSSRRouteSource(source) {
86
63
  if (source.registry.routes.length === 0) throw new Error("SSR requires a non-empty route registry.");
87
64
  return source.registry.routes.map((route) => ({
@@ -96,9 +73,9 @@ function resolveSSRRouteRender(opts) {
96
73
  const cspNonce = validateCspNonce(opts.cspNonce);
97
74
  const routeTable = resolveSSRRouteSource(opts);
98
75
  const resolvedRoute = resolvePolicyAwareSSRRoute(opts, routeTable);
99
- const requestUrl = new URL(resolvedRoute.url, "http://localhost");
76
+ const requestUrl = new URL(opts.url, "http://localhost");
100
77
  const ctx = createRenderContext(seed, {
101
- url: resolvedRoute.url,
78
+ url: opts.url,
102
79
  basePath: opts.registry.manifest.basePath,
103
80
  data,
104
81
  params: resolvedRoute.params,
@@ -111,8 +88,9 @@ function resolveSSRRouteRender(opts) {
111
88
  envelope: opts.envelope,
112
89
  cspNonce
113
90
  });
91
+ if (!opts.envelope && ctx.hydrationData) ctx.hydrationData = withHydratedAuth(ctx.hydrationData, opts.auth ?? opts.registry.manifest.auth, resolvedRoute.authContext);
114
92
  return {
115
- url: resolvedRoute.url,
93
+ url: opts.url,
116
94
  requestUrl,
117
95
  route: resolvedRoute.route,
118
96
  params: resolvedRoute.params,
@@ -209,17 +187,27 @@ function settleWithSignal(promise, signal) {
209
187
  function stripHydrationPayload(html) {
210
188
  return html.replace(/<script type="application\/json" data-askr-render-data="true">[\s\S]*?<\/script>$/, "");
211
189
  }
212
- function renderBoundary(boundary, state, payload, seed, data, cspNonce, authContext) {
190
+ function renderBoundary(boundary, state, payload, pass) {
213
191
  const styles = [];
192
+ const boundaries = [];
214
193
  return {
215
- html: stripHydrationPayload(renderToStringSync(() => state === "fulfilled" ? boundary.fulfilled(payload) : boundary.rejected(payload), {}, {
216
- seed,
217
- envelope: data ?? void 0,
218
- cspNonce,
219
- authContext,
220
- onContext: (context) => styles.push(...context.ssrStyles.values())
194
+ html: stripHydrationPayload(renderToStringSync(() => {
195
+ const context = getActiveRenderContext();
196
+ if (context) setDeferredBoundaryParent(context, boundary.id);
197
+ return state === "fulfilled" ? boundary.fulfilled(payload) : boundary.rejected(payload);
198
+ }, {}, {
199
+ seed: pass.seed,
200
+ envelope: pass.data ?? void 0,
201
+ cspNonce: pass.cspNonce,
202
+ authContext: pass.authContext,
203
+ route: pass.route,
204
+ onContext: (context) => {
205
+ styles.push(...context.ssrStyles.values());
206
+ boundaries.push(...context.deferredBoundaries);
207
+ }
221
208
  })),
222
- styles
209
+ styles,
210
+ boundaries
223
211
  };
224
212
  }
225
213
  function escapeHtmlAttribute(value) {
@@ -232,13 +220,18 @@ function patchChunk(id, html, styles, cspNonce) {
232
220
  const selector = `askr-resolve[data-askr-deferred=${JSON.stringify(id)}]`;
233
221
  return `${styles.length === 0 ? "" : `<style data-askr-style-registry="true" data-askr-deferred-style="true"${cspNonce ? ` nonce="${escapeHtmlAttribute(cspNonce)}"` : ""}>${escapeStyleRawText(styles.map((style) => style.cssText).join("\n"))}\n</style>`}<template data-askr-deferred-patch="${escapeHtmlAttribute(id)}">${html}</template><script${cspNonce ? ` nonce="${escapeHtmlAttribute(cspNonce)}"` : ""} data-askr-deferred-apply="${escapeHtmlAttribute(id)}">(function(s){var t=s.previousElementSibling;var c=t&&t.previousElementSibling;var r=document.querySelector('style[data-askr-style-registry="true"]:not([data-askr-deferred-style])');if(c&&c.tagName==='STYLE'&&r&&r!==c){r.append(c.textContent||'');c.remove();}else if(c&&c.tagName==='STYLE'){c.removeAttribute('data-askr-deferred-style');}var b=document.querySelector(${JSON.stringify(selector)});if(b&&t){b.replaceWith(t.content.cloneNode(true));}if(t)t.remove();s.remove();})(document.currentScript)<\/script>`;
234
222
  }
235
- function createDeferredRenderStream(initialHtml, boundaries, signal, seed, data, runtime, cspNonce, authContext) {
223
+ /**
224
+ * Stream the shell, then one patch per boundary in the order boundaries
225
+ * settle. A boundary whose content renders another pending `Resolve` queues
226
+ * that nested boundary with the same request state; hydration data follows
227
+ * once every boundary, nested ones included, has been patched.
228
+ */
229
+ function createDeferredRenderStream(initialHtml, boundaries, signal, pass, runtime) {
236
230
  const encoder = new TextEncoder();
237
231
  const local = new AbortController();
238
232
  let cancelled = false;
239
233
  let closed = false;
240
- let initialPending = true;
241
- let boundaryIndex = 0;
234
+ let unsettled = 0;
242
235
  let controllerRef;
243
236
  const cleanup = () => signal.removeEventListener("abort", abort);
244
237
  const close = (controller) => {
@@ -247,46 +240,51 @@ function createDeferredRenderStream(initialHtml, boundaries, signal, seed, data,
247
240
  cleanup();
248
241
  controller.close();
249
242
  };
243
+ const fail = (controller, error) => {
244
+ if (closed) return;
245
+ closed = true;
246
+ cleanup();
247
+ local.abort(error);
248
+ controller.error(error);
249
+ };
250
250
  const abort = () => {
251
251
  local.abort(signal.reason);
252
252
  if (controllerRef) close(controllerRef);
253
253
  };
254
254
  signal.addEventListener("abort", abort, { once: true });
255
+ const streamBoundary = (controller, boundary) => {
256
+ unsettled += 1;
257
+ settleWithSignal(boundary.promise, local.signal).then((value) => ({
258
+ state: "fulfilled",
259
+ payload: value
260
+ }), (error) => ({
261
+ state: "rejected",
262
+ payload: error
263
+ })).then(({ state, payload }) => {
264
+ if (cancelled || closed || local.signal.aborted) return;
265
+ try {
266
+ const rendered = renderBoundary(boundary, state, payload, pass);
267
+ controller.enqueue(encoder.encode(patchChunk(boundary.id, rendered.html, rendered.styles, pass.cspNonce)));
268
+ for (const nested of rendered.boundaries) streamBoundary(controller, nested);
269
+ unsettled -= 1;
270
+ if (unsettled > 0) return;
271
+ const hydration = serializeHydrationRenderData(pass.data ?? void 0, runtime);
272
+ if (hydration) controller.enqueue(encoder.encode(hydration));
273
+ close(controller);
274
+ } catch (error) {
275
+ fail(controller, error);
276
+ }
277
+ });
278
+ };
255
279
  return new ReadableStream({
256
280
  start(controller) {
257
281
  controllerRef = controller;
258
- },
259
- async pull(controller) {
260
- if (cancelled || closed) return;
261
- if (initialPending) {
262
- initialPending = false;
263
- controller.enqueue(encoder.encode(stripHydrationPayload(initialHtml)));
282
+ if (signal.aborted) {
283
+ abort();
264
284
  return;
265
285
  }
266
- const boundary = boundaries[boundaryIndex];
267
- if (boundary) {
268
- boundaryIndex += 1;
269
- try {
270
- const value = await settleWithSignal(boundary.promise, local.signal);
271
- if (!cancelled && !closed) {
272
- const rendered = renderBoundary(boundary, "fulfilled", value, seed, data, cspNonce, authContext);
273
- controller.enqueue(encoder.encode(patchChunk(boundary.id, rendered.html, rendered.styles, cspNonce)));
274
- }
275
- } catch (error) {
276
- if (local.signal.aborted) {
277
- if (!cancelled && !closed) close(controller);
278
- return;
279
- }
280
- if (!cancelled && !closed) {
281
- const rendered = renderBoundary(boundary, "rejected", error, seed, data, cspNonce, authContext);
282
- controller.enqueue(encoder.encode(patchChunk(boundary.id, rendered.html, rendered.styles, cspNonce)));
283
- }
284
- }
285
- return;
286
- }
287
- const hydration = serializeHydrationRenderData(data ?? void 0, runtime);
288
- if (hydration) controller.enqueue(encoder.encode(hydration));
289
- close(controller);
286
+ controller.enqueue(encoder.encode(stripHydrationPayload(initialHtml)));
287
+ for (const boundary of boundaries) streamBoundary(controller, boundary);
290
288
  },
291
289
  cancel(reason) {
292
290
  cancelled = true;
@@ -350,6 +348,7 @@ async function renderRouteRequestInternal(options) {
350
348
  if (!resolved) return { kind: "no-match" };
351
349
  if (resolved.kind !== "render") return resolved;
352
350
  context.params = resolved.params;
351
+ if (context.hydrationData) context.hydrationData = withHydratedAuth(context.hydrationData, options.auth ?? manifest.auth, context.authContext);
353
352
  const sink = new StringSink();
354
353
  renderSSRRouteAppToSink({
355
354
  route: {
@@ -364,11 +363,23 @@ async function renderRouteRequestInternal(options) {
364
363
  sink.end();
365
364
  const html = sink.toString();
366
365
  const boundaries = [...context.deferredBoundaries];
366
+ if (boundaries.length > 0) dehydrateDataRuntime(runtime);
367
367
  const result = {
368
368
  kind: "render",
369
369
  html,
370
370
  styles: Array.from(context.ssrStyles.values()),
371
- ...boundaries.length > 0 ? { stream: createDeferredRenderStream(html, boundaries, signal, options.seed, context.hydrationData, runtime, cspNonce, context.authContext) } : {},
371
+ ...boundaries.length > 0 ? { stream: createDeferredRenderStream(html, boundaries, signal, {
372
+ seed: options.seed,
373
+ data: context.hydrationData,
374
+ cspNonce,
375
+ authContext: context.authContext,
376
+ route: {
377
+ url: context.url,
378
+ routes: context.routes,
379
+ basePath: context.basePath,
380
+ params: context.params
381
+ }
382
+ }, runtime) } : {},
372
383
  params: resolved.params
373
384
  };
374
385
  if (resolved.record) Object.defineProperty(result, "record", {
@@ -390,4 +401,4 @@ function renderToStream(opts) {
390
401
  renderRouteToStream(opts, routeRenderHost);
391
402
  }
392
403
  //#endregion
393
- export { resolveRequest as a, renderRouteRequestToString as i, renderToString as n, renderRouteRequest as r, renderToStream as t };
404
+ export { SSRAccessDecisionError as a, renderRouteRequestToString as i, renderToString as n, renderRouteRequest as r, renderToStream as t };