@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.
- package/CHANGELOG.md +182 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +10 -13
- package/esm/build/contribution.mjs +45 -57
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +104 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +46 -12
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +54 -12
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +39 -8
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.mjs +2 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +12 -0
- package/esm/client/runtime/matcher.mjs +12 -0
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +14 -1
- package/esm/components/document-context.d.mts +39 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.mjs +3 -1
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +5 -101
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +36 -7
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/loaders.d.mts +6 -35
- package/esm/props.d.mts +15 -7
- package/esm/route.d.mts +1 -16
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +1 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/create-page-module-loader.mjs +1 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +30 -5
- package/esm/server/create-page-route-handler.mjs +127 -42
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +1 -12
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +6 -3
- package/esm/server/execute-page-request.mjs +78 -79
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +32 -91
- package/esm/server/hydration-client-url.mjs +1 -1
- package/esm/server/index.d.mts +5 -5
- package/esm/server/index.mjs +3 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +16 -6
- package/esm/server/install-page-routes-from-manifest.mjs +70 -9
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +31 -14
- package/esm/server/install-page-routes.mjs +90 -38
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +2 -35
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.mjs +1 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +2 -8
- package/esm/server/page-context.mjs +2 -15
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +24 -9
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +10 -13
- package/esm/server/render-page.mjs +153 -27
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +110 -45
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +40 -29
- package/esm/server/stylesheet-urls.mjs +136 -51
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +1 -2
- package/esm/server/web-connector-factory.mjs +6 -5
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.mjs +137 -17
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +17 -2
- package/esm/shared.mjs +2 -6
- package/esm/shared.mjs.map +1 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.mjs +66 -12
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.mjs +34 -16
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.mjs +5 -5
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +8 -5
- package/esm/vite/index.mjs +123 -10
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +21 -2
- package/esm/vite/page-registry-plugin.mjs +47 -13
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +1 -1
- package/esm/vite/projection.mjs +37 -14
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +220 -56
- package/llms.txt +5 -3
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +117 -10
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +1 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +14 -8
- package/skills/write-the-root/SKILL.md +3 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- 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
|
-
*
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
42
|
-
*
|
|
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
|
|
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,
|
|
20
|
-
*
|
|
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
|
|
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
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
*
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* `<link
|
|
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
|
-
|
|
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
|
-
*
|
|
115
|
+
* Find the manifest key for an app-root-relative POSIX source id.
|
|
88
116
|
*
|
|
89
|
-
*
|
|
90
|
-
* `
|
|
91
|
-
* `
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
-
*
|
|
96
|
-
*
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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":"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"}
|