@askrjs/askr 0.0.34 → 0.0.36

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 (151) hide show
  1. package/dist/bench/components/benchmark-row.js.map +1 -1
  2. package/dist/bench/components/benchmark-table.js.map +1 -1
  3. package/dist/benchmark.js +2 -2
  4. package/dist/benchmark.js.map +1 -1
  5. package/dist/bin/askr-ssg.js.map +1 -1
  6. package/dist/boot/index.d.ts.map +1 -1
  7. package/dist/boot/index.js +9 -29
  8. package/dist/boot/index.js.map +1 -1
  9. package/dist/common/control.js.map +1 -1
  10. package/dist/common/env.js.map +1 -1
  11. package/dist/common/router.d.ts +8 -1
  12. package/dist/common/router.d.ts.map +1 -1
  13. package/dist/common/ssr-errors.js.map +1 -1
  14. package/dist/common/vnode.js.map +1 -1
  15. package/dist/components/error-boundary.js.map +1 -1
  16. package/dist/components/link.js.map +1 -1
  17. package/dist/control/case.js.map +1 -1
  18. package/dist/control/for.js.map +1 -1
  19. package/dist/control/shared.js.map +1 -1
  20. package/dist/control/show.js.map +1 -1
  21. package/dist/data/index.d.ts.map +1 -1
  22. package/dist/data/index.js +165 -23
  23. package/dist/data/index.js.map +1 -1
  24. package/dist/dev/invariant.js.map +1 -1
  25. package/dist/dev/logger.js.map +1 -1
  26. package/dist/foundations/icon/icon.js.map +1 -1
  27. package/dist/foundations/interactions/dismissable.js.map +1 -1
  28. package/dist/foundations/interactions/focusable.js.map +1 -1
  29. package/dist/foundations/interactions/hoverable.js.map +1 -1
  30. package/dist/foundations/interactions/interaction-policy.js.map +1 -1
  31. package/dist/foundations/interactions/pressable.js.map +1 -1
  32. package/dist/foundations/interactions/roving-focus.js.map +1 -1
  33. package/dist/foundations/state/controllable.js.map +1 -1
  34. package/dist/foundations/structures/collection.js.map +1 -1
  35. package/dist/foundations/structures/layer.js.map +1 -1
  36. package/dist/foundations/structures/layout.js.map +1 -1
  37. package/dist/foundations/structures/portal.js.map +1 -1
  38. package/dist/foundations/structures/presence.js.map +1 -1
  39. package/dist/foundations/structures/slot.js.map +1 -1
  40. package/dist/foundations/utilities/aria.js.map +1 -1
  41. package/dist/foundations/utilities/compose-handlers.js.map +1 -1
  42. package/dist/foundations/utilities/compose-ref.js.map +1 -1
  43. package/dist/foundations/utilities/merge-props.js.map +1 -1
  44. package/dist/foundations/utilities/use-id.js.map +1 -1
  45. package/dist/fx/fx.js.map +1 -1
  46. package/dist/fx/timing.js.map +1 -1
  47. package/dist/index.d.ts +3 -3
  48. package/dist/index.js +2 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/jsx/utils.js.map +1 -1
  51. package/dist/jsx-dev-runtime.js.map +1 -1
  52. package/dist/jsx-runtime.js.map +1 -1
  53. package/dist/renderer/children.js.map +1 -1
  54. package/dist/renderer/cleanup.js.map +1 -1
  55. package/dist/renderer/dom.js +128 -53
  56. package/dist/renderer/dom.js.map +1 -1
  57. package/dist/renderer/evaluate.js +7 -0
  58. package/dist/renderer/evaluate.js.map +1 -1
  59. package/dist/renderer/fastpath.js.map +1 -1
  60. package/dist/renderer/index.js.map +1 -1
  61. package/dist/renderer/keyed.js +1 -1
  62. package/dist/renderer/keyed.js.map +1 -1
  63. package/dist/renderer/reconcile.js.map +1 -1
  64. package/dist/renderer/utils.js.map +1 -1
  65. package/dist/router/index.d.ts +3 -3
  66. package/dist/router/index.js +2 -2
  67. package/dist/router/match.js.map +1 -1
  68. package/dist/router/navigate.d.ts.map +1 -1
  69. package/dist/router/navigate.js +8 -3
  70. package/dist/router/navigate.js.map +1 -1
  71. package/dist/router/policy.js.map +1 -1
  72. package/dist/router/route-context.js.map +1 -1
  73. package/dist/router/route.d.ts +11 -2
  74. package/dist/router/route.d.ts.map +1 -1
  75. package/dist/router/route.js +268 -109
  76. package/dist/router/route.js.map +1 -1
  77. package/dist/runtime/child-scope.d.ts +1 -0
  78. package/dist/runtime/child-scope.d.ts.map +1 -1
  79. package/dist/runtime/child-scope.js +2 -0
  80. package/dist/runtime/child-scope.js.map +1 -1
  81. package/dist/runtime/component.js +1 -1
  82. package/dist/runtime/component.js.map +1 -1
  83. package/dist/runtime/context.js.map +1 -1
  84. package/dist/runtime/control.js.map +1 -1
  85. package/dist/runtime/derive.d.ts +2 -1
  86. package/dist/runtime/derive.d.ts.map +1 -1
  87. package/dist/runtime/derive.js +15 -10
  88. package/dist/runtime/derive.js.map +1 -1
  89. package/dist/runtime/dev-namespace.js.map +1 -1
  90. package/dist/runtime/effect.d.ts.map +1 -1
  91. package/dist/runtime/effect.js +34 -9
  92. package/dist/runtime/effect.js.map +1 -1
  93. package/dist/runtime/events.js.map +1 -1
  94. package/dist/runtime/execution-model.js.map +1 -1
  95. package/dist/runtime/fastlane.js.map +1 -1
  96. package/dist/runtime/for-bench.d.ts +1 -1
  97. package/dist/runtime/for-bench.d.ts.map +1 -1
  98. package/dist/runtime/for-bench.js +15 -14
  99. package/dist/runtime/for-bench.js.map +1 -1
  100. package/dist/runtime/for.d.ts +1 -0
  101. package/dist/runtime/for.d.ts.map +1 -1
  102. package/dist/runtime/for.js +63 -16
  103. package/dist/runtime/for.js.map +1 -1
  104. package/dist/runtime/operations.d.ts.map +1 -1
  105. package/dist/runtime/operations.js +9 -7
  106. package/dist/runtime/operations.js.map +1 -1
  107. package/dist/runtime/perf-metrics.js +8 -2
  108. package/dist/runtime/perf-metrics.js.map +1 -1
  109. package/dist/runtime/readable.d.ts +1 -1
  110. package/dist/runtime/readable.d.ts.map +1 -1
  111. package/dist/runtime/readable.js +2 -1
  112. package/dist/runtime/readable.js.map +1 -1
  113. package/dist/runtime/resource-cell.js +3 -2
  114. package/dist/runtime/resource-cell.js.map +1 -1
  115. package/dist/runtime/scheduler.d.ts +0 -1
  116. package/dist/runtime/scheduler.d.ts.map +1 -1
  117. package/dist/runtime/scheduler.js +6 -19
  118. package/dist/runtime/scheduler.js.map +1 -1
  119. package/dist/runtime/selector.d.ts.map +1 -1
  120. package/dist/runtime/selector.js +6 -4
  121. package/dist/runtime/selector.js.map +1 -1
  122. package/dist/runtime/snapshot-source.d.ts +10 -0
  123. package/dist/runtime/snapshot-source.d.ts.map +1 -0
  124. package/dist/runtime/snapshot-source.js +17 -0
  125. package/dist/runtime/snapshot-source.js.map +1 -0
  126. package/dist/runtime/ssr-bridge.js.map +1 -1
  127. package/dist/runtime/state.d.ts.map +1 -1
  128. package/dist/runtime/state.js +1 -5
  129. package/dist/runtime/state.js.map +1 -1
  130. package/dist/ssg/batch-render.js.map +1 -1
  131. package/dist/ssg/create-static-gen.js.map +1 -1
  132. package/dist/ssg/generate-metadata.js.map +1 -1
  133. package/dist/ssg/incremental-manifest.js.map +1 -1
  134. package/dist/ssg/resolve-ssg-data.js.map +1 -1
  135. package/dist/ssg/route-utils.js.map +1 -1
  136. package/dist/ssg/write-static-files.js.map +1 -1
  137. package/dist/ssr/attrs.js.map +1 -1
  138. package/dist/ssr/context.d.ts +1 -0
  139. package/dist/ssr/context.d.ts.map +1 -1
  140. package/dist/ssr/context.js +3 -0
  141. package/dist/ssr/context.js.map +1 -1
  142. package/dist/ssr/escape.js +56 -16
  143. package/dist/ssr/escape.js.map +1 -1
  144. package/dist/ssr/index.d.ts.map +1 -1
  145. package/dist/ssr/index.js +15 -3
  146. package/dist/ssr/index.js.map +1 -1
  147. package/dist/ssr/render-keys.js.map +1 -1
  148. package/dist/ssr/sink.js.map +1 -1
  149. package/dist/ssr/stream-render.js +14 -2
  150. package/dist/ssr/stream-render.js.map +1 -1
  151. package/package.json +8 -6
