@warlock.js/web 5.1.0 → 5.2.1

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 (148) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +126 -12
  3. package/esm/build/contribution.d.mts +10 -13
  4. package/esm/build/contribution.mjs +45 -57
  5. package/esm/build/contribution.mjs.map +1 -1
  6. package/esm/build/discover-pages.mjs +104 -44
  7. package/esm/build/discover-pages.mjs.map +1 -1
  8. package/esm/build/generate-client-registry.mjs +46 -12
  9. package/esm/build/generate-client-registry.mjs.map +1 -1
  10. package/esm/build/generate-pages-barrel.mjs +54 -12
  11. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  12. package/esm/build/page-default-export.mjs +48 -0
  13. package/esm/build/page-default-export.mjs.map +1 -0
  14. package/esm/build/page-routes-manifest.mjs +21 -0
  15. package/esm/build/page-routes-manifest.mjs.map +1 -0
  16. package/esm/build/public-files.mjs +45 -0
  17. package/esm/build/public-files.mjs.map +1 -0
  18. package/esm/build/read-route-exports.mjs.map +1 -1
  19. package/esm/client/build-hydrated-tree.mjs +39 -8
  20. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  21. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  22. package/esm/client/navigation/prefetch.mjs.map +1 -1
  23. package/esm/client/runtime/index.d.mts +2 -1
  24. package/esm/client/runtime/index.mjs +2 -1
  25. package/esm/client/runtime/manifest.mjs +2 -1
  26. package/esm/client/runtime/manifest.mjs.map +1 -1
  27. package/esm/client/runtime/matcher.d.mts +12 -0
  28. package/esm/client/runtime/matcher.mjs +12 -0
  29. package/esm/client/runtime/matcher.mjs.map +1 -1
  30. package/esm/client/runtime/types.d.mts +14 -1
  31. package/esm/components/document-context.d.mts +39 -1
  32. package/esm/components/document-context.mjs.map +1 -1
  33. package/esm/components/link.mjs.map +1 -1
  34. package/esm/components/scripts.mjs +3 -1
  35. package/esm/components/scripts.mjs.map +1 -1
  36. package/esm/context.d.mts +5 -101
  37. package/esm/hydration-payload.d.mts +1 -1
  38. package/esm/hydration-payload.mjs +36 -7
  39. package/esm/hydration-payload.mjs.map +1 -1
  40. package/esm/index.d.mts +3 -2
  41. package/esm/loaders.d.mts +6 -35
  42. package/esm/props.d.mts +15 -7
  43. package/esm/route.d.mts +1 -16
  44. package/esm/routing/filesystem-route.mjs +45 -0
  45. package/esm/routing/filesystem-route.mjs.map +1 -0
  46. package/esm/routing/layout-policy.mjs.map +1 -1
  47. package/esm/routing/query-string.mjs.map +1 -1
  48. package/esm/routing/route-table.mjs.map +1 -1
  49. package/esm/runtime/register-modules.d.mts +19 -0
  50. package/esm/runtime/register-modules.mjs +22 -0
  51. package/esm/runtime/register-modules.mjs.map +1 -0
  52. package/esm/server/build-hydration-payload.mjs +1 -0
  53. package/esm/server/build-hydration-payload.mjs.map +1 -1
  54. package/esm/server/create-page-module-loader.mjs +1 -0
  55. package/esm/server/create-page-module-loader.mjs.map +1 -1
  56. package/esm/server/create-page-route-handler.d.mts +30 -5
  57. package/esm/server/create-page-route-handler.mjs +127 -42
  58. package/esm/server/create-page-route-handler.mjs.map +1 -1
  59. package/esm/server/dev-server.mjs +1 -12
  60. package/esm/server/dev-server.mjs.map +1 -1
  61. package/esm/server/error-page.d.mts +14 -0
  62. package/esm/server/error-page.mjs +39 -0
  63. package/esm/server/error-page.mjs.map +1 -0
  64. package/esm/server/execute-page-request.d.mts +6 -3
  65. package/esm/server/execute-page-request.mjs +78 -79
  66. package/esm/server/execute-page-request.mjs.map +1 -1
  67. package/esm/server/execute-page-request.types.d.mts +32 -91
  68. package/esm/server/hydration-client-url.mjs +1 -1
  69. package/esm/server/index.d.mts +5 -5
  70. package/esm/server/index.mjs +3 -4
  71. package/esm/server/install-page-routes-from-manifest.d.mts +16 -6
  72. package/esm/server/install-page-routes-from-manifest.mjs +70 -9
  73. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  74. package/esm/server/install-page-routes.d.mts +31 -14
  75. package/esm/server/install-page-routes.mjs +90 -38
  76. package/esm/server/install-page-routes.mjs.map +1 -1
  77. package/esm/server/install-production-page-routes.mjs +2 -35
  78. package/esm/server/install-production-page-routes.mjs.map +1 -1
  79. package/esm/server/match-page-route.mjs +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/not-found-page.mjs +1 -1
  82. package/esm/server/not-found-page.mjs.map +1 -1
  83. package/esm/server/page-context.d.mts +2 -8
  84. package/esm/server/page-context.mjs +2 -15
  85. package/esm/server/page-context.mjs.map +1 -1
  86. package/esm/server/page-file-change.mjs +77 -0
  87. package/esm/server/page-file-change.mjs.map +1 -0
  88. package/esm/server/page-manifest.d.mts +24 -9
  89. package/esm/server/page-manifest.mjs.map +1 -1
  90. package/esm/server/page-render-bundle.mjs +38 -0
  91. package/esm/server/page-render-bundle.mjs.map +1 -0
  92. package/esm/server/page-route-reload.mjs +54 -0
  93. package/esm/server/page-route-reload.mjs.map +1 -0
  94. package/esm/server/register-production-public-files.mjs +40 -0
  95. package/esm/server/register-production-public-files.mjs.map +1 -0
  96. package/esm/server/render-page.d.mts +10 -13
  97. package/esm/server/render-page.mjs +153 -27
  98. package/esm/server/render-page.mjs.map +1 -1
  99. package/esm/server/resolve-page-metadata.mjs +1 -1
  100. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  101. package/esm/server/settle-page-response.d.mts +58 -1
  102. package/esm/server/settle-page-response.mjs +110 -45
  103. package/esm/server/settle-page-response.mjs.map +1 -1
  104. package/esm/server/stylesheet-urls.d.mts +40 -29
  105. package/esm/server/stylesheet-urls.mjs +136 -51
  106. package/esm/server/stylesheet-urls.mjs.map +1 -1
  107. package/esm/server/unregistered-pages.mjs +42 -0
  108. package/esm/server/unregistered-pages.mjs.map +1 -0
  109. package/esm/server/web-connector-factory.d.mts +1 -2
  110. package/esm/server/web-connector-factory.mjs +6 -5
  111. package/esm/server/web-connector-factory.mjs.map +1 -1
  112. package/esm/server/web-connector.mjs +137 -17
  113. package/esm/server/web-connector.mjs.map +1 -1
  114. package/esm/shared.d.mts +17 -2
  115. package/esm/shared.mjs +2 -6
  116. package/esm/shared.mjs.map +1 -1
  117. package/esm/vite/build-client.mjs +1 -1
  118. package/esm/vite/build-client.mjs.map +1 -1
  119. package/esm/vite/gate-a-resolve.mjs +66 -12
  120. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  121. package/esm/vite/gate-b-secrets.mjs +34 -16
  122. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  123. package/esm/vite/gate-c-verify.mjs +5 -5
  124. package/esm/vite/gate-c-verify.mjs.map +1 -1
  125. package/esm/vite/hydration-entries.mjs +1 -1
  126. package/esm/vite/hydration-entries.mjs.map +1 -1
  127. package/esm/vite/index.d.mts +8 -5
  128. package/esm/vite/index.mjs +123 -10
  129. package/esm/vite/index.mjs.map +1 -1
  130. package/esm/vite/page-registry-plugin.d.mts +21 -2
  131. package/esm/vite/page-registry-plugin.mjs +47 -13
  132. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  133. package/esm/vite/projection.d.mts +1 -1
  134. package/esm/vite/projection.mjs +37 -14
  135. package/esm/vite/projection.mjs.map +1 -1
  136. package/llms-full.txt +220 -56
  137. package/llms.txt +5 -3
  138. package/package.json +3 -3
  139. package/skills/add-web-to-an-app/SKILL.md +3 -3
  140. package/skills/create-a-page/SKILL.md +117 -10
  141. package/skills/load-page-data/SKILL.md +92 -12
  142. package/skills/navigate-on-the-client/SKILL.md +1 -1
  143. package/skills/serve-styles/SKILL.md +13 -12
  144. package/skills/use-layouts/SKILL.md +14 -8
  145. package/skills/write-the-root/SKILL.md +3 -1
  146. package/esm/server/buffered-response.d.mts +0 -58
  147. package/esm/server/buffered-response.mjs +0 -115
  148. package/esm/server/buffered-response.mjs.map +0 -1
