@warlock.js/web 5.0.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 (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. package/skills/write-the-root/SKILL.md +131 -0
@@ -0,0 +1,68 @@
1
+ //#region ../web/src/routing/route-identity.ts
2
+ /**
3
+ * Raised when a caller passes a `sourceFile` containing a backslash.
4
+ * Canonical means canonical: this module does not normalize Windows path
5
+ * separators on the caller's behalf.
6
+ */
7
+ var NonPosixSourceFilePathError = class extends Error {
8
+ sourceFile;
9
+ constructor(sourceFile) {
10
+ super(`route-identity: sourceFile "${sourceFile}" contains a backslash. This module's inputs must already be canonical app-root-relative POSIX paths (forward slashes only) — normalize with \`value.replace(/\\\\/g, "/")\` before calling deriveFallbackRouteName.`);
11
+ this.sourceFile = sourceFile;
12
+ this.name = "NonPosixSourceFilePathError";
13
+ }
14
+ };
15
+ /**
16
+ * Canonicalizes a declared `route` export — string or `{ path, name? }` —
17
+ * into `{ path, name? }`. Requires well-formed input; an export that is
18
+ * neither a string nor an object with a `path` is the extractor's problem,
19
+ * already rejected before this function is ever called.
20
+ */
21
+ function canonicalizeRouteExport(route) {
22
+ if (typeof route === "string") return { path: route };
23
+ return route.name === void 0 ? { path: route.path } : {
24
+ path: route.path,
25
+ name: route.name
26
+ };
27
+ }
28
+ /** Strips leading and trailing "." characters — mirrors `trim(value, ".")` as used by the installer's `deriveRouteName`. */
29
+ function trimDots(value) {
30
+ return value.replace(/^\.+|\.+$/g, "");
31
+ }
32
+ /**
33
+ * The module segment a canonical source path declares, or `undefined` for a
34
+ * global (`src/web/**`) page. `sourceFile` is `<srcDir>/app/<module>/web/...`
35
+ * or `<srcDir>/web/...` — the first segment is the (arbitrarily named) src
36
+ * dir, so the module test looks at the SECOND segment.
37
+ */
38
+ function moduleSegmentFor(sourceFile) {
39
+ const segments = sourceFile.split("/");
40
+ return segments[1] === "app" ? segments[2] : void 0;
41
+ }
42
+ /**
43
+ * Derives the fallback route name for a page whose declared route carries no
44
+ * `name` — the same derivation the dev installer's `deriveRouteName` applies
45
+ * today (`web/src/server/install-page-routes.ts`), expressed against
46
+ * canonical inputs instead of an absolute file path plus an `appSrcRoot`.
47
+ *
48
+ * The installer only ever calls its derivation for a page under
49
+ * `<appSrcRoot>/app/**`, so a global (`src/web/**`) page has no installer
50
+ * behaviour to mirror; for that case this function instead follows
51
+ * discovery's own convention (`routeNameFor` in `discover-pages.ts`) — no
52
+ * module prefix, and `"index"` when there is nothing left to say at all.
53
+ *
54
+ * Throws {@link NonPosixSourceFilePathError} when `sourceFile` contains a
55
+ * backslash.
56
+ */
57
+ function deriveFallbackRouteName(input) {
58
+ const { routePath, sourceFile } = input;
59
+ if (sourceFile.includes("\\")) throw new NonPosixSourceFilePathError(sourceFile);
60
+ const moduleName = moduleSegmentFor(sourceFile);
61
+ const suffix = trimDots(routePath.replace(/\//g, "."));
62
+ if (moduleName !== void 0) return suffix ? `${moduleName}.${suffix}` : moduleName;
63
+ return suffix || "index";
64
+ }
65
+
66
+ //#endregion
67
+ export { canonicalizeRouteExport, deriveFallbackRouteName };
68
+ //# sourceMappingURL=route-identity.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-identity.mjs","names":[],"sources":["../../../../../../../web/src/routing/route-identity.ts"],"sourcesContent":["/**\n * Route identity — the single, pure implementation of \"what is this page's\n * route path and name\". The dev installer\n * (`web/src/server/install-page-routes.ts`), the production manifest\n * installer (`web/src/server/install-page-routes-from-manifest.ts`) and\n * discovery (`web/src/build/discover-pages.ts`) each hand-derived this on\n * their own until all three were made to delegate here.\n *\n * Pure string logic only: no `fs`, no `path`, no Node built-ins. Every input\n * this module accepts is already CANONICAL — a POSIX, app-root-relative\n * source path (e.g. `\"src/app/users/web/account/settings.page.tsx\"` or\n * `\"src/web/index.page.tsx\"`) and an already-validated declared route export.\n * Turning an absolute, OS-specific file path into that canonical form is the\n * caller's job; this module refuses (see {@link NonPosixSourceFilePathError})\n * rather than guess at a normalization.\n *\n * Well-formedness of the declared `route` export itself (is it a string or an\n * object, does the object have a `path`) is the extractor's problem — already\n * rejected at build before either derivation function here is called.\n *\n * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing\n * here may import `node:fs`, `node:path`, `vite`, or `fastify`. Modules in\n * this directory receive canonical values and trust nothing — they assert\n * rather than trust, but they never repair. A module that needs the\n * filesystem does not belong here. The purity is deliberate: it keeps these\n * modules consumable from the dev server, the build, the production runtime,\n * and — if ever needed — the browser client, without dragging any of those\n * environments along.\n */\n\n/** The shape a page's `route` export may declare — mirrors `PageRouteExport` in `install-page-routes.ts`. */\nexport type DeclaredRouteExport = string | { path: string; name?: string };\n\n/** The canonical form every declared `route` export resolves to. */\nexport type CanonicalRoute = {\n path: string;\n name?: string;\n};\n\n/** The canonical inputs {@link deriveFallbackRouteName} requires — see the module doc comment for what \"canonical\" means. */\nexport type RouteNameFallbackInput = {\n /** The page's declared route path, e.g. `\"/settings\"` or `\"/\"`. */\n routePath: string;\n /** The page's app-root-relative POSIX source path, e.g. `\"src/app/main/web/contact-us.page.tsx\"`. */\n sourceFile: string;\n};\n\n/**\n * Raised when a caller passes a `sourceFile` containing a backslash.\n * Canonical means canonical: this module does not normalize Windows path\n * separators on the caller's behalf.\n */\nexport class NonPosixSourceFilePathError extends Error {\n public constructor(public readonly sourceFile: string) {\n super(\n `route-identity: sourceFile \"${sourceFile}\" contains a backslash. This module's inputs ` +\n \"must already be canonical app-root-relative POSIX paths (forward slashes only) — \" +\n 'normalize with `value.replace(/\\\\\\\\/g, \"/\")` before calling deriveFallbackRouteName.',\n );\n this.name = \"NonPosixSourceFilePathError\";\n }\n}\n\n/**\n * Canonicalizes a declared `route` export — string or `{ path, name? }` —\n * into `{ path, name? }`. Requires well-formed input; an export that is\n * neither a string nor an object with a `path` is the extractor's problem,\n * already rejected before this function is ever called.\n */\nexport function canonicalizeRouteExport(route: DeclaredRouteExport): CanonicalRoute {\n if (typeof route === \"string\") {\n return { path: route };\n }\n\n return route.name === undefined ? { path: route.path } : { path: route.path, name: route.name };\n}\n\n/** Strips leading and trailing \".\" characters — mirrors `trim(value, \".\")` as used by the installer's `deriveRouteName`. */\nfunction trimDots(value: string): string {\n return value.replace(/^\\.+|\\.+$/g, \"\");\n}\n\n/**\n * The module segment a canonical source path declares, or `undefined` for a\n * global (`src/web/**`) page. `sourceFile` is `<srcDir>/app/<module>/web/...`\n * or `<srcDir>/web/...` — the first segment is the (arbitrarily named) src\n * dir, so the module test looks at the SECOND segment.\n */\nfunction moduleSegmentFor(sourceFile: string): string | undefined {\n const segments = sourceFile.split(\"/\");\n\n return segments[1] === \"app\" ? segments[2] : undefined;\n}\n\n/**\n * Derives the fallback route name for a page whose declared route carries no\n * `name` — the same derivation the dev installer's `deriveRouteName` applies\n * today (`web/src/server/install-page-routes.ts`), expressed against\n * canonical inputs instead of an absolute file path plus an `appSrcRoot`.\n *\n * The installer only ever calls its derivation for a page under\n * `<appSrcRoot>/app/**`, so a global (`src/web/**`) page has no installer\n * behaviour to mirror; for that case this function instead follows\n * discovery's own convention (`routeNameFor` in `discover-pages.ts`) — no\n * module prefix, and `\"index\"` when there is nothing left to say at all.\n *\n * Throws {@link NonPosixSourceFilePathError} when `sourceFile` contains a\n * backslash.\n */\nexport function deriveFallbackRouteName(input: RouteNameFallbackInput): string {\n const { routePath, sourceFile } = input;\n\n if (sourceFile.includes(\"\\\\\")) {\n throw new NonPosixSourceFilePathError(sourceFile);\n }\n\n const moduleName = moduleSegmentFor(sourceFile);\n const suffix = trimDots(routePath.replace(/\\//g, \".\"));\n\n if (moduleName !== undefined) {\n return suffix ? `${moduleName}.${suffix}` : moduleName;\n }\n\n return suffix || \"index\";\n}\n"],"mappings":";;;;;;AAoDA,IAAa,8BAAb,cAAiD,MAAM;CAClB;CAAnC,AAAO,YAAY,AAAgB,YAAoB;EACrD,MACE,+BAA+B,WAAW,qNAG5C;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;;;;;;;AAQA,SAAgB,wBAAwB,OAA4C;CAClF,IAAI,OAAO,UAAU,UACnB,OAAO,EAAE,MAAM,MAAM;CAGvB,OAAO,MAAM,SAAS,SAAY,EAAE,MAAM,MAAM,KAAK,IAAI;EAAE,MAAM,MAAM;EAAM,MAAM,MAAM;CAAK;AAChG;;AAGA,SAAS,SAAS,OAAuB;CACvC,OAAO,MAAM,QAAQ,cAAc,EAAE;AACvC;;;;;;;AAQA,SAAS,iBAAiB,YAAwC;CAChE,MAAM,WAAW,WAAW,MAAM,GAAG;CAErC,OAAO,SAAS,OAAO,QAAQ,SAAS,KAAK;AAC/C;;;;;;;;;;;;;;;;AAiBA,SAAgB,wBAAwB,OAAuC;CAC7E,MAAM,EAAE,WAAW,eAAe;CAElC,IAAI,WAAW,SAAS,IAAI,GAC1B,MAAM,IAAI,4BAA4B,UAAU;CAGlD,MAAM,aAAa,iBAAiB,UAAU;CAC9C,MAAM,SAAS,SAAS,UAAU,QAAQ,OAAO,GAAG,CAAC;CAErD,IAAI,eAAe,QACjB,OAAO,SAAS,GAAG,WAAW,GAAG,WAAW;CAG9C,OAAO,UAAU;AACnB"}
@@ -0,0 +1,34 @@
1
+ import { QueryStringInput } from "./query-string.mjs";
2
+
3
+ //#region ../web/src/routing/route-table.d.ts
4
+ /** What `href` accepts for a `:param` segment. Rendered with `String(value)`. */
5
+ type RouteParameters = Readonly<Record<string, unknown>>;
6
+ /**
7
+ * Query values; an `undefined` value is omitted rather than serialized.
8
+ *
9
+ * A value may be a scalar, an array of scalars, or an object one level deep —
10
+ * the shapes `@warlock.js/core` parses back out of the URL. Anything deeper
11
+ * throws `UnserializableQueryValueError`; the grammar and the measurements
12
+ * behind it are documented in query-string.ts.
13
+ */
14
+ type RouteQuery = QueryStringInput;
15
+ /**
16
+ * Resolve a route NAME to a URL.
17
+ *
18
+ * A name, never a path: a moved page changes its URL and keeps its name, so
19
+ * every call site keeps working. That is the property the hardcoded table in
20
+ * the previous `<Link>` could not offer, because it restated six URLs by hand
21
+ * and silently refused every other page in the app.
22
+ *
23
+ * The query half is delegated to `queryStringOf`, which lives next to the
24
+ * DECODER it has to agree with. It used to live here, and being module-private
25
+ * meant the read half could not share it — the one-writer rule held only
26
+ * because nobody had written the second writer yet.
27
+ *
28
+ * @throws {UnserializableQueryValueError} when a query value nests deeper than
29
+ * the wire format core parses can carry.
30
+ */
31
+ declare function href(name: string, params?: RouteParameters, query?: RouteQuery): string;
32
+ //#endregion
33
+ export { RouteParameters, RouteQuery, href };
34
+ //# sourceMappingURL=route-table.d.mts.map
@@ -0,0 +1,182 @@
1
+ import { queryStringOf } from "./query-string.mjs";
2
+
3
+ //#region ../web/src/routing/route-table.ts
4
+ /**
5
+ * The name→URL primitive, and the one route table both sides read.
6
+ *
7
+ * `href(name, params, query)` is the DURABLE primitive; `<Link>` is sugar over
8
+ * it. That ordering is deliberate and ratified: a function serves emails,
9
+ * redirects, `Location` headers and non-React callers, none of which can render
10
+ * a component — and it keeps client-side navigation, when it lands, a behaviour
11
+ * change rather than an API change.
12
+ *
13
+ * ── Why a process-global table is correct HERE ───────────────────────────────
14
+ * Module-level mutable state is normally a defect in a server that handles
15
+ * concurrent requests, and this codebase has a real example of that defect to
16
+ * point at: `@mongez/react-router`'s `RouterWrapper` keeps the RENDERED PAGE
17
+ * CONTENT in a module-level binding, so request B overwrites the tree request A
18
+ * is about to serialize. That is why it is not on the SSR path.
19
+ *
20
+ * A route table is the opposite kind of value. It is derived from the file
21
+ * system at boot, identical for every request in the process, and never written
22
+ * during a request — the same category as the compiled route table the server's
23
+ * own router holds. Nothing here is per-request, so there is nothing for two
24
+ * requests to race over.
25
+ *
26
+ * The rule that keeps it that way: `publishRouteTable` is called at INSTALL
27
+ * time (server) or at hydration entry (browser), and never from a loader, a
28
+ * middleware, or a component.
29
+ */
30
+ const PARAMETER_PATTERN = /:([A-Za-z0-9_]+)|\*/g;
31
+ /**
32
+ * ── WHY THIS LIVES ON `globalThis` AND NOT IN A MODULE BINDING ───────────────
33
+ *
34
+ * A plain `let` here does not work in development, and the failure is silent
35
+ * enough to be worth spelling out.
36
+ *
37
+ * In dev the process runs TWO module graphs over the same files. Route
38
+ * installation is loaded by tsx/Node (it is CLI bootstrap and imports core
39
+ * directly), while page and layout modules are evaluated by Vite's SSR module
40
+ * runner — which keeps its own registry and its own instance of every module it
41
+ * transforms, `@warlock.js/web` included. So a module-level binding written by
42
+ * the installer is not the binding `<Link>` reads during render: the installer
43
+ * publishes into one instance and the component finds the other one empty.
44
+ *
45
+ * That was measured, not theorised. With a module-level `let`, every anchor on
46
+ * a server-rendered page threw `RouteTableNotPublishedError` with an empty
47
+ * known-names list while installation had demonstrably run.
48
+ *
49
+ * `Symbol.for` resolves through the per-ISOLATE symbol registry, which both
50
+ * graphs share because they are the same isolate. So the table is one value no
51
+ * matter which graph reaches it first, and the module keeps its module-shaped
52
+ * API. `publishedBy` is carried for diagnosis only.
53
+ */
54
+ const ROUTE_TABLE_SLOT = Symbol.for("warlock.web.routeTable");
55
+ /**
56
+ * `undefined` means "nobody has published yet", which is a DIFFERENT fault from
57
+ * "the table is published and this name is not in it" — an empty Map would
58
+ * conflate them and send the reader hunting a route that was never missing.
59
+ */
60
+ function readSlot() {
61
+ return globalThis[ROUTE_TABLE_SLOT];
62
+ }
63
+ var RouteTableNotPublishedError = class extends Error {
64
+ routeName;
65
+ constructor(routeName) {
66
+ super(`Warlock href(${JSON.stringify(routeName)}) was called before the route table was published, so no route name resolves yet. The table is published once at boot — by the server when it installs page routes, and by the hydration entry before it mounts. Seeing this means href() ran outside both: typically a module evaluating at import time, or a unit test that renders a component without publishing a table first.`);
67
+ this.routeName = routeName;
68
+ this.name = "RouteTableNotPublishedError";
69
+ }
70
+ };
71
+ var UnknownRouteNameError = class extends Error {
72
+ routeName;
73
+ knownRouteNames;
74
+ constructor(routeName, knownRouteNames) {
75
+ super(`Warlock href(${JSON.stringify(routeName)}) does not name a known route. ` + (knownRouteNames.length === 0 ? "The route table is published but empty, so no page declared a `route` discovery could see." : `The table knows: ${knownRouteNames.map((name) => JSON.stringify(name)).join(", ")}.`));
76
+ this.routeName = routeName;
77
+ this.knownRouteNames = knownRouteNames;
78
+ this.name = "UnknownRouteNameError";
79
+ }
80
+ };
81
+ var MissingRouteParameterError = class extends Error {
82
+ routeName;
83
+ parameterName;
84
+ routePath;
85
+ constructor(routeName, parameterName, routePath) {
86
+ super(`Warlock href(${JSON.stringify(routeName)}) is missing the parameter ${JSON.stringify(parameterName)}, required by the route path "${routePath}". It is not defaulted: a missing parameter would otherwise be interpolated as the literal text \`undefined\`, producing a link that renders correctly and 404s for a visitor.`);
87
+ this.routeName = routeName;
88
+ this.parameterName = parameterName;
89
+ this.routePath = routePath;
90
+ this.name = "MissingRouteParameterError";
91
+ }
92
+ };
93
+ var UnknownRouteParameterError = class extends Error {
94
+ routeName;
95
+ parameterNames;
96
+ routePath;
97
+ constructor(routeName, parameterNames, routePath) {
98
+ super(`Warlock href(${JSON.stringify(routeName)}) was given ${parameterNames.map((name) => JSON.stringify(name)).join(", ")}, which the route path "${routePath}" does not declare. Passing an undeclared parameter is a typo often enough that it is refused rather than dropped; values meant for the query string go in the third argument.`);
99
+ this.routeName = routeName;
100
+ this.parameterNames = parameterNames;
101
+ this.routePath = routePath;
102
+ this.name = "UnknownRouteParameterError";
103
+ }
104
+ };
105
+ var DuplicateRouteNameError = class extends Error {
106
+ routeName;
107
+ paths;
108
+ constructor(routeName, paths) {
109
+ super(`Warlock route table: two routes both claim the name ${JSON.stringify(routeName)} — "${paths[0]}" and "${paths[1]}". A name resolves to exactly one URL, so one of the two would silently win and every link to it would be wrong half the time.`);
110
+ this.routeName = routeName;
111
+ this.paths = paths;
112
+ this.name = "DuplicateRouteNameError";
113
+ }
114
+ };
115
+ /**
116
+ * Publish the table. WHOLESALE — the previous one is discarded, not merged
117
+ * into.
118
+ *
119
+ * Merging looks harmless until a page is deleted and the dev server restarts:
120
+ * the dead name would stay resolvable, and `<Link>` would go on rendering a URL
121
+ * the server no longer routes. The table has to be able to shrink.
122
+ */
123
+ function publishRouteTable(entries, publishedBy = "unnamed") {
124
+ const table = /* @__PURE__ */ new Map();
125
+ for (const entry of entries) {
126
+ const existing = table.get(entry.name);
127
+ if (existing !== void 0) throw new DuplicateRouteNameError(entry.name, [existing, entry.path]);
128
+ table.set(entry.name, entry.path);
129
+ }
130
+ globalThis[ROUTE_TABLE_SLOT] = {
131
+ table,
132
+ publishedBy
133
+ };
134
+ }
135
+ /** The published names, for diagnostics. Empty when nothing is published. */
136
+ function knownRouteNames() {
137
+ const slot = readSlot();
138
+ return slot === void 0 ? [] : [...slot.table.keys()];
139
+ }
140
+ function parameterNamesOf(routePath) {
141
+ const names = [];
142
+ for (const match of routePath.matchAll(PARAMETER_PATTERN)) names.push(match[1] ?? "*");
143
+ return names;
144
+ }
145
+ function interpolate(routeName, routePath, params) {
146
+ const declared = parameterNamesOf(routePath);
147
+ const undeclared = Object.keys(params ?? {}).filter((key) => params?.[key] !== void 0).filter((key) => !declared.includes(key));
148
+ if (undeclared.length > 0) throw new UnknownRouteParameterError(routeName, undeclared, routePath);
149
+ return routePath.replace(PARAMETER_PATTERN, (match) => {
150
+ const name = match === "*" ? "*" : match.slice(1);
151
+ const value = params?.[name];
152
+ if (value === void 0) throw new MissingRouteParameterError(routeName, name, routePath);
153
+ return encodeURIComponent(String(value));
154
+ });
155
+ }
156
+ /**
157
+ * Resolve a route NAME to a URL.
158
+ *
159
+ * A name, never a path: a moved page changes its URL and keeps its name, so
160
+ * every call site keeps working. That is the property the hardcoded table in
161
+ * the previous `<Link>` could not offer, because it restated six URLs by hand
162
+ * and silently refused every other page in the app.
163
+ *
164
+ * The query half is delegated to `queryStringOf`, which lives next to the
165
+ * DECODER it has to agree with. It used to live here, and being module-private
166
+ * meant the read half could not share it — the one-writer rule held only
167
+ * because nobody had written the second writer yet.
168
+ *
169
+ * @throws {UnserializableQueryValueError} when a query value nests deeper than
170
+ * the wire format core parses can carry.
171
+ */
172
+ function href(name, params, query) {
173
+ const slot = readSlot();
174
+ if (slot === void 0) throw new RouteTableNotPublishedError(name);
175
+ const routePath = slot.table.get(name);
176
+ if (routePath === void 0) throw new UnknownRouteNameError(name, [...slot.table.keys()]);
177
+ return `${interpolate(name, routePath, params)}${queryStringOf(query)}`;
178
+ }
179
+
180
+ //#endregion
181
+ export { href, knownRouteNames, publishRouteTable };
182
+ //# sourceMappingURL=route-table.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-table.mjs","names":[],"sources":["../../../../../../../web/src/routing/route-table.ts"],"sourcesContent":["/**\n * The name→URL primitive, and the one route table both sides read.\n *\n * `href(name, params, query)` is the DURABLE primitive; `<Link>` is sugar over\n * it. That ordering is deliberate and ratified: a function serves emails,\n * redirects, `Location` headers and non-React callers, none of which can render\n * a component — and it keeps client-side navigation, when it lands, a behaviour\n * change rather than an API change.\n *\n * ── Why a process-global table is correct HERE ───────────────────────────────\n * Module-level mutable state is normally a defect in a server that handles\n * concurrent requests, and this codebase has a real example of that defect to\n * point at: `@mongez/react-router`'s `RouterWrapper` keeps the RENDERED PAGE\n * CONTENT in a module-level binding, so request B overwrites the tree request A\n * is about to serialize. That is why it is not on the SSR path.\n *\n * A route table is the opposite kind of value. It is derived from the file\n * system at boot, identical for every request in the process, and never written\n * during a request — the same category as the compiled route table the server's\n * own router holds. Nothing here is per-request, so there is nothing for two\n * requests to race over.\n *\n * The rule that keeps it that way: `publishRouteTable` is called at INSTALL\n * time (server) or at hydration entry (browser), and never from a loader, a\n * middleware, or a component.\n */\n\nimport { queryStringOf, type QueryStringInput } from \"./query-string\";\n\n/** The two fields `href` needs. Callers may pass richer entries; the rest is ignored. */\nexport type RouteTableEntry = {\n readonly name: string;\n readonly path: string;\n};\n\n/** What `href` accepts for a `:param` segment. Rendered with `String(value)`. */\nexport type RouteParameters = Readonly<Record<string, unknown>>;\n\n/**\n * Query values; an `undefined` value is omitted rather than serialized.\n *\n * A value may be a scalar, an array of scalars, or an object one level deep —\n * the shapes `@warlock.js/core` parses back out of the URL. Anything deeper\n * throws `UnserializableQueryValueError`; the grammar and the measurements\n * behind it are documented in query-string.ts.\n */\nexport type RouteQuery = QueryStringInput;\n\nconst PARAMETER_PATTERN = /:([A-Za-z0-9_]+)|\\*/g;\n\n/**\n * ── WHY THIS LIVES ON `globalThis` AND NOT IN A MODULE BINDING ───────────────\n *\n * A plain `let` here does not work in development, and the failure is silent\n * enough to be worth spelling out.\n *\n * In dev the process runs TWO module graphs over the same files. Route\n * installation is loaded by tsx/Node (it is CLI bootstrap and imports core\n * directly), while page and layout modules are evaluated by Vite's SSR module\n * runner — which keeps its own registry and its own instance of every module it\n * transforms, `@warlock.js/web` included. So a module-level binding written by\n * the installer is not the binding `<Link>` reads during render: the installer\n * publishes into one instance and the component finds the other one empty.\n *\n * That was measured, not theorised. With a module-level `let`, every anchor on\n * a server-rendered page threw `RouteTableNotPublishedError` with an empty\n * known-names list while installation had demonstrably run.\n *\n * `Symbol.for` resolves through the per-ISOLATE symbol registry, which both\n * graphs share because they are the same isolate. So the table is one value no\n * matter which graph reaches it first, and the module keeps its module-shaped\n * API. `publishedBy` is carried for diagnosis only.\n */\nconst ROUTE_TABLE_SLOT = Symbol.for(\"warlock.web.routeTable\");\n\ntype RouteTableSlot = {\n table: Map<string, string>;\n publishedBy: string;\n};\n\ntype RouteTableHost = typeof globalThis & {\n [ROUTE_TABLE_SLOT]?: RouteTableSlot;\n};\n\n/**\n * `undefined` means \"nobody has published yet\", which is a DIFFERENT fault from\n * \"the table is published and this name is not in it\" — an empty Map would\n * conflate them and send the reader hunting a route that was never missing.\n */\nfunction readSlot(): RouteTableSlot | undefined {\n return (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\nexport class RouteTableNotPublishedError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was called before the route table was ` +\n \"published, so no route name resolves yet. The table is published once at boot — by \" +\n \"the server when it installs page routes, and by the hydration entry before it mounts. \" +\n \"Seeing this means href() ran outside both: typically a module evaluating at import \" +\n \"time, or a unit test that renders a component without publishing a table first.\",\n );\n this.name = \"RouteTableNotPublishedError\";\n }\n}\n\nexport class UnknownRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly knownRouteNames: readonly string[],\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) does not name a known route. ` +\n (knownRouteNames.length === 0\n ? \"The route table is published but empty, so no page declared a `route` discovery could see.\"\n : `The table knows: ${knownRouteNames.map(name => JSON.stringify(name)).join(\", \")}.`),\n );\n this.name = \"UnknownRouteNameError\";\n }\n}\n\nexport class MissingRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterName: string,\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) is missing the parameter ` +\n `${JSON.stringify(parameterName)}, required by the route path \"${routePath}\". ` +\n \"It is not defaulted: a missing parameter would otherwise be interpolated as the \" +\n \"literal text `undefined`, producing a link that renders correctly and 404s for a \" +\n \"visitor.\",\n );\n this.name = \"MissingRouteParameterError\";\n }\n}\n\nexport class UnknownRouteParameterError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly parameterNames: readonly string[],\n public readonly routePath: string,\n ) {\n super(\n `Warlock href(${JSON.stringify(routeName)}) was given ` +\n `${parameterNames.map(name => JSON.stringify(name)).join(\", \")}, which the route path ` +\n `\"${routePath}\" does not declare. Passing an undeclared parameter is a typo often ` +\n \"enough that it is refused rather than dropped; values meant for the query string go \" +\n \"in the third argument.\",\n );\n this.name = \"UnknownRouteParameterError\";\n }\n}\n\nexport class DuplicateRouteNameError extends Error {\n public constructor(\n public readonly routeName: string,\n public readonly paths: readonly [string, string],\n ) {\n super(\n `Warlock route table: two routes both claim the name ${JSON.stringify(routeName)} — ` +\n `\"${paths[0]}\" and \"${paths[1]}\". A name resolves to exactly one URL, so one of the ` +\n \"two would silently win and every link to it would be wrong half the time.\",\n );\n this.name = \"DuplicateRouteNameError\";\n }\n}\n\n/**\n * Publish the table. WHOLESALE — the previous one is discarded, not merged\n * into.\n *\n * Merging looks harmless until a page is deleted and the dev server restarts:\n * the dead name would stay resolvable, and `<Link>` would go on rendering a URL\n * the server no longer routes. The table has to be able to shrink.\n */\nexport function publishRouteTable(\n entries: readonly RouteTableEntry[],\n publishedBy = \"unnamed\",\n): void {\n const table = new Map<string, string>();\n\n for (const entry of entries) {\n const existing = table.get(entry.name);\n\n if (existing !== undefined) {\n throw new DuplicateRouteNameError(entry.name, [existing, entry.path]);\n }\n\n table.set(entry.name, entry.path);\n }\n\n (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT] = { table, publishedBy };\n}\n\n/**\n * Drop the table, returning the module to its pre-boot state.\n *\n * Exists for tests: the table is process-global, so a suite that published one\n * would otherwise leak it into every later test in the same worker and pass in\n * file order while failing under `--shuffle`.\n */\nexport function resetRouteTable(): void {\n delete (globalThis as RouteTableHost)[ROUTE_TABLE_SLOT];\n}\n\n/** The published names, for diagnostics. Empty when nothing is published. */\nexport function knownRouteNames(): readonly string[] {\n const slot = readSlot();\n\n return slot === undefined ? [] : [...slot.table.keys()];\n}\n\n/** Who published the live table, for diagnosis. `undefined` when nothing has. */\nexport function routeTablePublisher(): string | undefined {\n return readSlot()?.publishedBy;\n}\n\nfunction parameterNamesOf(routePath: string): readonly string[] {\n const names: string[] = [];\n\n for (const match of routePath.matchAll(PARAMETER_PATTERN)) {\n names.push(match[1] ?? \"*\");\n }\n\n return names;\n}\n\nfunction interpolate(\n routeName: string,\n routePath: string,\n params: RouteParameters | undefined,\n): string {\n const declared = parameterNamesOf(routePath);\n const supplied = Object.keys(params ?? {}).filter(key => params?.[key] !== undefined);\n const undeclared = supplied.filter(key => !declared.includes(key));\n\n if (undeclared.length > 0) {\n throw new UnknownRouteParameterError(routeName, undeclared, routePath);\n }\n\n return routePath.replace(PARAMETER_PATTERN, match => {\n const name = match === \"*\" ? \"*\" : match.slice(1);\n const value = params?.[name];\n\n if (value === undefined) {\n throw new MissingRouteParameterError(routeName, name, routePath);\n }\n\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Resolve a route NAME to a URL.\n *\n * A name, never a path: a moved page changes its URL and keeps its name, so\n * every call site keeps working. That is the property the hardcoded table in\n * the previous `<Link>` could not offer, because it restated six URLs by hand\n * and silently refused every other page in the app.\n *\n * The query half is delegated to `queryStringOf`, which lives next to the\n * DECODER it has to agree with. It used to live here, and being module-private\n * meant the read half could not share it — the one-writer rule held only\n * because nobody had written the second writer yet.\n *\n * @throws {UnserializableQueryValueError} when a query value nests deeper than\n * the wire format core parses can carry.\n */\nexport function href(\n name: string,\n params?: RouteParameters,\n query?: RouteQuery,\n): string {\n const slot = readSlot();\n\n if (slot === undefined) throw new RouteTableNotPublishedError(name);\n\n const routePath = slot.table.get(name);\n\n if (routePath === undefined) {\n throw new UnknownRouteNameError(name, [...slot.table.keys()]);\n }\n\n return `${interpolate(name, routePath, params)}${queryStringOf(query)}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;AAyB1B,MAAM,mBAAmB,OAAO,IAAI,wBAAwB;;;;;;AAgB5D,SAAS,WAAuC;CAC9C,OAAQ,WAA8B;AACxC;AAEA,IAAa,8BAAb,cAAiD,MAAM;CAClB;CAAnC,AAAO,YAAY,AAAgB,WAAmB;EACpD,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,oXAK5C;EAPiC;EAQjC,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,MAAM;CAE7B;CACA;CAFlB,AAAO,YACL,AAAgB,WAChB,AAAgB,iBAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,oCACvC,gBAAgB,WAAW,IACxB,+FACA,oBAAoB,gBAAgB,KAAI,SAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,GACzF;EARgB;EACA;EAQhB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CAElC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,eAChB,AAAgB,WAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,6BACrC,KAAK,UAAU,aAAa,EAAE,gCAAgC,UAAU,+KAI/E;EAVgB;EACA;EACA;EAShB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,6BAAb,cAAgD,MAAM;CAElC;CACA;CACA;CAHlB,AAAO,YACL,AAAgB,WAChB,AAAgB,gBAChB,AAAgB,WAChB;EACA,MACE,gBAAgB,KAAK,UAAU,SAAS,EAAE,cACrC,eAAe,KAAI,SAAQ,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,0BAC3D,UAAU,+KAGlB;EAVgB;EACA;EACA;EAShB,KAAK,OAAO;CACd;AACF;AAEA,IAAa,0BAAb,cAA6C,MAAM;CAE/B;CACA;CAFlB,AAAO,YACL,AAAgB,WAChB,AAAgB,OAChB;EACA,MACE,uDAAuD,KAAK,UAAU,SAAS,EAAE,MAC3E,MAAM,GAAG,SAAS,MAAM,GAAG,+HAEnC;EAPgB;EACA;EAOhB,KAAK,OAAO;CACd;AACF;;;;;;;;;AAUA,SAAgB,kBACd,SACA,cAAc,WACR;CACN,MAAM,wBAAQ,IAAI,IAAoB;CAEtC,KAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,WAAW,MAAM,IAAI,MAAM,IAAI;EAErC,IAAI,aAAa,QACf,MAAM,IAAI,wBAAwB,MAAM,MAAM,CAAC,UAAU,MAAM,IAAI,CAAC;EAGtE,MAAM,IAAI,MAAM,MAAM,MAAM,IAAI;CAClC;CAEA,AAAC,WAA8B,oBAAoB;EAAE;EAAO;CAAY;AAC1E;;AAcA,SAAgB,kBAAqC;CACnD,MAAM,OAAO,SAAS;CAEtB,OAAO,SAAS,SAAY,CAAC,IAAI,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC;AACxD;AAOA,SAAS,iBAAiB,WAAsC;CAC9D,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,UAAU,SAAS,iBAAiB,GACtD,MAAM,KAAK,MAAM,MAAM,GAAG;CAG5B,OAAO;AACT;AAEA,SAAS,YACP,WACA,WACA,QACQ;CACR,MAAM,WAAW,iBAAiB,SAAS;CAE3C,MAAM,aADW,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,QAAO,QAAO,SAAS,SAAS,MACjD,CAAC,CAAC,QAAO,QAAO,CAAC,SAAS,SAAS,GAAG,CAAC;CAEjE,IAAI,WAAW,SAAS,GACtB,MAAM,IAAI,2BAA2B,WAAW,YAAY,SAAS;CAGvE,OAAO,UAAU,QAAQ,oBAAmB,UAAS;EACnD,MAAM,OAAO,UAAU,MAAM,MAAM,MAAM,MAAM,CAAC;EAChD,MAAM,QAAQ,SAAS;EAEvB,IAAI,UAAU,QACZ,MAAM,IAAI,2BAA2B,WAAW,MAAM,SAAS;EAGjE,OAAO,mBAAmB,OAAO,KAAK,CAAC;CACzC,CAAC;AACH;;;;;;;;;;;;;;;;;AAkBA,SAAgB,KACd,MACA,QACA,OACQ;CACR,MAAM,OAAO,SAAS;CAEtB,IAAI,SAAS,QAAW,MAAM,IAAI,4BAA4B,IAAI;CAElE,MAAM,YAAY,KAAK,MAAM,IAAI,IAAI;CAErC,IAAI,cAAc,QAChB,MAAM,IAAI,sBAAsB,MAAM,CAAC,GAAG,KAAK,MAAM,KAAK,CAAC,CAAC;CAG9D,OAAO,GAAG,YAAY,MAAM,WAAW,MAAM,IAAI,cAAc,KAAK;AACtE"}
@@ -0,0 +1,159 @@
1
+ //#region ../web/src/routing/router-events.d.ts
2
+ /**
3
+ * The navigation lifecycle emitter — what a progress bar subscribes to.
4
+ *
5
+ * A client navigation is a fetch followed by a tree swap
6
+ * (`client/navigation/navigation-root.tsx`). Nothing about that is visible to
7
+ * the user while it is in flight, which is the whole reason this module
8
+ * exists: a progress bar, an analytics hook or a scroll restorer needs to know
9
+ * that a navigation STARTED, that it FINISHED, and that it FAILED — and none
10
+ * of them should have to reach into the navigation runtime to find out.
11
+ *
12
+ * So the runtime announces, and anyone may listen. The listeners are strangers
13
+ * to each other and to the runtime; that is the point.
14
+ *
15
+ * ## This module ships the emitter only
16
+ *
17
+ * Nothing here is wired into the navigation runtime yet — `navigation-root.tsx`
18
+ * calls the `emit*` methods in a later change. Until then this is a complete,
19
+ * self-contained emitter with no callers, which is why it has no dependency on
20
+ * anything in `client/`.
21
+ *
22
+ * ## What it deliberately does NOT do
23
+ *
24
+ * It carries URLs as OPAQUE STRINGS. There is no matching, no parsing, no
25
+ * "which route is this" — Warlock's server router is the only matcher (canon
26
+ * 9c8f878b), and an emitter that started parsing paths would be a second one.
27
+ * A listener that wants a route name gets it from the payload the navigation
28
+ * produced, not from this module.
29
+ *
30
+ * DIRECTORY CONTRACT — see `route-identity.ts`: nothing in `web/src/routing/`
31
+ * may import `node:fs`, `node:path`, `vite` or `fastify`. This module also
32
+ * touches no DOM global, which is what makes it importable from a server
33
+ * render (see below).
34
+ *
35
+ * ## Named for its ancestor
36
+ *
37
+ * `routerEvents` is the name `@mongez/react-router` used
38
+ * (`@mongez/react-router/src/events.ts`), and the subscription ergonomics are
39
+ * deliberately familiar: `routerEvents.onNavigating(callback)` returns
40
+ * something you call to stop listening. The implementation is NOT ported —
41
+ * MRR's delegates to a global `@mongez/events` bus keyed by string
42
+ * (`"router.navigating"`), which reaches its router singleton and gives up
43
+ * per-event payload typing in the process. Here the events are the object's
44
+ * own methods, so each one carries its own payload type and a typo is a
45
+ * compile error rather than a listener that never fires.
46
+ *
47
+ * MRR returns an `EventSubscription` object; this returns the unsubscribe
48
+ * FUNCTION itself, because that is what a React `useEffect` cleanup wants to
49
+ * be handed:
50
+ *
51
+ * ```ts
52
+ * useEffect(() => routerEvents.onNavigating(() => setLoading(true)), []);
53
+ * ```
54
+ */
55
+ /**
56
+ * How the navigation will be written to browser history.
57
+ *
58
+ * `"replace"` covers both an explicit `<Link replace>` and a Back/Forward
59
+ * press — the runtime replaces in both cases, because the history entry
60
+ * already exists. A listener that wants to stay quiet during Back/Forward
61
+ * cannot tell the two apart from here, and does not need to: what it actually
62
+ * cares about is that no new entry is being pushed.
63
+ */
64
+ type NavigationMode = "push" | "replace";
65
+ /** Emitted when a navigation begins — before anything has been fetched. */
66
+ type NavigationStartPayload = {
67
+ /** The URL the navigation was requested for, verbatim and unparsed. */url: string;
68
+ mode: NavigationMode;
69
+ };
70
+ /** Emitted when a navigation has completed and the new page is on screen. */
71
+ type NavigationEndPayload = {
72
+ /** The URL the navigation was requested for — the same string {@link NavigationStartPayload} carried. */url: string;
73
+ /**
74
+ * The URL the page data actually came from, which is what landed in the
75
+ * address bar. It differs from `url` whenever the server redirected — a
76
+ * page that requires auth answers from `/login` (see
77
+ * `client/navigation/fetch-page-data.ts`).
78
+ */
79
+ resolvedUrl: string;
80
+ mode: NavigationMode;
81
+ };
82
+ /**
83
+ * Emitted when a navigation could not complete.
84
+ *
85
+ * A failed client navigation is not a dead end — the runtime degrades to a
86
+ * full browser load — so a listener should treat this as "the in-flight
87
+ * navigation is over", not as an error to render. A progress bar hides on it.
88
+ */
89
+ type NavigationErrorPayload = {
90
+ /** The URL the navigation was requested for. */url: string;
91
+ mode: NavigationMode;
92
+ /**
93
+ * What went wrong. `unknown` rather than `Error` because a caught value has
94
+ * no such guarantee — narrow it before reading `.message`.
95
+ */
96
+ error: unknown;
97
+ };
98
+ /** A listener for one navigation event. Its return value is ignored. */
99
+ type RouterEventListener<Payload> = (payload: Payload) => void;
100
+ /** Call it to stop listening. Calling it more than once is a no-op. */
101
+ type RouterEventUnsubscribe = () => void;
102
+ /**
103
+ * The navigation lifecycle surface: three events, each with its own payload
104
+ * type, each subscribable and emittable.
105
+ *
106
+ * The `emit*` half belongs to the navigation runtime. Nothing stops other code
107
+ * from calling it, and nothing needs to — an emitter that lies about
108
+ * navigations is a bug in whoever called it, not a boundary worth policing.
109
+ */
110
+ type RouterEvents = {
111
+ /** Subscribe to the start of every navigation. @returns the unsubscribe function. */onNavigating: (listener: RouterEventListener<NavigationStartPayload>) => RouterEventUnsubscribe; /** Subscribe to every navigation that completed. @returns the unsubscribe function. */
112
+ onNavigated: (listener: RouterEventListener<NavigationEndPayload>) => RouterEventUnsubscribe; /** Subscribe to every navigation that failed. @returns the unsubscribe function. */
113
+ onNavigationError: (listener: RouterEventListener<NavigationErrorPayload>) => RouterEventUnsubscribe; /** Announce that a navigation has begun. Called by the navigation runtime. */
114
+ emitNavigating: (payload: NavigationStartPayload) => void; /** Announce that a navigation has completed. Called by the navigation runtime. */
115
+ emitNavigated: (payload: NavigationEndPayload) => void; /** Announce that a navigation has failed. Called by the navigation runtime. */
116
+ emitNavigationError: (payload: NavigationErrorPayload) => void;
117
+ };
118
+ /**
119
+ * Builds an independent emitter with no listeners.
120
+ *
121
+ * This is what makes the module TESTABLE without cross-test bleed, and it is
122
+ * the escape hatch for anything that needs a private lifecycle bus. The
123
+ * shared {@link routerEvents} is one of these, created once.
124
+ */
125
+ declare function createRouterEvents(): RouterEvents;
126
+ /**
127
+ * The shared navigation lifecycle emitter — the one a progress bar subscribes
128
+ * to and the one the navigation runtime emits on.
129
+ *
130
+ * ## Why a module-level singleton is safe here, when module-level state is a known SSR hazard
131
+ *
132
+ * Canon records MRR's `RouterWrapper.tsx:63` as the cautionary case: module
133
+ * state that holds RENDER state is shared by every concurrent request on the
134
+ * server, so one visitor's page leaks into another's. That hazard is about
135
+ * *what* is held, not about module scope itself — `routing/navigator.ts` in
136
+ * this same directory already holds a module-level registration for the same
137
+ * reason.
138
+ *
139
+ * This object holds ONLY listener registrations. No current route, no payload,
140
+ * no request-scoped anything — nothing a render reads and nothing a response
141
+ * is built from. Two concurrent SSR requests observe the same empty listener
142
+ * sets and neither can learn a thing about the other.
143
+ *
144
+ * It is also safe to IMPORT on the server: constructing it touches no DOM
145
+ * global, no `window`, no `history`, and performs no work beyond allocating
146
+ * three empty sets. On the server nothing subscribes and nothing emits, so it
147
+ * simply sits there — which is the correct server behaviour for a progress
148
+ * bar.
149
+ *
150
+ * A singleton (rather than a context or a factory at the call site) is what
151
+ * lets a progress bar living anywhere in the tree — or outside React
152
+ * entirely — hear a navigation without being handed a bus by every component
153
+ * between it and the root. That plumbing is the reason MRR made this global
154
+ * too. Use {@link createRouterEvents} when you want an isolated one.
155
+ */
156
+ declare const routerEvents: RouterEvents;
157
+ //#endregion
158
+ export { NavigationEndPayload, NavigationErrorPayload, NavigationStartPayload, createRouterEvents, routerEvents };
159
+ //# sourceMappingURL=router-events.d.mts.map
@@ -0,0 +1,83 @@
1
+ //#region ../web/src/routing/router-events.ts
2
+ /**
3
+ * One event's registrations and its delivery loop. A `Set` because insertion
4
+ * order is preserved (listeners fire in subscription order) and removal is by
5
+ * identity, which is exactly what an unsubscribe closure holds.
6
+ */
7
+ function createSignal(eventName) {
8
+ const registrations = /* @__PURE__ */ new Set();
9
+ return {
10
+ subscribe: (listener) => {
11
+ const registration = { listener };
12
+ registrations.add(registration);
13
+ return () => {
14
+ registrations.delete(registration);
15
+ };
16
+ },
17
+ emit: (payload) => {
18
+ for (const registration of [...registrations]) {
19
+ if (!registrations.has(registration)) continue;
20
+ try {
21
+ registration.listener(payload);
22
+ } catch (error) {
23
+ console.error(`Warlock routerEvents: a ${eventName} listener threw:`, error);
24
+ }
25
+ }
26
+ }
27
+ };
28
+ }
29
+ /**
30
+ * Builds an independent emitter with no listeners.
31
+ *
32
+ * This is what makes the module TESTABLE without cross-test bleed, and it is
33
+ * the escape hatch for anything that needs a private lifecycle bus. The
34
+ * shared {@link routerEvents} is one of these, created once.
35
+ */
36
+ function createRouterEvents() {
37
+ const navigating = createSignal("navigating");
38
+ const navigated = createSignal("navigated");
39
+ const navigationError = createSignal("navigation-error");
40
+ return {
41
+ onNavigating: navigating.subscribe,
42
+ onNavigated: navigated.subscribe,
43
+ onNavigationError: navigationError.subscribe,
44
+ emitNavigating: navigating.emit,
45
+ emitNavigated: navigated.emit,
46
+ emitNavigationError: navigationError.emit
47
+ };
48
+ }
49
+ /**
50
+ * The shared navigation lifecycle emitter — the one a progress bar subscribes
51
+ * to and the one the navigation runtime emits on.
52
+ *
53
+ * ## Why a module-level singleton is safe here, when module-level state is a known SSR hazard
54
+ *
55
+ * Canon records MRR's `RouterWrapper.tsx:63` as the cautionary case: module
56
+ * state that holds RENDER state is shared by every concurrent request on the
57
+ * server, so one visitor's page leaks into another's. That hazard is about
58
+ * *what* is held, not about module scope itself — `routing/navigator.ts` in
59
+ * this same directory already holds a module-level registration for the same
60
+ * reason.
61
+ *
62
+ * This object holds ONLY listener registrations. No current route, no payload,
63
+ * no request-scoped anything — nothing a render reads and nothing a response
64
+ * is built from. Two concurrent SSR requests observe the same empty listener
65
+ * sets and neither can learn a thing about the other.
66
+ *
67
+ * It is also safe to IMPORT on the server: constructing it touches no DOM
68
+ * global, no `window`, no `history`, and performs no work beyond allocating
69
+ * three empty sets. On the server nothing subscribes and nothing emits, so it
70
+ * simply sits there — which is the correct server behaviour for a progress
71
+ * bar.
72
+ *
73
+ * A singleton (rather than a context or a factory at the call site) is what
74
+ * lets a progress bar living anywhere in the tree — or outside React
75
+ * entirely — hear a navigation without being handed a bus by every component
76
+ * between it and the root. That plumbing is the reason MRR made this global
77
+ * too. Use {@link createRouterEvents} when you want an isolated one.
78
+ */
79
+ const routerEvents = createRouterEvents();
80
+
81
+ //#endregion
82
+ export { createRouterEvents, routerEvents };
83
+ //# sourceMappingURL=router-events.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router-events.mjs","names":[],"sources":["../../../../../../../web/src/routing/router-events.ts"],"sourcesContent":["/**\n * The navigation lifecycle emitter — what a progress bar subscribes to.\n *\n * A client navigation is a fetch followed by a tree swap\n * (`client/navigation/navigation-root.tsx`). Nothing about that is visible to\n * the user while it is in flight, which is the whole reason this module\n * exists: a progress bar, an analytics hook or a scroll restorer needs to know\n * that a navigation STARTED, that it FINISHED, and that it FAILED — and none\n * of them should have to reach into the navigation runtime to find out.\n *\n * So the runtime announces, and anyone may listen. The listeners are strangers\n * to each other and to the runtime; that is the point.\n *\n * ## This module ships the emitter only\n *\n * Nothing here is wired into the navigation runtime yet — `navigation-root.tsx`\n * calls the `emit*` methods in a later change. Until then this is a complete,\n * self-contained emitter with no callers, which is why it has no dependency on\n * anything in `client/`.\n *\n * ## What it deliberately does NOT do\n *\n * It carries URLs as OPAQUE STRINGS. There is no matching, no parsing, no\n * \"which route is this\" — Warlock's server router is the only matcher (canon\n * 9c8f878b), and an emitter that started parsing paths would be a second one.\n * A listener that wants a route name gets it from the payload the navigation\n * produced, not from this module.\n *\n * DIRECTORY CONTRACT — see `route-identity.ts`: nothing in `web/src/routing/`\n * may import `node:fs`, `node:path`, `vite` or `fastify`. This module also\n * touches no DOM global, which is what makes it importable from a server\n * render (see below).\n *\n * ## Named for its ancestor\n *\n * `routerEvents` is the name `@mongez/react-router` used\n * (`@mongez/react-router/src/events.ts`), and the subscription ergonomics are\n * deliberately familiar: `routerEvents.onNavigating(callback)` returns\n * something you call to stop listening. The implementation is NOT ported —\n * MRR's delegates to a global `@mongez/events` bus keyed by string\n * (`\"router.navigating\"`), which reaches its router singleton and gives up\n * per-event payload typing in the process. Here the events are the object's\n * own methods, so each one carries its own payload type and a typo is a\n * compile error rather than a listener that never fires.\n *\n * MRR returns an `EventSubscription` object; this returns the unsubscribe\n * FUNCTION itself, because that is what a React `useEffect` cleanup wants to\n * be handed:\n *\n * ```ts\n * useEffect(() => routerEvents.onNavigating(() => setLoading(true)), []);\n * ```\n */\n\n/**\n * How the navigation will be written to browser history.\n *\n * `\"replace\"` covers both an explicit `<Link replace>` and a Back/Forward\n * press — the runtime replaces in both cases, because the history entry\n * already exists. A listener that wants to stay quiet during Back/Forward\n * cannot tell the two apart from here, and does not need to: what it actually\n * cares about is that no new entry is being pushed.\n */\nexport type NavigationMode = \"push\" | \"replace\";\n\n/** Emitted when a navigation begins — before anything has been fetched. */\nexport type NavigationStartPayload = {\n /** The URL the navigation was requested for, verbatim and unparsed. */\n url: string;\n mode: NavigationMode;\n};\n\n/** Emitted when a navigation has completed and the new page is on screen. */\nexport type NavigationEndPayload = {\n /** The URL the navigation was requested for — the same string {@link NavigationStartPayload} carried. */\n url: string;\n /**\n * The URL the page data actually came from, which is what landed in the\n * address bar. It differs from `url` whenever the server redirected — a\n * page that requires auth answers from `/login` (see\n * `client/navigation/fetch-page-data.ts`).\n */\n resolvedUrl: string;\n mode: NavigationMode;\n};\n\n/**\n * Emitted when a navigation could not complete.\n *\n * A failed client navigation is not a dead end — the runtime degrades to a\n * full browser load — so a listener should treat this as \"the in-flight\n * navigation is over\", not as an error to render. A progress bar hides on it.\n */\nexport type NavigationErrorPayload = {\n /** The URL the navigation was requested for. */\n url: string;\n mode: NavigationMode;\n /**\n * What went wrong. `unknown` rather than `Error` because a caught value has\n * no such guarantee — narrow it before reading `.message`.\n */\n error: unknown;\n};\n\n/** A listener for one navigation event. Its return value is ignored. */\nexport type RouterEventListener<Payload> = (payload: Payload) => void;\n\n/** Call it to stop listening. Calling it more than once is a no-op. */\nexport type RouterEventUnsubscribe = () => void;\n\n/**\n * The navigation lifecycle surface: three events, each with its own payload\n * type, each subscribable and emittable.\n *\n * The `emit*` half belongs to the navigation runtime. Nothing stops other code\n * from calling it, and nothing needs to — an emitter that lies about\n * navigations is a bug in whoever called it, not a boundary worth policing.\n */\nexport type RouterEvents = {\n /** Subscribe to the start of every navigation. @returns the unsubscribe function. */\n onNavigating: (listener: RouterEventListener<NavigationStartPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that completed. @returns the unsubscribe function. */\n onNavigated: (listener: RouterEventListener<NavigationEndPayload>) => RouterEventUnsubscribe;\n /** Subscribe to every navigation that failed. @returns the unsubscribe function. */\n onNavigationError: (\n listener: RouterEventListener<NavigationErrorPayload>,\n ) => RouterEventUnsubscribe;\n /** Announce that a navigation has begun. Called by the navigation runtime. */\n emitNavigating: (payload: NavigationStartPayload) => void;\n /** Announce that a navigation has completed. Called by the navigation runtime. */\n emitNavigated: (payload: NavigationEndPayload) => void;\n /** Announce that a navigation has failed. Called by the navigation runtime. */\n emitNavigationError: (payload: NavigationErrorPayload) => void;\n};\n\n/**\n * One registration. An OBJECT rather than the callback itself, so that\n * subscribing the same function twice is two independent registrations — a\n * registry keyed by the callback would collapse them, and then one component's\n * cleanup would silently deafen another's.\n */\ntype Registration<Payload> = { listener: RouterEventListener<Payload> };\n\ntype Signal<Payload> = {\n subscribe: (listener: RouterEventListener<Payload>) => RouterEventUnsubscribe;\n emit: (payload: Payload) => void;\n};\n\n/**\n * One event's registrations and its delivery loop. A `Set` because insertion\n * order is preserved (listeners fire in subscription order) and removal is by\n * identity, which is exactly what an unsubscribe closure holds.\n */\nfunction createSignal<Payload>(eventName: string): Signal<Payload> {\n const registrations = new Set<Registration<Payload>>();\n\n return {\n subscribe: listener => {\n const registration: Registration<Payload> = { listener };\n\n registrations.add(registration);\n\n // `Set.delete` on an absent member is a no-op, so this is idempotent for\n // free — which matters because React StrictMode runs an effect's cleanup\n // twice in development.\n return () => {\n registrations.delete(registration);\n };\n },\n emit: payload => {\n /*\n A SNAPSHOT, not the live set. Listeners subscribe and unsubscribe from\n inside other listeners — a progress bar that hides itself, a one-shot\n analytics hook — and mutating the collection being iterated is how an\n emitter starts skipping listeners. Iterating a copy also fixes the\n cohort: a listener added during this emit belongs to the next\n navigation, not to the one already in flight.\n\n Removals during the emit are still honoured (the deleted-check below),\n because a listener that has just unsubscribed has said it no longer\n wants this event, and delivering it anyway is the bug that outlives the\n component.\n */\n for (const registration of [...registrations]) {\n if (!registrations.has(registration)) continue;\n\n try {\n registration.listener(payload);\n } catch (error) {\n /*\n THE POINT OF THE TRY. Everything downstream of an emit — the\n history entry, the tree swap — happens after this loop returns, so\n a listener that throws would otherwise take the navigation down\n with it. One broken progress bar is a broken progress bar; it is\n not a stuck page.\n\n Reported rather than swallowed: a listener failing silently on\n every navigation is worse than noisy. The listener STAYS\n subscribed, because one bad emit is not consent to deregister\n something the component still owns and will still try to clean up.\n */\n console.error(`Warlock routerEvents: a ${eventName} listener threw:`, error);\n }\n }\n },\n };\n}\n\n/**\n * Builds an independent emitter with no listeners.\n *\n * This is what makes the module TESTABLE without cross-test bleed, and it is\n * the escape hatch for anything that needs a private lifecycle bus. The\n * shared {@link routerEvents} is one of these, created once.\n */\nexport function createRouterEvents(): RouterEvents {\n const navigating = createSignal<NavigationStartPayload>(\"navigating\");\n const navigated = createSignal<NavigationEndPayload>(\"navigated\");\n const navigationError = createSignal<NavigationErrorPayload>(\"navigation-error\");\n\n return {\n onNavigating: navigating.subscribe,\n onNavigated: navigated.subscribe,\n onNavigationError: navigationError.subscribe,\n emitNavigating: navigating.emit,\n emitNavigated: navigated.emit,\n emitNavigationError: navigationError.emit,\n };\n}\n\n/**\n * The shared navigation lifecycle emitter — the one a progress bar subscribes\n * to and the one the navigation runtime emits on.\n *\n * ## Why a module-level singleton is safe here, when module-level state is a known SSR hazard\n *\n * Canon records MRR's `RouterWrapper.tsx:63` as the cautionary case: module\n * state that holds RENDER state is shared by every concurrent request on the\n * server, so one visitor's page leaks into another's. That hazard is about\n * *what* is held, not about module scope itself — `routing/navigator.ts` in\n * this same directory already holds a module-level registration for the same\n * reason.\n *\n * This object holds ONLY listener registrations. No current route, no payload,\n * no request-scoped anything — nothing a render reads and nothing a response\n * is built from. Two concurrent SSR requests observe the same empty listener\n * sets and neither can learn a thing about the other.\n *\n * It is also safe to IMPORT on the server: constructing it touches no DOM\n * global, no `window`, no `history`, and performs no work beyond allocating\n * three empty sets. On the server nothing subscribes and nothing emits, so it\n * simply sits there — which is the correct server behaviour for a progress\n * bar.\n *\n * A singleton (rather than a context or a factory at the call site) is what\n * lets a progress bar living anywhere in the tree — or outside React\n * entirely — hear a navigation without being handed a bus by every component\n * between it and the root. That plumbing is the reason MRR made this global\n * too. Use {@link createRouterEvents} when you want an isolated one.\n */\nexport const routerEvents: RouterEvents = createRouterEvents();\n"],"mappings":";;;;;;AAyJA,SAAS,aAAsB,WAAoC;CACjE,MAAM,gCAAgB,IAAI,IAA2B;CAErD,OAAO;EACL,YAAW,aAAY;GACrB,MAAM,eAAsC,EAAE,SAAS;GAEvD,cAAc,IAAI,YAAY;GAK9B,aAAa;IACX,cAAc,OAAO,YAAY;GACnC;EACF;EACA,OAAM,YAAW;GAcf,KAAK,MAAM,gBAAgB,CAAC,GAAG,aAAa,GAAG;IAC7C,IAAI,CAAC,cAAc,IAAI,YAAY,GAAG;IAEtC,IAAI;KACF,aAAa,SAAS,OAAO;IAC/B,SAAS,OAAO;KAad,QAAQ,MAAM,2BAA2B,UAAU,mBAAmB,KAAK;IAC7E;GACF;EACF;CACF;AACF;;;;;;;;AASA,SAAgB,qBAAmC;CACjD,MAAM,aAAa,aAAqC,YAAY;CACpE,MAAM,YAAY,aAAmC,WAAW;CAChE,MAAM,kBAAkB,aAAqC,kBAAkB;CAE/E,OAAO;EACL,cAAc,WAAW;EACzB,aAAa,UAAU;EACvB,mBAAmB,gBAAgB;EACnC,gBAAgB,WAAW;EAC3B,eAAe,UAAU;EACzB,qBAAqB,gBAAgB;CACvC;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAa,eAA6B,mBAAmB"}