@@ -1 +1 @@
1
- {"version":3,"file":"match.js","names":[],"sources":["../../src/router/match.ts"],"sourcesContent":["/**\n * Path matching, segment parsing, and specificity scoring.\n */\n\nimport type { ParsedSegment } from '../common/router';\n\n/**\n * Parse a route template path into typed segments.\n *\n * @example\n * parseSegments('/users/{id}') // [{kind:'static',value:'users'},{kind:'param',value:'id'}]\n * parseSegments('/*') // [{kind:'catchall',value:'*'}]\n * parseSegments('/posts/*') // [{kind:'static',value:'posts'},{kind:'wildcard',value:'*'}]\n */\nexport function parseSegments(path: string): ParsedSegment[] {\n const normalized =\n path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;\n\n const parts = normalized.split('/').filter(Boolean);\n\n // Bare catch-all: /*\n if (parts.length === 1 && parts[0] === '*') {\n return [{ kind: 'catchall', value: '*' }];\n }\n\n return parts.map((segment): ParsedSegment => {\n if (segment.startsWith('{') && segment.endsWith('}')) {\n return { kind: 'param', value: segment.slice(1, -1) };\n }\n if (segment === '*') {\n return { kind: 'wildcard', value: '*' };\n }\n return { kind: 'static', value: segment };\n });\n}\n\n/**\n * Compute a numeric specificity rank from a parsed segment list.\n *\n * Scoring: static = 3, param = 2, wildcard = 1, catchall = 0.\n * Higher rank wins when multiple routes match the same path.\n */\nexport function computeRank(segments: ParsedSegment[]): number {\n if (segments.length === 1 && segments[0].kind === 'catchall') return -1;\n let score = 0;\n for (const seg of segments) {\n if (seg.kind === 'static') score += 3;\n else if (seg.kind === 'param') score += 2;\n else if (seg.kind === 'wildcard') score += 1;\n // catchall contributes 0 per segment but is handled above\n }\n return score;\n}\n\n/** Reused frozen empty params object — returned for purely-static (no-capture) routes. */\nconst emptyParams: Record<string, string> = Object.freeze(\n Object.create(null) as Record<string, string>\n);\n\n/** Returned for every failed match — avoids per-call allocation. */\nconst noMatch: MatchResult = Object.freeze({\n matched: false,\n params: emptyParams,\n});\n\n/**\n * Match pre-split URL parts against pre-parsed route segments.\n *\n * This is the hot-path matcher used by `resolveRoute` and\n * `resolveRouteFromRoutes`. Callers split the URL path **once** and reuse\n * `urlParts` across all route comparisons in a single resolution call.\n *\n * - Returns a params object (possibly the shared `{}`) on match.\n * - Returns `null` on no match.\n * - Params are allocated lazily — purely-static routes return the frozen\n * empty sentinel without any heap allocation.\n */\nexport function matchSegments(\n urlParts: string[],\n segments: ParsedSegment[]\n): Record<string, string> | null {\n // catch-all /* — matches every URL at any depth\n if (segments.length === 1 && segments[0].kind === 'catchall') {\n return {\n '*':\n urlParts.length === 0\n ? '/'\n : urlParts.length === 1\n ? urlParts[0]\n : '/' + urlParts.join('/'),\n };\n }\n\n // non-catchall: part count must equal segment count\n if (urlParts.length !== segments.length) return null;\n\n // Walk segments; allocate the params object lazily on first capture\n let params: Record<string, string> | null = null;\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i];\n const part = urlParts[i];\n if (seg.kind === 'static') {\n if (seg.value !== part) return null;\n } else {\n if (params === null) params = {};\n if (seg.kind === 'param') {\n // Avoid decodeURIComponent when no percent-encoding is present\n params[seg.value] = part.includes('%')\n ? decodeURIComponent(part)\n : part;\n } else {\n // wildcard\n params['*'] = part;\n }\n }\n }\n return params ?? emptyParams;\n}\n\nexport interface MatchResult {\n matched: boolean;\n params: Record<string, string>;\n}\n\n/**\n * Match a path against a route pattern and extract params\n *\n * @example\n * match('/users/123', '/users/{id}')\n * // → { matched: true, params: { id: '123' } }\n *\n * match('/posts/hello-world/edit', '/posts/{slug}/{action}')\n * // → { matched: true, params: { slug: 'hello-world', action: 'edit' } }\n *\n * match('/users', '/posts/{id}')\n * // → { matched: false, params: {} }\n */\nexport function match(path: string, pattern: string): MatchResult {\n // Normalize trailing slashes\n const normalizedPath =\n path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;\n const normalizedPattern =\n pattern.endsWith('/') && pattern !== '/' ? pattern.slice(0, -1) : pattern;\n\n // Split into segments\n const pathSegments = normalizedPath.split('/').filter(Boolean);\n const patternSegments = normalizedPattern.split('/').filter(Boolean);\n\n // Support catch-all route: /* matches any path at any depth\n if (patternSegments.length === 1 && patternSegments[0] === '*') {\n // For multi-segment paths, preserve the leading slash\n // For single-segment paths, return just the segment\n return {\n matched: true,\n params: {\n '*': pathSegments.length > 1 ? normalizedPath : pathSegments[0],\n },\n };\n }\n\n // Check if lengths match (wildcard segments still need to match one segment)\n if (pathSegments.length !== patternSegments.length) {\n return { matched: false, params: {} };\n }\n\n const params: Record<string, string> = {};\n\n // Match each segment\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSegment = patternSegments[i];\n const pathSegment = pathSegments[i];\n\n // Parameter: {paramName}\n if (patternSegment.startsWith('{') && patternSegment.endsWith('}')) {\n const paramName = patternSegment.slice(1, -1);\n params[paramName] = pathSegment.includes('%')\n ? decodeURIComponent(pathSegment)\n : pathSegment;\n } else if (patternSegment === '*') {\n // Wildcard: match single segment\n params['*'] = pathSegment;\n } else if (patternSegment !== pathSegment) {\n // Literal segment mismatch\n return noMatch;\n }\n }\n\n return { matched: true, params };\n}\n"],"mappings":";;;;;;;;;AAcA,SAAgB,cAAc,MAA+B;CAI3D,MAAM,SAFJ,KAAK,SAAS,IAAI,IAAI,SAAS,MAAM,KAAK,MAAM,GAAG,GAAG,GAAG,MAElC,MAAM,IAAI,CAAC,OAAO,QAAQ;AAGnD,KAAI,MAAM,WAAW,KAAK,MAAM,OAAO,IACrC,QAAO,CAAC;EAAE,MAAM;EAAY,OAAO;EAAK,CAAC;AAG3C,QAAO,MAAM,KAAK,YAA2B;AAC3C,MAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,CAClD,QAAO;GAAE,MAAM;GAAS,OAAO,QAAQ,MAAM,GAAG,GAAG;GAAE;AAEvD,MAAI,YAAY,IACd,QAAO;GAAE,MAAM;GAAY,OAAO;GAAK;AAEzC,SAAO;GAAE,MAAM;GAAU,OAAO;GAAS;GACzC;;;;;;;;AASJ,SAAgB,YAAY,UAAmC;AAC7D,KAAI,SAAS,WAAW,KAAK,SAAS,GAAG,SAAS,WAAY,QAAO;CACrE,IAAI,QAAQ;AACZ,MAAK,MAAM,OAAO,SAChB,KAAI,IAAI,SAAS,SAAU,UAAS;UAC3B,IAAI,SAAS,QAAS,UAAS;UAC/B,IAAI,SAAS,WAAY,UAAS;AAG7C,QAAO;;;AAIT,MAAM,cAAsC,OAAO,OACjD,OAAO,OAAO,KAAK,CACpB;AAG4B,OAAO,OAAO;CACzC,SAAS;CACT,QAAQ;CACT,CAAC;;;;;;;;;;;;;AAcF,SAAgB,cACd,UACA,UAC+B;AAE/B,KAAI,SAAS,WAAW,KAAK,SAAS,GAAG,SAAS,WAChD,QAAO,EACL,KACE,SAAS,WAAW,IAChB,MACA,SAAS,WAAW,IAClB,SAAS,KACT,MAAM,SAAS,KAAK,IAAI,EACjC;AAIH,KAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;CAGhD,IAAI,SAAwC;AAC5C,MAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,SAAS;AACtB,MAAI,IAAI,SAAS,UACf;OAAI,IAAI,UAAU,KAAM,QAAO;SAC1B;AACL,OAAI,WAAW,KAAM,UAAS,EAAE;AAChC,OAAI,IAAI,SAAS,QAEf,QAAO,IAAI,SAAS,KAAK,SAAS,IAAI,GAClC,mBAAmB,KAAK,GACxB;OAGJ,QAAO,OAAO;;;AAIpB,QAAO,UAAU"}
1
+ {"version":3,"file":"match.js","names":[],"sources":["../../src/router/match.ts"],"sourcesContent":["/**\n * Path matching, segment parsing, and specificity scoring.\n */\n\nimport type { ParsedSegment } from '../common/router';\n\n/**\n * Parse a route template path into typed segments.\n *\n * @example\n * parseSegments('/users/{id}') // [{kind:'static',value:'users'},{kind:'param',value:'id'}]\n * parseSegments('/*') // [{kind:'catchall',value:'*'}]\n * parseSegments('/posts/*') // [{kind:'static',value:'posts'},{kind:'wildcard',value:'*'}]\n */\nexport function parseSegments(path: string): ParsedSegment[] {\n const normalized =\n path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;\n\n const parts = normalized.split('/').filter(Boolean);\n\n // Bare catch-all: /*\n if (parts.length === 1 && parts[0] === '*') {\n return [{ kind: 'catchall', value: '*' }];\n }\n\n return parts.map((segment): ParsedSegment => {\n if (segment.startsWith('{') && segment.endsWith('}')) {\n return { kind: 'param', value: segment.slice(1, -1) };\n }\n if (segment === '*') {\n return { kind: 'wildcard', value: '*' };\n }\n return { kind: 'static', value: segment };\n });\n}\n\n/**\n * Compute a numeric specificity rank from a parsed segment list.\n *\n * Scoring: static = 3, param = 2, wildcard = 1, catchall = 0.\n * Higher rank wins when multiple routes match the same path.\n */\nexport function computeRank(segments: ParsedSegment[]): number {\n if (segments.length === 1 && segments[0].kind === 'catchall') return -1;\n let score = 0;\n for (const seg of segments) {\n if (seg.kind === 'static') score += 3;\n else if (seg.kind === 'param') score += 2;\n else if (seg.kind === 'wildcard') score += 1;\n // catchall contributes 0 per segment but is handled above\n }\n return score;\n}\n\n/** Reused frozen empty params object — returned for purely-static (no-capture) routes. */\nconst emptyParams: Record<string, string> = Object.freeze(\n Object.create(null) as Record<string, string>\n);\n\n/** Returned for every failed match — avoids per-call allocation. */\nconst noMatch: MatchResult = Object.freeze({\n matched: false,\n params: emptyParams,\n});\n\n/**\n * Match pre-split URL parts against pre-parsed route segments.\n *\n * This is the hot-path matcher used by `resolveRoute` and\n * `resolveRouteFromRoutes`. Callers split the URL path **once** and reuse\n * `urlParts` across all route comparisons in a single resolution call.\n *\n * - Returns a params object (possibly the shared `{}`) on match.\n * - Returns `null` on no match.\n * - Params are allocated lazily — purely-static routes return the frozen\n * empty sentinel without any heap allocation.\n */\nexport function matchSegments(\n urlParts: string[],\n segments: ParsedSegment[]\n): Record<string, string> | null {\n // catch-all /* — matches every URL at any depth\n if (segments.length === 1 && segments[0].kind === 'catchall') {\n return {\n '*':\n urlParts.length === 0\n ? '/'\n : urlParts.length === 1\n ? urlParts[0]\n : '/' + urlParts.join('/'),\n };\n }\n\n // non-catchall: part count must equal segment count\n if (urlParts.length !== segments.length) return null;\n\n // Walk segments; allocate the params object lazily on first capture\n let params: Record<string, string> | null = null;\n for (let i = 0; i < segments.length; i++) {\n const seg = segments[i];\n const part = urlParts[i];\n if (seg.kind === 'static') {\n if (seg.value !== part) return null;\n } else {\n if (params === null) params = {};\n if (seg.kind === 'param') {\n // Avoid decodeURIComponent when no percent-encoding is present\n params[seg.value] = part.includes('%')\n ? decodeURIComponent(part)\n : part;\n } else {\n // wildcard\n params['*'] = part;\n }\n }\n }\n return params ?? emptyParams;\n}\n\nexport interface MatchResult {\n matched: boolean;\n params: Record<string, string>;\n}\n\n/**\n * Match a path against a route pattern and extract params\n *\n * @example\n * match('/users/123', '/users/{id}')\n * // → { matched: true, params: { id: '123' } }\n *\n * match('/posts/hello-world/edit', '/posts/{slug}/{action}')\n * // → { matched: true, params: { slug: 'hello-world', action: 'edit' } }\n *\n * match('/users', '/posts/{id}')\n * // → { matched: false, params: {} }\n */\nexport function match(path: string, pattern: string): MatchResult {\n // Normalize trailing slashes\n const normalizedPath =\n path.endsWith('/') && path !== '/' ? path.slice(0, -1) : path;\n const normalizedPattern =\n pattern.endsWith('/') && pattern !== '/' ? pattern.slice(0, -1) : pattern;\n\n // Split into segments\n const pathSegments = normalizedPath.split('/').filter(Boolean);\n const patternSegments = normalizedPattern.split('/').filter(Boolean);\n\n // Support catch-all route: /* matches any path at any depth\n if (patternSegments.length === 1 && patternSegments[0] === '*') {\n // For multi-segment paths, preserve the leading slash\n // For single-segment paths, return just the segment\n return {\n matched: true,\n params: {\n '*': pathSegments.length > 1 ? normalizedPath : pathSegments[0],\n },\n };\n }\n\n // Check if lengths match (wildcard segments still need to match one segment)\n if (pathSegments.length !== patternSegments.length) {\n return { matched: false, params: {} };\n }\n\n const params: Record<string, string> = {};\n\n // Match each segment\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSegment = patternSegments[i];\n const pathSegment = pathSegments[i];\n\n // Parameter: {paramName}\n if (patternSegment.startsWith('{') && patternSegment.endsWith('}')) {\n const paramName = patternSegment.slice(1, -1);\n params[paramName] = pathSegment.includes('%')\n ? decodeURIComponent(pathSegment)\n : pathSegment;\n } else if (patternSegment === '*') {\n // Wildcard: match single segment\n params['*'] = pathSegment;\n } else if (patternSegment !== pathSegment) {\n // Literal segment mismatch\n return noMatch;\n }\n }\n\n return { matched: true, params };\n}\n"],"mappings":";;;;;;;;;AAcA,SAAgB,cAAc,MAA+B;CAI3D,MAAM,SAFJ,KAAK,SAAS,IAAI,IAAI,SAAS,MAAM,KAAK,MAAM,GAAG,GAAG,GAAG,MAElC,MAAM,IAAI,CAAC,OAAO,QAAQ;CAGnD,IAAI,MAAM,WAAW,KAAK,MAAM,OAAO,KACrC,OAAO,CAAC;EAAE,MAAM;EAAY,OAAO;EAAK,CAAC;CAG3C,OAAO,MAAM,KAAK,YAA2B;EAC3C,IAAI,QAAQ,WAAW,IAAI,IAAI,QAAQ,SAAS,IAAI,EAClD,OAAO;GAAE,MAAM;GAAS,OAAO,QAAQ,MAAM,GAAG,GAAG;GAAE;EAEvD,IAAI,YAAY,KACd,OAAO;GAAE,MAAM;GAAY,OAAO;GAAK;EAEzC,OAAO;GAAE,MAAM;GAAU,OAAO;GAAS;GACzC;;;;;;;;AASJ,SAAgB,YAAY,UAAmC;CAC7D,IAAI,SAAS,WAAW,KAAK,SAAS,GAAG,SAAS,YAAY,OAAO;CACrE,IAAI,QAAQ;CACZ,KAAK,MAAM,OAAO,UAChB,IAAI,IAAI,SAAS,UAAU,SAAS;MAC/B,IAAI,IAAI,SAAS,SAAS,SAAS;MACnC,IAAI,IAAI,SAAS,YAAY,SAAS;CAG7C,OAAO;;;AAIT,MAAM,cAAsC,OAAO,OACjD,OAAO,OAAO,KAAK,CACpB;AAG4B,OAAO,OAAO;CACzC,SAAS;CACT,QAAQ;CACT,CAAC;;;;;;;;;;;;;AAcF,SAAgB,cACd,UACA,UAC+B;CAE/B,IAAI,SAAS,WAAW,KAAK,SAAS,GAAG,SAAS,YAChD,OAAO,EACL,KACE,SAAS,WAAW,IAChB,MACA,SAAS,WAAW,IAClB,SAAS,KACT,MAAM,SAAS,KAAK,IAAI,EACjC;CAIH,IAAI,SAAS,WAAW,SAAS,QAAQ,OAAO;CAGhD,IAAI,SAAwC;CAC5C,KAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;EACxC,MAAM,MAAM,SAAS;EACrB,MAAM,OAAO,SAAS;EACtB,IAAI,IAAI,SAAS,UACf;OAAI,IAAI,UAAU,MAAM,OAAO;SAC1B;GACL,IAAI,WAAW,MAAM,SAAS,EAAE;GAChC,IAAI,IAAI,SAAS,SAEf,OAAO,IAAI,SAAS,KAAK,SAAS,IAAI,GAClC,mBAAmB,KAAK,GACxB;QAGJ,OAAO,OAAO;;;CAIpB,OAAO,UAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"navigate.d.ts","names":[],"sources":["../../src/router/navigate.ts"],"mappings":";;;KAyBY,wBAAA;AAAA,KACA,qBAAA;AAAA,KAEA,wBAAA;EACV,UAAA,GAAa,wBAAA;EACb,OAAA,GAAU,qBAAA;AAAA;AAAA,KAqBA,eAAA;EACV,OAAA;EACA,MAAA,GAAS,wBAAA;AAAA;AAAA,iBAsIK,0BAAA,CACd,OAAA,aAAoB,wBAAA;;iBA6ON,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,IAAA;;;;;iBAiBc,QAAA,CAAS,IAAA,UAAc,OAAA,GAAS,eAAA;;AAzYhD;;iBAqdgB,oBAAA,CAAA;;;;iBASA,iBAAA,CAAA"}
1
+ {"version":3,"file":"navigate.d.ts","names":[],"sources":["../../src/router/navigate.ts"],"mappings":";;;KA2BY,wBAAA;AAAA,KACA,qBAAA;AAAA,KAEA,wBAAA;EACV,UAAA,GAAa,wBAAA;EACb,OAAA,GAAU,qBAAA;AAAA;AAAA,KAqBA,eAAA;EACV,OAAA;EACA,MAAA,GAAS,wBAAA;AAAA;AAAA,iBAsIK,0BAAA,CACd,OAAA,aAAoB,wBAAA;;iBAkPN,mBAAA,CACd,QAAA,EAAU,iBAAA,EACV,IAAA;;;;;iBAqBc,QAAA,CAAS,IAAA,UAAc,OAAA,GAAS,eAAA;;AAlZhD;;iBAoegB,oBAAA,CAAA;;;;iBAYA,iBAAA,CAAA"}
@@ -2,7 +2,7 @@ import { isDevelopmentEnvironment, isProductionEnvironment } from "../common/env
2
2
  import { logger } from "../dev/logger.js";
3
3
  import { ELEMENT_TYPE, Fragment } from "../common/jsx.js";
4
4
  import { mountComponent } from "../runtime/component.js";
5
- import { lockRouteRegistration, resolveRoute, resolveRouteRequest } from "./route.js";
5
+ import { lockRouteRegistration, resolveRoute, resolveRouteRequest, syncCurrentRouteSnapshot } from "./route.js";
6
6
  import "../jsx/index.js";
7
7
  import { DefaultPortal } from "../foundations/structures/portal.js";
8
8
  //#region src/router/navigate.ts
@@ -12,6 +12,7 @@ import { DefaultPortal } from "../foundations/structures/portal.js";
12
12
  let currentInstance = null;
13
13
  let currentPathname = "/";
14
14
  let currentHref = "/";
15
+ let navigationInitialized = false;
15
16
  const DEFAULT_SCROLL_RESTORATION = {
16
17
  enabled: true,
17
18
  navigation: "top",
@@ -211,6 +212,7 @@ function applyNavigationTarget(path, options, target) {
211
212
  };
212
213
  const historyMethod = options.history === "replace" ? "replaceState" : "pushState";
213
214
  window.history[historyMethod]({ path: href }, "", href);
215
+ syncCurrentRouteSnapshot(window.location.pathname, window.location.search, window.location.hash);
214
216
  if (currentInstance) {
215
217
  if (pathname === currentPathname && isRenderResult(resolved)) {
216
218
  rerenderResolvedRoute(pathname, href);
@@ -225,7 +227,7 @@ function registerAppInstance(instance, path) {
225
227
  currentInstance = instance;
226
228
  currentPathname = path;
227
229
  currentHref = getWindowHref();
228
- saveScrollPosition(currentHref);
230
+ syncCurrentRouteSnapshot(window.location.pathname, window.location.search, window.location.hash);
229
231
  if (isProductionEnvironment()) lockRouteRegistration();
230
232
  }
231
233
  /**
@@ -260,6 +262,7 @@ function handlePopState(_event) {
260
262
  navigate(resolved.to, { history: "replace" });
261
263
  return;
262
264
  }
265
+ syncCurrentRouteSnapshot(window.location.pathname, window.location.search, window.location.hash);
263
266
  if (pathname === currentPathname && isRenderResult(resolved)) {
264
267
  rerenderResolvedRoute(pathname, href);
265
268
  return;
@@ -280,7 +283,9 @@ function handlePopState(_event) {
280
283
  * Setup popstate listener for browser navigation
281
284
  */
282
285
  function initializeNavigation() {
283
- if (typeof window !== "undefined") window.addEventListener("popstate", handlePopState);
286
+ if (typeof window === "undefined" || navigationInitialized) return;
287
+ navigationInitialized = true;
288
+ window.addEventListener("popstate", handlePopState);
284
289
  }
285
290
  //#endregion
286
291
  export { configureScrollRestoration, initializeNavigation, navigate, registerAppInstance };
@@ -1 +1 @@
1
- {"version":3,"file":"navigate.js","names":[],"sources":["../../src/router/navigate.ts"],"sourcesContent":["/**\n * Client-side navigation with History API\n */\n\nimport {\n resolveRoute,\n resolveRouteRequest,\n lockRouteRegistration,\n type ResolvedRoute,\n} from './route';\nimport { mountComponent, type ComponentInstance } from '../runtime/component';\nimport {\n isDevelopmentEnvironment,\n isProductionEnvironment,\n} from '../common/env';\nimport { logger } from '../dev/logger';\nimport type { RouteRenderResult, RouteRequestResult } from '../common/router';\nimport { Fragment, ELEMENT_TYPE } from '../jsx';\nimport { DefaultPortal } from '../foundations/structures/portal';\n\n// Global app state for navigation\nlet currentInstance: ComponentInstance | null = null;\nlet currentPathname = '/';\nlet currentHref = '/';\n\nexport type NavigationScrollBehavior = 'top' | 'preserve';\nexport type HistoryScrollBehavior = 'restore' | 'top' | 'preserve';\n\nexport type ScrollRestorationOptions = {\n navigation?: NavigationScrollBehavior;\n history?: HistoryScrollBehavior;\n};\n\ntype NormalizedScrollRestorationOptions = {\n enabled: boolean;\n navigation: NavigationScrollBehavior;\n history: HistoryScrollBehavior;\n};\n\nconst DEFAULT_SCROLL_RESTORATION: NormalizedScrollRestorationOptions = {\n enabled: true,\n navigation: 'top',\n history: 'restore',\n};\n\nlet scrollRestorationOptions: NormalizedScrollRestorationOptions = {\n ...DEFAULT_SCROLL_RESTORATION,\n};\n\nconst scrollPositions = new Map<string, { x: number; y: number }>();\n\nexport type NavigateOptions = {\n history?: 'push' | 'replace';\n scroll?: NavigationScrollBehavior;\n};\n\nfunction getWindowHref(): string {\n if (typeof window === 'undefined') {\n return currentHref;\n }\n\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\nfunction normalizeScrollRestorationOptions(\n options?: boolean | ScrollRestorationOptions\n): NormalizedScrollRestorationOptions {\n if (options === false) {\n return {\n enabled: false,\n navigation: DEFAULT_SCROLL_RESTORATION.navigation,\n history: DEFAULT_SCROLL_RESTORATION.history,\n };\n }\n\n if (options === true || options === undefined) {\n return { ...DEFAULT_SCROLL_RESTORATION };\n }\n\n return {\n enabled: true,\n navigation: options.navigation ?? DEFAULT_SCROLL_RESTORATION.navigation,\n history: options.history ?? DEFAULT_SCROLL_RESTORATION.history,\n };\n}\n\nfunction readScrollPosition(): { x: number; y: number } {\n if (typeof window === 'undefined') {\n return { x: 0, y: 0 };\n }\n\n const x =\n typeof window.scrollX === 'number'\n ? window.scrollX\n : typeof window.pageXOffset === 'number'\n ? window.pageXOffset\n : 0;\n const y =\n typeof window.scrollY === 'number'\n ? window.scrollY\n : typeof window.pageYOffset === 'number'\n ? window.pageYOffset\n : 0;\n\n return { x, y };\n}\n\nfunction writeHistoryScrollPosition(\n href: string,\n position: { x: number; y: number }\n): void {\n if (typeof window === 'undefined' || getWindowHref() !== href) {\n return;\n }\n if (typeof window.history?.replaceState !== 'function') {\n return;\n }\n const state =\n window.history.state && typeof window.history.state === 'object'\n ? window.history.state\n : {};\n\n window.history.replaceState(\n {\n ...state,\n path: href,\n scroll: position,\n },\n '',\n href\n );\n}\n\nfunction saveScrollPosition(href: string): void {\n if (!scrollRestorationOptions.enabled || typeof window === 'undefined') {\n return;\n }\n\n const position = readScrollPosition();\n scrollPositions.set(href, position);\n writeHistoryScrollPosition(href, position);\n}\n\nfunction scrollToPosition(position: { x: number; y: number }): void {\n if (typeof window === 'undefined' || typeof window.scrollTo !== 'function') {\n return;\n }\n\n window.scrollTo(position.x, position.y);\n}\n\nfunction applyNavigationScroll(behavior: NavigationScrollBehavior): void {\n if (!scrollRestorationOptions.enabled || behavior === 'preserve') {\n return;\n }\n\n scrollToPosition({ x: 0, y: 0 });\n}\n\nfunction applyHistoryScroll(href: string, state: PopStateEvent['state']): void {\n if (!scrollRestorationOptions.enabled) {\n return;\n }\n\n if (scrollRestorationOptions.history === 'preserve') {\n return;\n }\n\n if (scrollRestorationOptions.history === 'top') {\n scrollToPosition({ x: 0, y: 0 });\n return;\n }\n\n const fromState =\n state && typeof state === 'object' && 'scroll' in state\n ? (state.scroll as { x?: unknown; y?: unknown })\n : undefined;\n const saved =\n fromState &&\n typeof fromState.x === 'number' &&\n typeof fromState.y === 'number'\n ? { x: fromState.x, y: fromState.y }\n : scrollPositions.get(href);\n\n scrollToPosition(saved ?? { x: 0, y: 0 });\n}\n\nexport function configureScrollRestoration(\n options?: boolean | ScrollRestorationOptions\n): void {\n scrollRestorationOptions = normalizeScrollRestorationOptions(options);\n\n if (typeof window === 'undefined') {\n return;\n }\n\n if ('scrollRestoration' in window.history) {\n try {\n window.history.scrollRestoration = scrollRestorationOptions.enabled\n ? 'manual'\n : 'auto';\n } catch {\n // Ignore environments that expose but do not allow setting scrollRestoration.\n }\n }\n}\n\nfunction parseTargetUrl(path: string): URL {\n const pathname = window.location.pathname || '/';\n const search = window.location.search || '';\n const hash = window.location.hash || '';\n const href =\n typeof window.location.href === 'string' ? window.location.href : '';\n const base =\n href &&\n href !== 'about:blank' &&\n !href.startsWith('about:') &&\n !href.startsWith('data:')\n ? href\n : `http://localhost${pathname}${search}${hash}`;\n\n return new URL(path, base);\n}\n\nfunction isRenderResult(\n result: RouteRequestResult\n): result is RouteRenderResult {\n return result !== null && result.kind === 'render';\n}\n\nfunction isPromise<T>(value: T | Promise<T>): value is Promise<T> {\n return value instanceof Promise;\n}\n\nfunction createDeniedResolvedRoute(status: number): ResolvedRoute {\n return {\n handler: () => ({\n type: 'div',\n props: {\n 'data-route-denied': String(status),\n },\n children: [String(status)],\n }),\n params: {},\n };\n}\n\nfunction bindResolvedRouteHandler(\n resolved: ResolvedRoute\n): ComponentInstance['fn'] {\n return () =>\n resolved.handler(resolved.params) as ReturnType<ComponentInstance['fn']>;\n}\n\nfunction wrapRootRouteHandler(\n componentFn: ComponentInstance['fn']\n): ComponentInstance['fn'] {\n const wrappedFn: ComponentInstance['fn'] = (props, ctx) => {\n const out = componentFn(props, ctx);\n const portalVNode = {\n $$typeof: ELEMENT_TYPE,\n type: DefaultPortal,\n props: {},\n key: '__default_portal',\n } as unknown;\n\n return {\n $$typeof: ELEMENT_TYPE,\n type: Fragment,\n props: {\n children:\n out === undefined || out === null\n ? [portalVNode]\n : [out, portalVNode],\n },\n } as ReturnType<ComponentInstance['fn']>;\n };\n\n Object.defineProperty(wrappedFn, 'name', {\n value: componentFn.name || 'Component',\n });\n\n return wrappedFn;\n}\n\nfunction remountResolvedRoute(\n resolved: ResolvedRoute,\n pathname: string,\n href: string\n): boolean {\n if (!currentInstance) {\n return false;\n }\n\n // The route handler IS the component function\n // It takes params as props and renders the route\n currentInstance.fn = wrapRootRouteHandler(bindResolvedRouteHandler(resolved));\n currentInstance.props = {};\n\n // Reset state to prevent leakage from previous route\n // Each route navigation starts completely fresh\n currentInstance.stateValues = [];\n currentInstance.expectedStateIndices = [];\n currentInstance.firstRenderComplete = false;\n currentInstance.stateIndexCheck = -1;\n // Increment generation to invalidate pending async evaluations from previous route\n currentInstance.evaluationGeneration++;\n currentInstance.notifyUpdate = null;\n currentInstance.mountOperations = [];\n currentInstance._placeholder = undefined;\n currentInstance.hasPendingUpdate = false;\n\n // Route-local async work should create a fresh abort controller lazily.\n currentInstance.abortController = null;\n\n // Re-execute component against the existing host so reconciliation can\n // preserve any shared layout DOM between sibling routes.\n mountComponent(currentInstance);\n currentPathname = pathname;\n currentHref = href;\n return true;\n}\n\nfunction rerenderResolvedRoute(pathname: string, href: string): boolean {\n if (!currentInstance) {\n return false;\n }\n\n const resolved = resolveRoute(pathname);\n if (!resolved) {\n return false;\n }\n\n currentPathname = pathname;\n currentHref = href;\n currentInstance._enqueueRun?.();\n return true;\n}\n\nfunction resolveNavigationTarget(path: string) {\n const target = parseTargetUrl(path);\n const pathname = target.pathname;\n const href = `${target.pathname}${target.search}${target.hash}`;\n const resolved = resolveRouteRequest(href);\n\n if (isPromise(resolved)) {\n return resolved.then((next) => ({\n href,\n pathname,\n resolved: next,\n }));\n }\n\n return {\n href,\n pathname,\n resolved,\n };\n}\n\nfunction applyNavigationTarget(\n path: string,\n options: NavigateOptions,\n target: {\n href: string;\n pathname: string;\n resolved: RouteRequestResult;\n }\n): void {\n const { href, pathname, resolved } = target;\n const previousPathname = currentPathname;\n\n saveScrollPosition(currentHref);\n\n if (!resolved) {\n if (isDevelopmentEnvironment()) {\n logger.warn(`No route found for path: ${path}`);\n }\n return;\n }\n\n if (resolved.kind === 'redirect') {\n const redirectTarget = parseTargetUrl(resolved.to);\n const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;\n if (redirectHref === href) {\n if (isDevelopmentEnvironment()) {\n logger.warn(\n `Navigation guard redirected to the same path: ${redirectHref}`\n );\n }\n return;\n }\n\n navigate(redirectHref, { history: 'replace' });\n return;\n }\n\n const nextResolved =\n resolved.kind === 'deny'\n ? createDeniedResolvedRoute(resolved.status)\n : {\n handler: resolved.handler,\n params: resolved.params,\n };\n\n const historyMethod =\n options.history === 'replace' ? 'replaceState' : 'pushState';\n window.history[historyMethod]({ path: href }, '', href);\n\n if (currentInstance) {\n if (pathname === currentPathname && isRenderResult(resolved)) {\n rerenderResolvedRoute(pathname, href);\n return;\n }\n\n remountResolvedRoute(nextResolved, pathname, href);\n if (pathname !== previousPathname) {\n applyNavigationScroll(\n options.scroll ?? scrollRestorationOptions.navigation\n );\n }\n }\n}\n\n/** Register the current app instance (called by createSPA/hydrateSPA). */\nexport function registerAppInstance(\n instance: ComponentInstance,\n path: string\n): void {\n currentInstance = instance;\n currentPathname = path;\n currentHref = getWindowHref();\n saveScrollPosition(currentHref);\n // Lock further route registrations after the app has started — but allow tests to register routes.\n // Enforce only in production to avoid breaking test infra which registers routes dynamically.\n if (isProductionEnvironment()) {\n lockRouteRegistration();\n }\n}\n\n/**\n * Navigate to a new path\n * Updates URL, resolves route, and re-mounts app with new handler\n */\nexport function navigate(path: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n // SSR context\n return;\n }\n\n const target = resolveNavigationTarget(path);\n if (isPromise(target)) {\n void target.then((resolvedTarget) =>\n applyNavigationTarget(path, options, resolvedTarget)\n );\n return;\n }\n\n applyNavigationTarget(path, options, target);\n}\n\n/**\n * Handle browser back/forward buttons\n */\nfunction handlePopState(_event: PopStateEvent): void {\n const previousHref = currentHref;\n const pathname = window.location.pathname;\n const href = `${window.location.pathname}${window.location.search}${window.location.hash}`;\n\n saveScrollPosition(previousHref);\n\n if (!currentInstance) {\n return;\n }\n\n const resolved = resolveRouteRequest(href);\n\n const applyResolved = (resolved: RouteRequestResult) => {\n if (!resolved) {\n if (isDevelopmentEnvironment()) {\n logger.warn(`No route found for path: ${pathname}`);\n }\n return;\n }\n\n if (resolved.kind === 'redirect') {\n navigate(resolved.to, { history: 'replace' });\n return;\n }\n\n if (pathname === currentPathname && isRenderResult(resolved)) {\n rerenderResolvedRoute(pathname, href);\n return;\n }\n\n remountResolvedRoute(\n resolved.kind === 'deny'\n ? createDeniedResolvedRoute(resolved.status)\n : {\n handler: resolved.handler,\n params: resolved.params,\n },\n pathname,\n href\n );\n\n applyHistoryScroll(href, _event.state);\n };\n\n if (isPromise(resolved)) {\n void resolved.then((next) => applyResolved(next));\n return;\n }\n\n applyResolved(resolved);\n}\n\n/**\n * Setup popstate listener for browser navigation\n */\nexport function initializeNavigation(): void {\n if (typeof window !== 'undefined') {\n window.addEventListener('popstate', handlePopState);\n }\n}\n\n/**\n * Cleanup navigation listeners\n */\nexport function cleanupNavigation(): void {\n if (typeof window !== 'undefined') {\n window.removeEventListener('popstate', handlePopState);\n }\n}\n"],"mappings":";;;;;;;;;;;AAqBA,IAAI,kBAA4C;AAChD,IAAI,kBAAkB;AACtB,IAAI,cAAc;AAgBlB,MAAM,6BAAiE;CACrE,SAAS;CACT,YAAY;CACZ,SAAS;CACV;AAED,IAAI,2BAA+D,EACjE,GAAG,4BACJ;AAED,MAAM,kCAAkB,IAAI,KAAuC;AAOnE,SAAS,gBAAwB;AAC/B,KAAI,OAAO,WAAW,YACpB,QAAO;AAGT,QAAO,GAAG,OAAO,SAAS,WAAW,OAAO,SAAS,SAAS,OAAO,SAAS;;AAGhF,SAAS,kCACP,SACoC;AACpC,KAAI,YAAY,MACd,QAAO;EACL,SAAS;EACT,YAAY,2BAA2B;EACvC,SAAS,2BAA2B;EACrC;AAGH,KAAI,YAAY,QAAQ,YAAY,OAClC,QAAO,EAAE,GAAG,4BAA4B;AAG1C,QAAO;EACL,SAAS;EACT,YAAY,QAAQ,cAAc,2BAA2B;EAC7D,SAAS,QAAQ,WAAW,2BAA2B;EACxD;;AAGH,SAAS,qBAA+C;AACtD,KAAI,OAAO,WAAW,YACpB,QAAO;EAAE,GAAG;EAAG,GAAG;EAAG;AAgBvB,QAAO;EAAE,GAZP,OAAO,OAAO,YAAY,WACtB,OAAO,UACP,OAAO,OAAO,gBAAgB,WAC5B,OAAO,cACP;EAQI,GANV,OAAO,OAAO,YAAY,WACtB,OAAO,UACP,OAAO,OAAO,gBAAgB,WAC5B,OAAO,cACP;EAEO;;AAGjB,SAAS,2BACP,MACA,UACM;AACN,KAAI,OAAO,WAAW,eAAe,eAAe,KAAK,KACvD;AAEF,KAAI,OAAO,OAAO,SAAS,iBAAiB,WAC1C;CAEF,MAAM,QACJ,OAAO,QAAQ,SAAS,OAAO,OAAO,QAAQ,UAAU,WACpD,OAAO,QAAQ,QACf,EAAE;AAER,QAAO,QAAQ,aACb;EACE,GAAG;EACH,MAAM;EACN,QAAQ;EACT,EACD,IACA,KACD;;AAGH,SAAS,mBAAmB,MAAoB;AAC9C,KAAI,CAAC,yBAAyB,WAAW,OAAO,WAAW,YACzD;CAGF,MAAM,WAAW,oBAAoB;AACrC,iBAAgB,IAAI,MAAM,SAAS;AACnC,4BAA2B,MAAM,SAAS;;AAG5C,SAAS,iBAAiB,UAA0C;AAClE,KAAI,OAAO,WAAW,eAAe,OAAO,OAAO,aAAa,WAC9D;AAGF,QAAO,SAAS,SAAS,GAAG,SAAS,EAAE;;AAGzC,SAAS,sBAAsB,UAA0C;AACvE,KAAI,CAAC,yBAAyB,WAAW,aAAa,WACpD;AAGF,kBAAiB;EAAE,GAAG;EAAG,GAAG;EAAG,CAAC;;AAGlC,SAAS,mBAAmB,MAAc,OAAqC;AAC7E,KAAI,CAAC,yBAAyB,QAC5B;AAGF,KAAI,yBAAyB,YAAY,WACvC;AAGF,KAAI,yBAAyB,YAAY,OAAO;AAC9C,mBAAiB;GAAE,GAAG;GAAG,GAAG;GAAG,CAAC;AAChC;;CAGF,MAAM,YACJ,SAAS,OAAO,UAAU,YAAY,YAAY,QAC7C,MAAM,SACP;AAQN,mBANE,aACA,OAAO,UAAU,MAAM,YACvB,OAAO,UAAU,MAAM,WACnB;EAAE,GAAG,UAAU;EAAG,GAAG,UAAU;EAAG,GAClC,gBAAgB,IAAI,KAAK,KAEL;EAAE,GAAG;EAAG,GAAG;EAAG,CAAC;;AAG3C,SAAgB,2BACd,SACM;AACN,4BAA2B,kCAAkC,QAAQ;AAErE,KAAI,OAAO,WAAW,YACpB;AAGF,KAAI,uBAAuB,OAAO,QAChC,KAAI;AACF,SAAO,QAAQ,oBAAoB,yBAAyB,UACxD,WACA;SACE;;AAMZ,SAAS,eAAe,MAAmB;CACzC,MAAM,WAAW,OAAO,SAAS,YAAY;CAC7C,MAAM,SAAS,OAAO,SAAS,UAAU;CACzC,MAAM,OAAO,OAAO,SAAS,QAAQ;CACrC,MAAM,OACJ,OAAO,OAAO,SAAS,SAAS,WAAW,OAAO,SAAS,OAAO;CACpE,MAAM,OACJ,QACA,SAAS,iBACT,CAAC,KAAK,WAAW,SAAS,IAC1B,CAAC,KAAK,WAAW,QAAQ,GACrB,OACA,mBAAmB,WAAW,SAAS;AAE7C,QAAO,IAAI,IAAI,MAAM,KAAK;;AAG5B,SAAS,eACP,QAC6B;AAC7B,QAAO,WAAW,QAAQ,OAAO,SAAS;;AAG5C,SAAS,UAAa,OAA4C;AAChE,QAAO,iBAAiB;;AAG1B,SAAS,0BAA0B,QAA+B;AAChE,QAAO;EACL,gBAAgB;GACd,MAAM;GACN,OAAO,EACL,qBAAqB,OAAO,OAAO,EACpC;GACD,UAAU,CAAC,OAAO,OAAO,CAAC;GAC3B;EACD,QAAQ,EAAE;EACX;;AAGH,SAAS,yBACP,UACyB;AACzB,cACE,SAAS,QAAQ,SAAS,OAAO;;AAGrC,SAAS,qBACP,aACyB;CACzB,MAAM,aAAsC,OAAO,QAAQ;EACzD,MAAM,MAAM,YAAY,OAAO,IAAI;EACnC,MAAM,cAAc;GAClB,UAAU;GACV,MAAM;GACN,OAAO,EAAE;GACT,KAAK;GACN;AAED,SAAO;GACL,UAAU;GACV,MAAM;GACN,OAAO,EACL,UACE,QAAQ,UAAa,QAAQ,OACzB,CAAC,YAAY,GACb,CAAC,KAAK,YAAY,EACzB;GACF;;AAGH,QAAO,eAAe,WAAW,QAAQ,EACvC,OAAO,YAAY,QAAQ,aAC5B,CAAC;AAEF,QAAO;;AAGT,SAAS,qBACP,UACA,UACA,MACS;AACT,KAAI,CAAC,gBACH,QAAO;AAKT,iBAAgB,KAAK,qBAAqB,yBAAyB,SAAS,CAAC;AAC7E,iBAAgB,QAAQ,EAAE;AAI1B,iBAAgB,cAAc,EAAE;AAChC,iBAAgB,uBAAuB,EAAE;AACzC,iBAAgB,sBAAsB;AACtC,iBAAgB,kBAAkB;AAElC,iBAAgB;AAChB,iBAAgB,eAAe;AAC/B,iBAAgB,kBAAkB,EAAE;AACpC,iBAAgB,eAAe;AAC/B,iBAAgB,mBAAmB;AAGnC,iBAAgB,kBAAkB;AAIlC,gBAAe,gBAAgB;AAC/B,mBAAkB;AAClB,eAAc;AACd,QAAO;;AAGT,SAAS,sBAAsB,UAAkB,MAAuB;AACtE,KAAI,CAAC,gBACH,QAAO;AAIT,KAAI,CADa,aAAa,SACzB,CACH,QAAO;AAGT,mBAAkB;AAClB,eAAc;AACd,iBAAgB,eAAe;AAC/B,QAAO;;AAGT,SAAS,wBAAwB,MAAc;CAC7C,MAAM,SAAS,eAAe,KAAK;CACnC,MAAM,WAAW,OAAO;CACxB,MAAM,OAAO,GAAG,OAAO,WAAW,OAAO,SAAS,OAAO;CACzD,MAAM,WAAW,oBAAoB,KAAK;AAE1C,KAAI,UAAU,SAAS,CACrB,QAAO,SAAS,MAAM,UAAU;EAC9B;EACA;EACA,UAAU;EACX,EAAE;AAGL,QAAO;EACL;EACA;EACA;EACD;;AAGH,SAAS,sBACP,MACA,SACA,QAKM;CACN,MAAM,EAAE,MAAM,UAAU,aAAa;CACrC,MAAM,mBAAmB;AAEzB,oBAAmB,YAAY;AAE/B,KAAI,CAAC,UAAU;AACb,MAAI,0BAA0B,CAC5B,QAAO,KAAK,4BAA4B,OAAO;AAEjD;;AAGF,KAAI,SAAS,SAAS,YAAY;EAChC,MAAM,iBAAiB,eAAe,SAAS,GAAG;EAClD,MAAM,eAAe,GAAG,eAAe,WAAW,eAAe,SAAS,eAAe;AACzF,MAAI,iBAAiB,MAAM;AACzB,OAAI,0BAA0B,CAC5B,QAAO,KACL,iDAAiD,eAClD;AAEH;;AAGF,WAAS,cAAc,EAAE,SAAS,WAAW,CAAC;AAC9C;;CAGF,MAAM,eACJ,SAAS,SAAS,SACd,0BAA0B,SAAS,OAAO,GAC1C;EACE,SAAS,SAAS;EAClB,QAAQ,SAAS;EAClB;CAEP,MAAM,gBACJ,QAAQ,YAAY,YAAY,iBAAiB;AACnD,QAAO,QAAQ,eAAe,EAAE,MAAM,MAAM,EAAE,IAAI,KAAK;AAEvD,KAAI,iBAAiB;AACnB,MAAI,aAAa,mBAAmB,eAAe,SAAS,EAAE;AAC5D,yBAAsB,UAAU,KAAK;AACrC;;AAGF,uBAAqB,cAAc,UAAU,KAAK;AAClD,MAAI,aAAa,iBACf,uBACE,QAAQ,UAAU,yBAAyB,WAC5C;;;;AAMP,SAAgB,oBACd,UACA,MACM;AACN,mBAAkB;AAClB,mBAAkB;AAClB,eAAc,eAAe;AAC7B,oBAAmB,YAAY;AAG/B,KAAI,yBAAyB,CAC3B,wBAAuB;;;;;;AAQ3B,SAAgB,SAAS,MAAc,UAA2B,EAAE,EAAQ;AAC1E,KAAI,OAAO,WAAW,YAEpB;CAGF,MAAM,SAAS,wBAAwB,KAAK;AAC5C,KAAI,UAAU,OAAO,EAAE;AACrB,EAAK,OAAO,MAAM,mBAChB,sBAAsB,MAAM,SAAS,eAAe,CACrD;AACD;;AAGF,uBAAsB,MAAM,SAAS,OAAO;;;;;AAM9C,SAAS,eAAe,QAA6B;CACnD,MAAM,eAAe;CACrB,MAAM,WAAW,OAAO,SAAS;CACjC,MAAM,OAAO,GAAG,OAAO,SAAS,WAAW,OAAO,SAAS,SAAS,OAAO,SAAS;AAEpF,oBAAmB,aAAa;AAEhC,KAAI,CAAC,gBACH;CAGF,MAAM,WAAW,oBAAoB,KAAK;CAE1C,MAAM,iBAAiB,aAAiC;AACtD,MAAI,CAAC,UAAU;AACb,OAAI,0BAA0B,CAC5B,QAAO,KAAK,4BAA4B,WAAW;AAErD;;AAGF,MAAI,SAAS,SAAS,YAAY;AAChC,YAAS,SAAS,IAAI,EAAE,SAAS,WAAW,CAAC;AAC7C;;AAGF,MAAI,aAAa,mBAAmB,eAAe,SAAS,EAAE;AAC5D,yBAAsB,UAAU,KAAK;AACrC;;AAGF,uBACE,SAAS,SAAS,SACd,0BAA0B,SAAS,OAAO,GAC1C;GACE,SAAS,SAAS;GAClB,QAAQ,SAAS;GAClB,EACL,UACA,KACD;AAED,qBAAmB,MAAM,OAAO,MAAM;;AAGxC,KAAI,UAAU,SAAS,EAAE;AACvB,EAAK,SAAS,MAAM,SAAS,cAAc,KAAK,CAAC;AACjD;;AAGF,eAAc,SAAS;;;;;AAMzB,SAAgB,uBAA6B;AAC3C,KAAI,OAAO,WAAW,YACpB,QAAO,iBAAiB,YAAY,eAAe"}
1
+ {"version":3,"file":"navigate.js","names":[],"sources":["../../src/router/navigate.ts"],"sourcesContent":["/**\n * Client-side navigation with History API\n */\n\nimport {\n resolveRoute,\n resolveRouteRequest,\n lockRouteRegistration,\n syncCurrentRouteSnapshot,\n type ResolvedRoute,\n} from './route';\nimport { mountComponent, type ComponentInstance } from '../runtime/component';\nimport {\n isDevelopmentEnvironment,\n isProductionEnvironment,\n} from '../common/env';\nimport { logger } from '../dev/logger';\nimport type { RouteRenderResult, RouteRequestResult } from '../common/router';\nimport { Fragment, ELEMENT_TYPE } from '../jsx';\nimport { DefaultPortal } from '../foundations/structures/portal';\n\n// Global app state for navigation\nlet currentInstance: ComponentInstance | null = null;\nlet currentPathname = '/';\nlet currentHref = '/';\nlet navigationInitialized = false;\n\nexport type NavigationScrollBehavior = 'top' | 'preserve';\nexport type HistoryScrollBehavior = 'restore' | 'top' | 'preserve';\n\nexport type ScrollRestorationOptions = {\n navigation?: NavigationScrollBehavior;\n history?: HistoryScrollBehavior;\n};\n\ntype NormalizedScrollRestorationOptions = {\n enabled: boolean;\n navigation: NavigationScrollBehavior;\n history: HistoryScrollBehavior;\n};\n\nconst DEFAULT_SCROLL_RESTORATION: NormalizedScrollRestorationOptions = {\n enabled: true,\n navigation: 'top',\n history: 'restore',\n};\n\nlet scrollRestorationOptions: NormalizedScrollRestorationOptions = {\n ...DEFAULT_SCROLL_RESTORATION,\n};\n\nconst scrollPositions = new Map<string, { x: number; y: number }>();\n\nexport type NavigateOptions = {\n history?: 'push' | 'replace';\n scroll?: NavigationScrollBehavior;\n};\n\nfunction getWindowHref(): string {\n if (typeof window === 'undefined') {\n return currentHref;\n }\n\n return `${window.location.pathname}${window.location.search}${window.location.hash}`;\n}\n\nfunction normalizeScrollRestorationOptions(\n options?: boolean | ScrollRestorationOptions\n): NormalizedScrollRestorationOptions {\n if (options === false) {\n return {\n enabled: false,\n navigation: DEFAULT_SCROLL_RESTORATION.navigation,\n history: DEFAULT_SCROLL_RESTORATION.history,\n };\n }\n\n if (options === true || options === undefined) {\n return { ...DEFAULT_SCROLL_RESTORATION };\n }\n\n return {\n enabled: true,\n navigation: options.navigation ?? DEFAULT_SCROLL_RESTORATION.navigation,\n history: options.history ?? DEFAULT_SCROLL_RESTORATION.history,\n };\n}\n\nfunction readScrollPosition(): { x: number; y: number } {\n if (typeof window === 'undefined') {\n return { x: 0, y: 0 };\n }\n\n const x =\n typeof window.scrollX === 'number'\n ? window.scrollX\n : typeof window.pageXOffset === 'number'\n ? window.pageXOffset\n : 0;\n const y =\n typeof window.scrollY === 'number'\n ? window.scrollY\n : typeof window.pageYOffset === 'number'\n ? window.pageYOffset\n : 0;\n\n return { x, y };\n}\n\nfunction writeHistoryScrollPosition(\n href: string,\n position: { x: number; y: number }\n): void {\n if (typeof window === 'undefined' || getWindowHref() !== href) {\n return;\n }\n if (typeof window.history?.replaceState !== 'function') {\n return;\n }\n const state =\n window.history.state && typeof window.history.state === 'object'\n ? window.history.state\n : {};\n\n window.history.replaceState(\n {\n ...state,\n path: href,\n scroll: position,\n },\n '',\n href\n );\n}\n\nfunction saveScrollPosition(href: string): void {\n if (!scrollRestorationOptions.enabled || typeof window === 'undefined') {\n return;\n }\n\n const position = readScrollPosition();\n scrollPositions.set(href, position);\n writeHistoryScrollPosition(href, position);\n}\n\nfunction scrollToPosition(position: { x: number; y: number }): void {\n if (typeof window === 'undefined' || typeof window.scrollTo !== 'function') {\n return;\n }\n\n window.scrollTo(position.x, position.y);\n}\n\nfunction applyNavigationScroll(behavior: NavigationScrollBehavior): void {\n if (!scrollRestorationOptions.enabled || behavior === 'preserve') {\n return;\n }\n\n scrollToPosition({ x: 0, y: 0 });\n}\n\nfunction applyHistoryScroll(href: string, state: PopStateEvent['state']): void {\n if (!scrollRestorationOptions.enabled) {\n return;\n }\n\n if (scrollRestorationOptions.history === 'preserve') {\n return;\n }\n\n if (scrollRestorationOptions.history === 'top') {\n scrollToPosition({ x: 0, y: 0 });\n return;\n }\n\n const fromState =\n state && typeof state === 'object' && 'scroll' in state\n ? (state.scroll as { x?: unknown; y?: unknown })\n : undefined;\n const saved =\n fromState &&\n typeof fromState.x === 'number' &&\n typeof fromState.y === 'number'\n ? { x: fromState.x, y: fromState.y }\n : scrollPositions.get(href);\n\n scrollToPosition(saved ?? { x: 0, y: 0 });\n}\n\nexport function configureScrollRestoration(\n options?: boolean | ScrollRestorationOptions\n): void {\n scrollRestorationOptions = normalizeScrollRestorationOptions(options);\n\n if (typeof window === 'undefined') {\n return;\n }\n\n if ('scrollRestoration' in window.history) {\n try {\n window.history.scrollRestoration = scrollRestorationOptions.enabled\n ? 'manual'\n : 'auto';\n } catch {\n // Ignore environments that expose but do not allow setting scrollRestoration.\n }\n }\n}\n\nfunction parseTargetUrl(path: string): URL {\n const pathname = window.location.pathname || '/';\n const search = window.location.search || '';\n const hash = window.location.hash || '';\n const href =\n typeof window.location.href === 'string' ? window.location.href : '';\n const base =\n href &&\n href !== 'about:blank' &&\n !href.startsWith('about:') &&\n !href.startsWith('data:')\n ? href\n : `http://localhost${pathname}${search}${hash}`;\n\n return new URL(path, base);\n}\n\nfunction isRenderResult(\n result: RouteRequestResult\n): result is RouteRenderResult {\n return result !== null && result.kind === 'render';\n}\n\nfunction isPromise<T>(value: T | Promise<T>): value is Promise<T> {\n return value instanceof Promise;\n}\n\nfunction createDeniedResolvedRoute(status: number): ResolvedRoute {\n return {\n handler: () => ({\n type: 'div',\n props: {\n 'data-route-denied': String(status),\n },\n children: [String(status)],\n }),\n params: {},\n };\n}\n\nfunction bindResolvedRouteHandler(\n resolved: ResolvedRoute\n): ComponentInstance['fn'] {\n return () =>\n resolved.handler(resolved.params) as ReturnType<ComponentInstance['fn']>;\n}\n\nfunction wrapRootRouteHandler(\n componentFn: ComponentInstance['fn']\n): ComponentInstance['fn'] {\n const wrappedFn: ComponentInstance['fn'] = (props, ctx) => {\n const out = componentFn(props, ctx);\n const portalVNode = {\n $$typeof: ELEMENT_TYPE,\n type: DefaultPortal,\n props: {},\n key: '__default_portal',\n } as unknown;\n\n return {\n $$typeof: ELEMENT_TYPE,\n type: Fragment,\n props: {\n children:\n out === undefined || out === null\n ? [portalVNode]\n : [out, portalVNode],\n },\n } as ReturnType<ComponentInstance['fn']>;\n };\n\n Object.defineProperty(wrappedFn, 'name', {\n value: componentFn.name || 'Component',\n });\n\n return wrappedFn;\n}\n\nfunction remountResolvedRoute(\n resolved: ResolvedRoute,\n pathname: string,\n href: string\n): boolean {\n if (!currentInstance) {\n return false;\n }\n\n // The route handler IS the component function\n // It takes params as props and renders the route\n currentInstance.fn = wrapRootRouteHandler(bindResolvedRouteHandler(resolved));\n currentInstance.props = {};\n\n // Reset state to prevent leakage from previous route\n // Each route navigation starts completely fresh\n currentInstance.stateValues = [];\n currentInstance.expectedStateIndices = [];\n currentInstance.firstRenderComplete = false;\n currentInstance.stateIndexCheck = -1;\n // Increment generation to invalidate pending async evaluations from previous route\n currentInstance.evaluationGeneration++;\n currentInstance.notifyUpdate = null;\n currentInstance.mountOperations = [];\n currentInstance._placeholder = undefined;\n currentInstance.hasPendingUpdate = false;\n\n // Route-local async work should create a fresh abort controller lazily.\n currentInstance.abortController = null;\n\n // Re-execute component against the existing host so reconciliation can\n // preserve any shared layout DOM between sibling routes.\n mountComponent(currentInstance);\n currentPathname = pathname;\n currentHref = href;\n return true;\n}\n\nfunction rerenderResolvedRoute(pathname: string, href: string): boolean {\n if (!currentInstance) {\n return false;\n }\n\n const resolved = resolveRoute(pathname);\n if (!resolved) {\n return false;\n }\n\n currentPathname = pathname;\n currentHref = href;\n currentInstance._enqueueRun?.();\n return true;\n}\n\nfunction resolveNavigationTarget(path: string) {\n const target = parseTargetUrl(path);\n const pathname = target.pathname;\n const href = `${target.pathname}${target.search}${target.hash}`;\n const resolved = resolveRouteRequest(href);\n\n if (isPromise(resolved)) {\n return resolved.then((next) => ({\n href,\n pathname,\n resolved: next,\n }));\n }\n\n return {\n href,\n pathname,\n resolved,\n };\n}\n\nfunction applyNavigationTarget(\n path: string,\n options: NavigateOptions,\n target: {\n href: string;\n pathname: string;\n resolved: RouteRequestResult;\n }\n): void {\n const { href, pathname, resolved } = target;\n const previousPathname = currentPathname;\n\n saveScrollPosition(currentHref);\n\n if (!resolved) {\n if (isDevelopmentEnvironment()) {\n logger.warn(`No route found for path: ${path}`);\n }\n return;\n }\n\n if (resolved.kind === 'redirect') {\n const redirectTarget = parseTargetUrl(resolved.to);\n const redirectHref = `${redirectTarget.pathname}${redirectTarget.search}${redirectTarget.hash}`;\n if (redirectHref === href) {\n if (isDevelopmentEnvironment()) {\n logger.warn(\n `Navigation guard redirected to the same path: ${redirectHref}`\n );\n }\n return;\n }\n\n navigate(redirectHref, { history: 'replace' });\n return;\n }\n\n const nextResolved =\n resolved.kind === 'deny'\n ? createDeniedResolvedRoute(resolved.status)\n : {\n handler: resolved.handler,\n params: resolved.params,\n };\n\n const historyMethod =\n options.history === 'replace' ? 'replaceState' : 'pushState';\n window.history[historyMethod]({ path: href }, '', href);\n syncCurrentRouteSnapshot(\n window.location.pathname,\n window.location.search,\n window.location.hash\n );\n\n if (currentInstance) {\n if (pathname === currentPathname && isRenderResult(resolved)) {\n rerenderResolvedRoute(pathname, href);\n return;\n }\n\n remountResolvedRoute(nextResolved, pathname, href);\n if (pathname !== previousPathname) {\n applyNavigationScroll(\n options.scroll ?? scrollRestorationOptions.navigation\n );\n }\n }\n}\n\n/** Register the current app instance (called by createSPA/hydrateSPA). */\nexport function registerAppInstance(\n instance: ComponentInstance,\n path: string\n): void {\n currentInstance = instance;\n currentPathname = path;\n currentHref = getWindowHref();\n syncCurrentRouteSnapshot(\n window.location.pathname,\n window.location.search,\n window.location.hash\n );\n // Lock further route registrations after the app has started — but allow tests to register routes.\n // Enforce only in production to avoid breaking test infra which registers routes dynamically.\n if (isProductionEnvironment()) {\n lockRouteRegistration();\n }\n}\n\n/**\n * Navigate to a new path\n * Updates URL, resolves route, and re-mounts app with new handler\n */\nexport function navigate(path: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n // SSR context\n return;\n }\n\n const target = resolveNavigationTarget(path);\n if (isPromise(target)) {\n void target.then((resolvedTarget) =>\n applyNavigationTarget(path, options, resolvedTarget)\n );\n return;\n }\n\n applyNavigationTarget(path, options, target);\n}\n\n/**\n * Handle browser back/forward buttons\n */\nfunction handlePopState(_event: PopStateEvent): void {\n const previousHref = currentHref;\n const pathname = window.location.pathname;\n const href = `${window.location.pathname}${window.location.search}${window.location.hash}`;\n\n saveScrollPosition(previousHref);\n\n if (!currentInstance) {\n return;\n }\n\n const resolved = resolveRouteRequest(href);\n\n const applyResolved = (resolved: RouteRequestResult) => {\n if (!resolved) {\n if (isDevelopmentEnvironment()) {\n logger.warn(`No route found for path: ${pathname}`);\n }\n return;\n }\n\n if (resolved.kind === 'redirect') {\n navigate(resolved.to, { history: 'replace' });\n return;\n }\n\n syncCurrentRouteSnapshot(\n window.location.pathname,\n window.location.search,\n window.location.hash\n );\n\n if (pathname === currentPathname && isRenderResult(resolved)) {\n rerenderResolvedRoute(pathname, href);\n return;\n }\n\n remountResolvedRoute(\n resolved.kind === 'deny'\n ? createDeniedResolvedRoute(resolved.status)\n : {\n handler: resolved.handler,\n params: resolved.params,\n },\n pathname,\n href\n );\n\n applyHistoryScroll(href, _event.state);\n };\n\n if (isPromise(resolved)) {\n void resolved.then((next) => applyResolved(next));\n return;\n }\n\n applyResolved(resolved);\n}\n\n/**\n * Setup popstate listener for browser navigation\n */\nexport function initializeNavigation(): void {\n if (typeof window === 'undefined' || navigationInitialized) {\n return;\n }\n\n navigationInitialized = true;\n window.addEventListener('popstate', handlePopState);\n}\n\n/**\n * Cleanup navigation listeners\n */\nexport function cleanupNavigation(): void {\n if (typeof window === 'undefined' || !navigationInitialized) {\n return;\n }\n\n navigationInitialized = false;\n window.removeEventListener('popstate', handlePopState);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","names":[],"sources":["../../src/router/policy.ts"],"sourcesContent":["import type {\n AccessDecision,\n AccessDenyStatus,\n AccessRedirectStatus,\n RouteAuthOptions,\n RouteContext,\n RoutePolicy,\n} from '../common/router';\n\nconst ROUTE_AUTH_OPTIONS = Symbol.for('__ASKR_ROUTE_AUTH_OPTIONS__');\n\ntype InternalRouteContext = RouteContext & {\n [ROUTE_AUTH_OPTIONS]?: RouteAuthOptions;\n};\n\ntype RoutePathResolver =\n | string\n | ((context: RouteContext) => string | Promise<string>);\n\nfunction isPromise<T>(value: T | Promise<T>): value is Promise<T> {\n return value instanceof Promise;\n}\n\nexport function allow(): AccessDecision {\n return { kind: 'allow' };\n}\n\nexport function redirect(\n to: string,\n init: {\n status?: AccessRedirectStatus;\n replace?: boolean;\n } = {}\n): AccessDecision {\n return {\n kind: 'redirect',\n to,\n ...(init.status ? { status: init.status } : {}),\n ...(init.replace !== undefined ? { replace: init.replace } : {}),\n };\n}\n\nexport function deny(status: AccessDenyStatus): AccessDecision {\n return { kind: 'deny', status };\n}\n\nexport function unauthorized(): AccessDecision {\n return deny(401);\n}\n\nexport function forbidden(): AccessDecision {\n return deny(403);\n}\n\nexport function notFound(): AccessDecision {\n return deny(404);\n}\n\nexport function normalizeAccessDecision(\n result: AccessDecision\n): AccessDecision {\n return result;\n}\n\nexport function withRouteAuthOptions(\n context: RouteContext,\n auth: RouteAuthOptions | undefined\n): RouteContext {\n const internal = context as InternalRouteContext;\n\n if (!auth) {\n delete internal[ROUTE_AUTH_OPTIONS];\n return context;\n }\n\n internal[ROUTE_AUTH_OPTIONS] = auth;\n return context;\n}\n\nfunction getRouteAuthOptions(\n context: RouteContext\n): RouteAuthOptions | undefined {\n return (context as InternalRouteContext)[ROUTE_AUTH_OPTIONS];\n}\n\nfunction assertRouteAuthOptions(context: RouteContext): RouteAuthOptions {\n const auth = getRouteAuthOptions(context);\n if (!auth?.resolve) {\n throw new Error(\n 'Built-in auth policies require router auth configuration. ' +\n 'Provide `auth` to registerRoutes(...), createSPA(...), hydrateSPA(...), or SSR route resolution.'\n );\n }\n\n return auth;\n}\n\nfunction resolvePathSetting(\n value: RoutePathResolver | undefined,\n fallback: string,\n context: RouteContext\n): string | Promise<string> {\n if (!value) {\n return fallback;\n }\n\n if (typeof value === 'function') {\n return value(context);\n }\n\n return value;\n}\n\nfunction appendNextTarget(path: string, href: string): string {\n const resolved = new URL(path, 'http://localhost');\n resolved.searchParams.set('next', href);\n return `${resolved.pathname}${resolved.search}${resolved.hash}`;\n}\n\nfunction collectStringValues(value: unknown): string[] {\n if (typeof value === 'string') {\n return [value];\n }\n\n if (Array.isArray(value)) {\n return value.filter((entry): entry is string => typeof entry === 'string');\n }\n\n if (value instanceof Set) {\n return Array.from(value).filter(\n (entry): entry is string => typeof entry === 'string'\n );\n }\n\n return [];\n}\n\nfunction defaultHasRole(user: unknown, role: string): boolean {\n if (!user || typeof user !== 'object') {\n return false;\n }\n\n return collectStringValues((user as { roles?: unknown }).roles).includes(\n role\n );\n}\n\nfunction defaultHasPermission(user: unknown, permission: string): boolean {\n if (!user || typeof user !== 'object') {\n return false;\n }\n\n return collectStringValues(\n (user as { permissions?: unknown }).permissions\n ).includes(permission);\n}\n\nfunction resolveUnauthenticatedRedirect(\n context: RouteContext\n): AccessDecision | Promise<AccessDecision> {\n const auth = assertRouteAuthOptions(context);\n const loginPath = resolvePathSetting(auth.loginPath, '/login', context);\n if (isPromise(loginPath)) {\n return loginPath.then((nextPath) =>\n redirect(appendNextTarget(nextPath, context.href), {\n replace: context.mode === 'spa',\n })\n );\n }\n\n return redirect(appendNextTarget(loginPath, context.href), {\n replace: context.mode === 'spa',\n });\n}\n\nfunction resolveAuthenticatedRedirect(\n context: RouteContext\n): AccessDecision | Promise<AccessDecision> {\n const auth = assertRouteAuthOptions(context);\n const target = resolvePathSetting(auth.guestRedirectTo, '/', context);\n if (isPromise(target)) {\n return target.then((nextTarget) =>\n redirect(nextTarget, {\n replace: true,\n })\n );\n }\n\n return redirect(target, {\n replace: true,\n });\n}\n\nexport function requireAuth(): RoutePolicy {\n return (context) => {\n if (context.session) {\n return allow();\n }\n\n return resolveUnauthenticatedRedirect(context);\n };\n}\n\nexport function requireGuest(): RoutePolicy {\n return (context) => {\n if (!context.session) {\n return allow();\n }\n\n return resolveAuthenticatedRedirect(context);\n };\n}\n\nexport function requireRole(role: string): RoutePolicy {\n return (context) => {\n if (!context.user) {\n return resolveUnauthenticatedRedirect(context);\n }\n\n const auth = assertRouteAuthOptions(context);\n const hasRole = auth.hasRole\n ? auth.hasRole(context.user, role, context)\n : defaultHasRole(context.user, role);\n\n if (isPromise(hasRole)) {\n return hasRole.then((next) => (next ? allow() : forbidden()));\n }\n\n return hasRole ? allow() : forbidden();\n };\n}\n\nexport function requirePermission(permission: string): RoutePolicy {\n return (context) => {\n if (!context.user) {\n return resolveUnauthenticatedRedirect(context);\n }\n\n const auth = assertRouteAuthOptions(context);\n const hasPermission = auth.hasPermission\n ? auth.hasPermission(context.user, permission, context)\n : defaultHasPermission(context.user, permission);\n\n if (isPromise(hasPermission)) {\n return hasPermission.then((next) => (next ? allow() : forbidden()));\n }\n\n return hasPermission ? allow() : forbidden();\n };\n}\n\nexport async function evaluateRoutePolicy(\n policy: RoutePolicy,\n context: RouteContext\n): Promise<AccessDecision> {\n const result = await policy(context);\n return result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"policy.js","names":[],"sources":["../../src/router/policy.ts"],"sourcesContent":["import type {\n AccessDecision,\n AccessDenyStatus,\n AccessRedirectStatus,\n RouteAuthOptions,\n RouteContext,\n RoutePolicy,\n} from '../common/router';\n\nconst ROUTE_AUTH_OPTIONS = Symbol.for('__ASKR_ROUTE_AUTH_OPTIONS__');\n\ntype InternalRouteContext = RouteContext & {\n [ROUTE_AUTH_OPTIONS]?: RouteAuthOptions;\n};\n\ntype RoutePathResolver =\n | string\n | ((context: RouteContext) => string | Promise<string>);\n\nfunction isPromise<T>(value: T | Promise<T>): value is Promise<T> {\n return value instanceof Promise;\n}\n\nexport function allow(): AccessDecision {\n return { kind: 'allow' };\n}\n\nexport function redirect(\n to: string,\n init: {\n status?: AccessRedirectStatus;\n replace?: boolean;\n } = {}\n): AccessDecision {\n return {\n kind: 'redirect',\n to,\n ...(init.status ? { status: init.status } : {}),\n ...(init.replace !== undefined ? { replace: init.replace } : {}),\n };\n}\n\nexport function deny(status: AccessDenyStatus): AccessDecision {\n return { kind: 'deny', status };\n}\n\nexport function unauthorized(): AccessDecision {\n return deny(401);\n}\n\nexport function forbidden(): AccessDecision {\n return deny(403);\n}\n\nexport function notFound(): AccessDecision {\n return deny(404);\n}\n\nexport function normalizeAccessDecision(\n result: AccessDecision\n): AccessDecision {\n return result;\n}\n\nexport function withRouteAuthOptions(\n context: RouteContext,\n auth: RouteAuthOptions | undefined\n): RouteContext {\n const internal = context as InternalRouteContext;\n\n if (!auth) {\n delete internal[ROUTE_AUTH_OPTIONS];\n return context;\n }\n\n internal[ROUTE_AUTH_OPTIONS] = auth;\n return context;\n}\n\nfunction getRouteAuthOptions(\n context: RouteContext\n): RouteAuthOptions | undefined {\n return (context as InternalRouteContext)[ROUTE_AUTH_OPTIONS];\n}\n\nfunction assertRouteAuthOptions(context: RouteContext): RouteAuthOptions {\n const auth = getRouteAuthOptions(context);\n if (!auth?.resolve) {\n throw new Error(\n 'Built-in auth policies require router auth configuration. ' +\n 'Provide `auth` to registerRoutes(...), createSPA(...), hydrateSPA(...), or SSR route resolution.'\n );\n }\n\n return auth;\n}\n\nfunction resolvePathSetting(\n value: RoutePathResolver | undefined,\n fallback: string,\n context: RouteContext\n): string | Promise<string> {\n if (!value) {\n return fallback;\n }\n\n if (typeof value === 'function') {\n return value(context);\n }\n\n return value;\n}\n\nfunction appendNextTarget(path: string, href: string): string {\n const resolved = new URL(path, 'http://localhost');\n resolved.searchParams.set('next', href);\n return `${resolved.pathname}${resolved.search}${resolved.hash}`;\n}\n\nfunction collectStringValues(value: unknown): string[] {\n if (typeof value === 'string') {\n return [value];\n }\n\n if (Array.isArray(value)) {\n return value.filter((entry): entry is string => typeof entry === 'string');\n }\n\n if (value instanceof Set) {\n return Array.from(value).filter(\n (entry): entry is string => typeof entry === 'string'\n );\n }\n\n return [];\n}\n\nfunction defaultHasRole(user: unknown, role: string): boolean {\n if (!user || typeof user !== 'object') {\n return false;\n }\n\n return collectStringValues((user as { roles?: unknown }).roles).includes(\n role\n );\n}\n\nfunction defaultHasPermission(user: unknown, permission: string): boolean {\n if (!user || typeof user !== 'object') {\n return false;\n }\n\n return collectStringValues(\n (user as { permissions?: unknown }).permissions\n ).includes(permission);\n}\n\nfunction resolveUnauthenticatedRedirect(\n context: RouteContext\n): AccessDecision | Promise<AccessDecision> {\n const auth = assertRouteAuthOptions(context);\n const loginPath = resolvePathSetting(auth.loginPath, '/login', context);\n if (isPromise(loginPath)) {\n return loginPath.then((nextPath) =>\n redirect(appendNextTarget(nextPath, context.href), {\n replace: context.mode === 'spa',\n })\n );\n }\n\n return redirect(appendNextTarget(loginPath, context.href), {\n replace: context.mode === 'spa',\n });\n}\n\nfunction resolveAuthenticatedRedirect(\n context: RouteContext\n): AccessDecision | Promise<AccessDecision> {\n const auth = assertRouteAuthOptions(context);\n const target = resolvePathSetting(auth.guestRedirectTo, '/', context);\n if (isPromise(target)) {\n return target.then((nextTarget) =>\n redirect(nextTarget, {\n replace: true,\n })\n );\n }\n\n return redirect(target, {\n replace: true,\n });\n}\n\nexport function requireAuth(): RoutePolicy {\n return (context) => {\n if (context.session) {\n return allow();\n }\n\n return resolveUnauthenticatedRedirect(context);\n };\n}\n\nexport function requireGuest(): RoutePolicy {\n return (context) => {\n if (!context.session) {\n return allow();\n }\n\n return resolveAuthenticatedRedirect(context);\n };\n}\n\nexport function requireRole(role: string): RoutePolicy {\n return (context) => {\n if (!context.user) {\n return resolveUnauthenticatedRedirect(context);\n }\n\n const auth = assertRouteAuthOptions(context);\n const hasRole = auth.hasRole\n ? auth.hasRole(context.user, role, context)\n : defaultHasRole(context.user, role);\n\n if (isPromise(hasRole)) {\n return hasRole.then((next) => (next ? allow() : forbidden()));\n }\n\n return hasRole ? allow() : forbidden();\n };\n}\n\nexport function requirePermission(permission: string): RoutePolicy {\n return (context) => {\n if (!context.user) {\n return resolveUnauthenticatedRedirect(context);\n }\n\n const auth = assertRouteAuthOptions(context);\n const hasPermission = auth.hasPermission\n ? auth.hasPermission(context.user, permission, context)\n : defaultHasPermission(context.user, permission);\n\n if (isPromise(hasPermission)) {\n return hasPermission.then((next) => (next ? allow() : forbidden()));\n }\n\n return hasPermission ? allow() : forbidden();\n };\n}\n\nexport async function evaluateRoutePolicy(\n policy: RoutePolicy,\n context: RouteContext\n): Promise<AccessDecision> {\n const result = await policy(context);\n return result;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"route-context.js","names":[],"sources":["../../src/router/route-context.ts"],"sourcesContent":["import { withRouteAuthOptions } from './policy';\n\nimport type {\n RouteAuthOptions,\n RouteContext,\n RouteQuery,\n} from '../common/router';\n\nexport function parseLocation(url: string): {\n pathname: string;\n search: string;\n hash: string;\n} {\n try {\n const parsedUrl = new URL(url, 'http://localhost');\n return {\n pathname: parsedUrl.pathname,\n search: parsedUrl.search,\n hash: parsedUrl.hash,\n };\n } catch {\n return { pathname: '/', search: '', hash: '' };\n }\n}\n\nexport function deepFreeze<T>(obj: T): T {\n if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {\n Object.freeze(obj as Record<string, unknown>);\n for (const key of Object.keys(obj as Record<string, unknown>)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value && typeof value === 'object') {\n deepFreeze(value);\n }\n }\n }\n\n return obj;\n}\n\nexport function makeQuery(search: string): RouteQuery {\n const searchParams = new URLSearchParams(search || '');\n const mapping = new Map<string, string[]>();\n\n for (const [key, value] of searchParams.entries()) {\n const existing = mapping.get(key);\n if (existing) {\n existing.push(value);\n } else {\n mapping.set(key, [value]);\n }\n }\n\n const query: RouteQuery = {\n get(key: string) {\n const values = mapping.get(key);\n return values ? values[0] : null;\n },\n getAll(key: string) {\n const values = mapping.get(key);\n return values ? [...values] : [];\n },\n has(key: string) {\n return mapping.has(key);\n },\n toJSON() {\n const output: Record<string, string | string[]> = {};\n for (const [key, values] of mapping.entries()) {\n output[key] = values.length > 1 ? [...values] : values[0];\n }\n return output;\n },\n };\n\n return deepFreeze(query);\n}\n\nexport function buildRouteContextBase(\n target: string,\n params: Record<string, string>,\n options: {\n mode: RouteContext['mode'];\n signal: AbortSignal;\n }\n): Omit<RouteContext, 'session' | 'user'> {\n const parsed = parseLocation(target);\n const href = `${parsed.pathname}${parsed.search}${parsed.hash}`;\n\n return {\n mode: options.mode,\n params,\n pathname: parsed.pathname,\n search: parsed.search,\n hash: parsed.hash,\n href,\n signal: options.signal,\n };\n}\n\nexport function buildRouteContext(\n target: string,\n params: Record<string, string>,\n options: {\n mode: RouteContext['mode'];\n signal: AbortSignal;\n auth?: RouteAuthOptions;\n session?: unknown;\n user?: unknown;\n }\n): RouteContext {\n const context: RouteContext = {\n ...buildRouteContextBase(target, params, options),\n session: options.session ?? null,\n user: options.user ?? null,\n };\n\n return withRouteAuthOptions(context, options.auth);\n}\n"],"mappings":";;AAQA,SAAgB,cAAc,KAI5B;AACA,KAAI;EACF,MAAM,YAAY,IAAI,IAAI,KAAK,mBAAmB;AAClD,SAAO;GACL,UAAU,UAAU;GACpB,QAAQ,UAAU;GAClB,MAAM,UAAU;GACjB;SACK;AACN,SAAO;GAAE,UAAU;GAAK,QAAQ;GAAI,MAAM;GAAI;;;AAIlD,SAAgB,WAAc,KAAW;AACvC,KAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,IAAI,EAAE;AAC3D,SAAO,OAAO,IAA+B;AAC7C,OAAK,MAAM,OAAO,OAAO,KAAK,IAA+B,EAAE;GAC7D,MAAM,QAAS,IAAgC;AAC/C,OAAI,SAAS,OAAO,UAAU,SAC5B,YAAW,MAAM;;;AAKvB,QAAO;;AAGT,SAAgB,UAAU,QAA4B;CACpD,MAAM,eAAe,IAAI,gBAAgB,UAAU,GAAG;CACtD,MAAM,0BAAU,IAAI,KAAuB;AAE3C,MAAK,MAAM,CAAC,KAAK,UAAU,aAAa,SAAS,EAAE;EACjD,MAAM,WAAW,QAAQ,IAAI,IAAI;AACjC,MAAI,SACF,UAAS,KAAK,MAAM;MAEpB,SAAQ,IAAI,KAAK,CAAC,MAAM,CAAC;;AAyB7B,QAAO,WAAW;EApBhB,IAAI,KAAa;GACf,MAAM,SAAS,QAAQ,IAAI,IAAI;AAC/B,UAAO,SAAS,OAAO,KAAK;;EAE9B,OAAO,KAAa;GAClB,MAAM,SAAS,QAAQ,IAAI,IAAI;AAC/B,UAAO,SAAS,CAAC,GAAG,OAAO,GAAG,EAAE;;EAElC,IAAI,KAAa;AACf,UAAO,QAAQ,IAAI,IAAI;;EAEzB,SAAS;GACP,MAAM,SAA4C,EAAE;AACpD,QAAK,MAAM,CAAC,KAAK,WAAW,QAAQ,SAAS,CAC3C,QAAO,OAAO,OAAO,SAAS,IAAI,CAAC,GAAG,OAAO,GAAG,OAAO;AAEzD,UAAO;;EAIO,CAAM;;AAG1B,SAAgB,sBACd,QACA,QACA,SAIwC;CACxC,MAAM,SAAS,cAAc,OAAO;CACpC,MAAM,OAAO,GAAG,OAAO,WAAW,OAAO,SAAS,OAAO;AAEzD,QAAO;EACL,MAAM,QAAQ;EACd;EACA,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,MAAM,OAAO;EACb;EACA,QAAQ,QAAQ;EACjB;;AAGH,SAAgB,kBACd,QACA,QACA,SAOc;AAOd,QAAO,qBAAqB;EAL1B,GAAG,sBAAsB,QAAQ,QAAQ,QAAQ;EACjD,SAAS,QAAQ,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EAGI,EAAS,QAAQ,KAAK"}
1
+ {"version":3,"file":"route-context.js","names":[],"sources":["../../src/router/route-context.ts"],"sourcesContent":["import { withRouteAuthOptions } from './policy';\n\nimport type {\n RouteAuthOptions,\n RouteContext,\n RouteQuery,\n} from '../common/router';\n\nexport function parseLocation(url: string): {\n pathname: string;\n search: string;\n hash: string;\n} {\n try {\n const parsedUrl = new URL(url, 'http://localhost');\n return {\n pathname: parsedUrl.pathname,\n search: parsedUrl.search,\n hash: parsedUrl.hash,\n };\n } catch {\n return { pathname: '/', search: '', hash: '' };\n }\n}\n\nexport function deepFreeze<T>(obj: T): T {\n if (obj && typeof obj === 'object' && !Object.isFrozen(obj)) {\n Object.freeze(obj as Record<string, unknown>);\n for (const key of Object.keys(obj as Record<string, unknown>)) {\n const value = (obj as Record<string, unknown>)[key];\n if (value && typeof value === 'object') {\n deepFreeze(value);\n }\n }\n }\n\n return obj;\n}\n\nexport function makeQuery(search: string): RouteQuery {\n const searchParams = new URLSearchParams(search || '');\n const mapping = new Map<string, string[]>();\n\n for (const [key, value] of searchParams.entries()) {\n const existing = mapping.get(key);\n if (existing) {\n existing.push(value);\n } else {\n mapping.set(key, [value]);\n }\n }\n\n const query: RouteQuery = {\n get(key: string) {\n const values = mapping.get(key);\n return values ? values[0] : null;\n },\n getAll(key: string) {\n const values = mapping.get(key);\n return values ? [...values] : [];\n },\n has(key: string) {\n return mapping.has(key);\n },\n toJSON() {\n const output: Record<string, string | string[]> = {};\n for (const [key, values] of mapping.entries()) {\n output[key] = values.length > 1 ? [...values] : values[0];\n }\n return output;\n },\n };\n\n return deepFreeze(query);\n}\n\nexport function buildRouteContextBase(\n target: string,\n params: Record<string, string>,\n options: {\n mode: RouteContext['mode'];\n signal: AbortSignal;\n }\n): Omit<RouteContext, 'session' | 'user'> {\n const parsed = parseLocation(target);\n const href = `${parsed.pathname}${parsed.search}${parsed.hash}`;\n\n return {\n mode: options.mode,\n params,\n pathname: parsed.pathname,\n search: parsed.search,\n hash: parsed.hash,\n href,\n signal: options.signal,\n };\n}\n\nexport function buildRouteContext(\n target: string,\n params: Record<string, string>,\n options: {\n mode: RouteContext['mode'];\n signal: AbortSignal;\n auth?: RouteAuthOptions;\n session?: unknown;\n user?: unknown;\n }\n): RouteContext {\n const context: RouteContext = {\n ...buildRouteContextBase(target, params, options),\n session: options.session ?? null,\n user: options.user ?? null,\n };\n\n return withRouteAuthOptions(context, options.auth);\n}\n"],"mappings":";;AAQA,SAAgB,cAAc,KAI5B;CACA,IAAI;EACF,MAAM,YAAY,IAAI,IAAI,KAAK,mBAAmB;EAClD,OAAO;GACL,UAAU,UAAU;GACpB,QAAQ,UAAU;GAClB,MAAM,UAAU;GACjB;SACK;EACN,OAAO;GAAE,UAAU;GAAK,QAAQ;GAAI,MAAM;GAAI;;;AAIlD,SAAgB,WAAc,KAAW;CACvC,IAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,OAAO,SAAS,IAAI,EAAE;EAC3D,OAAO,OAAO,IAA+B;EAC7C,KAAK,MAAM,OAAO,OAAO,KAAK,IAA+B,EAAE;GAC7D,MAAM,QAAS,IAAgC;GAC/C,IAAI,SAAS,OAAO,UAAU,UAC5B,WAAW,MAAM;;;CAKvB,OAAO;;AAGT,SAAgB,UAAU,QAA4B;CACpD,MAAM,eAAe,IAAI,gBAAgB,UAAU,GAAG;CACtD,MAAM,0BAAU,IAAI,KAAuB;CAE3C,KAAK,MAAM,CAAC,KAAK,UAAU,aAAa,SAAS,EAAE;EACjD,MAAM,WAAW,QAAQ,IAAI,IAAI;EACjC,IAAI,UACF,SAAS,KAAK,MAAM;OAEpB,QAAQ,IAAI,KAAK,CAAC,MAAM,CAAC;;CAyB7B,OAAO,WAAW;EApBhB,IAAI,KAAa;GACf,MAAM,SAAS,QAAQ,IAAI,IAAI;GAC/B,OAAO,SAAS,OAAO,KAAK;;EAE9B,OAAO,KAAa;GAClB,MAAM,SAAS,QAAQ,IAAI,IAAI;GAC/B,OAAO,SAAS,CAAC,GAAG,OAAO,GAAG,EAAE;;EAElC,IAAI,KAAa;GACf,OAAO,QAAQ,IAAI,IAAI;;EAEzB,SAAS;GACP,MAAM,SAA4C,EAAE;GACpD,KAAK,MAAM,CAAC,KAAK,WAAW,QAAQ,SAAS,EAC3C,OAAO,OAAO,OAAO,SAAS,IAAI,CAAC,GAAG,OAAO,GAAG,OAAO;GAEzD,OAAO;;EAIO,CAAM;;AAG1B,SAAgB,sBACd,QACA,QACA,SAIwC;CACxC,MAAM,SAAS,cAAc,OAAO;CACpC,MAAM,OAAO,GAAG,OAAO,WAAW,OAAO,SAAS,OAAO;CAEzD,OAAO;EACL,MAAM,QAAQ;EACd;EACA,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,MAAM,OAAO;EACb;EACA,QAAQ,QAAQ;EACjB;;AAGH,SAAgB,kBACd,QACA,QACA,SAOc;CAOd,OAAO,qBAAqB;EAL1B,GAAG,sBAAsB,QAAQ,QAAQ,QAAQ;EACjD,SAAS,QAAQ,WAAW;EAC5B,MAAM,QAAQ,QAAQ;EAGI,EAAS,QAAQ,KAAK"}
@@ -1,11 +1,16 @@
1
- import { AccessDecision, AccessDenyDecision, AccessRedirectDecision, GroupHelperOptions, LayoutScopeRecord, ParsedSegment, RegisterRoutesOptions, ResolvedRoute, Route, RouteAuthOptions, RouteComponent, RouteContext, RouteDefinition, RouteHandler, RouteManifest, RouteMatch, RouteOptions, RoutePolicy, RouteQuery, RouteRecord, RouteRenderResult, RouteRequestOptions, RouteRequestResult, RouteSnapshot } from "../common/router.js";
1
+ import { AccessDecision, AccessDenyDecision, AccessRedirectDecision, GroupHelperOptions, LayoutScopeRecord, PageHelperOptions, PageScopeRecord, ParsedSegment, RegisterRoutesOptions, ResolvedRoute, Route, RouteAuthOptions, RouteComponent, RouteContext, RouteDefinition, RouteHandler, RouteManifest, RouteMatch, RouteOptions, RoutePolicy, RouteQuery, RouteRecord, RouteRenderResult, RouteRequestOptions, RouteRequestResult, RouteSnapshot } from "../common/router.js";
2
2
 
3
3
  //#region src/router/route.d.ts
4
4
  declare function setServerLocation(url: string | null): void;
5
5
  declare function _setActiveRouteAuthOptions(auth: RouteAuthOptions | undefined): void;
6
+ /**
7
+ * Prevent route registrations after the app has started.
8
+ * Enforced in production; tests may unlock explicitly.
9
+ */
6
10
  declare function lockRouteRegistration(): void;
7
11
  declare function _lockRouteRegistrationForTests(): void;
8
12
  declare function _unlockRouteRegistrationForTests(): void;
13
+ declare function Outlet(): unknown;
9
14
  /**
10
15
  * Snapshot the current in-flight lazy() imports.
11
16
  * Boot uses this before clearing route state so manifest-based startup can
@@ -42,9 +47,13 @@ declare function lazy(factory: () => Promise<{
42
47
  */
43
48
  declare function _drainLazy(additionalPending?: Iterable<Promise<unknown>>): Promise<void>;
44
49
  declare function group(options: GroupHelperOptions, fn: RouteDefinition): void;
50
+ declare function page(path: string, Component: RouteComponent, fn: RouteDefinition): void;
51
+ declare function page(path: string, Component: RouteComponent, options: PageHelperOptions, fn: RouteDefinition): void;
52
+ declare function index(Component: RouteComponent, options?: RouteOptions): void;
45
53
  declare function fallback(Component: RouteComponent): void;
46
54
  declare function registerRoutes(definition: RouteDefinition, options?: RegisterRoutesOptions): void;
47
55
  declare function currentRoute(): RouteSnapshot;
56
+ declare function syncCurrentRouteSnapshot(pathname: string, search: string, hash: string): void;
48
57
  /**
49
58
  * Register a route.
50
59
  *
@@ -116,5 +125,5 @@ declare function resolveRouteRequest(target: string, options?: RouteRequestOptio
116
125
  */
117
126
  declare function resolveRouteFromRoutes(pathname: string, routeList: readonly Route[]): ResolvedRoute | null;
118
127
  //#endregion
119
- export { _applyManifest, _drainLazy, clearRoutes, currentRoute, fallback, getLoadedNamespaces, getManifest, getNamespaceRoutes, getRoutes, group, lazy, registerRoutes, resolveRouteRequest, route, setServerLocation, unloadNamespace };
128
+ export { Outlet, clearRoutes, currentRoute, fallback, getManifest, getRoutes, group, index, lazy, page, registerRoutes, route };
120
129
  //# sourceMappingURL=route.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","names":[],"sources":["../../src/router/route.ts"],"mappings":";;;iBAiMgB,iBAAA,CAAkB,GAAA;AAAA,iBAuElB,0BAAA,CACd,IAAA,EAAM,gBAAA;AAAA,iBAwDQ,qBAAA,CAAA;AAAA,iBAIA,8BAAA,CAAA;AAAA,iBAIA,gCAAA,CAAA;AAJhB;;;;;AAAA,iBA6EgB,aAAA,CAAA,GAAiB,OAAA;;;;;AAAjC;;;;;AAyBA;;;;;;;;;;;;iBAAgB,IAAA,CACd,OAAA,QAAe,OAAA;EAAU,OAAA,EAAS,cAAA;AAAA,IAAmB,cAAA,IACpD,cAAA;;;AAmCH;;iBAAgB,UAAA,CACd,iBAAA,GAAmB,QAAA,CAAS,OAAA,aAC3B,OAAA;AAAA,iBASa,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,EAAA,EAAI,eAAA;AAAA,iBAKvC,QAAA,CAAS,SAAA,EAAW,cAAA;AAAA,iBAmLpB,cAAA,CACd,UAAA,EAAY,eAAA,EACZ,OAAA,GAAS,qBAAA;AAAA,iBAoEK,YAAA,CAAA,GAAgB,aAAA;;;;;;;;AA9PhC;;;;;iBA8QgB,KAAA,CACd,IAAA,UACA,SAAA,EAAW,cAAA,EACX,OAAA,GAAU,YAAA;;;;;AA5QZ;;;;;AAmLA;;iBAyMgB,WAAA,CAAA,GAAe,aAAA;;;;;iBAWf,cAAA,CAAe,QAAA,EAAU,aAAA;;;AA9IzC;;iBAoKgB,SAAA,CAAA,GAAa,KAAA;;iBAKb,kBAAA,CAAmB,SAAA,WAAoB,KAAA;AAzJvD;AAAA,iBA8JgB,eAAA,CAAgB,SAAA;;iBA6BhB,WAAA,CAAA;;;;iBAiBA,mBAAA,CAAA;;;;AAzFhB;;;;;iBAqGgB,YAAA,CAAa,QAAA,WAAmB,aAAA;AAAA,iBA0GhC,mBAAA,CACd,MAAA,UACA,OAAA,GAAS,mBAAA,GACR,kBAAA,GAAqB,OAAA,CAAQ,kBAAA;;;;AAjLhC;;;;;AAKA;;;;;AAKA;iBA2OgB,sBAAA,CACd,QAAA,UACA,SAAA,WAAoB,KAAA,KACnB,aAAA"}
1
+ {"version":3,"file":"route.d.ts","names":[],"sources":["../../src/router/route.ts"],"mappings":";;;iBAgOgB,iBAAA,CAAkB,GAAA;AAAA,iBA8MlB,0BAAA,CACd,IAAA,EAAM,gBAAA;AAuFR;;;;AAAA,iBAJgB,qBAAA,CAAA;AAAA,iBAIA,8BAAA,CAAA;AAAA,iBAIA,gCAAA,CAAA;AAAA,iBAmIA,MAAA,CAAA;;;AAAhB;;;iBASgB,aAAA,CAAA,GAAiB,OAAA;;AAAjC;;;;;AAyBA;;;;;;;;;;;;;;;iBAAgB,IAAA,CACd,OAAA,QAAe,OAAA;EAAU,OAAA,EAAS,cAAA;AAAA,IAAmB,cAAA,IACpD,cAAA;;;;;iBAmCa,UAAA,CACd,iBAAA,GAAmB,QAAA,CAAS,OAAA,aAC3B,OAAA;AAAA,iBASa,KAAA,CAAM,OAAA,EAAS,kBAAA,EAAoB,EAAA,EAAI,eAAA;AAAA,iBAKvC,IAAA,CACd,IAAA,UACA,SAAA,EAAW,cAAA,EACX,EAAA,EAAI,eAAA;AAAA,iBAEU,IAAA,CACd,IAAA,UACA,SAAA,EAAW,cAAA,EACX,OAAA,EAAS,iBAAA,EACT,EAAA,EAAI,eAAA;AAAA,iBA2BU,KAAA,CAAM,SAAA,EAAW,cAAA,EAAgB,OAAA,GAAU,YAAA;AAAA,iBAI3C,QAAA,CAAS,SAAA,EAAW,cAAA;AAAA,iBAkUpB,cAAA,CACd,UAAA,EAAY,eAAA,EACZ,OAAA,GAAS,qBAAA;AAAA,iBAoEK,YAAA,CAAA,GAAgB,aAAA;AAAA,iBAiBhB,wBAAA,CACd,QAAA,UACA,MAAA,UACA,IAAA;AAzcF;;;;;;;;;;AAKA;;AALA,iBA0dgB,KAAA,CACd,IAAA,UACA,SAAA,EAAW,cAAA,EACX,OAAA,GAAU,YAAA;;;;;;;;;AAndZ;;;iBAohBgB,WAAA,CAAA,GAAe,aAAA;;;;;iBAWf,cAAA,CAAe,QAAA,EAAU,aAAA;;;;;iBA0BzB,SAAA,CAAA,GAAa,KAAA;;iBAKb,kBAAA,CAAmB,SAAA,WAAoB,KAAA;;iBAKvC,eAAA,CAAgB,SAAA;;iBA6BhB,WAAA,CAAA;;;;iBAiBA,mBAAA,CAAA;;;;AA9kBhB;;;;;iBAynBgB,YAAA,CAAa,QAAA,WAAmB,aAAA;AAAA,iBAuHhC,mBAAA,CACd,MAAA,UACA,OAAA,GAAS,mBAAA,GACR,kBAAA,GAAqB,OAAA,CAAQ,kBAAA;;;;;;;;;AA3WhC;;;;;AAiBA;iBA8ZgB,sBAAA,CACd,QAAA,UACA,SAAA,WAAoB,KAAA,KACnB,aAAA"}