@askrjs/askr 0.0.70 → 0.0.71

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 (64) hide show
  1. package/capabilities.json +66 -0
  2. package/dist/actions/index.d.ts +1 -1
  3. package/dist/actions/index.js +9 -2
  4. package/dist/activity-C2tPn9Fr.js +152 -0
  5. package/dist/activity-DZ-NCYr8.d.ts +10 -0
  6. package/dist/{authoring-uaJ556aG.js → authoring-CXmUcaCe.js} +2 -2
  7. package/dist/benchmark.d.ts +1 -1
  8. package/dist/benchmark.js +6 -6
  9. package/dist/boot/index.d.ts +2 -2
  10. package/dist/boot/index.js +1 -1
  11. package/dist/{boot-8gP6mB3c.js → boot-DIKbEA4Y.js} +11 -10
  12. package/dist/{cleanup-B4BiQHx-.js → cleanup-D8Ufd9Ix.js} +16 -2
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/components/index.js +1 -0
  15. package/dist/control/index.d.ts +1 -1
  16. package/dist/control/index.js +1 -1
  17. package/dist/{control-CA45Yvvu.js → control-DVRBkN2X.js} +7 -2
  18. package/dist/{csp-nonce-HGZ0vbqT.js → csp-nonce-CwSRP3Jl.js} +16 -1
  19. package/dist/css-BuPaTOfe.js +70 -0
  20. package/dist/data/index.js +3 -3
  21. package/dist/{data-DHt_zO1G.js → data-DFcSmGvN.js} +2 -2
  22. package/dist/{data-runtime-SJ1BSUvk.js → data-runtime-rpV6-3XW.js} +13 -2
  23. package/dist/{deferred-zHtUheIQ.js → deferred-Cr58yIQD.js} +3 -4
  24. package/dist/foundations/index.d.ts +1 -1
  25. package/dist/foundations/index.js +1 -1
  26. package/dist/foundations/structures/index.d.ts +1 -1
  27. package/dist/foundations/structures/index.js +1 -1
  28. package/dist/{index-BFnTM1a8.d.ts → index-BOBtoY_r.d.ts} +34 -2
  29. package/dist/{index-DSClfSoC.d.ts → index-DxYJPVC4.d.ts} +1 -1
  30. package/dist/{index-Dh6ZF9yW.d.ts → index-UmwPZ8ZN.d.ts} +5 -0
  31. package/dist/{index-DjusBb1I.d.ts → index-u3KYoHlM.d.ts} +1 -1
  32. package/dist/index.d.ts +4 -5
  33. package/dist/index.js +12 -7
  34. package/dist/jsx-dev-runtime.d.ts +1 -0
  35. package/dist/jsx-runtime-C6Fs8Mmc.d.ts +2 -0
  36. package/dist/{jsx-runtime-aYZ8zb4q.d.ts → jsx-runtime-CL2r2HHQ.d.ts} +1 -0
  37. package/dist/jsx-runtime.d.ts +1 -1
  38. package/dist/{lifecycle-operations-SlPEK68H.js → lifecycle-operations-Db-_MG-O.js} +9 -12
  39. package/dist/{navigate-CV-RDqYP.d.ts → navigate-Cbq6QHVl.d.ts} +1 -1
  40. package/dist/{navigate-BTPrVhqI.js → navigate-CuGQzS_Y.js} +8 -104
  41. package/dist/{portal-D7TC986A.js → portal-wY3j0vvp.js} +59 -1
  42. package/dist/{query-registry-CQIT4mXr.js → query-registry-Bu_cuMch.js} +1 -1
  43. package/dist/{renderer-DF0hyjEr.js → renderer-BJYY2Y8s.js} +91 -10
  44. package/dist/resources/index.d.ts +27 -8
  45. package/dist/resources/index.js +218 -4
  46. package/dist/{route-matching-DLBWDeXX.js → route-matching-M-f1VQRu.js} +276 -2
  47. package/dist/router/index.d.ts +7 -7
  48. package/dist/router/index.js +28 -17
  49. package/dist/{router-C0XQA62W.d.ts → router-C3vENhUh.d.ts} +1 -1
  50. package/dist/{selector-DueeR6Oz.js → selector-DEfqO1rA.js} +8 -0
  51. package/dist/ssg/index.d.ts +8 -1
  52. package/dist/ssg/index.js +53 -17
  53. package/dist/ssr/index.d.ts +15 -1
  54. package/dist/ssr/index.js +1 -1
  55. package/dist/{ssr-vE-KUgDV.js → ssr-BSUA9Y64.js} +72 -78
  56. package/dist/testing/index.d.ts +35 -2
  57. package/dist/testing/index.js +182 -3
  58. package/dist/{verify-hydration-ChyEiBAn.js → verify-hydration-CVSGv6Vs.js} +1 -1
  59. package/package.json +4 -2
  60. package/dist/attr-names-CjhzmHOn.js +0 -15
  61. package/dist/component-scope-CvPba6O6.d.ts +0 -12
  62. package/dist/invalidation-listeners-Des3AHYs.js +0 -13
  63. package/dist/jsx-runtime-X2SX2y8V.d.ts +0 -2
  64. package/dist/resource-operation-BJiLoLYZ.js +0 -276
@@ -1,7 +1,9 @@
1
- import { C as lazy, a as resolveDeferredValues, b as Outlet, i as isDeferred, l as createRouteRegistry, m as resolveRouteRequest, n as Resolve, r as defer, s as routeData, y as currentAuth } from "../deferred-zHtUheIQ.js";
1
+ import { C as lazy, a as resolveDeferredValues, b as Outlet, i as isDeferred, l as createRouteRegistry, m as resolveRouteRequest, n as Resolve, r as defer, s as routeData, y as currentAuth } from "../deferred-Cr58yIQD.js";
2
2
  import { jsx } from "../jsx-runtime.js";