@@ -1,47 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
 
3
3
  //#region ../web/src/server/settle-page-response.ts
4
- /** Root → leaf. The pipeline's one ordering, used by every stage that walks levels. */
5
4
  const LEVEL_ORDER = [
6
5
  "app",
7
6
  "layout",
8
7
  "page"
9
8
  ];
10
- /**
11
- * Settle the surviving buffers root→leaf, per cookie name / header key: a
12
- * leafward level re-writing the same key wins it, everything else merges.
13
- *
14
- * **Headers and status are mirrored onto the live response here; COOKIES ARE
15
- * NOT**, and that asymmetry is the fix for a real defect rather than an
16
- * oversight. `header()` and `setStatusCode()` are keyed SETs, so mirroring here
17
- * and re-applying at the emit is idempotent. `cookie()` APPENDS — Fastify emits
18
- * one `Set-Cookie` per call — so doing both put the same cookie on the wire
19
- * twice, on every page response including the happy path.
20
- *
21
- * The emit is the authoritative application site (`createPageRouteHandler`
22
- * replays `rendered.cookies` through `applyBufferedCookie`), so the cookie loop
23
- * is the one that goes.
24
- */
25
- function commitBuffers(realResponse, ordered, forcedStatusCode) {
26
- const headers = /* @__PURE__ */ new Map();
27
- const cookies = /* @__PURE__ */ new Map();
28
- let statusCode;
29
- for (const { buffer } of ordered) {
30
- for (const header of buffer.headers) headers.set(header.key.toLowerCase(), header);
31
- for (const cookie of buffer.cookies) cookies.set(cookie.name, cookie);
32
- if (buffer.statusCode !== void 0) statusCode = buffer.statusCode;
33
- }
34
- if (forcedStatusCode !== void 0) statusCode = forcedStatusCode;
35
- for (const header of headers.values()) realResponse.header(header.key, header.value);
36
- if (statusCode !== void 0) realResponse.setStatusCode(statusCode);
37
- return {
38
- headers: [...headers.values()],
39
- cookies: [...cookies.values()],
40
- statusCode,
41
- committedLevels: ordered.map(({ level }) => level)
42
- };
43
- }
44
- /** Nearest `ErrorBoundary` at or rootward of the throw; `app` is terminal. */
45
9
  function designateBoundary(throwingLevel, triple) {
46
10
  const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);
47
11
  for (let index = throwingIndex; index >= 0; index--) {
@@ -56,14 +20,6 @@ function designateBoundary(throwingLevel, triple) {
56
20
  boundaryLevel: "app"
57
21
  };
58
22
  }
59
- /**
60
- * The one place a throw enters the bundle.
61
- *
62
- * **Production never lets the raw error reach a client** — the boundary gets a
63
- * surrogate carrying only `digest`, which is all the reference app's
64
- * ErrorBoundary renders. Dev keeps the real thrown value so the stack survives,
65
- * and it is never mutated to attach `digest`: that lives on the RECORD only.
66
- */
67
23
  function buildErrorRecord(thrown, boundary, requestPath) {
68
24
  const digest = randomUUID();
69
25
  console.error("[warlock] page error", digest, ...requestPath ? [requestPath] : [], thrown);
@@ -71,6 +27,7 @@ function buildErrorRecord(thrown, boundary, requestPath) {
71
27
  const surrogate = /* @__PURE__ */ new Error("An unexpected error occurred.");
72
28
  surrogate.digest = digest;
73
29
  return {
30
+ originalError: thrown,
74
31
  error: surrogate,
75
32
  boundary,
76
33
  digest,
@@ -78,13 +35,121 @@ function buildErrorRecord(thrown, boundary, requestPath) {
78
35
  };
79
36
  }
80
37
  return {
38
+ originalError: void 0,
81
39
  error: thrown,
82
40
  boundary,
83
41
  digest,
84
42
  scrubbed: false
85
43
  };
86
44
  }
45
+ const LOADER_SHORT_CIRCUIT = Symbol("warlock.page.loaderShortCircuit");
46
+ function isLoaderShortCircuit(value) {
47
+ return Boolean(value) && typeof value === "object" && LOADER_SHORT_CIRCUIT in value;
48
+ }
49
+ function createLevelBuffer() {
50
+ return {
51
+ headers: [],
52
+ cookies: []
53
+ };
54
+ }
55
+ function createBufferedResponse(buffer) {
56
+ const bufferedResponse = {
57
+ header(key, value) {
58
+ buffer.headers.push({
59
+ key,
60
+ value: String(value)
61
+ });
62
+ return bufferedResponse;
63
+ },
64
+ headers(bag) {
65
+ for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);
66
+ return bufferedResponse;
67
+ },
68
+ cookie(name, value, options) {
69
+ buffer.cookies.push({
70
+ name,
71
+ value,
72
+ options
73
+ });
74
+ return bufferedResponse;
75
+ },
76
+ setStatusCode(statusCode) {
77
+ buffer.statusCode = statusCode;
78
+ return bufferedResponse;
79
+ },
80
+ redirect(url, statusCode = 302) {
81
+ buffer.statusCode = statusCode;
82
+ buffer.headers.push({
83
+ key: "Location",
84
+ value: url
85
+ });
86
+ return {
87
+ [LOADER_SHORT_CIRCUIT]: true,
88
+ kind: "redirect",
89
+ statusCode,
90
+ url,
91
+ body: void 0
92
+ };
93
+ },
94
+ permanentRedirect(url) {
95
+ return bufferedResponse.redirect(url, 301);
96
+ },
97
+ notFound(body) {
98
+ buffer.statusCode = 404;
99
+ return {
100
+ [LOADER_SHORT_CIRCUIT]: true,
101
+ kind: "notFound",
102
+ statusCode: 404,
103
+ url: void 0,
104
+ body
105
+ };
106
+ }
107
+ };
108
+ return bufferedResponse;
109
+ }
110
+ /**
111
+ * Fold every surviving buffer root→leaf into ONE map per key (header key
112
+ * case-insensitively, cookie by name) — leafward wins, insertion position
113
+ * stays where the key FIRST appeared. Applies the folded headers and status
114
+ * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed
115
+ * sets, so this is safe even though `commitBuffers` can run before render
116
+ * changes its mind about the status later). Cookies are NOT applied to the
117
+ * real response here — `cookie()` APPENDS, so mirroring it here and again at
118
+ * the wire emit would duplicate every `Set-Cookie`. The single application
119
+ * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`
120
+ * over `bundle.commit.cookies`).
121
+ */
122
+ function commitBuffers(response, buffers, committedLevels) {
123
+ const headerOrder = [];
124
+ const headerMap = /* @__PURE__ */ new Map();
125
+ const cookieOrder = [];
126
+ const cookieMap = /* @__PURE__ */ new Map();
127
+ let statusCode;
128
+ for (const level of committedLevels) {
129
+ const buffer = buffers[level];
130
+ for (const header of buffer.headers) {
131
+ const key = header.key.toLowerCase();
132
+ if (!headerMap.has(key)) headerOrder.push(key);
133
+ headerMap.set(key, header);
134
+ }
135
+ for (const cookie of buffer.cookies) {
136
+ if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);
137
+ cookieMap.set(cookie.name, cookie);
138
+ }
139
+ if (buffer.statusCode !== void 0) statusCode = buffer.statusCode;
140
+ }
141
+ const headers = headerOrder.map((key) => headerMap.get(key));
142
+ const cookies = cookieOrder.map((name) => cookieMap.get(name));
143
+ for (const header of headers) response.header(header.key, header.value);
144
+ if (statusCode !== void 0) response.setStatusCode(statusCode);
145
+ return {
146
+ committedLevels,
147
+ headers,
148
+ cookies,
149
+ statusCode
150
+ };
151
+ }
87
152
 
