@warlock.js/web 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/esm/build/contribution.d.mts +55 -0
- package/esm/build/contribution.mjs +132 -0
- package/esm/build/contribution.mjs.map +1 -0
- package/esm/build/discover-pages.mjs +294 -0
- package/esm/build/discover-pages.mjs.map +1 -0
- package/esm/build/generate-client-registry.mjs +123 -0
- package/esm/build/generate-client-registry.mjs.map +1 -0
- package/esm/build/generate-pages-barrel.mjs +330 -0
- package/esm/build/generate-pages-barrel.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +212 -0
- package/esm/build/read-route-exports.mjs.map +1 -0
- package/esm/client/build-hydrated-tree.mjs +113 -0
- package/esm/client/build-hydrated-tree.mjs.map +1 -0
- package/esm/client/hydrate-page.mjs +73 -0
- package/esm/client/hydrate-page.mjs.map +1 -0
- package/esm/client/navigation/current-route.d.mts +91 -0
- package/esm/client/navigation/current-route.mjs +66 -0
- package/esm/client/navigation/current-route.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +97 -0
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
- package/esm/client/navigation/navigation-root.mjs +196 -0
- package/esm/client/navigation/navigation-root.mjs.map +1 -0
- package/esm/client/navigation/prefetch.mjs +110 -0
- package/esm/client/navigation/prefetch.mjs.map +1 -0
- package/esm/client/navigation/refresh.d.mts +40 -0
- package/esm/client/navigation/refresh.mjs +146 -0
- package/esm/client/navigation/refresh.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +55 -0
- package/esm/client/navigation/verbs.mjs +66 -0
- package/esm/client/navigation/verbs.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +4 -0
- package/esm/client/runtime/index.mjs +4 -0
- package/esm/client/runtime/manifest.d.mts +8 -0
- package/esm/client/runtime/manifest.mjs +77 -0
- package/esm/client/runtime/manifest.mjs.map +1 -0
- package/esm/client/runtime/matcher.d.mts +7 -0
- package/esm/client/runtime/matcher.mjs +169 -0
- package/esm/client/runtime/matcher.mjs.map +1 -0
- package/esm/client/runtime/types.d.mts +22 -0
- package/esm/components/default-app.mjs +32 -0
- package/esm/components/default-app.mjs.map +1 -0
- package/esm/components/document-context.d.mts +1 -0
- package/esm/components/document-context.mjs +24 -0
- package/esm/components/document-context.mjs.map +1 -0
- package/esm/components/head.d.mts +14 -0
- package/esm/components/head.mjs +63 -0
- package/esm/components/head.mjs.map +1 -0
- package/esm/components/link.d.mts +157 -0
- package/esm/components/link.mjs +194 -0
- package/esm/components/link.mjs.map +1 -0
- package/esm/components/scripts.d.mts +14 -0
- package/esm/components/scripts.mjs +21 -0
- package/esm/components/scripts.mjs.map +1 -0
- package/esm/connector/index.d.mts +6 -0
- package/esm/connector/index.mjs +7 -0
- package/esm/context.d.mts +107 -0
- package/esm/hydration/index.d.mts +1 -0
- package/esm/hydration/index.mjs +33 -0
- package/esm/hydration/index.mjs.map +1 -0
- package/esm/hydration-payload.d.mts +1 -0
- package/esm/hydration-payload.mjs +72 -0
- package/esm/hydration-payload.mjs.map +1 -0
- package/esm/index.d.mts +35 -0
- package/esm/index.mjs +12 -0
- package/esm/loaders.d.mts +47 -0
- package/esm/metadata.d.mts +54 -0
- package/esm/props.d.mts +42 -0
- package/esm/route.d.mts +30 -0
- package/esm/routing/compose-route-path.mjs +33 -0
- package/esm/routing/compose-route-path.mjs.map +1 -0
- package/esm/routing/data-request.mjs +52 -0
- package/esm/routing/data-request.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +78 -0
- package/esm/routing/layout-policy.mjs.map +1 -0
- package/esm/routing/navigator.mjs +21 -0
- package/esm/routing/navigator.mjs.map +1 -0
- package/esm/routing/query-string.d.mts +250 -0
- package/esm/routing/query-string.mjs +340 -0
- package/esm/routing/query-string.mjs.map +1 -0
- package/esm/routing/route-identity.mjs +68 -0
- package/esm/routing/route-identity.mjs.map +1 -0
- package/esm/routing/route-table.d.mts +34 -0
- package/esm/routing/route-table.mjs +182 -0
- package/esm/routing/route-table.mjs.map +1 -0
- package/esm/routing/router-events.d.mts +159 -0
- package/esm/routing/router-events.mjs +83 -0
- package/esm/routing/router-events.mjs.map +1 -0
- package/esm/server/buffered-response.mjs +115 -0
- package/esm/server/buffered-response.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +27 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -0
- package/esm/server/client-asset-url-prefix.d.mts +22 -0
- package/esm/server/client-asset-url-prefix.mjs +23 -0
- package/esm/server/client-asset-url-prefix.mjs.map +1 -0
- package/esm/server/create-page-module-loader.mjs +49 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -0
- package/esm/server/create-page-route-handler.mjs +107 -0
- package/esm/server/create-page-route-handler.mjs.map +1 -0
- package/esm/server/dev-server.mjs +167 -0
- package/esm/server/dev-server.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +1 -0
- package/esm/server/execute-page-request.mjs +166 -0
- package/esm/server/execute-page-request.mjs.map +1 -0
- package/esm/server/execute-page-request.types.d.mts +1 -0
- package/esm/server/hydration-client-url.d.mts +37 -0
- package/esm/server/hydration-client-url.mjs +101 -0
- package/esm/server/hydration-client-url.mjs.map +1 -0
- package/esm/server/index.mjs +13 -0
- package/esm/server/install-page-routes-from-manifest.mjs +133 -0
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
- package/esm/server/install-page-routes.d.mts +1 -0
- package/esm/server/install-page-routes.mjs +6 -0
- package/esm/server/install-production-page-routes.mjs +55 -0
- package/esm/server/install-production-page-routes.mjs.map +1 -0
- package/esm/server/match-page-route.mjs +48 -0
- package/esm/server/match-page-route.mjs.map +1 -0
- package/esm/server/page-context.d.mts +1 -0
- package/esm/server/page-context.mjs +33 -0
- package/esm/server/page-context.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +102 -0
- package/esm/server/page-manifest.mjs +31 -0
- package/esm/server/page-manifest.mjs.map +1 -0
- package/esm/server/render-page.mjs +214 -0
- package/esm/server/render-page.mjs.map +1 -0
- package/esm/server/resolve-page-metadata.mjs +33 -0
- package/esm/server/resolve-page-metadata.mjs.map +1 -0
- package/esm/server/resolve-validation-data.mjs +27 -0
- package/esm/server/resolve-validation-data.mjs.map +1 -0
- package/esm/server/settle-page-response.d.mts +1 -0
- package/esm/server/settle-page-response.mjs +90 -0
- package/esm/server/settle-page-response.mjs.map +1 -0
- package/esm/server/stylesheet-urls.mjs +65 -0
- package/esm/server/stylesheet-urls.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +25 -0
- package/esm/server/web-connector-factory.mjs +121 -0
- package/esm/server/web-connector-factory.mjs.map +1 -0
- package/esm/server/web-connector.d.mts +34 -0
- package/esm/server/web-connector.mjs +516 -0
- package/esm/server/web-connector.mjs.map +1 -0
- package/esm/shared.d.mts +22 -0
- package/esm/shared.mjs +223 -0
- package/esm/shared.mjs.map +1 -0
- package/esm/validation.d.mts +31 -0
- package/esm/vite/app-convention-aliases.mjs +43 -0
- package/esm/vite/app-convention-aliases.mjs.map +1 -0
- package/esm/vite/build-client.d.mts +38 -0
- package/esm/vite/build-client.mjs +72 -0
- package/esm/vite/build-client.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +57 -0
- package/esm/vite/gate-a-resolve.mjs +834 -0
- package/esm/vite/gate-a-resolve.mjs.map +1 -0
- package/esm/vite/gate-b-secrets.d.mts +57 -0
- package/esm/vite/gate-b-secrets.mjs +299 -0
- package/esm/vite/gate-b-secrets.mjs.map +1 -0
- package/esm/vite/gate-c-verify.d.mts +118 -0
- package/esm/vite/gate-c-verify.mjs +263 -0
- package/esm/vite/gate-c-verify.mjs.map +1 -0
- package/esm/vite/hydration-entries.d.mts +17 -0
- package/esm/vite/hydration-entries.mjs +45 -0
- package/esm/vite/hydration-entries.mjs.map +1 -0
- package/esm/vite/index.d.mts +126 -0
- package/esm/vite/index.mjs +146 -0
- package/esm/vite/index.mjs.map +1 -0
- package/esm/vite/page-registry-plugin.d.mts +43 -0
- package/esm/vite/page-registry-plugin.mjs +138 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -0
- package/esm/vite/projection.d.mts +31 -0
- package/esm/vite/projection.mjs +384 -0
- package/esm/vite/projection.mjs.map +1 -0
- package/llms-full.txt +1041 -0
- package/llms.txt +21 -0
- package/package.json +77 -0
- package/skills/add-web-to-an-app/SKILL.md +103 -0
- package/skills/create-a-page/SKILL.md +141 -0
- package/skills/load-page-data/SKILL.md +183 -0
- package/skills/navigate-on-the-client/SKILL.md +182 -0
- package/skills/serve-styles/SKILL.md +145 -0
- package/skills/use-layouts/SKILL.md +121 -0
- package/skills/write-the-root/SKILL.md +131 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { enterSharedScope, sealShared, shared } from "../shared.mjs";
|
|
2
|
+
import { createBufferedResponse, isLoaderShortCircuit } from "./buffered-response.mjs";
|
|
3
|
+
import { connectPageContext, enterAdditionalSharedScope, requireRunner } from "./page-context.mjs";
|
|
4
|
+
import { matchRoute } from "./match-page-route.mjs";
|
|
5
|
+
import { resolvePageMetadata } from "./resolve-page-metadata.mjs";
|
|
6
|
+
import { resolveValidationData } from "./resolve-validation-data.mjs";
|
|
7
|
+
import { LEVEL_ORDER, buildErrorRecord, commitBuffers, designateBoundary } from "./settle-page-response.mjs";
|
|
8
|
+
import { v } from "@warlock.js/seal";
|
|
9
|
+
|
|
10
|
+
//#region ../web/src/server/execute-page-request.ts
|
|
11
|
+
async function executePageRequest(options) {
|
|
12
|
+
const runner = requireRunner();
|
|
13
|
+
const [pathname, queryString] = options.url.split("?");
|
|
14
|
+
const matched = matchRoute(pathname, options.routes);
|
|
15
|
+
if (!matched) return void 0;
|
|
16
|
+
const query = Object.fromEntries(new URLSearchParams(queryString ?? ""));
|
|
17
|
+
const match = {
|
|
18
|
+
entry: matched.entry,
|
|
19
|
+
params: matched.params,
|
|
20
|
+
query
|
|
21
|
+
};
|
|
22
|
+
const { triple } = matched.entry;
|
|
23
|
+
const { request, response } = options.createHttp(match);
|
|
24
|
+
const store = runner.buildStore ? runner.buildStore({
|
|
25
|
+
request,
|
|
26
|
+
response
|
|
27
|
+
}) : {
|
|
28
|
+
request,
|
|
29
|
+
response
|
|
30
|
+
};
|
|
31
|
+
return await runner.run(store, async () => {
|
|
32
|
+
enterSharedScope(store);
|
|
33
|
+
enterAdditionalSharedScope(store);
|
|
34
|
+
const finish = async (bundle) => options.finish ? await options.finish(bundle) : bundle;
|
|
35
|
+
const bundle = { route: {
|
|
36
|
+
name: matched.entry.name,
|
|
37
|
+
path: matched.entry.path,
|
|
38
|
+
params: match.params,
|
|
39
|
+
query
|
|
40
|
+
} };
|
|
41
|
+
for (const level of LEVEL_ORDER) for (const middleware of triple[level].middleware ?? []) {
|
|
42
|
+
let output;
|
|
43
|
+
try {
|
|
44
|
+
output = await middleware({
|
|
45
|
+
request,
|
|
46
|
+
response
|
|
47
|
+
});
|
|
48
|
+
} catch (thrown) {
|
|
49
|
+
bundle.error = buildErrorRecord(thrown, designateBoundary(level, triple), pathname);
|
|
50
|
+
bundle.commit = commitBuffers(response, [], 500);
|
|
51
|
+
return finish(bundle);
|
|
52
|
+
}
|
|
53
|
+
if (output !== void 0) {
|
|
54
|
+
bundle.shortCircuit = {
|
|
55
|
+
stage: "middleware",
|
|
56
|
+
level,
|
|
57
|
+
value: output,
|
|
58
|
+
statusCode: response.statusCode
|
|
59
|
+
};
|
|
60
|
+
return finish(bundle);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const validation = triple.page.validation;
|
|
64
|
+
if (validation?.schema) {
|
|
65
|
+
const data = resolveValidationData(validation.validating, request);
|
|
66
|
+
const result = await v.validate(validation.schema, data);
|
|
67
|
+
if (result.isValid && result.data) request.setValidatedData?.(result.data);
|
|
68
|
+
if (!result.isValid) {
|
|
69
|
+
bundle.shortCircuit = {
|
|
70
|
+
stage: "validation",
|
|
71
|
+
status: 422,
|
|
72
|
+
errors: result.errors
|
|
73
|
+
};
|
|
74
|
+
return finish(bundle);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const sealedShared = await sealShared(store);
|
|
78
|
+
bundle.shared = sealedShared;
|
|
79
|
+
const buffers = LEVEL_ORDER.map((level) => {
|
|
80
|
+
const { response: bufferedResponse, buffer } = createBufferedResponse();
|
|
81
|
+
return {
|
|
82
|
+
level,
|
|
83
|
+
buffer,
|
|
84
|
+
bufferedResponse
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
const settled = await Promise.allSettled(buffers.map(({ level, bufferedResponse }) => {
|
|
88
|
+
const loader = triple[level].loader;
|
|
89
|
+
return (async () => loader ? await loader({
|
|
90
|
+
request,
|
|
91
|
+
response: bufferedResponse,
|
|
92
|
+
shared
|
|
93
|
+
}) : void 0)();
|
|
94
|
+
}));
|
|
95
|
+
let firstError;
|
|
96
|
+
let firstSignal;
|
|
97
|
+
function toSignal(value) {
|
|
98
|
+
return isLoaderShortCircuit(value) ? value : void 0;
|
|
99
|
+
}
|
|
100
|
+
for (let index = 0; index < LEVEL_ORDER.length; index++) {
|
|
101
|
+
const result = settled[index];
|
|
102
|
+
if (result.status === "rejected") {
|
|
103
|
+
firstError = {
|
|
104
|
+
level: LEVEL_ORDER[index],
|
|
105
|
+
index,
|
|
106
|
+
error: result.reason
|
|
107
|
+
};
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
const signal = toSignal(result.value);
|
|
111
|
+
if (signal) {
|
|
112
|
+
firstSignal = {
|
|
113
|
+
level: LEVEL_ORDER[index],
|
|
114
|
+
index,
|
|
115
|
+
signal
|
|
116
|
+
};
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const dataKeys = {
|
|
121
|
+
app: "appData",
|
|
122
|
+
layout: "layoutData",
|
|
123
|
+
page: "pageData"
|
|
124
|
+
};
|
|
125
|
+
const dataCutoff = firstSignal ? firstSignal.index : LEVEL_ORDER.length;
|
|
126
|
+
for (let index = 0; index < LEVEL_ORDER.length; index++) {
|
|
127
|
+
const result = settled[index];
|
|
128
|
+
if (result.status !== "fulfilled") continue;
|
|
129
|
+
if (firstError && index === firstError.index) continue;
|
|
130
|
+
if (index > dataCutoff) continue;
|
|
131
|
+
if (toSignal(result.value)) continue;
|
|
132
|
+
bundle[dataKeys[LEVEL_ORDER[index]]] = result.value;
|
|
133
|
+
}
|
|
134
|
+
if (firstError) {
|
|
135
|
+
const boundary = designateBoundary(firstError.level, triple);
|
|
136
|
+
bundle.error = buildErrorRecord(firstError.error, boundary, pathname);
|
|
137
|
+
bundle.commit = commitBuffers(response, buffers.slice(0, firstError.index), boundary.boundaryLevel === "app" ? 500 : void 0);
|
|
138
|
+
} else if (firstSignal) {
|
|
139
|
+
bundle.shortCircuit = {
|
|
140
|
+
stage: "loaders",
|
|
141
|
+
level: firstSignal.level,
|
|
142
|
+
kind: firstSignal.signal.kind,
|
|
143
|
+
statusCode: firstSignal.signal.statusCode,
|
|
144
|
+
url: firstSignal.signal.url,
|
|
145
|
+
body: firstSignal.signal.body
|
|
146
|
+
};
|
|
147
|
+
bundle.commit = commitBuffers(response, buffers.slice(0, firstSignal.index + 1));
|
|
148
|
+
} else bundle.commit = commitBuffers(response, buffers);
|
|
149
|
+
if (!bundle.shortCircuit) {
|
|
150
|
+
const resolved = resolvePageMetadata({
|
|
151
|
+
metadata: triple.page.metadata,
|
|
152
|
+
data: bundle.pageData,
|
|
153
|
+
error: bundle.error?.error,
|
|
154
|
+
failed: Boolean(bundle.error),
|
|
155
|
+
shared: sealedShared
|
|
156
|
+
});
|
|
157
|
+
bundle.metadata = resolved.metadata;
|
|
158
|
+
if (resolved.thrown !== void 0) bundle.error = buildErrorRecord(resolved.thrown, designateBoundary("page", triple), bundle.route.path);
|
|
159
|
+
}
|
|
160
|
+
return finish(bundle);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { executePageRequest };
|
|
166
|
+
//# sourceMappingURL=execute-page-request.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute-page-request.mjs","names":[],"sources":["../../../../../../../web/src/server/execute-page-request.ts"],"sourcesContent":["import { v } from \"@warlock.js/seal\";\nimport { createBufferedResponse, isLoaderShortCircuit } from \"./buffered-response\";\nimport type { LoaderShortCircuitSignal } from \"./buffered-response\";\nimport { enterSharedScope, sealShared, shared } from \"../shared\";\nimport { enterAdditionalSharedScope, requireRunner } from \"./page-context\";\nimport { matchRoute } from \"./match-page-route\";\nimport { resolvePageMetadata } from \"./resolve-page-metadata\";\nimport { resolveValidationData } from \"./resolve-validation-data\";\nimport {\n buildErrorRecord,\n commitBuffers,\n designateBoundary,\n LEVEL_ORDER,\n} from \"./settle-page-response\";\nimport type {\n ExecutePageRequestOptions,\n PageDataBundle,\n PageLevelName,\n PageRouteMatch,\n PipelineResponse,\n PipelineStore,\n} from \"./execute-page-request.types\";\n\n/**\n * Pipeline stages 1–8 of a page request; stages 9–10 (render/emit) are the\n * render slice's, and this ends at the DATA BUNDLE.\n *\n * The founding sentence governs the shape — a page route is an ordinary Warlock\n * route whose handler renders React instead of returning JSON — so every stage\n * mirrors what core already does for an API request: core's own ALS store and\n * `run`, core's middleware short-circuit rule (`output !== undefined`), and\n * `validateAll`'s validation with its 422, recorded as a designation because\n * sending is the emit's job.\n *\n * The pieces live beside this file: `page-context.ts` (stage 2 wiring),\n * `match-page-route.ts` (stage 1), `resolve-validation-data.ts` (stage 4),\n * `settle-page-response.ts` (stage 7), `resolve-page-metadata.ts` (stage 8).\n */\n\nexport * from \"./execute-page-request.types\";\nexport { connectPageContext, connectPageSharedScope } from \"./page-context\";\nexport { buildErrorRecord, designateBoundary } from \"./settle-page-response\";\n\nexport async function executePageRequest<TResult = PageDataBundle>(\n options: ExecutePageRequestOptions<TResult>,\n): Promise<TResult | undefined> {\n const runner = requireRunner();\n\n // ── stage 1 · MATCH ─────────────────────────────────────────────────────\n const [pathname, queryString] = options.url.split(\"?\");\n const matched = matchRoute(pathname, options.routes);\n\n if (!matched) return undefined;\n\n const query = Object.fromEntries(new URLSearchParams(queryString ?? \"\"));\n const match: PageRouteMatch = { entry: matched.entry, params: matched.params, query };\n const { triple } = matched.entry;\n\n const { request, response } = options.createHttp(match);\n\n // ── stage 2 · CONTEXT ───────────────────────────────────────────────────\n // Core's own store shape, built by core's own `buildStore` when the runner\n // carries one. `enterSharedScope` keys the shared target by this exact object.\n const store: PipelineStore = runner.buildStore\n ? runner.buildStore({ request, response })\n : { request, response };\n\n return await runner.run(store, async () => {\n enterSharedScope(store);\n enterAdditionalSharedScope(store);\n\n const finish = async (bundle: PageDataBundle): Promise<TResult> =>\n options.finish ? await options.finish(bundle) : (bundle as TResult);\n\n const bundle: PageDataBundle = {\n route: {\n name: matched.entry.name,\n path: matched.entry.path,\n params: match.params,\n query,\n },\n };\n\n // ── stage 3 · MIDDLEWARE ──────────────────────────────────────────────\n // Sequential, outermost first. Middleware gets the REAL request/response —\n // it runs alone, before any parallelism — and is the only writer of `shared`.\n for (const level of LEVEL_ORDER) {\n for (const middleware of triple[level].middleware ?? []) {\n let output: unknown;\n\n try {\n output = await middleware({ request, response });\n } catch (thrown) {\n /*\n A middleware throw means NOTHING was served: no loaders ran, no\n buffers exist. The \"a nested boundary keeps the committed status\"\n rule assumes the page was substantially served, so it cannot apply\n here — the framework forces 500 whichever level the boundary\n designates to.\n */\n bundle.error = buildErrorRecord(thrown, designateBoundary(level, triple), pathname);\n bundle.commit = commitBuffers(response, [], 500);\n\n return finish(bundle);\n }\n\n if (output !== undefined) {\n bundle.shortCircuit = {\n stage: \"middleware\",\n level,\n value: output,\n statusCode: (response as PipelineResponse & { statusCode?: number }).statusCode,\n };\n\n return finish(bundle);\n }\n }\n }\n\n // ── stage 4 · VALIDATION ──────────────────────────────────────────────\n // Failure stops before the seal: a 422 designation in the bundle, since\n // sending is the emit slice's job.\n const validation = triple.page.validation;\n\n if (validation?.schema) {\n const data = resolveValidationData(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.isValid && result.data) {\n request.setValidatedData?.(result.data);\n }\n\n if (!result.isValid) {\n bundle.shortCircuit = { stage: \"validation\", status: 422, errors: result.errors };\n\n return finish(bundle);\n }\n }\n\n // ── stage 5 · SEAL shared ─────────────────────────────────────────────\n // From here `shared` is immutable and identical for every reader. The\n // bundle carries `sealShared`'s RETURN, never a proxy re-read.\n const sealedShared = await sealShared(store);\n\n bundle.shared = sealedShared;\n\n // ── stage 6 · LOADERS ─────────────────────────────────────────────────\n // Parallel, each with its OWN buffered response. Loaders never read each\n // other and cannot write `shared` — it is sealed, and a write throws.\n const buffers = LEVEL_ORDER.map(level => {\n const { response: bufferedResponse, buffer } = createBufferedResponse();\n\n return { level, buffer, bufferedResponse };\n });\n\n const settled = await Promise.allSettled(\n buffers.map(({ level, bufferedResponse }) => {\n const loader = triple[level].loader;\n\n return (async () =>\n loader ? await loader({ request, response: bufferedResponse, shared }) : undefined)();\n }),\n );\n\n // ── stage 7 · SETTLE / COMMIT ─────────────────────────────────────────\n // Walk root→leaf for the first abnormal outcome. A throw discards the\n // throwing layer's buffer and everything below it, and the framework owns\n // the status. A redirect/notFound commits the signalling layer's own buffer\n // and discards its siblings below.\n let firstError: { level: PageLevelName; index: number; error: unknown } | undefined;\n let firstSignal:\n | { level: PageLevelName; index: number; signal: LoaderShortCircuitSignal }\n | undefined;\n\n function toSignal(value: unknown) {\n return isLoaderShortCircuit(value) ? value : undefined;\n }\n\n for (let index = 0; index < LEVEL_ORDER.length; index++) {\n const result = settled[index];\n\n if (result.status === \"rejected\") {\n firstError = { level: LEVEL_ORDER[index], index, error: result.reason };\n break;\n }\n\n const signal = toSignal(result.value);\n\n if (signal) {\n firstSignal = { level: LEVEL_ORDER[index], index, signal };\n break;\n }\n }\n\n const dataKeys: Record<PageLevelName, \"appData\" | \"layoutData\" | \"pageData\"> = {\n app: \"appData\",\n layout: \"layoutData\",\n page: \"pageData\",\n };\n\n // A throw elsewhere never costs a healthy layer its data. Levels leafward\n // of a short-circuit go, because they will not render.\n const dataCutoff = firstSignal ? firstSignal.index : LEVEL_ORDER.length;\n\n for (let index = 0; index < LEVEL_ORDER.length; index++) {\n const result = settled[index];\n\n if (result.status !== \"fulfilled\") continue;\n if (firstError && index === firstError.index) continue;\n if (index > dataCutoff) continue;\n if (toSignal(result.value)) continue;\n\n bundle[dataKeys[LEVEL_ORDER[index]]] = result.value;\n }\n\n if (firstError) {\n const boundary = designateBoundary(firstError.level, triple);\n\n bundle.error = buildErrorRecord(firstError.error, boundary, pathname);\n\n // Commit strictly rootward of the throw. A NESTED boundary keeps the\n // committed status — the page was substantially served; only the ROOT\n // boundary forces 5xx.\n bundle.commit = commitBuffers(\n response,\n buffers.slice(0, firstError.index),\n boundary.boundaryLevel === \"app\" ? 500 : undefined,\n );\n } else if (firstSignal) {\n bundle.shortCircuit = {\n stage: \"loaders\",\n level: firstSignal.level,\n kind: firstSignal.signal.kind,\n statusCode: firstSignal.signal.statusCode,\n url: firstSignal.signal.url,\n body: firstSignal.signal.body,\n };\n bundle.commit = commitBuffers(response, buffers.slice(0, firstSignal.index + 1));\n } else {\n bundle.commit = commitBuffers(response, buffers);\n }\n\n // ── stage 8 · METADATA ────────────────────────────────────────────────\n // Skipped entirely after a loader short-circuit: a redirect/notFound answer\n // has no page render to describe.\n if (!bundle.shortCircuit) {\n const resolved = resolvePageMetadata({\n metadata: triple.page.metadata,\n data: bundle.pageData,\n error: bundle.error?.error,\n failed: Boolean(bundle.error),\n shared: sealedShared,\n });\n\n bundle.metadata = resolved.metadata;\n\n /*\n The page's own metadata function threw with no earlier error to mask.\n Record it exactly as a loader throw, so the boundary renders and the\n framework keeps the status — rather than letting it escape stage 8,\n where nothing is left to catch it and stage 7's buffers are already\n committed.\n */\n if (resolved.thrown !== undefined) {\n bundle.error = buildErrorRecord(\n resolved.thrown,\n designateBoundary(\"page\", triple),\n bundle.route.path,\n );\n }\n }\n\n return finish(bundle);\n });\n}\n"],"mappings":";;;;;;;;;;AA2CA,eAAsB,mBACpB,SAC8B;CAC9B,MAAM,SAAS,cAAc;CAG7B,MAAM,CAAC,UAAU,eAAe,QAAQ,IAAI,MAAM,GAAG;CACrD,MAAM,UAAU,WAAW,UAAU,QAAQ,MAAM;CAEnD,IAAI,CAAC,SAAS,OAAO;CAErB,MAAM,QAAQ,OAAO,YAAY,IAAI,gBAAgB,eAAe,EAAE,CAAC;CACvE,MAAM,QAAwB;EAAE,OAAO,QAAQ;EAAO,QAAQ,QAAQ;EAAQ;CAAM;CACpF,MAAM,EAAE,WAAW,QAAQ;CAE3B,MAAM,EAAE,SAAS,aAAa,QAAQ,WAAW,KAAK;CAKtD,MAAM,QAAuB,OAAO,aAChC,OAAO,WAAW;EAAE;EAAS;CAAS,CAAC,IACvC;EAAE;EAAS;CAAS;CAExB,OAAO,MAAM,OAAO,IAAI,OAAO,YAAY;EACzC,iBAAiB,KAAK;EACtB,2BAA2B,KAAK;EAEhC,MAAM,SAAS,OAAO,WACpB,QAAQ,SAAS,MAAM,QAAQ,OAAO,MAAM,IAAK;EAEnD,MAAM,SAAyB,EAC7B,OAAO;GACL,MAAM,QAAQ,MAAM;GACpB,MAAM,QAAQ,MAAM;GACpB,QAAQ,MAAM;GACd;EACF,EACF;EAKA,KAAK,MAAM,SAAS,aAClB,KAAK,MAAM,cAAc,OAAO,MAAM,CAAC,cAAc,CAAC,GAAG;GACvD,IAAI;GAEJ,IAAI;IACF,SAAS,MAAM,WAAW;KAAE;KAAS;IAAS,CAAC;GACjD,SAAS,QAAQ;IAQf,OAAO,QAAQ,iBAAiB,QAAQ,kBAAkB,OAAO,MAAM,GAAG,QAAQ;IAClF,OAAO,SAAS,cAAc,UAAU,CAAC,GAAG,GAAG;IAE/C,OAAO,OAAO,MAAM;GACtB;GAEA,IAAI,WAAW,QAAW;IACxB,OAAO,eAAe;KACpB,OAAO;KACP;KACA,OAAO;KACP,YAAa,SAAwD;IACvE;IAEA,OAAO,OAAO,MAAM;GACtB;EACF;EAMF,MAAM,aAAa,OAAO,KAAK;EAE/B,IAAI,YAAY,QAAQ;GACtB,MAAM,OAAO,sBAAsB,WAAW,YAAY,OAAO;GACjE,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,WAAW,OAAO,MAC3B,QAAQ,mBAAmB,OAAO,IAAI;GAGxC,IAAI,CAAC,OAAO,SAAS;IACnB,OAAO,eAAe;KAAE,OAAO;KAAc,QAAQ;KAAK,QAAQ,OAAO;IAAO;IAEhF,OAAO,OAAO,MAAM;GACtB;EACF;EAKA,MAAM,eAAe,MAAM,WAAW,KAAK;EAE3C,OAAO,SAAS;EAKhB,MAAM,UAAU,YAAY,KAAI,UAAS;GACvC,MAAM,EAAE,UAAU,kBAAkB,WAAW,uBAAuB;GAEtE,OAAO;IAAE;IAAO;IAAQ;GAAiB;EAC3C,CAAC;EAED,MAAM,UAAU,MAAM,QAAQ,WAC5B,QAAQ,KAAK,EAAE,OAAO,uBAAuB;GAC3C,MAAM,SAAS,OAAO,MAAM,CAAC;GAE7B,QAAQ,YACN,SAAS,MAAM,OAAO;IAAE;IAAS,UAAU;IAAkB;GAAO,CAAC,IAAI,OAAS,CAAE;EACxF,CAAC,CACH;EAOA,IAAI;EACJ,IAAI;EAIJ,SAAS,SAAS,OAAgB;GAChC,OAAO,qBAAqB,KAAK,IAAI,QAAQ;EAC/C;EAEA,KAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,QAAQ,SAAS;GACvD,MAAM,SAAS,QAAQ;GAEvB,IAAI,OAAO,WAAW,YAAY;IAChC,aAAa;KAAE,OAAO,YAAY;KAAQ;KAAO,OAAO,OAAO;IAAO;IACtE;GACF;GAEA,MAAM,SAAS,SAAS,OAAO,KAAK;GAEpC,IAAI,QAAQ;IACV,cAAc;KAAE,OAAO,YAAY;KAAQ;KAAO;IAAO;IACzD;GACF;EACF;EAEA,MAAM,WAAyE;GAC7E,KAAK;GACL,QAAQ;GACR,MAAM;EACR;EAIA,MAAM,aAAa,cAAc,YAAY,QAAQ,YAAY;EAEjE,KAAK,IAAI,QAAQ,GAAG,QAAQ,YAAY,QAAQ,SAAS;GACvD,MAAM,SAAS,QAAQ;GAEvB,IAAI,OAAO,WAAW,aAAa;GACnC,IAAI,cAAc,UAAU,WAAW,OAAO;GAC9C,IAAI,QAAQ,YAAY;GACxB,IAAI,SAAS,OAAO,KAAK,GAAG;GAE5B,OAAO,SAAS,YAAY,WAAW,OAAO;EAChD;EAEA,IAAI,YAAY;GACd,MAAM,WAAW,kBAAkB,WAAW,OAAO,MAAM;GAE3D,OAAO,QAAQ,iBAAiB,WAAW,OAAO,UAAU,QAAQ;GAKpE,OAAO,SAAS,cACd,UACA,QAAQ,MAAM,GAAG,WAAW,KAAK,GACjC,SAAS,kBAAkB,QAAQ,MAAM,MAC3C;EACF,OAAO,IAAI,aAAa;GACtB,OAAO,eAAe;IACpB,OAAO;IACP,OAAO,YAAY;IACnB,MAAM,YAAY,OAAO;IACzB,YAAY,YAAY,OAAO;IAC/B,KAAK,YAAY,OAAO;IACxB,MAAM,YAAY,OAAO;GAC3B;GACA,OAAO,SAAS,cAAc,UAAU,QAAQ,MAAM,GAAG,YAAY,QAAQ,CAAC,CAAC;EACjF,OACE,OAAO,SAAS,cAAc,UAAU,OAAO;EAMjD,IAAI,CAAC,OAAO,cAAc;GACxB,MAAM,WAAW,oBAAoB;IACnC,UAAU,OAAO,KAAK;IACtB,MAAM,OAAO;IACb,OAAO,OAAO,OAAO;IACrB,QAAQ,QAAQ,OAAO,KAAK;IAC5B,QAAQ;GACV,CAAC;GAED,OAAO,WAAW,SAAS;GAS3B,IAAI,SAAS,WAAW,QACtB,OAAO,QAAQ,iBACb,SAAS,QACT,kBAAkB,QAAQ,MAAM,GAChC,OAAO,MAAM,IACf;EAEJ;EAEA,OAAO,OAAO,MAAM;CACtB,CAAC;AACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region ../web/src/server/hydration-client-url.d.ts
|
|
2
|
+
interface ResolveHydrationClientUrlOptions {
|
|
3
|
+
/** Absolute path to the client build output dir: `<outdir>/client`. */
|
|
4
|
+
clientDir: string;
|
|
5
|
+
}
|
|
6
|
+
/** The manifest file is absent — the client build never ran, or ran elsewhere. */
|
|
7
|
+
declare class WebClientManifestMissingError extends Error {
|
|
8
|
+
constructor(manifestPath: string, cause: unknown);
|
|
9
|
+
}
|
|
10
|
+
/** The manifest file exists but is not parsable JSON, or is not a JSON object. */
|
|
11
|
+
declare class WebClientManifestMalformedError extends Error {
|
|
12
|
+
constructor(manifestPath: string, cause: unknown);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The manifest parses but carries no usable `hydration` entry.
|
|
16
|
+
*
|
|
17
|
+
* The entry name is a shared contract between the build (`build-client.ts`
|
|
18
|
+
* rollup input) and this runtime read; drift fails loudly HERE. Nobody adds a
|
|
19
|
+
* default.
|
|
20
|
+
*/
|
|
21
|
+
declare class WebClientManifestEntryMissingError extends Error {
|
|
22
|
+
constructor(manifestPath: string, entryName: string);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The manifest names an entry file that does NOT live under the asset
|
|
26
|
+
* directory `CLIENT_ASSET_URL_PREFIX` is mounted from.
|
|
27
|
+
*
|
|
28
|
+
* Serving it would 404 — the static route only exposes that one directory —
|
|
29
|
+
* so the artifact is rejected at boot instead of at first page view.
|
|
30
|
+
*/
|
|
31
|
+
declare class WebClientAssetPrefixViolationError extends Error {
|
|
32
|
+
constructor(manifestPath: string, file: string);
|
|
33
|
+
}
|
|
34
|
+
declare function resolveHydrationClientUrl(options: ResolveHydrationClientUrlOptions): string;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { ResolveHydrationClientUrlOptions, WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, resolveHydrationClientUrl };
|
|
37
|
+
//# sourceMappingURL=hydration-client-url.d.mts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
|
|
2
|
+
import { HYDRATION_CLIENT_ENTRY_NAME } from "../vite/hydration-entries.mjs";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region ../web/src/server/hydration-client-url.ts
|
|
7
|
+
/**
|
|
8
|
+
* The ONE runtime manifest read in production: one boot-time JSON
|
|
9
|
+
* lookup of the `hydration` entry in `<clientDir>/.vite/manifest.json`,
|
|
10
|
+
* producing the string `install-page-routes.ts:139` already accepts as
|
|
11
|
+
* `hydrationClientModuleUrl`.
|
|
12
|
+
*
|
|
13
|
+
* NEVER FALLS BACK. Each failure is its own named error so the boot log says
|
|
14
|
+
* which half of the build→runtime handoff broke: a missing
|
|
15
|
+
* file means the client build never ran, malformed JSON means the artifact is
|
|
16
|
+
* corrupt, and a missing entry means the build/runtime entry-name contract
|
|
17
|
+
* drifted. "Serve without hydration" is not among the outcomes.
|
|
18
|
+
*/
|
|
19
|
+
/** The manifest file is absent — the client build never ran, or ran elsewhere. */
|
|
20
|
+
var WebClientManifestMissingError = class extends Error {
|
|
21
|
+
constructor(manifestPath, cause) {
|
|
22
|
+
super(`Cannot resolve the hydration client URL: the Vite client manifest "${manifestPath}" is missing or unreadable. This app was started in production without its \`warlock build\` client artifacts.`, { cause });
|
|
23
|
+
this.name = "WebClientManifestMissingError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** The manifest file exists but is not parsable JSON, or is not a JSON object. */
|
|
27
|
+
var WebClientManifestMalformedError = class extends Error {
|
|
28
|
+
constructor(manifestPath, cause) {
|
|
29
|
+
super(`Cannot resolve the hydration client URL: the Vite client manifest "${manifestPath}" is not valid JSON object content.`, { cause });
|
|
30
|
+
this.name = "WebClientManifestMalformedError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The manifest parses but carries no usable `hydration` entry.
|
|
35
|
+
*
|
|
36
|
+
* The entry name is a shared contract between the build (`build-client.ts`
|
|
37
|
+
* rollup input) and this runtime read; drift fails loudly HERE. Nobody adds a
|
|
38
|
+
* default.
|
|
39
|
+
*/
|
|
40
|
+
var WebClientManifestEntryMissingError = class extends Error {
|
|
41
|
+
constructor(manifestPath, entryName) {
|
|
42
|
+
super(`Cannot resolve the hydration client URL: the Vite client manifest "${manifestPath}" has no "${entryName}" entry with a "file" string. The build and runtime hydration entry names have drifted.`);
|
|
43
|
+
this.name = "WebClientManifestEntryMissingError";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The manifest names an entry file that does NOT live under the asset
|
|
48
|
+
* directory `CLIENT_ASSET_URL_PREFIX` is mounted from.
|
|
49
|
+
*
|
|
50
|
+
* Serving it would 404 — the static route only exposes that one directory —
|
|
51
|
+
* so the artifact is rejected at boot instead of at first page view.
|
|
52
|
+
*/
|
|
53
|
+
var WebClientAssetPrefixViolationError = class extends Error {
|
|
54
|
+
constructor(manifestPath, file) {
|
|
55
|
+
super(`Cannot resolve the hydration client URL: the Vite client manifest "${manifestPath}" points its entry at "${file}", which is not under the "${CLIENT_ASSET_URL_PREFIX}" asset directory this framework serves. This client artifact was not produced by this framework's client build configuration. Re-run the client build instead of hand-editing the manifest or the build output.`);
|
|
56
|
+
this.name = "WebClientAssetPrefixViolationError";
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Find the hydration entry in a parsed Vite manifest.
|
|
61
|
+
*
|
|
62
|
+
* VITE KEYS BY SOURCE PATH, NOT BY ENTRY NAME. A rollup input of
|
|
63
|
+
* `{ hydration: "src/hydration/index.ts" }` produces the record
|
|
64
|
+
* `"src/hydration/index.ts": { name: "hydration", isEntry: true, file: "assets/hydration-<hash>.js" }`
|
|
65
|
+
* — the name the build and this module share lives in the `name` FIELD, and
|
|
66
|
+
* there is no `"hydration"` key at all. Indexing the manifest by
|
|
67
|
+
* {@link HYDRATION_CLIENT_ENTRY_NAME} therefore missed every real manifest and
|
|
68
|
+
* reported an entry-name drift that had not happened.
|
|
69
|
+
*
|
|
70
|
+
* Matching on `isEntry` as well as `name` keeps this unambiguous: a shared
|
|
71
|
+
* chunk can carry a `name` too, and only entries are addressable as a module
|
|
72
|
+
* URL.
|
|
73
|
+
*/
|
|
74
|
+
function findHydrationEntry(manifest) {
|
|
75
|
+
for (const entry of Object.values(manifest)) if (entry !== void 0 && typeof entry === "object" && entry.isEntry === true && entry.name === "hydration") return entry;
|
|
76
|
+
}
|
|
77
|
+
function resolveHydrationClientUrl(options) {
|
|
78
|
+
const manifestPath = path.join(options.clientDir, ".vite", "manifest.json");
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = readFileSync(manifestPath, "utf-8");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw new WebClientManifestMissingError(manifestPath, error);
|
|
84
|
+
}
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(raw);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new WebClientManifestMalformedError(manifestPath, error);
|
|
90
|
+
}
|
|
91
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new WebClientManifestMalformedError(manifestPath, /* @__PURE__ */ new TypeError("The manifest root is not a JSON object."));
|
|
92
|
+
const entry = findHydrationEntry(parsed);
|
|
93
|
+
if (entry === void 0 || typeof entry !== "object" || typeof entry.file !== "string") throw new WebClientManifestEntryMissingError(manifestPath, HYDRATION_CLIENT_ENTRY_NAME);
|
|
94
|
+
const url = `/${entry.file}`;
|
|
95
|
+
if (!url.startsWith(`${"/assets"}/`)) throw new WebClientAssetPrefixViolationError(manifestPath, entry.file);
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
//#endregion
|
|
100
|
+
export { WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, resolveHydrationClientUrl };
|
|
101
|
+
//# sourceMappingURL=hydration-client-url.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-client-url.mjs","names":[],"sources":["../../../../../../../web/src/server/hydration-client-url.ts"],"sourcesContent":["/**\n * The ONE runtime manifest read in production: one boot-time JSON\n * lookup of the `hydration` entry in `<clientDir>/.vite/manifest.json`,\n * producing the string `install-page-routes.ts:139` already accepts as\n * `hydrationClientModuleUrl`.\n *\n * NEVER FALLS BACK. Each failure is its own named error so the boot log says\n * which half of the build→runtime handoff broke: a missing\n * file means the client build never ran, malformed JSON means the artifact is\n * corrupt, and a missing entry means the build/runtime entry-name contract\n * drifted. \"Serve without hydration\" is not among the outcomes.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { HYDRATION_CLIENT_ENTRY_NAME } from \"../vite/hydration-entries\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\nexport interface ResolveHydrationClientUrlOptions {\n /** Absolute path to the client build output dir: `<outdir>/client`. */\n clientDir: string;\n}\n\n/** The manifest file is absent — the client build never ran, or ran elsewhere. */\nexport class WebClientManifestMissingError extends Error {\n public constructor(manifestPath: string, cause: unknown) {\n super(\n `Cannot resolve the hydration client URL: the Vite client manifest \"${manifestPath}\" is missing or unreadable. ` +\n \"This app was started in production without its `warlock build` client artifacts.\",\n { cause },\n );\n this.name = \"WebClientManifestMissingError\";\n }\n}\n\n/** The manifest file exists but is not parsable JSON, or is not a JSON object. */\nexport class WebClientManifestMalformedError extends Error {\n public constructor(manifestPath: string, cause: unknown) {\n super(\n `Cannot resolve the hydration client URL: the Vite client manifest \"${manifestPath}\" is not valid JSON object content.`,\n { cause },\n );\n this.name = \"WebClientManifestMalformedError\";\n }\n}\n\n/**\n * The manifest parses but carries no usable `hydration` entry.\n *\n * The entry name is a shared contract between the build (`build-client.ts`\n * rollup input) and this runtime read; drift fails loudly HERE. Nobody adds a\n * default.\n */\nexport class WebClientManifestEntryMissingError extends Error {\n public constructor(manifestPath: string, entryName: string) {\n super(\n `Cannot resolve the hydration client URL: the Vite client manifest \"${manifestPath}\" has no \"${entryName}\" entry with a \"file\" string. ` +\n \"The build and runtime hydration entry names have drifted.\",\n );\n this.name = \"WebClientManifestEntryMissingError\";\n }\n}\n\n/**\n * The manifest names an entry file that does NOT live under the asset\n * directory `CLIENT_ASSET_URL_PREFIX` is mounted from.\n *\n * Serving it would 404 — the static route only exposes that one directory —\n * so the artifact is rejected at boot instead of at first page view.\n */\nexport class WebClientAssetPrefixViolationError extends Error {\n public constructor(manifestPath: string, file: string) {\n super(\n `Cannot resolve the hydration client URL: the Vite client manifest \"${manifestPath}\" points its entry at \"${file}\", ` +\n `which is not under the \"${CLIENT_ASSET_URL_PREFIX}\" asset directory this framework serves. ` +\n \"This client artifact was not produced by this framework's client build configuration. \" +\n \"Re-run the client build instead of hand-editing the manifest or the build output.\",\n );\n this.name = \"WebClientAssetPrefixViolationError\";\n }\n}\n\ntype ManifestEntry = { file?: unknown; name?: unknown; isEntry?: unknown };\n\n/**\n * Find the hydration entry in a parsed Vite manifest.\n *\n * VITE KEYS BY SOURCE PATH, NOT BY ENTRY NAME. A rollup input of\n * `{ hydration: \"src/hydration/index.ts\" }` produces the record\n * `\"src/hydration/index.ts\": { name: \"hydration\", isEntry: true, file: \"assets/hydration-<hash>.js\" }`\n * — the name the build and this module share lives in the `name` FIELD, and\n * there is no `\"hydration\"` key at all. Indexing the manifest by\n * {@link HYDRATION_CLIENT_ENTRY_NAME} therefore missed every real manifest and\n * reported an entry-name drift that had not happened.\n *\n * Matching on `isEntry` as well as `name` keeps this unambiguous: a shared\n * chunk can carry a `name` too, and only entries are addressable as a module\n * URL.\n */\nfunction findHydrationEntry(manifest: Record<string, ManifestEntry | undefined>): ManifestEntry | undefined {\n for (const entry of Object.values(manifest)) {\n if (\n entry !== undefined &&\n typeof entry === \"object\" &&\n entry.isEntry === true &&\n entry.name === HYDRATION_CLIENT_ENTRY_NAME\n ) {\n return entry;\n }\n }\n\n return undefined;\n}\n\nexport function resolveHydrationClientUrl(options: ResolveHydrationClientUrlOptions): string {\n const manifestPath = path.join(options.clientDir, \".vite\", \"manifest.json\");\n\n let raw: string;\n\n try {\n raw = readFileSync(manifestPath, \"utf-8\");\n } catch (error) {\n throw new WebClientManifestMissingError(manifestPath, error);\n }\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new WebClientManifestMalformedError(manifestPath, error);\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new WebClientManifestMalformedError(\n manifestPath,\n new TypeError(\"The manifest root is not a JSON object.\"),\n );\n }\n\n const entry = findHydrationEntry(parsed as Record<string, ManifestEntry | undefined>);\n\n if (entry === undefined || typeof entry !== \"object\" || typeof entry.file !== \"string\") {\n throw new WebClientManifestEntryMissingError(manifestPath, HYDRATION_CLIENT_ENTRY_NAME);\n }\n\n const url = `/${entry.file}`;\n\n // The prefix is DERIVED from CLIENT_ASSET_URL_PREFIX, never restated: this\n // file must not carry a second copy of the literal the constant owns.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) {\n throw new WebClientAssetPrefixViolationError(manifestPath, entry.file);\n }\n\n // Past this point the returned URL starts with CLIENT_ASSET_URL_PREFIX by\n // construction — which is exactly what the static-file route mounting\n // `<clientDir>/assets` at that same imported symbol relies on.\n return url;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuBA,IAAa,gCAAb,cAAmD,MAAM;CACvD,AAAO,YAAY,cAAsB,OAAgB;EACvD,MACE,sEAAsE,aAAa,iHAEnF,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;;AAGA,IAAa,kCAAb,cAAqD,MAAM;CACzD,AAAO,YAAY,cAAsB,OAAgB;EACvD,MACE,sEAAsE,aAAa,sCACnF,EAAE,MAAM,CACV;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,IAAa,qCAAb,cAAwD,MAAM;CAC5D,AAAO,YAAY,cAAsB,WAAmB;EAC1D,MACE,sEAAsE,aAAa,YAAY,UAAU,wFAE3G;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,IAAa,qCAAb,cAAwD,MAAM;CAC5D,AAAO,YAAY,cAAsB,MAAc;EACrD,MACE,sEAAsE,aAAa,yBAAyB,KAAK,6BACpF,wBAAwB,iNAGvD;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;;AAmBA,SAAS,mBAAmB,UAAgF;CAC1G,KAAK,MAAM,SAAS,OAAO,OAAO,QAAQ,GACxC,IACE,UAAU,UACV,OAAO,UAAU,YACjB,MAAM,YAAY,QAClB,MAAM,sBAEN,OAAO;AAKb;AAEA,SAAgB,0BAA0B,SAAmD;CAC3F,MAAM,eAAe,KAAK,KAAK,QAAQ,WAAW,SAAS,eAAe;CAE1E,IAAI;CAEJ,IAAI;EACF,MAAM,aAAa,cAAc,OAAO;CAC1C,SAAS,OAAO;EACd,MAAM,IAAI,8BAA8B,cAAc,KAAK;CAC7D;CAEA,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,gCAAgC,cAAc,KAAK;CAC/D;CAEA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GACvE,MAAM,IAAI,gCACR,8BACA,IAAI,UAAU,yCAAyC,CACzD;CAGF,MAAM,QAAQ,mBAAmB,MAAmD;CAEpF,IAAI,UAAU,UAAa,OAAO,UAAU,YAAY,OAAO,MAAM,SAAS,UAC5E,MAAM,IAAI,mCAAmC,cAAc,2BAA2B;CAGxF,MAAM,MAAM,IAAI,MAAM;CAItB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAC/C,MAAM,IAAI,mCAAmC,cAAc,MAAM,IAAI;CAMvE,OAAO;AACT"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { connectSharedStore } from "../shared.mjs";
|
|
2
|
+
import { PAYLOAD_SCRIPT_ID, escapePayload } from "../components/document-context.mjs";
|
|
3
|
+
import { composeRoutePath } from "../routing/compose-route-path.mjs";
|
|
4
|
+
import { LOADER_SHORT_CIRCUIT, createBufferedResponse, isLoaderShortCircuit } from "./buffered-response.mjs";
|
|
5
|
+
import { connectPageContext } from "./page-context.mjs";
|
|
6
|
+
import { executePageRequest } from "./execute-page-request.mjs";
|
|
7
|
+
import { renderPageRequest } from "./render-page.mjs";
|
|
8
|
+
import { PageModuleNotInManifestError, createPageModuleLoader } from "./create-page-module-loader.mjs";
|
|
9
|
+
import { installPageRoutesFromManifest } from "./install-page-routes-from-manifest.mjs";
|
|
10
|
+
import "./install-page-routes.mjs";
|
|
11
|
+
import { productionStylesheetUrls } from "./stylesheet-urls.mjs";
|
|
12
|
+
|
|
13
|
+
export { connectPageContext, connectSharedStore, installPageRoutesFromManifest, productionStylesheetUrls };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { publishRouteTable } from "../routing/route-table.mjs";
|
|
2
|
+
import { composeRoutePath } from "../routing/compose-route-path.mjs";
|
|
3
|
+
import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
|
|
4
|
+
import { canonicalizeRouteExport, deriveFallbackRouteName } from "../routing/route-identity.mjs";
|
|
5
|
+
import { createPageModuleLoader } from "./create-page-module-loader.mjs";
|
|
6
|
+
import { createPageRouteHandler } from "./create-page-route-handler.mjs";
|
|
7
|
+
|
|
8
|
+
//#region ../web/src/server/install-page-routes-from-manifest.ts
|
|
9
|
+
/**
|
|
10
|
+
* Page-route registration for a built application.
|
|
11
|
+
*
|
|
12
|
+
* `installPageRoutes` answers "which pages exist?" by walking the filesystem
|
|
13
|
+
* and "what is this module?" by asking Vite to evaluate it. Neither question
|
|
14
|
+
* can be asked of a running production process: there is no `app/` tree beside
|
|
15
|
+
* the bundle and no Vite. Both answers were therefore moved to build time — the
|
|
16
|
+
* generated `pages.ts` barrel statically imported every page, layout and the
|
|
17
|
+
* app root and handed them over as a {@link PageManifest}, and this module
|
|
18
|
+
* turns that table into registered routes.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS DELIBERATELY IDENTICAL TO DEVELOPMENT: the route a page ends up on,
|
|
21
|
+
* and the guards that run before it renders. A page's `route` export and the
|
|
22
|
+
* `prefix` and `middleware` exports of EVERY layout on its path are read off the
|
|
23
|
+
* module namespaces here, at boot, and composed by the same rules dev composes
|
|
24
|
+
* them by ({@link layoutLevelOf}, {@link composeLayoutLevel}) — so the URL a page
|
|
25
|
+
* answers on and the chain that guards it are decided by the page's own source
|
|
26
|
+
* in both modes, and a build cannot quietly disagree with the dev server about
|
|
27
|
+
* either.
|
|
28
|
+
*
|
|
29
|
+
* WHAT IS DELIBERATELY DIFFERENT: this is synchronous. Every module is already
|
|
30
|
+
* in memory, so registration has nothing to await; the loader handed to each
|
|
31
|
+
* handler is a lookup over the same table, not an evaluation step.
|
|
32
|
+
*/
|
|
33
|
+
function resolveRoute(routeExport, sourceFile) {
|
|
34
|
+
const canonical = canonicalizeRouteExport(routeExport);
|
|
35
|
+
return {
|
|
36
|
+
path: canonical.path,
|
|
37
|
+
name: canonical.name ?? deriveFallbackRouteName({
|
|
38
|
+
routePath: canonical.path,
|
|
39
|
+
sourceFile
|
|
40
|
+
})
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function layoutLevelOf(page) {
|
|
44
|
+
const selection = selectPageLayout(page.layouts.map((layout) => ({
|
|
45
|
+
layout: layout.sourceFile,
|
|
46
|
+
renders: typeof layout.module.default !== "undefined"
|
|
47
|
+
})));
|
|
48
|
+
if (selection.type === "rejected") throw new NestedLayoutsNotSupportedError(page.sourceFile, selection.layouts);
|
|
49
|
+
return {
|
|
50
|
+
host: selection.type === "selected" ? page.layouts.find((layout) => layout.sourceFile === selection.layout) : page.layouts.at(-1),
|
|
51
|
+
prefix: page.layouts.reduce((composed, layout) => composeRoutePath(composed, layout.module.prefix ?? "/"), "/")
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The layout slot's module for one page: the slot host's own namespace, with the
|
|
56
|
+
* whole chain's middleware in place of its own — outermost first, which is the
|
|
57
|
+
* order stage 3 runs the array in (`execute-page-request.ts:519-524`) and the
|
|
58
|
+
* order an outer `optionalAuth` needs in order to have resolved an identity
|
|
59
|
+
* before an inner `gate()` checks it.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately NOT core's route-level `middleware` option: that runs before the
|
|
62
|
+
* pipeline's App-level middleware, which would invert outermost-first — the one
|
|
63
|
+
* property this composition exists to guarantee.
|
|
64
|
+
*
|
|
65
|
+
* Built once at registration, not per request: unlike dev, every module here is
|
|
66
|
+
* already in memory and cannot change under a running process.
|
|
67
|
+
*/
|
|
68
|
+
function composeLayoutLevel(page, host) {
|
|
69
|
+
return {
|
|
70
|
+
...host.module,
|
|
71
|
+
middleware: page.layouts.flatMap((layout) => [...layout.module.middleware ?? []])
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Registers every page the manifest carries into `options.router`.
|
|
76
|
+
*
|
|
77
|
+
* An empty manifest registers nothing and is not an error: "built with web, no
|
|
78
|
+
* pages" is a legal state of a built application, and treating it as a failure
|
|
79
|
+
* would make an empty project unbootable. A manifest that DOES carry pages but
|
|
80
|
+
* no app root is the opposite — every page renders inside the application root,
|
|
81
|
+
* so that combination is a broken table rather than an empty one, and it is
|
|
82
|
+
* refused before any route exists to serve a request with a missing root.
|
|
83
|
+
*
|
|
84
|
+
* Two pages composing to the same path is refused the moment the second one is
|
|
85
|
+
* seen, naming both — a registration-time failure, rather than a route one of
|
|
86
|
+
* them silently loses at runtime.
|
|
87
|
+
*/
|
|
88
|
+
function installPageRoutesFromManifest(options) {
|
|
89
|
+
const { router, manifest, hydrationClientModuleUrl, stylesheetUrls, applyBufferedCookie, createHandler = createPageRouteHandler } = options;
|
|
90
|
+
if (manifest.pages.length === 0) return [];
|
|
91
|
+
const app = manifest.app;
|
|
92
|
+
if (app === void 0) throw new Error(`installPageRoutesFromManifest: this build's page manifest carries ${manifest.pages.length} page(s) but no application root. Every page renders inside the app component, so no page can be registered without it. Re-run the build so the generated pages barrel provides an \`app\` entry.`);
|
|
93
|
+
const loadModule = createPageModuleLoader(manifest);
|
|
94
|
+
const installed = [];
|
|
95
|
+
const fileByPath = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const page of manifest.pages) {
|
|
97
|
+
const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);
|
|
98
|
+
const routeExport = page.module.route;
|
|
99
|
+
if (routeExport === void 0) continue;
|
|
100
|
+
const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);
|
|
101
|
+
const effectivePath = composeRoutePath(layoutPrefix, routePath);
|
|
102
|
+
const existingFile = fileByPath.get(effectivePath);
|
|
103
|
+
if (existingFile) throw new Error(`installPageRoutesFromManifest: composed route path "${effectivePath}" (layout prefix "${layoutPrefix}" + route.path "${routePath}") is declared by two pages — "${existingFile}" and "${page.sourceFile}". Every page's composed route path must be unique.`);
|
|
104
|
+
fileByPath.set(effectivePath, page.sourceFile);
|
|
105
|
+
const composedLayout = page.layouts.length > 1 && layout !== void 0 ? composeLayoutLevel(page, layout) : void 0;
|
|
106
|
+
router.get(effectivePath, createHandler({
|
|
107
|
+
path: effectivePath,
|
|
108
|
+
name,
|
|
109
|
+
appFile: app.sourceFile,
|
|
110
|
+
pageFile: page.sourceFile,
|
|
111
|
+
layoutFile: layout?.sourceFile,
|
|
112
|
+
loadModule: composedLayout === void 0 ? loadModule : (moduleId) => moduleId === layout?.sourceFile ? Promise.resolve(composedLayout) : loadModule(moduleId),
|
|
113
|
+
hydrationClientModuleUrl,
|
|
114
|
+
stylesheetUrls,
|
|
115
|
+
applyBufferedCookie
|
|
116
|
+
}), {
|
|
117
|
+
name,
|
|
118
|
+
isPage: true
|
|
119
|
+
});
|
|
120
|
+
installed.push({
|
|
121
|
+
path: effectivePath,
|
|
122
|
+
name,
|
|
123
|
+
file: page.sourceFile,
|
|
124
|
+
layoutFile: layout?.sourceFile
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
publishRouteTable(installed, "installPageRoutesFromManifest (production)");
|
|
128
|
+
return installed;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
export { installPageRoutesFromManifest };
|
|
133
|
+
//# sourceMappingURL=install-page-routes-from-manifest.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-page-routes-from-manifest.mjs","names":[],"sources":["../../../../../../../web/src/server/install-page-routes-from-manifest.ts"],"sourcesContent":["/**\r\n * Page-route registration for a built application.\r\n *\r\n * `installPageRoutes` answers \"which pages exist?\" by walking the filesystem\r\n * and \"what is this module?\" by asking Vite to evaluate it. Neither question\r\n * can be asked of a running production process: there is no `app/` tree beside\r\n * the bundle and no Vite. Both answers were therefore moved to build time — the\r\n * generated `pages.ts` barrel statically imported every page, layout and the\r\n * app root and handed them over as a {@link PageManifest}, and this module\r\n * turns that table into registered routes.\r\n *\r\n * WHAT IS DELIBERATELY IDENTICAL TO DEVELOPMENT: the route a page ends up on,\r\n * and the guards that run before it renders. A page's `route` export and the\r\n * `prefix` and `middleware` exports of EVERY layout on its path are read off the\r\n * module namespaces here, at boot, and composed by the same rules dev composes\r\n * them by ({@link layoutLevelOf}, {@link composeLayoutLevel}) — so the URL a page\r\n * answers on and the chain that guards it are decided by the page's own source\r\n * in both modes, and a build cannot quietly disagree with the dev server about\r\n * either.\r\n *\r\n * WHAT IS DELIBERATELY DIFFERENT: this is synchronous. Every module is already\r\n * in memory, so registration has nothing to await; the loader handed to each\r\n * handler is a lookup over the same table, not an evaluation step.\r\n */\r\nimport { composeRoutePath } from \"../routing/compose-route-path\";\r\nimport { NestedLayoutsNotSupportedError, selectPageLayout } from \"../routing/layout-policy\";\r\nimport { canonicalizeRouteExport, deriveFallbackRouteName } from \"../routing/route-identity\";\r\nimport { publishRouteTable } from \"../routing/route-table\";\r\nimport type { Response, Router } from \"@warlock.js/core\";\r\nimport type { BufferedCookie } from \"./buffered-response\";\r\nimport { createPageModuleLoader } from \"./create-page-module-loader\";\r\nimport {\r\n createPageRouteHandler,\r\n type PageRouteHandler,\r\n type PageRouteHandlerOptions,\r\n} from \"./create-page-route-handler\";\r\nimport type { PipelineMiddleware } from \"./execute-page-request\";\r\nimport type { PageManifest, PageManifestLayoutEntry, PageManifestPageEntry } from \"./page-manifest\";\r\n\r\n/** A page declares either a bare path or a path plus an explicit route name. */\r\ntype PageRouteExport = string | { path: string; name?: string };\r\n\r\n/** The only export this module reads off a page module namespace. */\r\ntype PageModuleShape = {\r\n route?: PageRouteExport;\r\n};\r\n\r\n/** The exports this module reads off a layout module namespace. */\r\ntype LayoutModuleShape = {\r\n prefix?: string;\r\n /**\r\n * The default export — the thing that puts an element in the document, and\r\n * therefore the ONLY export that decides whether a layout counts against the\r\n * single-rendering-layout rule (`../routing/layout-policy.ts`). The manifest\r\n * carries LOADED modules, so this is a fact rather than a guess, exactly as it\r\n * is in dev (`install-page-routes.ts:145-150`).\r\n */\r\n default?: unknown;\r\n /** The layout's guards, in the order it declared them. */\r\n middleware?: readonly PipelineMiddleware[];\r\n};\r\n\r\n/**\r\n * How a handler is built for one page. Defaults to `createPageRouteHandler`;\r\n * taking it as an input keeps this module's own job — reading the manifest and\r\n * registering routes — provable without a render pipeline behind it.\r\n */\r\nexport type PageRouteHandlerFactory = (options: PageRouteHandlerOptions) => PageRouteHandler;\r\n\r\nexport type InstalledManifestPageRoute = {\r\n /** The composed path the route was registered on. */\r\n path: string;\r\n /** The resolved route name; shared namespace with API routes. */\r\n name: string;\r\n /** The page's manifest `sourceFile`. */\r\n file: string;\r\n /** The layout's manifest `sourceFile`, when the page has one. */\r\n layoutFile: string | undefined;\r\n};\r\n\r\nexport type InstallPageRoutesFromManifestOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /** Browser module loaded after the server-rendered application and payload. */\r\n hydrationClientModuleUrl?: string;\r\n /** Stylesheet URLs emitted into every page's `<head>`. */\r\n stylesheetUrls?: readonly string[];\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void;\r\n createHandler?: PageRouteHandlerFactory;\r\n};\r\n\r\nfunction resolveRoute(\r\n routeExport: PageRouteExport,\r\n sourceFile: string,\r\n): { path: string; name: string } {\r\n const canonical = canonicalizeRouteExport(routeExport);\r\n\r\n return {\r\n path: canonical.path,\r\n name: canonical.name ?? deriveFallbackRouteName({ routePath: canonical.path, sourceFile }),\r\n };\r\n}\r\n\r\n/**\r\n * The page's layout LEVEL, resolved from the whole chain the manifest carries\r\n * rather than from the one layout nearest to it — the same resolution dev makes\r\n * (`install-page-routes.ts:138-164`), against loaded modules instead of Vite's.\r\n *\r\n * The manifest carries the FULL chain, outermost first, and the render pipeline\r\n * has exactly one layout slot per page (`execute-page-request.ts`'s\r\n * `PageRouteEntry[\"triple\"]`), so the chain has to be collapsed into one module\r\n * before it reaches a handler. Two things collapse differently and both matter:\r\n *\r\n * - RENDERING is a selection: at most one layout on the chain may render, and\r\n * the policy picks it. `renders` is read off the loaded module\r\n * (`typeof module.default !== \"undefined\"`), never off the entry's presence in\r\n * the chain — a `middleware`-only layout has no default export and is not a\r\n * wrapper. Passing bare `sourceFile` strings had every layout read as a\r\n * rendering one, so boot refused a middleware-only guard chain that the build\r\n * had already accepted: an application that builds and will not start.\r\n * - MIDDLEWARE and PREFIX are compositions: every layout on the path\r\n * contributes, outermost first. A guard on an outer layout that the page's own\r\n * directory knows nothing about is exactly the guard that must still run, and\r\n * a prefix nobody composed is a URL nobody wrote down.\r\n *\r\n * A chain with more than one RENDERING layout is still refused here, at boot,\r\n * before a single request can observe the wrong document. Like the missing\r\n * app-root refusal below, that arm defends against stale or hand-edited build\r\n * artifacts: the build refuses to emit such a chain, but a manifest can reach a\r\n * running process without that build having produced it.\r\n */\r\ntype LayoutLevel = {\r\n /**\r\n * The layout entry the handler's layout slot is registered under, or\r\n * `undefined` when the page has no layout at all: the layout that RENDERS,\r\n * or — when none does — the nearest one, which is the slot production has\r\n * always used and so the choice that changes nothing but the middleware for a\r\n * chain with no wrapper in it.\r\n */\r\n host: PageManifestLayoutEntry | undefined;\r\n /** Every layout's `prefix`, composed outermost first — `discoverPages`' own reduction. */\r\n prefix: string;\r\n};\r\n\r\nfunction layoutLevelOf(page: PageManifestPageEntry): LayoutLevel {\r\n const selection = selectPageLayout(\r\n page.layouts.map((layout) => ({\r\n layout: layout.sourceFile,\r\n renders: typeof (layout.module as LayoutModuleShape).default !== \"undefined\",\r\n })),\r\n );\r\n\r\n if (selection.type === \"rejected\") {\r\n throw new NestedLayoutsNotSupportedError(page.sourceFile, selection.layouts);\r\n }\r\n\r\n return {\r\n host:\r\n selection.type === \"selected\"\r\n ? page.layouts.find((layout) => layout.sourceFile === selection.layout)\r\n : page.layouts.at(-1),\r\n prefix: page.layouts.reduce(\r\n (composed, layout) =>\r\n composeRoutePath(composed, (layout.module as LayoutModuleShape).prefix ?? \"/\"),\r\n \"/\",\r\n ),\r\n };\r\n}\r\n\r\n/**\r\n * The layout slot's module for one page: the slot host's own namespace, with the\r\n * whole chain's middleware in place of its own — outermost first, which is the\r\n * order stage 3 runs the array in (`execute-page-request.ts:519-524`) and the\r\n * order an outer `optionalAuth` needs in order to have resolved an identity\r\n * before an inner `gate()` checks it.\r\n *\r\n * Deliberately NOT core's route-level `middleware` option: that runs before the\r\n * pipeline's App-level middleware, which would invert outermost-first — the one\r\n * property this composition exists to guarantee.\r\n *\r\n * Built once at registration, not per request: unlike dev, every module here is\r\n * already in memory and cannot change under a running process.\r\n */\r\nfunction composeLayoutLevel(\r\n page: PageManifestPageEntry,\r\n host: PageManifestLayoutEntry,\r\n): Record<string, unknown> {\r\n return {\r\n ...host.module,\r\n middleware: page.layouts.flatMap((layout) => [\r\n ...((layout.module as LayoutModuleShape).middleware ?? []),\r\n ]),\r\n };\r\n}\r\n\r\n/**\r\n * Registers every page the manifest carries into `options.router`.\r\n *\r\n * An empty manifest registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and treating it as a failure\r\n * would make an empty project unbootable. A manifest that DOES carry pages but\r\n * no app root is the opposite — every page renders inside the application root,\r\n * so that combination is a broken table rather than an empty one, and it is\r\n * refused before any route exists to serve a request with a missing root.\r\n *\r\n * Two pages composing to the same path is refused the moment the second one is\r\n * seen, naming both — a registration-time failure, rather than a route one of\r\n * them silently loses at runtime.\r\n */\r\nexport function installPageRoutesFromManifest(\r\n options: InstallPageRoutesFromManifestOptions,\r\n): InstalledManifestPageRoute[] {\r\n const {\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl,\r\n stylesheetUrls,\r\n applyBufferedCookie,\r\n createHandler = createPageRouteHandler,\r\n } = options;\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n const app = manifest.app;\r\n\r\n if (app === undefined) {\r\n throw new Error(\r\n `installPageRoutesFromManifest: this build's page manifest carries ${manifest.pages.length} ` +\r\n \"page(s) but no application root. Every page renders inside the app component, so no \" +\r\n \"page can be registered without it. Re-run the build so the generated pages barrel \" +\r\n \"provides an `app` entry.\",\r\n );\r\n }\r\n\r\n // Ids are the manifest's own `sourceFile` strings and are passed on untouched:\r\n // the loader below matches them by exact string equality, so resolving,\r\n // joining or swapping separators on one side of that comparison would turn\r\n // every lookup into a miss.\r\n const loadModule = createPageModuleLoader(manifest);\r\n\r\n const installed: InstalledManifestPageRoute[] = [];\r\n const fileByPath = new Map<string, string>();\r\n\r\n for (const page of manifest.pages) {\r\n const { host: layout, prefix: layoutPrefix } = layoutLevelOf(page);\r\n const routeExport = (page.module as PageModuleShape).route;\r\n\r\n // A page that declares no route has no public URL to be registered under —\r\n // the same page discovery skips in development.\r\n if (routeExport === undefined) continue;\r\n\r\n const { path: routePath, name } = resolveRoute(routeExport, page.sourceFile);\r\n const effectivePath = composeRoutePath(layoutPrefix, routePath);\r\n const existingFile = fileByPath.get(effectivePath);\r\n\r\n if (existingFile) {\r\n throw new Error(\r\n `installPageRoutesFromManifest: composed route path \"${effectivePath}\" (layout ` +\r\n `prefix \"${layoutPrefix}\" + route.path \"${routePath}\") is declared by two pages — ` +\r\n `\"${existingFile}\" and \"${page.sourceFile}\". Every page's composed route path must ` +\r\n \"be unique.\",\r\n );\r\n }\r\n\r\n fileByPath.set(effectivePath, page.sourceFile);\r\n\r\n // The layout slot's id resolves to the COMPOSED level — every layout's\r\n // middleware, in chain order — and every other id goes straight to the\r\n // manifest lookup. A one-layout chain has nothing to compose, so it is left\r\n // to resolve as the exact namespace object the manifest carries, untouched.\r\n const composedLayout =\r\n page.layouts.length > 1 && layout !== undefined\r\n ? composeLayoutLevel(page, layout)\r\n : undefined;\r\n\r\n router.get(\r\n effectivePath,\r\n createHandler({\r\n path: effectivePath,\r\n name,\r\n appFile: app.sourceFile,\r\n pageFile: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n loadModule:\r\n composedLayout === undefined\r\n ? loadModule\r\n : (moduleId) =>\r\n moduleId === layout?.sourceFile\r\n ? Promise.resolve(composedLayout)\r\n : loadModule(moduleId),\r\n hydrationClientModuleUrl,\r\n stylesheetUrls,\r\n applyBufferedCookie,\r\n }),\r\n // `isPage` marks this route as SSR-served. Pages and API routes share one\r\n // router and one route-name namespace, so the router's duplicate-name\r\n // error reads this flag to say which claimant is the page.\r\n { name, isPage: true },\r\n );\r\n\r\n installed.push({\r\n path: effectivePath,\r\n name,\r\n file: page.sourceFile,\r\n layoutFile: layout?.sourceFile,\r\n });\r\n }\r\n\r\n /*\r\n Same publish as the dev installer, for the same reason: `href()` and the\r\n router must agree, and they only can if both read the one loop that\r\n registered the routes. Production installs once at boot, so the wholesale\r\n replacement is a single write before the first request.\r\n */\r\n publishRouteTable(installed, \"installPageRoutesFromManifest (production)\");\r\n\r\n return installed;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAS,aACP,aACA,YACgC;CAChC,MAAM,YAAY,wBAAwB,WAAW;CAErD,OAAO;EACL,MAAM,UAAU;EAChB,MAAM,UAAU,QAAQ,wBAAwB;GAAE,WAAW,UAAU;GAAM;EAAW,CAAC;CAC3F;AACF;AA2CA,SAAS,cAAc,MAA0C;CAC/D,MAAM,YAAY,iBAChB,KAAK,QAAQ,KAAK,YAAY;EAC5B,QAAQ,OAAO;EACf,SAAS,OAAQ,OAAO,OAA6B,YAAY;CACnE,EAAE,CACJ;CAEA,IAAI,UAAU,SAAS,YACrB,MAAM,IAAI,+BAA+B,KAAK,YAAY,UAAU,OAAO;CAG7E,OAAO;EACL,MACE,UAAU,SAAS,aACf,KAAK,QAAQ,MAAM,WAAW,OAAO,eAAe,UAAU,MAAM,IACpE,KAAK,QAAQ,GAAG,EAAE;EACxB,QAAQ,KAAK,QAAQ,QAClB,UAAU,WACT,iBAAiB,UAAW,OAAO,OAA6B,UAAU,GAAG,GAC/E,GACF;CACF;AACF;;;;;;;;;;;;;;;AAgBA,SAAS,mBACP,MACA,MACyB;CACzB,OAAO;EACL,GAAG,KAAK;EACR,YAAY,KAAK,QAAQ,SAAS,WAAW,CAC3C,GAAK,OAAO,OAA6B,cAAc,CAAC,CAC1D,CAAC;CACH;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,8BACd,SAC8B;CAC9B,MAAM,EACJ,QACA,UACA,0BACA,gBACA,qBACA,gBAAgB,2BACd;CAEJ,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAEzC,MAAM,MAAM,SAAS;CAErB,IAAI,QAAQ,QACV,MAAM,IAAI,MACR,qEAAqE,SAAS,MAAM,OAAO,kMAI7F;CAOF,MAAM,aAAa,uBAAuB,QAAQ;CAElD,MAAM,YAA0C,CAAC;CACjD,MAAM,6BAAa,IAAI,IAAoB;CAE3C,KAAK,MAAM,QAAQ,SAAS,OAAO;EACjC,MAAM,EAAE,MAAM,QAAQ,QAAQ,iBAAiB,cAAc,IAAI;EACjE,MAAM,cAAe,KAAK,OAA2B;EAIrD,IAAI,gBAAgB,QAAW;EAE/B,MAAM,EAAE,MAAM,WAAW,SAAS,aAAa,aAAa,KAAK,UAAU;EAC3E,MAAM,gBAAgB,iBAAiB,cAAc,SAAS;EAC9D,MAAM,eAAe,WAAW,IAAI,aAAa;EAEjD,IAAI,cACF,MAAM,IAAI,MACR,uDAAuD,cAAc,oBACxD,aAAa,kBAAkB,UAAU,iCAChD,aAAa,SAAS,KAAK,WAAW,oDAE9C;EAGF,WAAW,IAAI,eAAe,KAAK,UAAU;EAM7C,MAAM,iBACJ,KAAK,QAAQ,SAAS,KAAK,WAAW,SAClC,mBAAmB,MAAM,MAAM,IAC/B;EAEN,OAAO,IACL,eACA,cAAc;GACZ,MAAM;GACN;GACA,SAAS,IAAI;GACb,UAAU,KAAK;GACf,YAAY,QAAQ;GACpB,YACE,mBAAmB,SACf,cACC,aACC,aAAa,QAAQ,aACjB,QAAQ,QAAQ,cAAc,IAC9B,WAAW,QAAQ;GAC/B;GACA;GACA;EACF,CAAC,GAID;GAAE;GAAM,QAAQ;EAAK,CACvB;EAEA,UAAU,KAAK;GACb,MAAM;GACN;GACA,MAAM,KAAK;GACX,YAAY,QAAQ;EACtB,CAAC;CACH;CAQA,kBAAkB,WAAW,4CAA4C;CAEzE,OAAO;AACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region ../web/src/server/install-production-page-routes.ts
|
|
2
|
+
/**
|
|
3
|
+
* A manifest page whose module namespace carries no `route` export.
|
|
4
|
+
*
|
|
5
|
+
* Page discovery only emits a module it read a `route` off, so a valid build
|
|
6
|
+
* cannot produce this — a stale artifact or a hand-edited barrel can. The
|
|
7
|
+
* alternative to refusing it is a page table that silently shrinks: the
|
|
8
|
+
* operator gets a clean boot log and a 404 on a page that is demonstrably in
|
|
9
|
+
* the bundle, with nothing anywhere naming the file. The file is named here
|
|
10
|
+
* because it is the entire diagnosis.
|
|
11
|
+
*/
|
|
12
|
+
var PageManifestEntryMissingRouteError = class extends Error {
|
|
13
|
+
constructor(sourceFile) {
|
|
14
|
+
super(`Cannot serve pages: the page manifest entry for "${sourceFile}" has no \`route\` export, so it has no URL to be registered under. This build's generated \`pages.ts\` barrel does not match the sources it was generated from — re-run \`warlock build\`, and if the page is meant to be served, give it a \`route\` export.`);
|
|
15
|
+
this.name = "PageManifestEntryMissingRouteError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Refuse the whole table before any of it is registered.
|
|
20
|
+
*
|
|
21
|
+
* Checked in one pass UP FRONT rather than per page inside the registration
|
|
22
|
+
* loop: a half-installed page table is worse than a refused boot, because it
|
|
23
|
+
* serves.
|
|
24
|
+
*/
|
|
25
|
+
function assertEveryPageDeclaresRoute(manifest) {
|
|
26
|
+
for (const page of manifest.pages) if (page.module.route === void 0) throw new PageManifestEntryMissingRouteError(page.sourceFile);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Register every page the manifest carries, and connect the pipeline the
|
|
30
|
+
* handlers will run in.
|
|
31
|
+
*
|
|
32
|
+
* An empty table registers nothing and is not an error: "built with web, no
|
|
33
|
+
* pages" is a legal state of a built application, and it is also the one state
|
|
34
|
+
* where there is no pipeline to connect and no hydration entry to point at, so
|
|
35
|
+
* it returns before doing either.
|
|
36
|
+
*/
|
|
37
|
+
async function installProductionPageRoutes(options) {
|
|
38
|
+
const { router, manifest, pageContext, sharedStore, applyBufferedCookie, resolveHydrationClientModuleUrl, clientDir } = options;
|
|
39
|
+
assertEveryPageDeclaresRoute(manifest);
|
|
40
|
+
if (manifest.pages.length === 0) return [];
|
|
41
|
+
const webServer = await import("./index.mjs");
|
|
42
|
+
webServer.connectSharedStore(sharedStore);
|
|
43
|
+
webServer.connectPageContext(pageContext);
|
|
44
|
+
return webServer.installPageRoutesFromManifest({
|
|
45
|
+
router,
|
|
46
|
+
manifest,
|
|
47
|
+
hydrationClientModuleUrl: resolveHydrationClientModuleUrl(),
|
|
48
|
+
stylesheetUrls: clientDir === void 0 ? [] : webServer.productionStylesheetUrls(clientDir),
|
|
49
|
+
applyBufferedCookie
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
export { installProductionPageRoutes };
|
|
55
|
+
//# sourceMappingURL=install-production-page-routes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"install-production-page-routes.mjs","names":[],"sources":["../../../../../../../web/src/server/install-production-page-routes.ts"],"sourcesContent":["/**\r\n * Standing up page serving for a BUILT application — the production half of\r\n * `WebConnector.boot()`, kept out of the connector so the mode branch there\r\n * stays one `if` and this path stays readable on its own.\r\n *\r\n * The development path answers \"which pages exist?\" by walking `app/` and\r\n * \"what is this module?\" by asking Vite to evaluate it. A production process\r\n * can ask neither question: there is no source tree beside the bundle and no\r\n * Vite. Both answers arrived at import time instead, in the generated pages\r\n * barrel's {@link PageManifest}, and everything below is what remains once\r\n * those two questions are already answered — a guard, a wiring step and a\r\n * registration step.\r\n *\r\n * NOTHING HERE REACHES FOR VITE, transitively included. `vite` is an optional\r\n * peer, so a production install does not carry it; an import that only ever\r\n * runs in development still fails at module load in production, which is why\r\n * the one import that has to happen at boot is a dynamic one and why it names\r\n * the pipeline barrel and nothing else.\r\n */\r\nimport type { Response, Router } from \"@warlock.js/core\";\r\nimport type { SharedStoreResolver } from \"../shared\";\r\nimport type { BufferedCookie } from \"./buffered-response\";\r\nimport type { PageContextRunner } from \"./execute-page-request\";\r\nimport type { InstalledManifestPageRoute } from \"./install-page-routes-from-manifest\";\r\nimport type { PageManifest } from \"./page-manifest\";\r\n\r\n/** The only export this module reads off a page module namespace. */\r\ntype PageModuleShape = {\r\n route?: unknown;\r\n};\r\n\r\nexport type InstallProductionPageRoutesOptions = {\r\n router: Router;\r\n /** The table the generated production barrel provided at import time. */\r\n manifest: PageManifest;\r\n /**\r\n * Core's per-request context, handed over rather than imported: core is a\r\n * type-only peer of web, and two copies of core would mean two\r\n * AsyncLocalStorage stores and a page pipeline reading an empty one.\r\n */\r\n pageContext: PageContextRunner;\r\n /** Reads the current request's store out of {@link pageContext}. */\r\n sharedStore: SharedStoreResolver;\r\n /** Same helper `dev-server.ts` exports — passed in, never imported. */\r\n applyBufferedCookie: (response: Response, cookie: BufferedCookie) => void;\r\n /**\r\n * Where the browser fetches the hydration entry from.\r\n *\r\n * A THUNK, not a value: the URL is read out of the client build's manifest,\r\n * and a build that discovered no pages has no hydration entry for that read\r\n * to find. Deferring it means a page-free production bundle boots without\r\n * demanding a file it had no reason to emit.\r\n */\r\n resolveHydrationClientModuleUrl: () => string;\r\n /**\r\n * Where the client build wrote its output. The stylesheets every page must\r\n * link are read from the manifest inside it — resolved HERE rather than by\r\n * the caller, because the barrel that owns that reader is the one this\r\n * function already imports at boot.\r\n *\r\n * OPTIONAL for the same reason `PageManifest.clientDir` is: a build that\r\n * discovered zero pages emits no client bundle, so there is no directory to\r\n * name — and no page that could need a stylesheet either.\r\n */\r\n clientDir?: string;\r\n};\r\n\r\n/**\r\n * A manifest page whose module namespace carries no `route` export.\r\n *\r\n * Page discovery only emits a module it read a `route` off, so a valid build\r\n * cannot produce this — a stale artifact or a hand-edited barrel can. The\r\n * alternative to refusing it is a page table that silently shrinks: the\r\n * operator gets a clean boot log and a 404 on a page that is demonstrably in\r\n * the bundle, with nothing anywhere naming the file. The file is named here\r\n * because it is the entire diagnosis.\r\n */\r\nexport class PageManifestEntryMissingRouteError extends Error {\r\n public constructor(sourceFile: string) {\r\n super(\r\n `Cannot serve pages: the page manifest entry for \"${sourceFile}\" has no \\`route\\` export, ` +\r\n \"so it has no URL to be registered under. This build's generated `pages.ts` barrel does \" +\r\n \"not match the sources it was generated from — re-run `warlock build`, and if the page is \" +\r\n \"meant to be served, give it a `route` export.\",\r\n );\r\n this.name = \"PageManifestEntryMissingRouteError\";\r\n }\r\n}\r\n\r\n/**\r\n * Refuse the whole table before any of it is registered.\r\n *\r\n * Checked in one pass UP FRONT rather than per page inside the registration\r\n * loop: a half-installed page table is worse than a refused boot, because it\r\n * serves.\r\n */\r\nfunction assertEveryPageDeclaresRoute(manifest: PageManifest): void {\r\n for (const page of manifest.pages) {\r\n if ((page.module as PageModuleShape).route === undefined) {\r\n throw new PageManifestEntryMissingRouteError(page.sourceFile);\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Register every page the manifest carries, and connect the pipeline the\r\n * handlers will run in.\r\n *\r\n * An empty table registers nothing and is not an error: \"built with web, no\r\n * pages\" is a legal state of a built application, and it is also the one state\r\n * where there is no pipeline to connect and no hydration entry to point at, so\r\n * it returns before doing either.\r\n */\r\nexport async function installProductionPageRoutes(\r\n options: InstallProductionPageRoutesOptions,\r\n): Promise<InstalledManifestPageRoute[]> {\r\n const {\r\n router,\r\n manifest,\r\n pageContext,\r\n sharedStore,\r\n applyBufferedCookie,\r\n resolveHydrationClientModuleUrl,\r\n clientDir,\r\n } = options;\r\n\r\n assertEveryPageDeclaresRoute(manifest);\r\n\r\n if (manifest.pages.length === 0) return [];\r\n\r\n // Loaded at boot rather than statically, and through the BARREL: production\r\n // has one module graph, so the barrel's instance of the pipeline is the same\r\n // instance the manifest's page modules were linked against, and connecting\r\n // the request context on it connects it for every handler registered below.\r\n // Development cannot do this — its modules live in Vite's separate graph, and\r\n // it wires the same two seams on the copy Vite evaluated.\r\n const webServer = await import(\"./index\");\r\n\r\n webServer.connectSharedStore(sharedStore);\r\n webServer.connectPageContext(pageContext);\r\n\r\n return webServer.installPageRoutesFromManifest({\r\n router,\r\n manifest,\r\n hydrationClientModuleUrl: resolveHydrationClientModuleUrl(),\r\n stylesheetUrls:\r\n clientDir === undefined ? [] : webServer.productionStylesheetUrls(clientDir),\r\n applyBufferedCookie,\r\n });\r\n}\r\n"],"mappings":";;;;;;;;;;;AA6EA,IAAa,qCAAb,cAAwD,MAAM;CAC5D,AAAO,YAAY,YAAoB;EACrC,MACE,oDAAoD,WAAW,+PAIjE;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAS,6BAA6B,UAA8B;CAClE,KAAK,MAAM,QAAQ,SAAS,OAC1B,IAAK,KAAK,OAA2B,UAAU,QAC7C,MAAM,IAAI,mCAAmC,KAAK,UAAU;AAGlE;;;;;;;;;;AAWA,eAAsB,4BACpB,SACuC;CACvC,MAAM,EACJ,QACA,UACA,aACA,aACA,qBACA,iCACA,cACE;CAEJ,6BAA6B,QAAQ;CAErC,IAAI,SAAS,MAAM,WAAW,GAAG,OAAO,CAAC;CAQzC,MAAM,YAAY,MAAM,OAAO;CAE/B,UAAU,mBAAmB,WAAW;CACxC,UAAU,mBAAmB,WAAW;CAExC,OAAO,UAAU,8BAA8B;EAC7C;EACA;EACA,0BAA0B,gCAAgC;EAC1D,gBACE,cAAc,SAAY,CAAC,IAAI,UAAU,yBAAyB,SAAS;EAC7E;CACF,CAAC;AACH"}
|