3
- import { a as route, i as page, n as group, r as index, t as fallback } from "../authoring-uaJ556aG.js";
4
- import { _ as currentRoute, a as serializeRouteMeta, c as getCurrentHref, d as parseTargetUrl, h as syncRegisteredRouteSnapshot, i as resolveRouteMeta, l as getRegisteredAppsSnapshot, m as syncAppRegistrationLocation, n as navigate, p as setCurrentRouteLocation, r as reconcileRouteMeta, s as saveScrollPosition, u as getWindowHref } from "../navigate-BTPrVhqI.js";
3
+ import { a as isSafeHref } from "../csp-nonce-CwSRP3Jl.js";
4
+ import { a as route, i as page, n as group, r as index, t as fallback } from "../authoring-CXmUcaCe.js";
5
+ import { n as onRouteChange, t as currentRoute } from "../activity-C2tPn9Fr.js";
6
+ import { a as serializeRouteMeta, c as getCurrentHref, d as parseTargetUrl, h as syncRegisteredRouteSnapshot, i as resolveRouteMeta, l as getRegisteredAppsSnapshot, m as syncAppRegistrationLocation, n as navigate, p as setCurrentRouteLocation, r as reconcileRouteMeta, s as saveScrollPosition, u as getWindowHref } from "../navigate-CuGQzS_Y.js";
5
7
  import { o as mergeProps } from "../compose-ref-Cc_YkUpM.js";
6
8
  import { t as applyInteractionPolicy } from "../interactions-w7gQN4ha.js";
7
9
  //#region src/router/route-query.ts