88
153
  //#endregion
89
- export { LEVEL_ORDER, buildErrorRecord, commitBuffers, designateBoundary };
154
+ export { LEVEL_ORDER, buildErrorRecord, commitBuffers, createBufferedResponse, createLevelBuffer, designateBoundary, isLoaderShortCircuit };
90
155
  //# sourceMappingURL=settle-page-response.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settle-page-response.mjs","names":[],"sources":["../../../../../../../web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { BufferedCookie, BufferedHeader, ResponseBuffer } from \"./buffered-response\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageResponseCommit,\n PageRouteEntry,\n PipelineResponse,\n} from \"./execute-page-request.types\";\n\n/** Root → leaf. The pipeline's one ordering, used by every stage that walks levels. */\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\n/**\n * Settle the surviving buffers root→leaf, per cookie name / header key: a\n * leafward level re-writing the same key wins it, everything else merges.\n *\n * **Headers and status are mirrored onto the live response here; COOKIES ARE\n * NOT**, and that asymmetry is the fix for a real defect rather than an\n * oversight. `header()` and `setStatusCode()` are keyed SETs, so mirroring here\n * and re-applying at the emit is idempotent. `cookie()` APPENDS — Fastify emits\n * one `Set-Cookie` per call — so doing both put the same cookie on the wire\n * twice, on every page response including the happy path.\n *\n * The emit is the authoritative application site (`createPageRouteHandler`\n * replays `rendered.cookies` through `applyBufferedCookie`), so the cookie loop\n * is the one that goes.\n */\nexport function commitBuffers(\n realResponse: PipelineResponse,\n ordered: readonly { level: PageLevelName; buffer: ResponseBuffer }[],\n forcedStatusCode?: number,\n): PageResponseCommit {\n const headers = new Map<string, BufferedHeader>();\n const cookies = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const { buffer } of ordered) {\n for (const header of buffer.headers) headers.set(header.key.toLowerCase(), header);\n for (const cookie of buffer.cookies) cookies.set(cookie.name, cookie);\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n if (forcedStatusCode !== undefined) statusCode = forcedStatusCode;\n\n for (const header of headers.values()) realResponse.header(header.key, header.value);\n if (statusCode !== undefined) realResponse.setStatusCode(statusCode);\n\n // No `realResponse.cookie(...)` loop — see above. Settled cookies leave\n // through the return value only, and the emit applies them exactly once.\n\n return {\n headers: [...headers.values()],\n cookies: [...cookies.values()],\n statusCode,\n committedLevels: ordered.map(({ level }) => level),\n };\n}\n\n/** Nearest `ErrorBoundary` at or rootward of the throw; `app` is terminal. */\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n // The framework owns a root boundary.\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\n/**\n * The one place a throw enters the bundle.\n *\n * **Production never lets the raw error reach a client** — the boundary gets a\n * surrogate carrying only `digest`, which is all the reference app's\n * ErrorBoundary renders. Dev keeps the real thrown value so the stack survives,\n * and it is never mutated to attach `digest`: that lives on the RECORD only.\n */\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n // The boundary tells the user this was logged, so log it. A digest that\n // appears in no log is worse than no digest.\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { error: surrogate, boundary, digest, scrubbed: true };\n }\n\n return { error: thrown, boundary, digest, scrubbed: false };\n}\n"],"mappings":";;;;AAYA,MAAa,cAAwC;CAAC;CAAO;CAAU;AAAM;;;;;;;;;;;;;;;;AAiB7E,SAAgB,cACd,cACA,SACA,kBACoB;CACpB,MAAM,0BAAU,IAAI,IAA4B;CAChD,MAAM,0BAAU,IAAI,IAA4B;CAChD,IAAI;CAEJ,KAAK,MAAM,EAAE,YAAY,SAAS;EAChC,KAAK,MAAM,UAAU,OAAO,SAAS,QAAQ,IAAI,OAAO,IAAI,YAAY,GAAG,MAAM;EACjF,KAAK,MAAM,UAAU,OAAO,SAAS,QAAQ,IAAI,OAAO,MAAM,MAAM;EACpE,IAAI,OAAO,eAAe,QAAW,aAAa,OAAO;CAC3D;CAEA,IAAI,qBAAqB,QAAW,aAAa;CAEjD,KAAK,MAAM,UAAU,QAAQ,OAAO,GAAG,aAAa,OAAO,OAAO,KAAK,OAAO,KAAK;CACnF,IAAI,eAAe,QAAW,aAAa,cAAc,UAAU;CAKnE,OAAO;EACL,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC;EAC7B,SAAS,CAAC,GAAG,QAAQ,OAAO,CAAC;EAC7B;EACA,iBAAiB,QAAQ,KAAK,EAAE,YAAY,KAAK;CACnD;AACF;;AAGA,SAAgB,kBACd,eACA,QACyB;CACzB,MAAM,gBAAgB,YAAY,QAAQ,aAAa;CAEvD,KAAK,IAAI,QAAQ,eAAe,SAAS,GAAG,SAAS;EACnD,MAAM,QAAQ,YAAY;EAE1B,IAAI,OAAO,MAAM,CAAC,eAChB,OAAO;GAAE;GAAe,eAAe;EAAM;CAEjD;CAGA,OAAO;EAAE;EAAe,eAAe;CAAM;AAC/C;;;;;;;;;AAUA,SAAgB,iBACd,QACA,UACA,aACiB;CACjB,MAAM,SAAS,WAAW;CAI1B,QAAQ,MAAM,wBAAwB,QAAQ,GAAI,cAAc,CAAC,WAAW,IAAI,CAAC,GAAI,MAAM;CAE3F,IAAI,QAAQ,IAAI,aAAa,cAAc;EACzC,MAAM,4BAAY,IAAI,MAAM,+BAA+B;EAE3D,AAAC,UAAyC,SAAS;EAEnD,OAAO;GAAE,OAAO;GAAW;GAAU;GAAQ,UAAU;EAAK;CAC9D;CAEA,OAAO;EAAE,OAAO;EAAQ;EAAU;EAAQ,UAAU;CAAM;AAC5D"}
