@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
@@ -77,9 +77,9 @@ declare function mount(
77
77
  declare function renderRoute(
78
78
  options: RouteRenderOptions
79
79
  ): Promise<RenderResult>;
80
- /** Dispatch the browser click sequence expected by Askr's delegated events. */
80
+ /** Dispatch one bubbling `click` MouseEvent (no pointerdown/mousedown/mouseup sequence). */
81
81
  declare function click(element: Element): boolean;
82
- /** Set a text control's value and emit an input event for each character. */
82
+ /** Append `text` to a text control's value one character at a time, emitting an input event for each. */
83
83
  declare function type(
84
84
  element: HTMLInputElement | HTMLTextAreaElement,
85
85
  text: string
@@ -94,7 +94,9 @@ interface QueryTestRegistry {
94
94
  clear(): void;
95
95
  }
96
96
  /** Create a keyed query fixture registry for a test render runtime. */
97
- declare function createQueryTestRegistry(): QueryTestRegistry;
97
+ declare function createQueryTestRegistry(
98
+ runtime?: DataRuntime
99
+ ): QueryTestRegistry;
98
100
  /** Keyed mutation fixture registry returned by {@link createMutationTestRegistry}. */
99
101
  interface MutationTestRegistry {
100
102
  readonly runtime: DataRuntime;
@@ -103,7 +105,9 @@ interface MutationTestRegistry {
103
105
  clear(): void;
104
106
  }
105
107
  /** Create a keyed mutation fixture registry for a test render runtime. */
106
- declare function createMutationTestRegistry(): MutationTestRegistry;
108
+ declare function createMutationTestRegistry(
109
+ runtime?: DataRuntime
110
+ ): MutationTestRegistry;
107
111
  /** Initial state for {@link mutationState}; exactly one of `pending`/`error`/`result` may be set. */
108
112
  type MutationFixtureInitial<TResult> = {
109
113
  pending?: boolean;
@@ -1,10 +1,10 @@
1
- import { r as flushRuntimeScheduler } from "../access-CEA2pOry.js";
2
- import { v as recordReadableRead } from "../notify-DxxuDPeY.js";
3
- import { l as addInvalidationListener, t as createDataRuntime } from "../data-runtime-CxoY98lX.js";
4
- import { a as loadingQueryState, i as freshQueryState, o as refreshingQueryState, r as errorQueryState, s as staleQueryState } from "../data-2edW0jhF.js";
5
- import { a as normalizeAsyncDataError, o as notifySource, t as createReadableSource } from "../shared-kU8yvryp.js";
6
- import { a as cleanupApp, n as createSPA, r as createIsland } from "../boot-BMBMWUGI.js";
7
- import { r as computeRouteActivityMatches } from "../route-matching-CUzZ31gT.js";
1
+ import { y as flushSync } from "../instance-BAnfmXOR.js";
2
+ import { n as createSource, s as notify, u as readSource } from "../hooks-BimKStr4.js";
3
+ import { v as matchRoute } from "../resolution-Cx82-xjW.js";
4
+ import { a as cleanupApp, n as createSPA, r as createIsland } from "../boot-DaoTYoCw.js";
5
+ import { h as addInvalidationListener, t as createDataRuntime, u as resolveDataRuntimeState } from "../data-runtime-C1bFAlba.js";
6
+ import { a as loadingQueryState, i as freshQueryState, o as refreshingQueryState, r as errorQueryState, s as staleQueryState } from "../data-BCwClJiF.js";
7
+ import { i as normalizeAsyncDataError } from "../shared-O2ZnmYAf.js";
8
8
  //#region src/testing/render.ts
9
9
  const renderStates = /* @__PURE__ */ new WeakMap();
10
10
  const eventConstructorNames = {
@@ -99,7 +99,7 @@ function isEventForTarget(target, value) {
99
99
  }
100
100
  /** Synchronously flush the runtime's scheduled work (renders, effects). */
101
101
  function flush() {
102
- flushRuntimeScheduler();
102
+ flushSync();
103
103
  }
104
104
  /** Dispatch an event (constructed from a type string, or given directly) on `target`. */
105
105
  function dispatch(target, event, init = {}) {
@@ -191,12 +191,12 @@ async function renderRoute(options) {
191
191
  }
192
192
  //#endregion
193
193
  //#region src/testing/interactions.ts
194
- /** Dispatch the browser click sequence expected by Askr's delegated events. */
194
+ /** Dispatch one bubbling `click` MouseEvent (no pointerdown/mousedown/mouseup sequence). */
195
195
  function click(element) {
196
196
  if (!element || typeof element.dispatchEvent !== "function") throw new TypeError("@askrjs/askr/testing click requires an Element.");
197
197
  return dispatch(element, "click");
198
198
  }
199
- /** Set a text control's value and emit an input event for each character. */
199
+ /** Append `text` to a text control's value one character at a time, emitting an input event for each. */
200
200
  function type(element, text) {
201
201
  if (!element || typeof element.dispatchEvent !== "function") throw new TypeError("@askrjs/askr/testing type requires a text control.");
202
202
  for (const character of text) {
@@ -215,40 +215,40 @@ function submit(form) {
215
215
  //#endregion
216
216
  //#region src/testing/registries.ts
217
217
  /** Create a keyed query fixture registry for a test render runtime. */
218
- function createQueryTestRegistry() {
219
- const runtime = createDataRuntime();
218
+ function createQueryTestRegistry(runtime = createDataRuntime()) {
219
+ const overrides = resolveDataRuntimeState(runtime).queryTestOverrides;
220
220
  return {
221
221
  runtime,
222
222
  set(key, query) {
223
223
  if (typeof key !== "string" || key.length === 0) throw new TypeError("@askrjs/askr/testing query registry keys must be non-empty strings.");
224
- runtime.queryTestOverrides.set(key, query);
224
+ overrides.set(key, query);
225
225
  },
226
226
  delete(key) {
227
- runtime.queryTestOverrides.delete(key);
227
+ overrides.delete(key);
228
228
  },
229
229
  clear() {
230
- runtime.queryTestOverrides.clear();
230
+ overrides.clear();
231
231
  }
232
232
  };
233
233
  }
234
234
  /** Create a keyed mutation fixture registry for a test render runtime. */
235
- function createMutationTestRegistry() {
236
- const runtime = createDataRuntime();
235
+ function createMutationTestRegistry(runtime = createDataRuntime()) {
236
+ const overrides = resolveDataRuntimeState(runtime).mutationTestOverrides;
237
237
  return {
238
238
  runtime,
239
239
  set(key, mutation) {
240
240
  if (typeof key !== "string" || key.length === 0) throw new TypeError("@askrjs/askr/testing mutation registry keys must be non-empty strings.");
241
- const previous = runtime.mutationTestOverrides.get(key);
241
+ const previous = overrides.get(key);
242
242
  if (previous && previous !== mutation) previous.reset();
243
- runtime.mutationTestOverrides.set(key, mutation);
243
+ overrides.set(key, mutation);
244
244
  },
245
245
  delete(key) {
246
- runtime.mutationTestOverrides.get(key)?.reset();
247
- runtime.mutationTestOverrides.delete(key);
246
+ overrides.get(key)?.reset();
247
+ overrides.delete(key);
248
248
  },
249
249
  clear() {
250
- for (const mutation of runtime.mutationTestOverrides.values()) mutation.reset();
251
- runtime.mutationTestOverrides.clear();
250
+ for (const mutation of overrides.values()) mutation.reset();
251
+ overrides.clear();
252
252
  }
253
253
  };
254
254
  }
@@ -256,7 +256,7 @@ function createMutationTestRegistry() {
256
256
  //#region src/testing/mutation-fixtures.ts
257
257
  var MutableMutationFixture = class {
258
258
  constructor(initial = {}) {
259
- this.source = createReadableSource();
259
+ this.source = createSource();
260
260
  this.receivedInputs = [];
261
261
  this.active = null;
262
262
  const hasResult = Object.prototype.hasOwnProperty.call(initial, "result");
@@ -281,19 +281,19 @@ var MutableMutationFixture = class {
281
281
  };
282
282
  }
283
283
  get status() {
284
- recordReadableRead(this.source);
284
+ readSource(this.source);
285
285
  return this.state.status;
286
286
  }
287
287
  get pending() {
288
- recordReadableRead(this.source);
288
+ readSource(this.source);
289
289
  return this.state.status === "pending";
290
290
  }
291
291
  get error() {
292
- recordReadableRead(this.source);
292
+ readSource(this.source);
293
293
  return this.state.error;
294
294
  }
295
295
  get result() {
296
- recordReadableRead(this.source);
296
+ readSource(this.source);
297
297
  return this.state.result;
298
298
  }
299
299
  get inputs() {
@@ -357,7 +357,7 @@ var MutableMutationFixture = class {
357
357
  }
358
358
  setState(state) {
359
359
  this.state = state;
360
- notifySource(this.source);
360
+ notify(this.source);
361
361
  }
362
362
  };
363
363
  function createMutationFixture(initial = {}) {
@@ -458,11 +458,6 @@ function createInvalidationRecorder() {
458
458
  }
459
459
  //#endregion
460
460
  //#region src/testing/routes.ts
461
- /** Match `path` against a route registry for tests, without mounting the app. */
462
- function matchRoute(path, options) {
463
- if (!options?.registry) throw new TypeError("matchRoute requires options.registry.");
464
- return computeRouteActivityMatches(path, { registry: options.registry })[0] ?? null;
465
- }
466
461
  function getRoutePatternRecords(options) {
467
462
  if (!options?.registry) throw new TypeError("getRouteWarnings requires options.registry.");
468
463
  return options.registry.manifest.records.map((record) => ({
@@ -0,0 +1,185 @@
1
+ import { F as logger, I as isDevelopmentEnvironment } from "./instance-BAnfmXOR.js";
2
+ //#region src/common/attr-names.ts
3
+ /**
4
+ * JSX prop name to rendered attribute name, shared by the DOM renderer and SSR.
5
+ *
6
+ * The DOM renderer additionally lowercases the result for non-SVG elements,
7
+ * using the element's real namespace. SSR renders to text and has no element to
8
+ * ask, so every camelCase prop that does not name a real camelCase attribute is
9
+ * mapped explicitly here — otherwise SSR emits `tabIndex` where the DOM renderer
10
+ * writes `tabindex`, or `strokeDasharray`, which browsers ignore. Every mapped
11
+ * name is lowercase in both HTML and SVG, so the mapping is
12
+ * namespace-independent. SVG attributes that really are camelCase (`viewBox`,
13
+ * `gradientUnits`, `preserveAspectRatio`, ...) are not listed and pass through.
14
+ *
15
+ * Custom elements (a tag name containing `-`) define their own attributes, so
16
+ * they only get this base table and never the SVG table below.
17
+ */
18
+ const PUBLIC_ATTRIBUTE_NAME_MAP = {
19
+ className: "class",
20
+ htmlFor: "for",
21
+ autoComplete: "autocomplete",
22
+ colSpan: "colspan",
23
+ contentEditable: "contenteditable",
24
+ enterKeyHint: "enterkeyhint",
25
+ inputMode: "inputmode",
26
+ maxLength: "maxlength",
27
+ minLength: "minlength",
28
+ noValidate: "novalidate",
29
+ readOnly: "readonly",
30
+ rowSpan: "rowspan",
31
+ spellCheck: "spellcheck",
32
+ tabIndex: "tabindex",
33
+ writingSuggestions: "writingsuggestions",
34
+ strokeWidth: "stroke-width",
35
+ strokeLinecap: "stroke-linecap",
36
+ strokeLinejoin: "stroke-linejoin",
37
+ fillRule: "fill-rule",
38
+ clipRule: "clip-rule"
39
+ };
40
+ const SVG_ATTRIBUTE_NAME_MAP = {};
41
+ /**
42
+ * SVG presentation attributes are hyphenated, and the XML-namespaced ones use
43
+ * a prefix; JSX spells both in camelCase (`strokeDasharray`, `xlinkHref`).
44
+ * Listing the rendered names and deriving the prop names keeps this table
45
+ * small in the client bundle.
46
+ */
47
+ for (const attributeName of "alignment-baseline baseline-shift clip-path clip-rule color-interpolation color-interpolation-filters color-rendering dominant-baseline fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight image-rendering letter-spacing lighting-color marker-end marker-mid marker-start mask-type paint-order pointer-events shape-rendering stop-color stop-opacity stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-overflow text-rendering transform-origin unicode-bidi vector-effect white-space word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space xmlns:xlink".split(" ")) SVG_ATTRIBUTE_NAME_MAP[attributeName.replace(/[-:]([a-z])/g, (_, char) => char.toUpperCase())] = attributeName;
48
+ function isCustomElementName(tagName) {
49
+ return tagName.includes("-");
50
+ }
51
+ function getPublicAttributeName(propName, customElement = false) {
52
+ if (propName.startsWith("attr:")) return propName.slice(5);
53
+ return PUBLIC_ATTRIBUTE_NAME_MAP[propName] ?? (customElement ? void 0 : SVG_ATTRIBUTE_NAME_MAP[propName]) ?? propName;
54
+ }
55
+ const ATTRIBUTE_PREFIX_NAMESPACES = {
56
+ xlink: "http://www.w3.org/1999/xlink",
57
+ xml: "http://www.w3.org/XML/1998/namespace",
58
+ xmlns: "http://www.w3.org/2000/xmlns/"
59
+ };
60
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
61
+ const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
62
+ /**
63
+ * The XML namespace of a prefixed attribute name such as `xlink:href` on an
64
+ * element in `elementNamespace`, or `null` for ordinary attributes. The HTML
65
+ * parser puts these attributes in their namespaces only on SVG and MathML
66
+ * elements when it reads SSR markup; on HTML elements `xml:lang` stays a plain
67
+ * attribute. The DOM renderer must do the same so both sides produce the same
68
+ * element.
69
+ */
70
+ function attributeNamespace(elementNamespace, attributeName) {
71
+ if (elementNamespace !== SVG_NAMESPACE && elementNamespace !== MATHML_NAMESPACE) return null;
72
+ const colon = attributeName.indexOf(":");
73
+ if (colon === -1) return null;
74
+ return ATTRIBUTE_PREFIX_NAMESPACES[attributeName.slice(0, colon)] ?? null;
75
+ }
76
+ //#endregion
77
+ //#region src/common/url.ts
78
+ const SAFE_URL_SCHEMES = /* @__PURE__ */ new Set([
79
+ "http",
80
+ "https",
81
+ "mailto",
82
+ "sms",
83
+ "tel"
84
+ ]);
85
+ const SCRIPT_URL_SCHEMES = /* @__PURE__ */ new Set(["javascript", "vbscript"]);
86
+ const URL_SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
87
+ function urlScheme(value) {
88
+ const compact = value.trim().replace(/[\u0000-\u0020\u007f-\u009f]/g, "");
89
+ return URL_SCHEME_RE.exec(compact)?.[1]?.toLowerCase();
90
+ }
91
+ const EXPLICIT_HTTP_URL_RE = /^[\u0000-\u0020]*https?:/i;
92
+ const NAVIGATION_BASE = "http://askr.invalid/";
93
+ /**
94
+ * Resolve a navigation or redirect target against `base`. Only a target
95
+ * written with an explicit `http:`/`https:` scheme may leave the base origin.
96
+ * A path-like string that URL parsing resolves to another host
97
+ * (`//evil.example`, `/\\evil.example`, `\\\\evil.example`) throws a
98
+ * `TypeError`, so an app path can never become an open redirect. So does a
99
+ * same-origin URL whose pathname starts with `//` (`/.//evil.example`,
100
+ * `/%2e//evil.example`): its root-relative form `//evil.example` would name
101
+ * another host. No window is needed: the check holds for any base origin.
102
+ */
103
+ function resolveNavigationUrl(target, base = NAVIGATION_BASE) {
104
+ const url = new URL(target, base);
105
+ const sameOrigin = url.origin === new URL(base).origin;
106
+ if (!sameOrigin && !EXPLICIT_HTTP_URL_RE.test(target) || sameOrigin && url.pathname.startsWith("//")) throw new TypeError(`Navigation target ${JSON.stringify(target)} resolves to another origin without an explicit http: or https: scheme.`);
107
+ return url;
108
+ }
109
+ /**
110
+ * The public form of a navigation target: root-relative for the base origin,
111
+ * absolute for an explicit URL on another origin.
112
+ */
113
+ function formatNavigationUrl(url, base = NAVIGATION_BASE) {
114
+ return url.origin === new URL(base).origin ? `${url.pathname}${url.search}${url.hash}` : url.href;
115
+ }
116
+ /**
117
+ * Reject a scheme-less href (`/\\evil.example`, `//evil.example`) that the
118
+ * browser would follow to another origin. Hrefs with a scheme are checked by
119
+ * {@link isSafeHref}.
120
+ */
121
+ function assertPathHrefStaysOnOrigin(value) {
122
+ const scheme = urlScheme(value);
123
+ if (scheme === void 0 || scheme === "http" || scheme === "https") resolveNavigationUrl(value);
124
+ }
125
+ function isSafeHref(value) {
126
+ const scheme = urlScheme(value);
127
+ return scheme === void 0 || SAFE_URL_SCHEMES.has(scheme);
128
+ }
129
+ /** Whether a resource URL avoids script-executing schemes; data:, blob: and custom schemes stay allowed. */
130
+ function isSafeResourceUrl(value) {
131
+ const scheme = urlScheme(value);
132
+ return scheme === void 0 || !SCRIPT_URL_SCHEMES.has(scheme);
133
+ }
134
+ /**
135
+ * Attribute names (lowercased) that can carry a browser-navigable/executable
136
+ * URL and must be checked against isSafeHref. Shared between the client
137
+ * renderer (renderer/attributes.ts) and SSR (ssr/attrs.ts) so their unsafe-
138
+ * URL handling can't drift.
139
+ */
140
+ const UNSAFE_URL_SCHEME_ATTRIBUTES = /* @__PURE__ */ new Set([
141
+ "href",
142
+ "formaction",
143
+ "action",
144
+ "xlink:href"
145
+ ]);
146
+ /**
147
+ * Resource attributes (lowercased) that execute script-scheme URLs, such as
148
+ * `<iframe src>` and `<object data>`. They legitimately carry data:, blob: and
149
+ * other non-navigable URLs, so only script schemes are rejected, not the
150
+ * navigation allowlist.
151
+ */
152
+ const SCRIPT_URL_RESOURCE_ATTRIBUTES = /* @__PURE__ */ new Set(["src", "data"]);
153
+ /** Accepts the JSX prop name (`xlinkHref`) or the rendered attribute name. */
154
+ function isUnsafeUrlAttribute(key, value) {
155
+ const name = getPublicAttributeName(key).toLowerCase();
156
+ if (UNSAFE_URL_SCHEME_ATTRIBUTES.has(name)) return !isSafeHref(String(value));
157
+ if (SCRIPT_URL_RESOURCE_ATTRIBUTES.has(name)) return !isSafeResourceUrl(String(value));
158
+ return false;
159
+ }
160
+ const WARNED_URL_PREVIEW_LENGTH = 80;
161
+ const WARNED_URL_LIMIT = 256;
162
+ const warnedUrls = /* @__PURE__ */ new Set();
163
+ const MISLEADING_PREVIEW_CHARS_RE = /[\u202A-\u202E\u2066-\u2069\u2028\u2029]/g;
164
+ function warnUnsafeUrl(name, text) {
165
+ const key = `${name}=${text}`;
166
+ if (warnedUrls.has(key)) return;
167
+ if (warnedUrls.size >= WARNED_URL_LIMIT) warnedUrls.clear();
168
+ warnedUrls.add(key);
169
+ const preview = (text.length > WARNED_URL_PREVIEW_LENGTH ? `${text.slice(0, WARNED_URL_PREVIEW_LENGTH)}...` : text).replace(MISLEADING_PREVIEW_CHARS_RE, "?");
170
+ const allowed = SCRIPT_URL_RESOURCE_ATTRIBUTES.has(name) ? "script-executing schemes are never rendered" : `only relative URLs and ${[...SAFE_URL_SCHEMES].join(", ")} are allowed`;
171
+ logger.warn(`[Askr] Omitted ${name}=${JSON.stringify(preview)}: the "${urlScheme(text)}:" scheme is blocked (${allowed}).`);
172
+ }
173
+ /**
174
+ * {@link isUnsafeUrlAttribute} for a value about to be rendered: in
175
+ * development, a blocked value logs a warning naming the attribute and scheme,
176
+ * once per attribute and value, so a stripped custom-scheme link (`vscode:`,
177
+ * `slack:`) is not silent.
178
+ */
179
+ function rejectUnsafeUrlAttribute(key, value) {
180
+ if (!isUnsafeUrlAttribute(key, value)) return false;
181
+ if (isDevelopmentEnvironment()) warnUnsafeUrl(getPublicAttributeName(key).toLowerCase(), String(value));
182
+ return true;
183
+ }
184
+ //#endregion
185
+ export { isSafeHref as a, resolveNavigationUrl as c, isCustomElementName as d, formatNavigationUrl as i, attributeNamespace as l, UNSAFE_URL_SCHEME_ATTRIBUTES as n, isUnsafeUrlAttribute as o, assertPathHrefStaysOnOrigin as r, rejectUnsafeUrlAttribute as s, SCRIPT_URL_RESOURCE_ATTRIBUTES as t, getPublicAttributeName as u };
@@ -63,14 +63,59 @@ declare function composeHandlers<A extends readonly unknown[]>(
63
63
  second?: (...args: A) => void,
64
64
  options?: ComposeHandlersOptions
65
65
  ): (...args: A) => void;
66
+ type IsAny<T> = 0 extends 1 & T ? true : false;
67
+ type HasIndexSignature<T> = string extends keyof T
68
+ ? true
69
+ : number extends keyof T
70
+ ? true
71
+ : false;
72
+ type RequiredKeys<T> = {
73
+ [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
74
+ }[keyof T];
75
+
76
+ /** A base value, or the injected one where the base may be `undefined`. */
77
+ type MergedValue<TBase, TInjected> =
78
+ IsAny<TBase> extends true
79
+ ? TBase
80
+ : [TBase] extends [undefined]
81
+ ? TInjected
82
+ : undefined extends TBase
83
+ ? Exclude<TBase, undefined> | TInjected
84
+ : TBase;
85
+
86
+ /**
87
+ * The props {@link mergeProps} returns: base keys win unless `undefined`, and
88
+ * a base key is required when the injected props always supply it. Props
89
+ * with an index signature fall back to the intersection of both sides.
90
+ */
91
+ type MergedProps<TBase extends object, TInjected extends object> =
92
+ HasIndexSignature<TBase> extends true
93
+ ? TInjected & TBase
94
+ : HasIndexSignature<TInjected> extends true
95
+ ? TInjected & TBase
96
+ : Omit<TInjected, keyof TBase> & {
97
+ [
98
+ K in keyof TBase as K extends RequiredKeys<TInjected> ? K : never
99
+ ]-?: MergedValue<TBase[K], TInjected[K & keyof TInjected]>;
100
+ } & {
101
+ [
102
+ K in keyof TBase as K extends RequiredKeys<TInjected> ? never : K
103
+ ]: MergedValue<
104
+ TBase[K],
105
+ K extends keyof TInjected ? TInjected[K] : undefined
106
+ >;
107
+ };
108
+
66
109
  /**
67
110
  * Merge `base` props over `injected` props: non-handler keys in `base` win,
68
- * and matching event handlers are composed (`injected` runs first).
111
+ * and matching event handlers are composed (`injected` runs first). `base`
112
+ * values that are `undefined` are treated as absent and never overwrite an
113
+ * injected value; use `null` to clear one explicitly.
69
114
  */
70
115
  declare function mergeProps<TBase extends object, TInjected extends object>(
71
116
  base: TBase,
72
117
  injected: TInjected
73
- ): TInjected & TBase;
118
+ ): MergedProps<TBase, TInjected>;
74
119
  /**
75
120
  * Tiny aria helpers
76
121
  */
@@ -124,6 +169,7 @@ export {
124
169
  DefaultPreventable,
125
170
  formatId,
126
171
  mergeProps,
172
+ MergedProps,
127
173
  PropagationStoppable,
128
174
  ariaDisabled,
129
175
  ComposeHandlersOptions,
@@ -0,0 +1,152 @@
1
+ import { r as validateCspNonce } from "./csp-nonce-DwGw1A6Q.js";
2
+ import { g as withHydrationVerificationRender, v as getHydrationRenderUrl } from "./render-context-DGHnEveC.js";
3
+ import { C as removeRouteBasePath, S as normalizeRouteBasePath, f as currentAuth, n as getRouteRenderContext, y as resolveRouteFromRoutes } from "./resolution-Cx82-xjW.js";
4
+ import { t as SSR_RENDER_DATA_ATTR } from "./ssr-DoV-vJ-5.js";
5
+ import { i as StringSink, s as createRenderContext, t as renderSSRRouteAppToSink } from "./render-sync-IC5lMFpq.js";
6
+ //#region src/ssr/render-resolved.ts
7
+ function sameRouteParams(left, right) {
8
+ if (!left || !right) return !left && !right;
9
+ const leftKeys = Object.keys(left);
10
+ const rightKeys = Object.keys(right);
11
+ if (leftKeys.length !== rightKeys.length) return false;
12
+ for (const key of leftKeys) if (left[key] !== right[key]) return false;
13
+ return true;
14
+ }
15
+ /** @internal Render a resolved route handler, with the request-local auth context used by hydration verification. */
16
+ function renderResolvedForHydrationSync(opts, authContext) {
17
+ const { url, registry, handler, params, options } = opts;
18
+ const routes = registry.routes;
19
+ const requestUrl = new URL(url, "http://localhost");
20
+ const logicalTarget = removeRouteBasePath(`${requestUrl.pathname}${requestUrl.search}${requestUrl.hash}`, normalizeRouteBasePath(registry.manifest.basePath));
21
+ const matchedIndex = routes.findIndex((route) => {
22
+ if (logicalTarget === void 0) return false;
23
+ const resolved = resolveRouteFromRoutes(new URL(logicalTarget, "http://localhost").pathname, [route]);
24
+ return resolved !== null && resolved.handler === route.handler && sameRouteParams(resolved.params, params ?? {});
25
+ });
26
+ if (matchedIndex < 0) throw new Error(`renderResolvedForHydrationSync: no route found for url: ${url}`);
27
+ const effectiveRoutes = routes.map((route, index) => index === matchedIndex ? {
28
+ ...route,
29
+ handler
30
+ } : route);
31
+ const matchedRoute = routes[matchedIndex];
32
+ const cspNonce = validateCspNonce(options?.cspNonce);
33
+ const ctx = createRenderContext(options?.seed, {
34
+ url,
35
+ basePath: registry.manifest.basePath,
36
+ data: options?.data,
37
+ params,
38
+ routes: effectiveRoutes,
39
+ routeAuth: registry.manifest.auth,
40
+ authContext,
41
+ dataRuntime: options?.dataRuntime,
42
+ envelope: options?.envelope,
43
+ cspNonce
44
+ });
45
+ const sink = new StringSink();
46
+ renderSSRRouteAppToSink({
47
+ route: {
48
+ ...matchedRoute,
49
+ handler
50
+ },
51
+ params: params ?? {},
52
+ data: options?.data,
53
+ ctx,
54
+ sink
55
+ });
56
+ sink.end();
57
+ return sink.toString();
58
+ }
59
+ //#endregion
60
+ //#region src/ssr/verify-hydration.ts
61
+ const SSR_STYLE_REGISTRY_SELECTOR = "style[data-askr-style-registry]";
62
+ const RENDERER_BOOKKEEPING_ATTRS = [
63
+ "data-skip-hydrate",
64
+ "data-key",
65
+ "data-askr-key-kind"
66
+ ];
67
+ /**
68
+ * Parsed declarations in a canonical order. Each parsed longhand is unique, so
69
+ * order carries no meaning here, but the DOM renderer may reorder properties
70
+ * it rewrites.
71
+ */
72
+ function normalizeStyleDeclarations(style) {
73
+ const declarations = [];
74
+ for (let index = 0; index < style.length; index += 1) {
75
+ const name = style.item(index);
76
+ const priority = style.getPropertyPriority(name);
77
+ declarations.push(`${name}: ${style.getPropertyValue(name)}${priority ? ` !${priority}` : ""};`);
78
+ }
79
+ return declarations.sort().join(" ");
80
+ }
81
+ /**
82
+ * Serialize markup in a form that is stable across the SSR serializer and the
83
+ * DOM renderer: transport carriers, comments (range anchors) and the
84
+ * key and `data-skip-hydrate` bookkeeping the renderers add are dropped, and style
85
+ * attributes are re-serialized through the CSSOM so `color:red;` and the
86
+ * DOM's `color: red;` compare equal.
87
+ */
88
+ function normalizeHydrationHtml(html) {
89
+ const template = document.createElement("template");
90
+ template.innerHTML = html;
91
+ for (const carrier of Array.from(template.content.querySelectorAll(`script[${SSR_RENDER_DATA_ATTR}],${SSR_STYLE_REGISTRY_SELECTOR}`))) carrier.remove();
92
+ for (const name of RENDERER_BOOKKEEPING_ATTRS) for (const element of Array.from(template.content.querySelectorAll(`[${name}]`))) element.removeAttribute(name);
93
+ for (const element of Array.from(template.content.querySelectorAll("[style]"))) {
94
+ const style = element.style;
95
+ if (!style) continue;
96
+ const cssText = normalizeStyleDeclarations(style);
97
+ if (cssText) element.setAttribute("style", cssText);
98
+ else element.removeAttribute("style");
99
+ }
100
+ const comments = document.createTreeWalker(template.content, NodeFilter.SHOW_COMMENT);
101
+ const found = [];
102
+ while (comments.nextNode()) found.push(comments.currentNode);
103
+ for (const comment of found) comment.parentNode?.removeChild(comment);
104
+ return template.innerHTML;
105
+ }
106
+ /**
107
+ * Whether the client renders the same URL the server markup was rendered
108
+ * for. A static page opened with a client-only query or hash is verified
109
+ * against its queryless render URL, then hydration applies the browser URL on
110
+ * purpose, so its client output legitimately differs.
111
+ */
112
+ function clientRendersServerUrl(url, envelope) {
113
+ const live = new URL(url, "http://localhost");
114
+ const renderUrl = getHydrationRenderUrl(envelope);
115
+ const rendered = new URL(renderUrl ?? live.pathname, "http://localhost");
116
+ return rendered.search === live.search && rendered.hash === live.hash;
117
+ }
118
+ /**
119
+ * Capture the server markup under `root` before the client renderer touches
120
+ * it, normalized for {@link verifyClientHydrationMarkup}. Returns `null` when
121
+ * the client output is not expected to reproduce it because the page is
122
+ * hydrated at a different URL than it was rendered for.
123
+ */
124
+ function captureServerHydrationMarkup(root, url, envelope) {
125
+ const html = root.innerHTML;
126
+ if (!clientRendersServerUrl(url, envelope)) return null;
127
+ return normalizeHydrationHtml(html);
128
+ }
129
+ /**
130
+ * Whether client markup left by hydration (`clientHtml`) matches the server
131
+ * markup captured before it ran. Hydration reconciles the server DOM in place,
132
+ * so an SSR/client renderer divergence shows up here even when a fresh server
133
+ * render reproduces the server HTML exactly.
134
+ */
135
+ function verifyClientHydrationMarkup(clientHtml, serverMarkup) {
136
+ return normalizeHydrationHtml(clientHtml) === serverMarkup;
137
+ }
138
+ function verifyHydrationSyncForUrl(opts) {
139
+ const { root, url, registry, resolved, options } = opts;
140
+ const verificationUrl = getHydrationRenderUrl(options?.envelope) ?? new URL(url, "http://localhost").pathname;
141
+ const authContext = getRouteRenderContext(resolved)?.auth ?? currentAuth();
142
+ const expected = withHydrationVerificationRender(() => renderResolvedForHydrationSync({
143
+ url: verificationUrl,
144
+ registry,
145
+ handler: resolved.handler,
146
+ params: resolved.params,
147
+ options
148
+ }, authContext));
149
+ return normalizeHydrationHtml(root.innerHTML) === normalizeHydrationHtml(expected);
150
+ }
151
+ //#endregion
152
+ export { captureServerHydrationMarkup, verifyClientHydrationMarkup, verifyHydrationSyncForUrl };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@askrjs/askr",
3
- "version": "0.3.0",
4
- "description": "Lane-scheduled deterministic UI framework",
3
+ "version": "0.4.0",
4
+ "description": "A TypeScript UI runtime with getter-based state, fine-grained bindings, transactional commits with rollback, and one route graph for SPA, SSR, and SSG.",
5
5
  "keywords": [
6
6
  "askr",
7
7
  "framework",
@@ -128,7 +128,7 @@
128
128
  "dev": "vp dev",
129
129
  "fmt": "vp fmt .",
130
130
  "lint": "vp lint src tests test-utils benches && npm run typecheck",
131
- "typecheck": "tsc --noEmit --pretty false",
131
+ "typecheck": "tsc --noEmit --pretty false && tsc --noEmit --pretty false -p test-utils && tsc --noEmit --pretty false -p benches && tsc --noEmit --pretty false -p tests",
132
132
  "test": "npm run test:unit && npm run test:checks && npm run test:jsdom && npm run test:browser",
133
133
  "test:browser": "vp test run -c vitest.test.browser.config.ts tests/browser",
134
134
  "test:checks": "vp test run -c vitest.test.checks.config.ts",
@@ -146,22 +146,32 @@
146
146
  "prepack": "npm run build",
147
147
  "prepublishOnly": "npm run check"
148
148
  },
149
- "dependencies": {
150
- "@askrjs/auth": ">=0.3.0 <0.4.0",
151
- "@askrjs/schema": ">=0.3.0 <0.4.0"
152
- },
153
149
  "devDependencies": {
154
- "@types/node": "^26.4.1",
150
+ "@askrjs/auth": ">=0.4.0 <0.5.0",
151
+ "@askrjs/schema": ">=0.4.0 <0.5.0",
152
+ "@types/node": "^26.6.2",
155
153
  "@typescript/typescript6": "^6.0.2",
156
154
  "@vitest/browser-playwright": "4.1.11",
157
155
  "cross-env": "^10.1.0",
158
- "jsdom": "^30.0.1",
156
+ "jsdom": "^30.1.1",
159
157
  "playwright": "^1.63.0",
160
158
  "publint": "^0.3.24",
161
159
  "tsd": "^0.33.0",
162
160
  "typescript": "^7.0.2",
163
161
  "vite-plus": "^0.3.1"
164
162
  },
163
+ "peerDependencies": {
164
+ "@askrjs/auth": ">=0.4.0 <0.5.0",
165
+ "@askrjs/schema": ">=0.4.0 <0.5.0"
166
+ },
167
+ "peerDependenciesMeta": {
168
+ "@askrjs/auth": {
169
+ "optional": true
170
+ },
171
+ "@askrjs/schema": {
172
+ "optional": true
173
+ }
174
+ },
165
175
  "tsd": {
166
176
  "compilerOptions": {
167
177
  "composite": false,