@@ -162,22 +164,31 @@ function isSameOriginNavigableHref(href) {
162
164
  * - Keyboard handling automatic
163
165
  * - Composable via mergeProps
164
166
  */
165
- function Link({ href: suppliedHref, to, class: className, children, rel, target, "aria-current": ariaCurrent, "aria-label": ariaLabel, ...rest }) {
167
+ function Link({ href: suppliedHref, to, class: className, children, rel, target, "aria-current": ariaCurrent, "aria-label": ariaLabel, onPress, onClick, ...rest }) {
166
168
  const href = to?.href ?? suppliedHref;
167
169
  if (!href) throw new Error("Link requires href or to.");
170
+ if (!isSafeHref(href)) throw new TypeError("Link href uses an unsafe URL scheme.");
171
+ const handleNavigation = (e) => {
172
+ if (e.defaultPrevented) return;
173
+ const event = e;
174
+ if ((event.button ?? 0) !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
175
+ if (target || !isSameOriginNavigableHref(href)) return;
176
+ event.preventDefault();
177
+ navigate(href);
178
+ };
179
+ const interaction = applyInteractionPolicy({
180
+ isNative: true,
181
+ disabled: false,
182
+ onPress: handleNavigation
183
+ });
184
+ const consumerActivation = (e) => {
185
+ onPress?.(e);
186
+ onClick?.(e);
187
+ };
168
188
  return /* @__PURE__ */ jsx("a", {
169
- ...mergeProps(rest, {
170
- ...applyInteractionPolicy({
171
- isNative: true,
172
- disabled: false,
173
- onPress: (e) => {
174
- const event = e;
175
- if ((event.button ?? 0) !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
176
- if (target || !isSameOriginNavigableHref(href)) return;
177
- event.preventDefault();
178
- navigate(href);
179
- }
180
- }),
189
+ ...mergeProps(interaction, {
190
+ ...rest,
191
+ onClick: consumerActivation,
181
192
  href,
182
193
  class: className,
183
194
  rel,
@@ -189,4 +200,4 @@ function Link({ href: suppliedHref, to, class: className, children, rel, target,
189
200
  });
190
201
  }
191
202
  //#endregion
192
- export { Link, Outlet, Resolve, allow, createRouteRegistry, currentAuth, currentRoute, defer, deny, fallback, forbidden, group, index, isDeferred, lazy, navigate, notFound, page, reconcileRouteMeta, redirect, resolveDeferredValues, resolveRouteMeta, resolveRouteRequest, route, routeData, serializeRouteMeta, to, unauthorized, updateRouteQuery };
203
+ export { Link, Outlet, Resolve, allow, createRouteRegistry, currentAuth, currentRoute, defer, deny, fallback, forbidden, group, index, isDeferred, lazy, navigate, notFound, onRouteChange, page, reconcileRouteMeta, redirect, resolveDeferredValues, resolveRouteMeta, resolveRouteRequest, route, routeData, serializeRouteMeta, to, unauthorized, updateRouteQuery };
@@ -1,6 +1,6 @@
1
1
  import { c as RenderableChild } from "./control-DO-iBEtV.js";
2
2
  import { D as QueryPrefetchContext, u as CoreTelemetry } from "./index-Bki2rqcr.js";
3
- import { t as ActionDescriptor } from "./index-DSClfSoC.js";
3
+ import { t as ActionDescriptor } from "./index-DxYJPVC4.js";
4
4
  import { AuthContext, AuthRequirement } from "@askrjs/auth";
5
5
  import { InferSchema, ObjectSchema } from "@askrjs/schema";
6
6
  //#region src/common/router.d.ts
@@ -261,6 +261,14 @@ function getOrCreateSelectorHook(instance, hookIndex, source, equals) {
261
261
  store.set(hookIndex, created);
262
262
  return created;
263
263
  }
264
+ /**
265
+ * Creates a render-scoped predicate for keyed membership in reactive list rows.
266
+ *
267
+ * Use this when a `<For>` child needs to compare each stable item with a
268
+ * changing selected value. Unlike a plain closure capture, the predicate
269
+ * subscribes the affected rows and updates them without rebuilding the list.
270
+ * Must be called during component render.
271
+ */
264
272
  function selector(source, equals = Object.is) {
265
273
  markReadableUsage(source);
266
274
  const instance = getCurrentInstance();
@@ -1,5 +1,5 @@
1
1
  import { d as DocumentRenderContext, f as DocumentRenderer, u as DocumentRenderArgs } from "../control-DO-iBEtV.js";
2
- import { D as RoutePathParams, N as RouteRegistry, O as RoutePolicy, T as RouteOptions, y as RouteHandler } from "../router-C0XQA62W.js";
2
+ import { D as RoutePathParams, N as RouteRegistry, O as RoutePolicy, T as RouteOptions, y as RouteHandler } from "../router-C3vENhUh.js";
3
3
  import { AuthRequirement } from "@askrjs/auth";
4
4
  import "node:fs/promises";
5
5
  //#region src/ssg/types.d.ts
@@ -105,6 +105,13 @@ interface RouteRenderResult {
105
105
  written: boolean;
106
106
  /** Error message if rendering failed */
107
107
  error?: string;
108
+ /** Original exception preserved for programmatic diagnostics. */
109
+ errorCause?: unknown;
110
+ /** Route and phase context for the original exception. */
111
+ errorContext?: {
112
+ route: string;
113
+ phase: 'load' | 'render' | 'write';
114
+ };
108
115
  }
109
116
  /** Overall result from SSG generation */
110
117
  interface SSGResult {
package/dist/ssg/index.js CHANGED
@@ -1,14 +1,18 @@
1
1
  import "../access-ePqdQqkV.js";
2
- import { S as getRenderHandler, a as resolveDeferredValues, l as createRouteRegistry, u as bindResolvedRouteData } from "../deferred-zHtUheIQ.js";
3
- import { a as route } from "../authoring-uaJ556aG.js";
2
+ import { S as getRenderHandler, a as resolveDeferredValues, l as createRouteRegistry, u as bindResolvedRouteData } from "../deferred-Cr58yIQD.js";
3
+ import { a as route } from "../authoring-CXmUcaCe.js";
4
4
  import { n as renderDocument } from "../ssr-CbQO5xd3.js";
5
- import { n as renderToString } from "../ssr-vE-KUgDV.js";
5
+ import { n as renderToString } from "../ssr-BSUA9Y64.js";
6
6
  import * as fs from "node:fs/promises";
7
7
  import * as pathModule from "node:path";
8
8
  import * as os from "node:os";
9
9
  import * as fsSync from "node:fs";
10
10
  //#region src/ssg/route-utils.ts
11
+ function assertSafeRoutePath(pathStr) {
12
+ if (!pathStr.startsWith("/") || pathStr.startsWith("//") || pathStr.includes("\\") || pathStr.includes("\0") || pathStr.split("/").some((segment) => segment === "." || segment === "..")) throw new Error(`SSG route path must be an absolute URL path without dot segments or backslashes: ${pathStr}`);
13
+ }
11
14
  function getOutputFilePath(pathStr) {
15
+ assertSafeRoutePath(pathStr);
12
16
  if (pathStr === "/") return "index.html";
13
17
  return `${pathStr.replace(/^\/|\/$/g, "")}/index.html`;
14
18
  }
@@ -153,6 +157,7 @@ async function batchRenderRoutes(routes, options = {}) {
153
157
  const baseData = resolvedData.hasData ? resolvedData.data : void 0;
154
158
  const resourceCount = resolvedData.hasData && baseData ? Object.keys(baseData).length : 0;
155
159
  const mergedHandler = route$2.handler;
160
+ let phase = route$2.loader ? "load" : "render";
156
161
  try {
157
162
  let routeData;
158
163
  let hasRouteData = false;
@@ -182,11 +187,13 @@ async function batchRenderRoutes(routes, options = {}) {
182
187
  hasRouteData = true;
183
188
  }
184
189
  const renderedHandler = hasRouteData ? bindResolvedRouteData(mergedHandler, routeData) : mergedHandler;
190
+ const registry = hasRouteData || !sharedRegistry ? createRouteRegistry(() => {
191
+ route(route$2.path, renderedHandler, { namespace: route$2.namespace });
192
+ }) : sharedRegistry;
193
+ phase = "render";
185
194
  const html = renderToString({
186
195
  url,
187
- registry: hasRouteData || !sharedRegistry ? createRouteRegistry(() => {
188
- route(route$2.path, renderedHandler, { namespace: route$2.namespace });
189
- }) : sharedRegistry,
196
+ registry,
190
197
  seed,
191
198
  data: baseData,
192
199
  document: document === void 0 ? void 0 : ({ appHtml }) => renderDocument(document, {
@@ -231,7 +238,12 @@ async function batchRenderRoutes(routes, options = {}) {
231
238
  status: "error",
232
239
  reason: "full",
233
240
  written: false,
234
- error: error instanceof Error ? error.message : String(error)
241
+ error: error instanceof Error ? error.message : String(error),
242
+ errorCause: error,
243
+ errorContext: {
244
+ route: url,
245
+ phase
246
+ }
235
247
  };
236
248
  }
237
249
  };
@@ -247,6 +259,15 @@ async function batchRenderRoutes(routes, options = {}) {
247
259
  return results;
248
260
  }
249
261
  //#endregion
262
+ //#region src/ssg/output-path.ts
263
+ function resolveSsgOutputPath(outputDir, filePath) {
264
+ const root = pathModule.resolve(outputDir);
265
+ const candidate = pathModule.resolve(root, filePath);
266
+ const relative = pathModule.relative(root, candidate);
267
+ if (!relative || relative === ".." || relative.startsWith(`..${pathModule.sep}`) || pathModule.isAbsolute(relative)) throw new Error(`SSG output path must stay inside outputDir: ${filePath}`);
268
+ return candidate;
269
+ }
270
+ //#endregion
250
271
  //#region src/ssg/write-static-files.ts
251
272
  /**
252
273
  * File I/O for Static Site Generation
@@ -262,17 +283,22 @@ async function writeStaticFiles(results, outputDir, options = {}, fileOperations
262
283
  await fileOperations.mkdir(outputDir, { recursive: true });
263
284
  for (const result of results) {
264
285
  if (result.status !== "removed") continue;
265
- const fullPath = pathModule.join(outputDir, result.filePath);
286
+ const fullPath = resolveSsgOutputPath(outputDir, result.filePath);
266
287
  if (fsSync.existsSync(fullPath)) {
267
288
  await fileOperations.rm(fullPath, { force: true });
268
289
  await pruneEmptyDirs(pathModule.dirname(fullPath), outputDir, fileOperations);
269
290
  }
270
291
  }
271
292
  const pendingWrites = [];
293
+ const reportedErrors = /* @__PURE__ */ new Set();
272
294
  for (let index = 0; index < results.length; index += 1) {
273
295
  const result = results[index];
274
296
  if (result.status === "error") {
275
- console.warn(`Skipping failed route: ${result.path} - ${result.error}`);
297
+ const diagnostic = `Skipping failed route: ${result.path} - ${result.error}`;
298
+ if (!reportedErrors.has(diagnostic)) {
299
+ reportedErrors.add(diagnostic);
300
+ console.warn(diagnostic);
301
+ }
276
302
  continue;
277
303
  }
278
304
  if (result.status === "success" && result.written) pendingWrites.push(result);
@@ -280,7 +306,7 @@ async function writeStaticFiles(results, outputDir, options = {}, fileOperations
280
306
  const directories = [];
281
307
  const seenDirectories = /* @__PURE__ */ new Set();
282
308
  for (const result of pendingWrites) {
283
- const dir = pathModule.dirname(pathModule.join(outputDir, result.filePath));
309
+ const dir = pathModule.dirname(resolveSsgOutputPath(outputDir, result.filePath));
284
310
  if (!seenDirectories.has(dir)) {
285
311
  seenDirectories.add(dir);
286
312
  directories.push(dir);
@@ -295,7 +321,7 @@ async function writeStaticFiles(results, outputDir, options = {}, fileOperations
295
321
  nextIndex += 1;
296
322
  if (current >= pendingWrites.length) return;
297
323
  const result = pendingWrites[current];
298
- const fullPath = pathModule.join(outputDir, result.filePath);
324
+ const fullPath = resolveSsgOutputPath(outputDir, result.filePath);
299
325
  const tempPath = pathModule.join(pathModule.dirname(fullPath), `.${pathModule.basename(fullPath)}.askr-${process.pid}-${current}.tmp`);
300
326
  try {
301
327
  await fileOperations.writeFile(tempPath, result.html, "utf8");
@@ -310,11 +336,12 @@ async function writeStaticFiles(results, outputDir, options = {}, fileOperations
310
336
  async function pruneEmptyDirs(startDir, rootDir, fileOperations) {
311
337
  let current = startDir;
312
338
  const normalizedRoot = pathModule.resolve(rootDir);
313
- while (current.startsWith(normalizedRoot)) {
339
+ while (pathModule.resolve(current) !== normalizedRoot) {
340
+ const relative = pathModule.relative(normalizedRoot, current);
341
+ if (relative === ".." || relative.startsWith(`..${pathModule.sep}`) || pathModule.isAbsolute(relative)) break;
314
342
  if (!fsSync.existsSync(current)) break;
315
343
  if ((await fileOperations.readdir(current)).length > 0) break;
316
344
  await fileOperations.rmdir(current);
317
- if (pathModule.resolve(current) === normalizedRoot) break;
318
345
  current = pathModule.dirname(current);
319
346
  }
320
347
  }
@@ -418,7 +445,7 @@ function hashHtml(html) {
418
445
  return (hash >>> 0).toString(16).padStart(8, "0");
419
446
  }
420
447
  function getIncrementalManifestPath(outputDir) {
421
- return pathModule.join(outputDir, MANIFEST_DIR, MANIFEST_FILE);
448
+ return resolveSsgOutputPath(outputDir, pathModule.join(MANIFEST_DIR, MANIFEST_FILE));
422
449
  }
423
450
  function readIncrementalManifest(outputDir, seed) {
424
451
  const manifestPath = getIncrementalManifestPath(outputDir);
@@ -427,7 +454,7 @@ function readIncrementalManifest(outputDir, seed) {
427
454
  const raw = fsSync.readFileSync(manifestPath, "utf8");
428
455
  const parsed = JSON.parse(raw);
429
456
  if (parsed.schemaVersion !== 1 || parsed.seed !== seed || !Array.isArray(parsed.routes)) return null;
430
- const routes = parsed.routes.filter((entry) => !!entry && typeof entry.routeId === "string" && typeof entry.path === "string" && typeof entry.filePath === "string" && Array.isArray(entry.invalidationKeys) && (entry.htmlHash === null || typeof entry.htmlHash === "string") && (entry.lastStatus === "success" || entry.lastStatus === "error"));
457
+ const routes = parsed.routes.filter((entry) => !!entry && typeof entry.routeId === "string" && typeof entry.path === "string" && typeof entry.filePath === "string" && isContainedManifestFilePath(outputDir, entry.filePath) && Array.isArray(entry.invalidationKeys) && (entry.htmlHash === null || typeof entry.htmlHash === "string") && (entry.lastStatus === "success" || entry.lastStatus === "error"));
431
458
  if (routes.length !== parsed.routes.length) return null;
432
459
  return {
433
460
  schemaVersion: 1,
@@ -439,13 +466,22 @@ function readIncrementalManifest(outputDir, seed) {
439
466
  return null;
440
467
  }
441
468
  }
469
+ function isContainedManifestFilePath(outputDir, filePath) {
470
+ try {
471
+ resolveSsgOutputPath(outputDir, filePath);
472
+ return true;
473
+ } catch {
474
+ return false;
475
+ }
476
+ }
442
477
  async function writeIncrementalManifest(manifest, outputDir) {
478
+ if (!manifest.routes.every((entry) => isContainedManifestFilePath(outputDir, entry.filePath))) throw new Error("SSG manifest contains a path outside outputDir.");
443
479
  const manifestPath = getIncrementalManifestPath(outputDir);
444
480
  await fs.mkdir(pathModule.dirname(manifestPath), { recursive: true });
445
481
  await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf8");
446
482
  }
447
483
  function getExistingOutputFileSize(outputDir, filePath) {
448
- const fullPath = pathModule.join(outputDir, filePath);
484
+ const fullPath = resolveSsgOutputPath(outputDir, filePath);
449
485
  if (!fsSync.existsSync(fullPath)) return 0;
450
486
  try {
451
487
  return fsSync.statSync(fullPath).size;
@@ -454,7 +490,7 @@ function getExistingOutputFileSize(outputDir, filePath) {
454
490
  }
455
491
  }
456
492
  function outputFileExists(outputDir, filePath) {
457
- return fsSync.existsSync(pathModule.join(outputDir, filePath));
493
+ return fsSync.existsSync(resolveSsgOutputPath(outputDir, filePath));
458
494
  }
459
495
  //#endregion
460
496
  //#region src/ssg/generation-plan.ts
@@ -1,7 +1,7 @@
1
1
  import { c as RenderableChild, d as DocumentRenderContext, f as DocumentRenderer, p as SSRData, u as DocumentRenderArgs } from "../control-DO-iBEtV.js";
2
2
  import { r as JSXElement, s as Props } from "../jsx-Dvr2SGRG.js";
3
3
  import { D as QueryPrefetchContext, n as ServerQueryRegistry, u as CoreTelemetry, v as DataRuntime } from "../index-Bki2rqcr.js";
4
- import { A as RouteRecord, L as RouteRequestResult, N as RouteRegistry, a as AccessRedirectDecision, f as Route, p as RouteAuthOptions, r as AccessDenyDecision, y as RouteHandler } from "../router-C0XQA62W.js";
4
+ import { A as RouteRecord, L as RouteRequestResult, N as RouteRegistry, a as AccessRedirectDecision, f as Route, p as RouteAuthOptions, r as AccessDenyDecision, y as RouteHandler } from "../router-C3vENhUh.js";
5
5
  import { AuthContext } from "@askrjs/auth";
6
6
  //#region src/common/page-render-envelope.d.ts
7
7
  declare const PAGE_RENDER_ENVELOPE_VERSION: 1;
@@ -19,6 +19,19 @@ interface DeferredBoundaryRegistration {
19
19
  fulfilled(value: unknown): RenderableChild;
20
20
  rejected(error: unknown): RenderableChild;
21
21
  }
22
+ interface SSRPortalHostRegistration {
23
+ token: string;
24
+ automatic: boolean;
25
+ }
26
+ interface SSRPortalSlot {
27
+ hasValue: boolean;
28
+ value: RenderableChild | undefined;
29
+ hosts: SSRPortalHostRegistration[];
30
+ }
31
+ interface SSRPortalState {
32
+ slots: Map<object, SSRPortalSlot>;
33
+ nextHostId: number;
34
+ }
22
35
  //#endregion
23
36
  //#region src/common/ssr-errors.d.ts
24
37
  /**
@@ -49,6 +62,7 @@ interface RenderContext {
49
62
  renderData: PageRenderEnvelope | null;
50
63
  hydrationData: PageRenderEnvelope | null;
51
64
  deferredBoundaries: DeferredBoundaryRegistration[];
65
+ ssrPortals: SSRPortalState;
52
66
  cspNonce?: string;
53
67
  }
54
68
  declare function createRenderContext(seed?: number, opts?: {
package/dist/ssr/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { u as SSRDataMissingError } from "../render-context-Clw3rFiO.js";
2
- import { a as renderResolvedToStringSync, c as createRenderContext, d as withRenderContextAsync, i as renderRouteRequestToString, l as getRenderContext, n as renderToString, o as renderToStringSync, r as renderRouteRequest, s as resolveRequest, t as renderToStream, u as withRenderContext } from "../ssr-vE-KUgDV.js";
2
+ import { a as renderResolvedToStringSync, c as createRenderContext, d as withRenderContextAsync, i as renderRouteRequestToString, l as getRenderContext, n as renderToString, o as renderToStringSync, r as renderRouteRequest, s as resolveRequest, t as renderToStream, u as withRenderContext } from "../ssr-BSUA9Y64.js";
3
3
  export { SSRDataMissingError, createRenderContext, getRenderContext, renderResolvedToStringSync, renderRouteRequest, renderRouteRequestToString, renderToStream, renderToString, renderToStringSync, resolveRequest, withRenderContext, withRenderContextAsync };
@@ -7,15 +7,15 @@ import { f as getCurrentComponentInstance, w as setCurrentComponentInstance } fr
7
7
  import { t as cleanupComponent } from "./component-cleanup-1ZFoLLHt.js";
8
8
  import { p as isPromiseLike } from "./lifecycle-batch-COgl8jMS.js";
9
9
  import { g as evaluateShowState, h as evaluateCaseState, i as evaluateForState, o as rollbackForStateTransaction, r as commitForStateTransaction } from "./for-internal-BtK9Htl0.js";
10
- import { _ as isPageRenderEnvelope, g as isEmptyPageRenderEnvelope, h as createPageRenderEnvelope, i as isDeferred, l as createRouteRegistry, m as resolveRouteRequest, t as DEFERRED_BOUNDARY } from "./deferred-zHtUheIQ.js";
10
+ import { _ as isPageRenderEnvelope, g as isEmptyPageRenderEnvelope, h as createPageRenderEnvelope, i as isDeferred, l as createRouteRegistry, m as resolveRouteRequest, t as DEFERRED_BOUNDARY } from "./deferred-Cr58yIQD.js";
11
11
  import { c as stopRenderPhase$1, o as startRenderPhase$1, t as configureRenderContextProvider, u as SSRDataMissingError } from "./render-context-Clw3rFiO.js";
12
- import { a as disposeDefaultPortalScope, t as DefaultPortal } from "./portal-D7TC986A.js";
13
- import { t as getPublicAttributeName } from "./attr-names-CjhzmHOn.js";
14
- import { r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-HGZ0vbqT.js";
15
- import { t as createDataRuntime } from "./data-runtime-SJ1BSUvk.js";
16
- import { c as withTelemetry, i as dehydrateDataRuntime, t as createQueryPrefetchContext } from "./query-registry-CQIT4mXr.js";
17
- import { a as resolveRouteFromRoutes, t as _resolveRouteMatchFromRoutes } from "./route-matching-DLBWDeXX.js";
18
- import { a as route } from "./authoring-uaJ556aG.js";
12
+ import { a as resolveRouteFromRoutes, t as _resolveRouteMatchFromRoutes } from "./route-matching-M-f1VQRu.js";
13
+ import { a as disposeDefaultPortalScope, o as SSR_PORTAL_HOST, t as DefaultPortal } from "./portal-wY3j0vvp.js";
14
+ import { n as getPublicAttributeName, t as sanitizeCssValue } from "./css-BuPaTOfe.js";
15
+ import { a as isSafeHref, r as validateCspNonce, t as CspNonceScope } from "./csp-nonce-CwSRP3Jl.js";
16
+ import { t as createDataRuntime } from "./data-runtime-rpV6-3XW.js";
17
+ import { c as withTelemetry, i as dehydrateDataRuntime, t as createQueryPrefetchContext } from "./query-registry-Bu_cuMch.js";
18
+ import { a as route } from "./authoring-CXmUcaCe.js";
19
19
  import { n as renderDocument, t as SSR_RENDER_DATA_ATTR } from "./ssr-CbQO5xd3.js";
20
20
  //#region src/ssr/escape.ts
21
21
  /**
@@ -42,51 +42,6 @@ const VOID_ELEMENTS = /* @__PURE__ */ new Set([
42
42
  ]);
43
43
  const escapeCache = /* @__PURE__ */ new Map();
44
44
  const MAX_CACHE_SIZE = 256;
45
- const CSS_UNSAFE_RE = /[{}<>\\]/g;
46
- const CSS_URI_SCHEME_RE = /(?:^|[\s(,])([a-z][a-z0-9+.-]*):/i;
47
- const CSS_FUNCTION_NAME_RE = /([a-z-][a-z0-9-]*)\s*\(/gi;
48
- const CSS_ALLOWED_FUNCTIONS = /* @__PURE__ */ new Set([
49
- "var",
50
- "calc",
51
- "min",
52
- "max",
53
- "clamp",
54
- "rgb",
55
- "rgba",
56
- "hsl",
57
- "hsla",
58
- "lab",
59
- "lch",
60
- "oklab",
61
- "oklch",
62
- "color",
63
- "color-mix",
64
- "translate",
65
- "translatex",
66
- "translatey",
67
- "translatez",
68
- "scale",
69
- "scalex",
70
- "scaley",
71
- "scalez",
72
- "rotate",
73
- "rotatex",
74
- "rotatey",
75
- "rotatez",
76
- "skew",
77
- "skewx",
78
- "skewy",
79
- "matrix",
80
- "matrix3d",
81
- "linear-gradient",
82
- "radial-gradient",
83
- "conic-gradient",
84
- "repeating-linear-gradient",
85
- "repeating-radial-gradient",
86
- "repeating-conic-gradient",
87
- "cubic-bezier",
88
- "steps"
89
- ]);
90
45
  const STYLE_PROP_CACHE = /* @__PURE__ */ new Map();
91
46
  const MAX_STYLE_PROP_CACHE_SIZE = 512;
92
47
  function toKebabCached(prop) {
@@ -178,19 +133,7 @@ function escapeAttr(value) {
178
133
  return lastIndex === 0 ? value : escaped + value.slice(lastIndex);
179
134
  }
180
135
  /**
181
- * Escape CSS value to prevent injection attacks.
182
- * Removes characters that could break out of CSS context.
183
- */
184
- function escapeCssValue(value) {
185
- const str = String(value);
186
- if (CSS_URI_SCHEME_RE.test(str)) return "";
187
- if (str.includes("(")) {
188
- for (const match of str.matchAll(CSS_FUNCTION_NAME_RE)) if (!CSS_ALLOWED_FUNCTIONS.has(match[1].toLowerCase())) return "";
189
- }
190
- return str.replace(CSS_UNSAFE_RE, "");
191
- }
192
- /**
193
- * Convert style object to CSS string with value escaping
136
+ * Convert a style object to a CSS string, omitting unsafe values.
194
137
  * Optimized to avoid Object.entries allocation
195
138
  */
196
139
  function styleObjToCss(value) {
@@ -201,7 +144,7 @@ function styleObjToCss(value) {
201
144
  const v = styleObj[k];
202
145
  if (v === null || v === void 0 || v === false) continue;
203
146
  const prop = toKebabCached(k);
204
- const safeValue = escapeCssValue(String(v));
147
+ const safeValue = sanitizeCssValue(String(v));
205
148
  if (safeValue) result += `${prop}:${safeValue};`;
206
149
  }
207
150
  return result || null;
@@ -264,6 +207,10 @@ function createRenderContext(seed = 12345, opts = {}) {
264
207
  renderData: envelope,
265
208
  hydrationData: envelope,
266
209
  deferredBoundaries: [],
210
+ ssrPortals: {
211
+ slots: /* @__PURE__ */ new Map(),
212
+ nextHostId: 0
213
+ },
267
214
  cspNonce: opts.cspNonce
268
215
  };
269
216
  }
@@ -310,13 +257,7 @@ async function getAsyncRenderContextAccessor() {
310
257
  async function withRenderContextAsync(ctx, fn) {
311
258
  const accessor = await getAsyncRenderContextAccessor();
312
259
  if (accessor) return accessor.run(ctx, () => Promise.resolve(fn()));
313
- const previous = fallbackStack;
314
- fallbackStack = ctx;
315
- try {
316
- return await fn();
317
- } finally {
318
- fallbackStack = previous;
319
- }
260
+ throw new Error(FALLBACK_ASYNC_CONTEXT_ERROR);
320
261
  }
321
262
  /**
322
263
  * Get the current render context.
@@ -833,6 +774,7 @@ function renderAttrsDirect(props, sink) {
833
774
  }
834
775
  if (value === false || value === null || value === void 0) continue;
835
776
  const strValue = String(value);
777
+ if (attrName.toLowerCase() === "href" && !isSafeHref(strValue)) continue;
836
778
  sink.write(" ");
837
779
  sink.write(attrName);
838
780
  sink.write("=\"");
@@ -867,6 +809,7 @@ function renderAttrs(props, opts) {
867
809
  else if (value === false || value === null || value === void 0) continue;
868
810
  else {
869
811
  const strValue = String(value);
812
+ if (attrName.toLowerCase() === "href" && !isSafeHref(strValue)) continue;
870
813
  attrParts.push(` ${attrName}="${getEscapedAttrValue(strValue)}"`);
871
814
  }
872
815
  }
@@ -969,6 +912,47 @@ function renderRenderableSync(value, ctx) {
969
912
  if (value && typeof value === "object" && "type" in value) return renderNodeSync(value, ctx);
970
913
  return "";
971
914
  }
915
+ function resolveSSRPortals(html, ctx) {
916
+ let resolved = html;
917
+ const renderedHosts = /* @__PURE__ */ new Set();
918
+ for (;;) {
919
+ let foundHost = false;
920
+ for (const slot of ctx.ssrPortals.slots.values()) {
921
+ const explicitHosts = slot.hosts.filter((host) => !host.automatic);
922
+ const activeHosts = explicitHosts.length > 0 ? new Set(explicitHosts.map((host) => host.token)) : new Set(slot.hosts.map((host) => host.token));
923
+ for (const host of slot.hosts) {
924
+ if (renderedHosts.has(host.token)) continue;
925
+ foundHost = true;
926
+ const content = activeHosts.has(host.token) && slot.hasValue ? renderRenderableSync(slot.value, ctx) : "";
927
+ resolved = resolved.replace(host.token, () => content);
928
+ renderedHosts.add(host.token);
929
+ }
930
+ }
931
+ if (!foundHost) return resolved;
932
+ }
933
+ }
934
+ var SSRPortalSink = class {
935
+ constructor(sink) {
936
+ this.sink = sink;
937
+ this.bufferedChunks = null;
938
+ }
939
+ write(html) {
940
+ if (!html) return;
941
+ if (this.bufferedChunks) {
942
+ this.bufferedChunks.push(html);
943
+ return;
944
+ }
945
+ this.sink.write(html);
946
+ }
947
+ writePortalHost(token) {
948
+ this.bufferedChunks ??= [];
949
+ this.bufferedChunks.push(token);
950
+ }
951
+ flush(ctx) {
952
+ if (!this.bufferedChunks) return;
953
+ this.sink.write(resolveSSRPortals(this.bufferedChunks.join(""), ctx));
954
+ }
955
+ };
972
956
  function renderChildSync(child, ctx) {
973
957
  return renderRenderableSync(child, ctx);
974
958
  }
@@ -1037,6 +1021,7 @@ function renderNodeSync(node, ctx) {
1037
1021
  const { type, props } = node;
1038
1022
  if (typeof type === "function") return renderRenderableSync(inheritRenderableKey(node, executeComponentSync(type, props, ctx, getVNodeContextFrame(node) ?? null)), ctx);
1039
1023
  if (typeof type === "symbol") {
1024
+ if (type === SSR_PORTAL_HOST) return String(props?.token ?? "");
1040
1025
  if (type === Fragment) return renderChildrenSync(getRenderableChildren(node), ctx);
1041
1026
  if (type === __CONTROL_BOUNDARY__) return renderControlChildrenSync(node, ctx);
1042
1027
  if (type === DEFERRED_BOUNDARY) return renderDeferredBoundarySync(node, ctx);
@@ -1076,6 +1061,13 @@ function renderNodeSyncToSink(node, sink, ctx) {
1076
1061
  return;
1077
1062
  }
1078
1063
  if (typeof type === "symbol") {
1064
+ if (type === SSR_PORTAL_HOST) {
1065
+ const token = String(props?.token ?? "");
1066
+ const portalSink = sink;
1067
+ if (portalSink.writePortalHost) portalSink.writePortalHost(token);
1068
+ else sink.write(token);
1069
+ return;
1070
+ }
1079
1071
  if (type === Fragment) {
1080
1072
  renderChildrenSyncToSink(getRenderableChildren(node), sink, ctx);
1081
1073
  return;
@@ -1182,9 +1174,8 @@ function renderToStringSync(component, props, options) {
1182
1174
  if (!node) throw new Error("renderToStringSync: wrapped component returned empty");
1183
1175
  const sink = new StringSink();
1184
1176
  renderNodeSyncToSink(node, sink, ctx);
1185
- sink.write(serializeHydrationRenderData(ctx.hydrationData ?? void 0, ctx.dataRuntime));
1186
1177
  sink.end();
1187
- return sink.toString();
1178
+ return resolveSSRPortals(sink.toString(), ctx) + serializeHydrationRenderData(ctx.hydrationData ?? void 0, ctx.dataRuntime);
1188
1179
  } finally {
1189
1180
  try {
1190
1181
  stopRenderPhase();
@@ -1199,10 +1190,13 @@ function renderSSRRouteAppToSink(input) {
1199
1190
  withRenderContext(ctx, () => {
1200
1191
  startRenderPhase(ctx.renderData);
1201
1192
  try {
1202
- renderRenderableSyncToSink(executeComponentSync(ctx.cspNonce === void 0 ? route.handler : () => CspNonceScope({
1193
+ const app = executeComponentSync(ctx.cspNonce === void 0 ? route.handler : () => CspNonceScope({
1203
1194
  value: ctx.cspNonce,
1204
1195
  children: () => route.handler(params)
1205
- }), params, ctx), sink, ctx);
1196
+ }), params, ctx);
1197
+ const appSink = new SSRPortalSink(sink);
1198
+ renderRenderableSyncToSink(wrapWithDefaultPortal(app), appSink, ctx);
1199
+ appSink.flush(ctx);
1206
1200
  if (ctx.deferredBoundaries.length === 0) sink.write(serializeHydrationRenderData(ctx.hydrationData ?? data, ctx.dataRuntime));
1207
1201
  } finally {
1208
1202
  try {
@@ -1,5 +1,38 @@
1
+ import { s as ComponentFunction } from "../control-DO-iBEtV.js";
1
2
  import { C as Query, k as QueryStaleReason } from "../index-Bki2rqcr.js";
2
- import { N as RouteRegistry, x as RouteMatch } from "../router-C0XQA62W.js";
3
+ import { N as RouteRegistry, p as RouteAuthOptions, x as RouteMatch } from "../router-C3vENhUh.js";
4
+ //#region src/testing/render.d.ts
5
+ interface RenderOptions {
6
+ /**
7
+ * Existing element to own for the duration of the render. When omitted, the
8
+ * harness appends a managed `<div>` to `document.body`.
9
+ */
10
+ container?: HTMLElement;
11
+ /** Surface lifecycle cleanup errors during unmount. */
12
+ cleanupStrict?: boolean;
13
+ }
14
+ interface RouteRenderOptions extends RenderOptions {
15
+ registry: RouteRegistry;
16
+ /** Initial path, query, and hash for the routed render. */
17
+ url?: string;
18
+ auth?: RouteAuthOptions;
19
+ }
20
+ type DispatchEventInit = EventInit | UIEventInit | FocusEventInit | MouseEventInit | WheelEventInit | KeyboardEventInit | InputEventInit | CompositionEventInit | PointerEventInit | TouchEventInit | DragEventInit;
21
+ interface RenderResult {
22
+ readonly container: HTMLElement;
23
+ readonly root: HTMLElement;
24
+ flush(): void;
25
+ dispatch(target: EventTarget, event: Event | string, init?: DispatchEventInit): boolean;
26
+ unmount(): void;
27
+ cleanup(): void;
28
+ }
29
+ declare function flush(): void;
30
+ declare function dispatch(target: EventTarget, event: Event | string, init?: DispatchEventInit): boolean;
31
+ declare function cleanup(target: RenderResult | HTMLElement): void;
32
+ declare function render(component: ComponentFunction, options?: RenderOptions): RenderResult;
33
+ declare function mount(component: ComponentFunction, options?: RenderOptions): RenderResult;
34
+ declare function renderRoute(options: RouteRenderOptions): Promise<RenderResult>;
35
+ //#endregion
3
36
  //#region src/testing/index.d.ts
4
37
  type MockRefresh = () => void | Promise<void>;
5
38
  interface MockQueryOptions {
@@ -47,4 +80,4 @@ declare function createInvalidationRecorder(): InvalidationRecorder;
47
80
  declare function matchRoute(path: string, options: MatchRouteOptions): RouteMatch | null;
48
81
  declare function getRouteWarnings(options: MatchRouteOptions): RoutePatternWarning[];
49
82
  //#endregion
50
- export { InvalidationRecord, InvalidationRecorder, MockQueryOptions, MockRefresh, RoutePatternWarning, createInvalidationRecorder, getRouteWarnings, matchRoute, mockQuery, queryState };
83
+ export { InvalidationRecord, InvalidationRecorder, MockQueryOptions, MockRefresh, type RenderOptions, type RenderResult, RoutePatternWarning, type RouteRenderOptions, cleanup, createInvalidationRecorder, dispatch, flush, getRouteWarnings, matchRoute, mockQuery, mount, queryState, render, renderRoute };