1
+ {"version":3,"file":"settle-page-response.mjs","names":[],"sources":["../../../../../../../web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Response } from \"@warlock.js/core\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageRouteEntry,\n} from \"./execute-page-request.types\";\n\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { originalError: thrown, error: surrogate, boundary, digest, scrubbed: true };\n }\n\n // `error` already IS the real thrown value here — `originalError` only ever\n // needs to diverge from it on the scrubbed (production) path above. Leaving\n // it `undefined` rather than a redundant second reference to the same object\n // keeps the record's `toEqual` shape honest (undefined properties compare as\n // absent) and readers still get the real error via\n // `record.originalError ?? record.error`.\n return { originalError: undefined, error: thrown, boundary, digest, scrubbed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Stage 6/7 — buffered per-level responses, and the root→leaf commit\n// ---------------------------------------------------------------------------\n\n/** A single committed response header, in application order. */\nexport type BufferedHeader = { key: string; value: string };\n\n/** A single committed response cookie — the shape `applyBufferedCookie` replays. */\nexport type BufferedCookie = {\n name: string;\n value: unknown;\n options?: Record<string, unknown>;\n};\n\n/** The two loader short-circuit kinds a buffered response can signal. */\nexport type LoaderShortCircuitKind = \"redirect\" | \"notFound\";\n\nconst LOADER_SHORT_CIRCUIT = Symbol(\"warlock.page.loaderShortCircuit\");\n\n/**\n * What `response.redirect()` / `response.permanentRedirect()` / `response.notFound()`\n * return from inside a loader — a branded value the stage 7 settle scan\n * recognises by symbol, never by shape (so an app returning an\n * accidentally-similar plain object can't be mistaken for one).\n */\nexport type LoaderShortCircuitSignal = {\n readonly [LOADER_SHORT_CIRCUIT]: true;\n kind: LoaderShortCircuitKind;\n statusCode: number;\n url?: string;\n body?: unknown;\n};\n\nexport function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal {\n return Boolean(value) && typeof value === \"object\" && LOADER_SHORT_CIRCUIT in (value as object);\n}\n\n/** One level's scratch buffer — what `response.header()`/`.cookie()` write into. */\nexport type LevelBuffer = {\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\nexport function createLevelBuffer(): LevelBuffer {\n return { headers: [], cookies: [] };\n}\n\n/**\n * The response surface a LOADER sees — never the live core `Response`.\n * `header()`/`cookie()` queue into the level's own buffer; nothing here\n * touches the real reply. `redirect()`/`permanentRedirect()`/`notFound()`\n * queue the buffer's own status (+ `Location`, for the two redirects) AND\n * return the branded signal stage 7 detects — the loader is expected to\n * `return response.redirect(...)`.\n */\nexport type BufferedResponse = {\n header(key: string, value: unknown): BufferedResponse;\n headers(bag: Record<string, unknown>): BufferedResponse;\n cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedResponse;\n setStatusCode(statusCode: number): BufferedResponse;\n redirect(url: string, statusCode?: number): LoaderShortCircuitSignal;\n permanentRedirect(url: string): LoaderShortCircuitSignal;\n notFound(body?: unknown): LoaderShortCircuitSignal;\n};\n\nexport function createBufferedResponse(buffer: LevelBuffer): BufferedResponse {\n const bufferedResponse: BufferedResponse = {\n header(key, value) {\n buffer.headers.push({ key, value: String(value) });\n return bufferedResponse;\n },\n headers(bag) {\n for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);\n return bufferedResponse;\n },\n cookie(name, value, options) {\n buffer.cookies.push({ name, value, options });\n return bufferedResponse;\n },\n setStatusCode(statusCode) {\n buffer.statusCode = statusCode;\n return bufferedResponse;\n },\n redirect(url, statusCode = 302) {\n buffer.statusCode = statusCode;\n buffer.headers.push({ key: \"Location\", value: url });\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"redirect\", statusCode, url, body: undefined };\n },\n permanentRedirect(url) {\n return bufferedResponse.redirect(url, 301);\n },\n notFound(body) {\n buffer.statusCode = 404;\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"notFound\", statusCode: 404, url: undefined, body };\n },\n };\n\n return bufferedResponse;\n}\n\n/** Stage 7's folded, applied result — what `bundle.commit` carries. */\nexport type PageResponseCommit = {\n committedLevels: PageLevelName[];\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\n/**\n * Fold every surviving buffer root→leaf into ONE map per key (header key\n * case-insensitively, cookie by name) — leafward wins, insertion position\n * stays where the key FIRST appeared. Applies the folded headers and status\n * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed\n * sets, so this is safe even though `commitBuffers` can run before render\n * changes its mind about the status later). Cookies are NOT applied to the\n * real response here — `cookie()` APPENDS, so mirroring it here and again at\n * the wire emit would duplicate every `Set-Cookie`. The single application\n * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`\n * over `bundle.commit.cookies`).\n */\nexport function commitBuffers(\n response: Response,\n buffers: Record<PageLevelName, LevelBuffer>,\n committedLevels: PageLevelName[],\n): PageResponseCommit {\n const headerOrder: string[] = [];\n const headerMap = new Map<string, BufferedHeader>();\n const cookieOrder: string[] = [];\n const cookieMap = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const level of committedLevels) {\n const buffer = buffers[level];\n\n for (const header of buffer.headers) {\n const key = header.key.toLowerCase();\n if (!headerMap.has(key)) headerOrder.push(key);\n headerMap.set(key, header);\n }\n\n for (const cookie of buffer.cookies) {\n if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);\n cookieMap.set(cookie.name, cookie);\n }\n\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n const headers = headerOrder.map(key => headerMap.get(key)!);\n const cookies = cookieOrder.map(name => cookieMap.get(name)!);\n\n for (const header of headers) response.header(header.key, header.value);\n if (statusCode !== undefined) response.setStatusCode(statusCode);\n\n return { committedLevels, headers, cookies, statusCode };\n}\n"],"mappings":"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"}
@@ -1,24 +1,4 @@
1
1
  //#region ../web/src/server/stylesheet-urls.d.ts
2
- /**
3
- * The stylesheets the ROOT document imports, as dev URLs.
4
- *
5
- * Dev has no manifest, so the source is the root file itself: whatever
6
- * `root.tsx` imports with a stylesheet extension is what the document needs.
7
- * That is deliberately narrow — it answers "what CSS does this application
8
- * set up globally", which is where `app.css` lives and where Tailwind is
9
- * wired, and it does NOT try to reproduce Vite's per-route CSS graph.
10
- *
11
- * The narrowness is the honest part: production splits CSS per chunk, dev
12
- * links the root's stylesheets on every page. A page whose own module imports
13
- * its own stylesheet still gets it in dev — Vite's client graph injects it as
14
- * before — it simply is not render-blocking the way the root's is. That is a
15
- * smaller gap than the flash this removes, and it is stated rather than
16
- * hidden.
17
- *
18
- * Specifiers are resolved against the root file and expressed relative to the
19
- * app root, because that is the shape Vite's dev server serves from.
20
- */
21
- declare function devStylesheetUrls(appRoot: string, appFile: string): string[];
22
2
  /**
23
3
  * A stylesheet Vite serves in DEV must be requested with `?direct`.
24
4
  *
@@ -30,23 +10,54 @@ declare function devStylesheetUrls(appRoot: string, appFile: string): string[];
30
10
  */
31
11
  declare const VITE_DIRECT_CSS_QUERY = "?direct";
32
12
  /**
33
- * Every stylesheet the client build emitted, as URLs the asset route serves.
13
+ * ONE source file's own directly imported stylesheets, as dev URLs.
14
+ *
15
+ * Dev has no manifest, so the source is the file itself: whatever it imports
16
+ * with a stylesheet extension is what it needs. This is deliberately narrow —
17
+ * it answers "what CSS does THIS file set up", not "what CSS does the whole
18
+ * module graph this file eventually reaches end up pulling in" — because the
19
+ * latter would require reproducing Vite's module graph, which is precisely
20
+ * what dev has no manifest to shortcut.
21
+ *
22
+ * The narrowness is the honest part: production splits CSS per chunk and can
23
+ * follow imported chunks (`productionStylesheetUrls` below); dev can only read
24
+ * the ONE file handed to it. A page whose own module imports its own
25
+ * stylesheet still gets it in dev — Vite's client graph injects it as before —
26
+ * it simply is not render-blocking the way a chain member's own import is.
27
+ *
28
+ * Called once per chain member — root, then every matched layout outer to
29
+ * inner, then the page — by the installers below, which is what turns "one
30
+ * file's own imports" into a handler's whole CSS chain.
31
+ *
32
+ * Specifiers are resolved against `sourceFile` and expressed relative to
33
+ * `appRoot`, because that is the shape Vite's dev server serves from.
34
+ */
35
+ declare function devStylesheetUrls(appRoot: string, sourceFile: string): string[];
36
+ /**
37
+ * ONE handler's whole production CSS chain.
34
38
  *
35
- * Vite records CSS against the CHUNK that imported it — an app whose
36
- * `root.tsx` imports `app.css` produces a `root.tsx` entry carrying
37
- * `css: ["assets/root-<hash>.css"]`, not a hydration entry carrying it. So
38
- * this collects across every entry rather than looking under one name, which
39
- * would silently find nothing the moment a stylesheet moved file.
39
+ * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an
40
+ * app-root-relative POSIX source id the SAME id
41
+ * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on
42
+ * every manifest entry, because that identity is what lets this function match
43
+ * EXPLICITLY rather than guess: every id is looked up on its own
44
+ * (`findManifestKey`), its own chunk's CSS is collected
45
+ * (`collectManifestCss`), and an id with no matching entry contributes
46
+ * nothing — it is never treated as license to fall back to scanning the whole
47
+ * manifest, which is what let an unrelated page's CSS leak onto this handler
48
+ * before.
40
49
  *
41
- * Duplicates are collapsed and order is preserved: two chunks importing the
42
- * same stylesheet must not emit two `<link>` tags.
50
+ * Duplicates are collapsed and order is preserved across the WHOLE chain, in
51
+ * the order `sourceFiles` was given root's own CSS first, then each
52
+ * layout's outer to inner, then the page's — so cascade order matches the
53
+ * chain's own outer-to-inner rendering order.
43
54
  *
44
55
  * A missing or malformed manifest returns NOTHING rather than throwing. The
45
56
  * hydration resolver already fails loudly on exactly those conditions, from
46
57
  * exactly the same file, and it runs first — a second, worse error for the
47
58
  * same cause helps nobody.
48
59
  */
49
- declare function productionStylesheetUrls(clientDir: string): string[];
60
+ declare function productionStylesheetUrls(clientDir: string, sourceFiles: readonly string[]): string[];
50
61
  //#endregion
51
62
  export { VITE_DIRECT_CSS_QUERY, devStylesheetUrls, productionStylesheetUrls };
52
63
  //# sourceMappingURL=stylesheet-urls.d.mts.map
@@ -1,10 +1,10 @@
1
1
  import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
2
- import path from "node:path";
3
2
  import { readFileSync } from "node:fs";
3
+ import path from "node:path";
4
4
 
5
5
  //#region ../web/src/server/stylesheet-urls.ts
6
6
  /**
7
- * Which stylesheets a document must link, in each of the two modes.
7
+ * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.
8
8
  *
9
9
  * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered
10
10
  * document. A stylesheet reached the browser only because the CLIENT bundle
@@ -13,13 +13,22 @@ import { readFileSync } from "node:fs";
13
13
  * moment later. The markup was correct the whole time, which is precisely why
14
14
  * it was easy to miss.
15
15
  *
16
+ * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside
17
+ * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple
18
+ * (widened to a chain) `create-page-route-handler.ts` loads per request — so a
19
+ * handler's CSS is the ordered, deduped union of what those specific source
20
+ * files pull in, and nothing else. Collecting across the WHOLE application
21
+ * (every page's manifest entry, every root import) is the bug this shape
22
+ * exists to avoid: it ships page B's stylesheet on page A's response, and it
23
+ * only gets worse as an application grows.
24
+ *
16
25
  * The two modes learn the answer from different places, and neither can use
17
26
  * the other's:
18
27
  *
19
- * - PRODUCTION reads Vite's client manifest, the same artifact the hydration
20
- * entry is already resolved from.
28
+ * - PRODUCTION reads Vite's client manifest, matching each source file's own
29
+ * id and walking its recorded `css` and imported chunks.
21
30
  * - DEV has no manifest — Vite serves modules on demand — so the URLs are
22
- * derived from the source files themselves.
31
+ * derived from each source file's own import statements.
23
32
  */
24
33
  /** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */
25
34
  const STYLE_EXTENSIONS = [
@@ -30,39 +39,53 @@ const STYLE_EXTENSIONS = [
30
39
  ".styl"
31
40
  ];
32
41
  /**
33
- * The stylesheets the ROOT document imports, as dev URLs.
34
- *
35
- * Dev has no manifest, so the source is the root file itself: whatever
36
- * `root.tsx` imports with a stylesheet extension is what the document needs.
37
- * That is deliberately narrow it answers "what CSS does this application
38
- * set up globally", which is where `app.css` lives and where Tailwind is
39
- * wired, and it does NOT try to reproduce Vite's per-route CSS graph.
40
- *
41
- * The narrowness is the honest part: production splits CSS per chunk, dev
42
- * links the root's stylesheets on every page. A page whose own module imports
43
- * its own stylesheet still gets it in dev — Vite's client graph injects it as
44
- * before — it simply is not render-blocking the way the root's is. That is a
45
- * smaller gap than the flash this removes, and it is stated rather than
46
- * hidden.
47
- *
48
- * Specifiers are resolved against the root file and expressed relative to the
49
- * app root, because that is the shape Vite's dev server serves from.
42
+ * A stylesheet Vite serves in DEV must be requested with `?direct`.
43
+ *
44
+ * Without it Vite answers the same URL with `text/javascript` its CSS-as-JS
45
+ * module transform, meant for `import "./app.css"` and a
46
+ * `<link rel="stylesheet">` pointing at a JavaScript response applies
47
+ * NOTHING, silently. No console error, no network failure, just an unstyled
48
+ * page. `?direct` is what makes Vite reply with real `text/css`.
50
49
  */
51
- function devStylesheetUrls(appRoot, appFile) {
50
+ const VITE_DIRECT_CSS_QUERY = "?direct";
51
+ /**
52
+ * ONE source file's own directly imported stylesheets, as dev URLs.
53
+ *
54
+ * Dev has no manifest, so the source is the file itself: whatever it imports
55
+ * with a stylesheet extension is what it needs. This is deliberately narrow —
56
+ * it answers "what CSS does THIS file set up", not "what CSS does the whole
57
+ * module graph this file eventually reaches end up pulling in" — because the
58
+ * latter would require reproducing Vite's module graph, which is precisely
59
+ * what dev has no manifest to shortcut.
60
+ *
61
+ * The narrowness is the honest part: production splits CSS per chunk and can
62
+ * follow imported chunks (`productionStylesheetUrls` below); dev can only read
63
+ * the ONE file handed to it. A page whose own module imports its own
64
+ * stylesheet still gets it in dev — Vite's client graph injects it as before —
65
+ * it simply is not render-blocking the way a chain member's own import is.
66
+ *
67
+ * Called once per chain member — root, then every matched layout outer to
68
+ * inner, then the page — by the installers below, which is what turns "one
69
+ * file's own imports" into a handler's whole CSS chain.
70
+ *
71
+ * Specifiers are resolved against `sourceFile` and expressed relative to
72
+ * `appRoot`, because that is the shape Vite's dev server serves from.
73
+ */
74
+ function devStylesheetUrls(appRoot, sourceFile) {
52
75
  let source;
53
76
  try {
54
- source = readFileSync(appFile, "utf-8");
77
+ source = readFileSync(sourceFile, "utf-8");
55
78
  } catch {
56
79
  return [];
57
80
  }
58
81
  const urls = [];
59
- const pattern = /\bimport\s*["']([^"']+)["']/g;
82
+ const pattern = /\bimport\s*(?:\(\s*)?["']([^"']+)["']/g;
60
83
  let match = pattern.exec(source);
61
84
  while (match !== null) {
62
85
  const specifier = match[1];
63
86
  const lowered = specifier.toLowerCase();
64
87
  if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {
65
- const absolute = path.resolve(path.dirname(appFile), specifier);
88
+ const absolute = path.resolve(path.dirname(sourceFile), specifier);
66
89
  const relative = path.relative(appRoot, absolute).split(path.sep).join("/");
67
90
  if (!relative.startsWith("..")) {
68
91
  const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;
@@ -74,33 +97,93 @@ function devStylesheetUrls(appRoot, appFile) {
74
97
  return urls;
75
98
  }
76
99
  /**
77
- * A stylesheet Vite serves in DEV must be requested with `?direct`.
100
+ * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct
101
+ * stylesheet imports (`devStylesheetUrls`), in the order given — the caller
102
+ * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and
103
+ * deduped across the WHOLE chain, not just within one file.
78
104
  *
79
- * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS
80
- * module transform, meant for `import "./app.css"` and a
81
- * `<link rel="stylesheet">` pointing at a JavaScript response applies
82
- * NOTHING, silently. No console error, no network failure, just an unstyled
83
- * page. `?direct` is what makes Vite reply with real `text/css`.
105
+ * Cross-file dedup matters as much as within-file dedup: an application-wide
106
+ * `app.css` imported by both the root and a page must still produce one
107
+ * `<link>`, not two.
84
108
  */
85
- const VITE_DIRECT_CSS_QUERY = "?direct";
109
+ function devHandlerStylesheetUrls(appRoot, sourceFiles) {
110
+ const urls = [];
111
+ for (const sourceFile of sourceFiles) for (const url of devStylesheetUrls(appRoot, sourceFile)) if (!urls.includes(url)) urls.push(url);
112
+ return urls;
113
+ }
86
114
  /**
87
- * Every stylesheet the client build emitted, as URLs the asset route serves.
115
+ * Find the manifest key for an app-root-relative POSIX source id.
88
116
  *
89
- * Vite records CSS against the CHUNK that imported it an app whose
90
- * `root.tsx` imports `app.css` produces a `root.tsx` entry carrying
91
- * `css: ["assets/root-<hash>.css"]`, not a hydration entry carrying it. So
92
- * this collects across every entry rather than looking under one name, which
93
- * would silently find nothing the moment a stylesheet moved file.
117
+ * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's
118
+ * `appRoot` the client build's `root` is the framework's own package
119
+ * (`build-client.ts`), so a key for an app source file carries a `../`-laden
120
+ * prefix (`"../my-app/src/web/root.tsx"`) rather than matching `sourceFile`
121
+ * (`"src/web/root.tsx"`) byte for byte. The two forms always share the same
122
+ * TAIL, though — both are anchored at the same file — so an exact match is
123
+ * tried first (the case where the client build's root IS the app root, which
124
+ * every fixture and every test below uses) and a `/`-boundary suffix match
125
+ * second, rather than trying to reconstruct the build's own root here, which
126
+ * this runtime read has no way to independently confirm.
127
+ */
128
+ function findManifestKey(manifest, sourceFile) {
129
+ if (manifest[sourceFile] !== void 0) return sourceFile;
130
+ const suffix = `/${sourceFile}`;
131
+ for (const key of Object.keys(manifest)) if (key.endsWith(suffix)) return key;
132
+ }
133
+ /**
134
+ * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,
135
+ * plus the same walk repeated over every chunk it STATICALLY `imports`.
136
+ *
137
+ * `imports` only, never `dynamicImports`. Vite's manifest records
138
+ * `dynamicImports` on shared entry points (the hydration entry names every
139
+ * page as one) precisely because the browser must NOT download them eagerly —
140
+ * walking that array here would pull every other page's CSS onto this one,
141
+ * which is the exact "unrelated pages" leak this module exists to end.
142
+ * `imports`, by contrast, are chunks THIS module synchronously depends on:
143
+ * code Vite split out of it but that loads whenever it does, so their CSS is
144
+ * this handler's CSS too.
94
145
  *
95
- * Duplicates are collapsed and order is preserved: two chunks importing the
96
- * same stylesheet must not emit two `<link>` tags.
146
+ * `visited` guards against a chunk graph cycle; sharing one set across the
147
+ * whole walk from a single entry is enough; a shared chunk revisited from a
148
+ * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately
149
+ * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,
150
+ * and a fresh `visited` set per entry is simpler to reason about than one
151
+ * threaded across unrelated chains.
152
+ */
153
+ function collectManifestCss(manifest, key, visited) {
154
+ if (visited.has(key)) return [];
155
+ visited.add(key);
156
+ const entry = manifest[key];
157
+ if (entry === void 0) return [];
158
+ const ownCss = Array.isArray(entry.css) ? entry.css.filter((file) => typeof file === "string" && file !== "") : [];
159
+ const imports = Array.isArray(entry.imports) ? entry.imports.filter((id) => typeof id === "string") : [];
160
+ return [...ownCss, ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited))];
161
+ }
162
+ /**
163
+ * ONE handler's whole production CSS chain.
164
+ *
165
+ * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an
166
+ * app-root-relative POSIX source id — the SAME id
167
+ * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on
168
+ * every manifest entry, because that identity is what lets this function match
169
+ * EXPLICITLY rather than guess: every id is looked up on its own
170
+ * (`findManifestKey`), its own chunk's CSS is collected
171
+ * (`collectManifestCss`), and an id with no matching entry contributes
172
+ * nothing — it is never treated as license to fall back to scanning the whole
173
+ * manifest, which is what let an unrelated page's CSS leak onto this handler
174
+ * before.
175
+ *
176
+ * Duplicates are collapsed and order is preserved across the WHOLE chain, in
177
+ * the order `sourceFiles` was given — root's own CSS first, then each
178
+ * layout's outer to inner, then the page's — so cascade order matches the
179
+ * chain's own outer-to-inner rendering order.
97
180
  *
98
181
  * A missing or malformed manifest returns NOTHING rather than throwing. The
99
182
  * hydration resolver already fails loudly on exactly those conditions, from
100
183
  * exactly the same file, and it runs first — a second, worse error for the
101
184
  * same cause helps nobody.
102
185
  */
103
- function productionStylesheetUrls(clientDir) {
186
+ function productionStylesheetUrls(clientDir, sourceFiles) {
104
187
  const manifestPath = path.join(clientDir, ".vite", "manifest.json");
105
188
  let manifest;
106
189
  try {
@@ -109,19 +192,21 @@ function productionStylesheetUrls(clientDir) {
109
192
  return [];
110
193
  }
111
194
  if (typeof manifest !== "object" || manifest === null) return [];
195
+ const files = [];
196
+ for (const sourceFile of sourceFiles) {
197
+ const key = findManifestKey(manifest, sourceFile);
198
+ if (key === void 0) continue;
199
+ files.push(...collectManifestCss(manifest, key, /* @__PURE__ */ new Set()));
200
+ }
112
201
  const urls = [];
113
- for (const entry of Object.values(manifest)) {
114
- if (entry === void 0 || !Array.isArray(entry.css)) continue;
115
- for (const file of entry.css) {
116
- if (typeof file !== "string" || file === "") continue;
117
- const url = `/${file}`;
118
- if (!url.startsWith(`${"/assets"}/`)) continue;
119
- if (!urls.includes(url)) urls.push(url);
120
- }
202
+ for (const file of files) {
203
+ const url = `/${file}`;
204
+ if (!url.startsWith(`${"/assets"}/`)) continue;
205
+ if (!urls.includes(url)) urls.push(url);
121
206
  }
122
207
  return urls;
123
208
  }
124
209
 
125
210
  //#endregion
126
- export { VITE_DIRECT_CSS_QUERY, devStylesheetUrls, productionStylesheetUrls };
211
+ export { VITE_DIRECT_CSS_QUERY, devHandlerStylesheetUrls, devStylesheetUrls, productionStylesheetUrls };
127
212
  //# sourceMappingURL=stylesheet-urls.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["../../../../../../../web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a document must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, the same artifact the hydration\n * entry is already resolved from.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from the source files themselves.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * The stylesheets the ROOT document imports, as dev URLs.\n *\n * Dev has no manifest, so the source is the root file itself: whatever\n * `root.tsx` imports with a stylesheet extension is what the document needs.\n * That is deliberately narrow — it answers \"what CSS does this application\n * set up globally\", which is where `app.css` lives and where Tailwind is\n * wired, and it does NOT try to reproduce Vite's per-route CSS graph.\n *\n * The narrowness is the honest part: production splits CSS per chunk, dev\n * links the root's stylesheets on every page. A page whose own module imports\n * its own stylesheet still gets it in dev — Vite's client graph injects it as\n * before — it simply is not render-blocking the way the root's is. That is a\n * smaller gap than the flash this removes, and it is stated rather than\n * hidden.\n *\n * Specifiers are resolved against the root file and expressed relative to the\n * app root, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, appFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(appFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(appFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\ntype ManifestEntry = {\n css?: unknown;\n file?: unknown;\n};\n\n/**\n * Every stylesheet the client build emitted, as URLs the asset route serves.\n *\n * Vite records CSS against the CHUNK that imported it — an app whose\n * `root.tsx` imports `app.css` produces a `root.tsx` entry carrying\n * `css: [\"assets/root-<hash>.css\"]`, not a hydration entry carrying it. So\n * this collects across every entry rather than looking under one name, which\n * would silently find nothing the moment a stylesheet moved file.\n *\n * Duplicates are collapsed and order is preserved: two chunks importing the\n * same stylesheet must not emit two `<link>` tags.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(clientDir: string): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const urls: string[] = [];\n\n for (const entry of Object.values(manifest)) {\n if (entry === undefined || !Array.isArray(entry.css)) continue;\n\n for (const file of entry.css) {\n if (typeof file !== \"string\" || file === \"\") continue;\n\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;;;;;;;;;;;;;AAqBpE,SAAgB,kBAAkB,SAAiB,SAA2B;CAC5E,IAAI;CAEJ,IAAI;EACF,SAAS,aAAa,SAAS,OAAO;CACxC,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,OAAiB,CAAC;CACxB,MAAM,UAAU;CAEhB,IAAI,QAAQ,QAAQ,KAAK,MAAM;CAE/B,OAAO,UAAU,MAAM;EACrB,MAAM,YAAY,MAAM;EACxB,MAAM,UAAU,UAAU,YAAY;EAEtC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAAG;GACrE,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,OAAO,GAAG,SAAS;GAC9D,MAAM,WAAW,KAAK,SAAS,SAAS,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG;GAK1E,IAAI,CAAC,SAAS,WAAW,IAAI,GAAG;IAC9B,MAAM,MAAM,IAAI,WAAW;IAE3B,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;GACxC;EACF;EAEA,QAAQ,QAAQ,KAAK,MAAM;CAC7B;CAEA,OAAO;AACT;;;;;;;;;;AAWA,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;AAwBrC,SAAgB,yBAAyB,WAA6B;CACpE,MAAM,eAAe,KAAK,KAAK,WAAW,SAAS,eAAe;CAElE,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;CAI3D,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO,CAAC;CAE/D,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,SAAS,OAAO,OAAO,QAAQ,GAAG;EAC3C,IAAI,UAAU,UAAa,CAAC,MAAM,QAAQ,MAAM,GAAG,GAAG;EAEtD,KAAK,MAAM,QAAQ,MAAM,KAAK;GAC5B,IAAI,OAAO,SAAS,YAAY,SAAS,IAAI;GAO7C,MAAM,MAAM,IAAI;GAKhB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAAG;GAEpD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;EACxC;CACF;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["../../../../../../../web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside\n * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple\n * (widened to a chain) `create-page-route-handler.ts` loads per request — so a\n * handler's CSS is the ordered, deduped union of what those specific source\n * files pull in, and nothing else. Collecting across the WHOLE application\n * (every page's manifest entry, every root import) is the bug this shape\n * exists to avoid: it ships page B's stylesheet on page A's response, and it\n * only gets worse as an application grows.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, matching each source file's own\n * id and walking its recorded `css` and imported chunks.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from each source file's own import statements.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\n/**\n * ONE source file's own directly imported stylesheets, as dev URLs.\n *\n * Dev has no manifest, so the source is the file itself: whatever it imports\n * with a stylesheet extension is what it needs. This is deliberately narrow —\n * it answers \"what CSS does THIS file set up\", not \"what CSS does the whole\n * module graph this file eventually reaches end up pulling in\" — because the\n * latter would require reproducing Vite's module graph, which is precisely\n * what dev has no manifest to shortcut.\n *\n * The narrowness is the honest part: production splits CSS per chunk and can\n * follow imported chunks (`productionStylesheetUrls` below); dev can only read\n * the ONE file handed to it. A page whose own module imports its own\n * stylesheet still gets it in dev — Vite's client graph injects it as before —\n * it simply is not render-blocking the way a chain member's own import is.\n *\n * Called once per chain member — root, then every matched layout outer to\n * inner, then the page — by the installers below, which is what turns \"one\n * file's own imports\" into a handler's whole CSS chain.\n *\n * Specifiers are resolved against `sourceFile` and expressed relative to\n * `appRoot`, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, sourceFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(sourceFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*(?:\\(\\s*)?[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(sourceFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct\n * stylesheet imports (`devStylesheetUrls`), in the order given — the caller\n * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and\n * deduped across the WHOLE chain, not just within one file.\n *\n * Cross-file dedup matters as much as within-file dedup: an application-wide\n * `app.css` imported by both the root and a page must still produce one\n * `<link>`, not two.\n */\nexport function devHandlerStylesheetUrls(\n appRoot: string,\n sourceFiles: readonly string[],\n): string[] {\n const urls: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n for (const url of devStylesheetUrls(appRoot, sourceFile)) {\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n\ntype ManifestEntry = {\n css?: unknown;\n imports?: unknown;\n};\n\n/**\n * Find the manifest key for an app-root-relative POSIX source id.\n *\n * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's\n * `appRoot` — the client build's `root` is the framework's own package\n * (`build-client.ts`), so a key for an app source file carries a `../`-laden\n * prefix (`\"../my-app/src/web/root.tsx\"`) rather than matching `sourceFile`\n * (`\"src/web/root.tsx\"`) byte for byte. The two forms always share the same\n * TAIL, though — both are anchored at the same file — so an exact match is\n * tried first (the case where the client build's root IS the app root, which\n * every fixture and every test below uses) and a `/`-boundary suffix match\n * second, rather than trying to reconstruct the build's own root here, which\n * this runtime read has no way to independently confirm.\n */\nfunction findManifestKey(\n manifest: Record<string, ManifestEntry | undefined>,\n sourceFile: string,\n): string | undefined {\n if (manifest[sourceFile] !== undefined) return sourceFile;\n\n const suffix = `/${sourceFile}`;\n\n for (const key of Object.keys(manifest)) {\n if (key.endsWith(suffix)) return key;\n }\n\n return undefined;\n}\n\n/**\n * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,\n * plus the same walk repeated over every chunk it STATICALLY `imports`.\n *\n * `imports` only, never `dynamicImports`. Vite's manifest records\n * `dynamicImports` on shared entry points (the hydration entry names every\n * page as one) precisely because the browser must NOT download them eagerly —\n * walking that array here would pull every other page's CSS onto this one,\n * which is the exact \"unrelated pages\" leak this module exists to end.\n * `imports`, by contrast, are chunks THIS module synchronously depends on:\n * code Vite split out of it but that loads whenever it does, so their CSS is\n * this handler's CSS too.\n *\n * `visited` guards against a chunk graph cycle; sharing one set across the\n * whole walk from a single entry is enough; a shared chunk revisited from a\n * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately\n * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,\n * and a fresh `visited` set per entry is simpler to reason about than one\n * threaded across unrelated chains.\n */\nfunction collectManifestCss(\n manifest: Record<string, ManifestEntry | undefined>,\n key: string,\n visited: Set<string>,\n): string[] {\n if (visited.has(key)) return [];\n visited.add(key);\n\n const entry = manifest[key];\n if (entry === undefined) return [];\n\n const ownCss = Array.isArray(entry.css)\n ? entry.css.filter((file): file is string => typeof file === \"string\" && file !== \"\")\n : [];\n\n const imports = Array.isArray(entry.imports)\n ? entry.imports.filter((id): id is string => typeof id === \"string\")\n : [];\n\n return [\n ...ownCss,\n ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited)),\n ];\n}\n\n/**\n * ONE handler's whole production CSS chain.\n *\n * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an\n * app-root-relative POSIX source id — the SAME id\n * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on\n * every manifest entry, because that identity is what lets this function match\n * EXPLICITLY rather than guess: every id is looked up on its own\n * (`findManifestKey`), its own chunk's CSS is collected\n * (`collectManifestCss`), and an id with no matching entry contributes\n * nothing — it is never treated as license to fall back to scanning the whole\n * manifest, which is what let an unrelated page's CSS leak onto this handler\n * before.\n *\n * Duplicates are collapsed and order is preserved across the WHOLE chain, in\n * the order `sourceFiles` was given — root's own CSS first, then each\n * layout's outer to inner, then the page's — so cascade order matches the\n * chain's own outer-to-inner rendering order.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(\n clientDir: string,\n sourceFiles: readonly string[],\n): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const files: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n const key = findManifestKey(manifest, sourceFile);\n if (key === undefined) continue;\n\n files.push(...collectManifestCss(manifest, key, new Set()));\n }\n\n const urls: string[] = [];\n\n for (const file of files) {\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n\n return urls;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;;;AAWpE,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;AAyBrC,SAAgB,kBAAkB,SAAiB,YAA8B;CAC/E,IAAI;CAEJ,IAAI;EACF,SAAS,aAAa,YAAY,OAAO;CAC3C,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,OAAiB,CAAC;CACxB,MAAM,UAAU;CAEhB,IAAI,QAAQ,QAAQ,KAAK,MAAM;CAE/B,OAAO,UAAU,MAAM;EACrB,MAAM,YAAY,MAAM;EACxB,MAAM,UAAU,UAAU,YAAY;EAEtC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAAG;GACrE,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,UAAU,GAAG,SAAS;GACjE,MAAM,WAAW,KAAK,SAAS,SAAS,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;GAK1E,IAAI,CAAC,SAAS,WAAW,IAAI,GAAG;IAC9B,MAAM,MAAM,IAAI,WAAW;IAE3B,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;GACxC;EACF;EAEA,QAAQ,QAAQ,KAAK,MAAM;CAC7B;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,yBACd,SACA,aACU;CACV,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,cAAc,aACvB,KAAK,MAAM,OAAO,kBAAkB,SAAS,UAAU,GACrD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CAI1C,OAAO;AACT;;;;;;;;;;;;;;;AAqBA,SAAS,gBACP,UACA,YACoB;CACpB,IAAI,SAAS,gBAAgB,QAAW,OAAO;CAE/C,MAAM,SAAS,IAAI;CAEnB,KAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,GACpC,IAAI,IAAI,SAAS,MAAM,GAAG,OAAO;AAIrC;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,mBACP,UACA,KACA,SACU;CACV,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,CAAC;CAC9B,QAAQ,IAAI,GAAG;CAEf,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,QAAW,OAAO,CAAC;CAEjC,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,IAClC,MAAM,IAAI,QAAQ,SAAyB,OAAO,SAAS,YAAY,SAAS,EAAE,IAClF,CAAC;CAEL,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IACvC,MAAM,QAAQ,QAAQ,OAAqB,OAAO,OAAO,QAAQ,IACjE,CAAC;CAEL,OAAO,CACL,GAAG,QACH,GAAG,QAAQ,SAAS,gBAAgB,mBAAmB,UAAU,aAAa,OAAO,CAAC,CACxF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,yBACd,WACA,aACU;CACV,MAAM,eAAe,KAAK,KAAK,WAAW,SAAS,eAAe;CAElE,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;CAI3D,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO,CAAC;CAE/D,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,MAAM,gBAAgB,UAAU,UAAU;EAChD,IAAI,QAAQ,QAAW;EAEvB,MAAM,KAAK,GAAG,mBAAmB,UAAU,qBAAK,IAAI,IAAI,CAAC,CAAC;CAC5D;CAEA,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,QAAQ,OAAO;EAMxB,MAAM,MAAM,IAAI;EAKhB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAAG;EAEpD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CACxC;CAEA,OAAO;AACT"}