@warlock.js/web 5.0.2 → 5.2.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 +250 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -4
- package/esm/build/contribution.mjs +46 -22
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +318 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- 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 +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +44 -7
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.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.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +15 -1
- package/esm/metadata.mjs +45 -0
- package/esm/metadata.mjs.map +1 -0
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +120 -0
- package/esm/routing/url-fragment.mjs.map +1 -0
- 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 +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +58 -6
- package/esm/server/create-page-route-handler.mjs +127 -40
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- 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 +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +6 -5
- package/esm/server/index.mjs +4 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +91 -8
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -13
- package/esm/server/install-page-routes.mjs +112 -32
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -31
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +126 -0
- package/esm/server/not-found-page.mjs +157 -0
- package/esm/server/not-found-page.mjs.map +1 -0
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- 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 +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- 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 +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- 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 +2 -3
- package/esm/server/web-connector-factory.mjs +6 -4
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +233 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +10 -6
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +247 -2
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +241 -49
- package/llms.txt +6 -2
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +130 -7
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +3 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +20 -8
- package/skills/write-the-root/SKILL.md +5 -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
package/esm/vite/index.mjs
CHANGED
|
@@ -1,18 +1,123 @@
|
|
|
1
1
|
import { HYDRATION_CLIENT_ENTRY_NAME, createHydrationClientEntry } from "./hydration-entries.mjs";
|
|
2
2
|
import { buildHydrationClient } from "./build-client.mjs";
|
|
3
|
-
import { gateAResolve } from "./gate-a-resolve.mjs";
|
|
3
|
+
import { gateAResolve, isAppSourcePath, isRecognizedUniversalSurface, isServerFile, isWithinModuleWebFolder } from "./gate-a-resolve.mjs";
|
|
4
4
|
import { createPublicEnvTracker, gateBSecrets } from "./gate-b-secrets.mjs";
|
|
5
|
-
import { ProjectionAmbiguityError, projection } from "./projection.mjs";
|
|
5
|
+
import { ProjectionAmbiguityError, isProjectableFile, projectModule, projection } from "./projection.mjs";
|
|
6
6
|
import { buildPublicEnvManifest, findLeakedServerExports, findLeakedServerImportEdges, gateCVerify } from "./gate-c-verify.mjs";
|
|
7
|
-
import { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry } from "./page-registry-plugin.mjs";
|
|
7
|
+
import { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry, invalidateClientPageRegistry } from "./page-registry-plugin.mjs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { parse } from "@babel/parser";
|
|
8
10
|
|
|
9
|
-
//#region
|
|
10
|
-
|
|
11
|
+
//#region web/src/vite/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* `@warlock.js/web/vite` — build-tooling subpath, kept separate from the
|
|
14
|
+
* runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into
|
|
15
|
+
* a project that doesn't build with Vite.
|
|
16
|
+
*/
|
|
17
|
+
function moduleKey(id) {
|
|
18
|
+
return id.split("?")[0].replace(/\\/g, "/");
|
|
19
|
+
}
|
|
20
|
+
const CODE_MODULE_EXTENSION = /\.([cm]?[jt]sx?)$/;
|
|
21
|
+
function isStatelessClientSurface(id, appRoot) {
|
|
22
|
+
const bare = moduleKey(id);
|
|
23
|
+
if (!isAppSourcePath(bare, appRoot)) return false;
|
|
24
|
+
if (isServerFile(bare, appRoot)) return false;
|
|
25
|
+
return isProjectableFile(bare) || isRecognizedUniversalSurface(bare) || isWithinModuleWebFolder(bare, appRoot);
|
|
26
|
+
}
|
|
27
|
+
function collectImportSpecifiers(code) {
|
|
28
|
+
const ast = parse(code, {
|
|
29
|
+
sourceType: "module",
|
|
30
|
+
plugins: ["typescript", "jsx"]
|
|
31
|
+
});
|
|
32
|
+
const imports = /* @__PURE__ */ new Set();
|
|
33
|
+
function walk(node) {
|
|
34
|
+
if (!node || typeof node !== "object") return;
|
|
35
|
+
if (Array.isArray(node)) {
|
|
36
|
+
for (const child of node) walk(child);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const record = node;
|
|
40
|
+
if ((record.type === "ImportDeclaration" || record.type === "ExportNamedDeclaration" || record.type === "ExportAllDeclaration") && record.source?.type === "StringLiteral") imports.add(record.source.value);
|
|
41
|
+
if (record.type === "CallExpression" && record.callee?.type === "Import" && record.arguments?.[0]?.type === "StringLiteral") imports.add(record.arguments[0].value);
|
|
42
|
+
if (record.type === "ImportExpression" && record.source?.type === "StringLiteral") imports.add(record.source.value);
|
|
43
|
+
for (const [key, child] of Object.entries(record)) {
|
|
44
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range" || key.endsWith("Comments") || key === "extra") continue;
|
|
45
|
+
walk(child);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
walk(ast.program);
|
|
49
|
+
return imports;
|
|
50
|
+
}
|
|
51
|
+
function clientViewOf(state, code, id) {
|
|
52
|
+
const key = moduleKey(id);
|
|
53
|
+
if (!state.clientBoundModules.has(key) && !isStatelessClientSurface(key, state.appRoot)) return;
|
|
54
|
+
state.clientBoundModules.add(key);
|
|
55
|
+
if (!CODE_MODULE_EXTENSION.test(key)) {
|
|
56
|
+
state.clientImportsByModule.set(key, /* @__PURE__ */ new Set());
|
|
57
|
+
return code;
|
|
58
|
+
}
|
|
59
|
+
const clientCode = isProjectableFile(key) ? projectModule(code, key).code : code;
|
|
60
|
+
state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));
|
|
61
|
+
return clientCode;
|
|
62
|
+
}
|
|
63
|
+
function markResolvedClientModule(state, resolved) {
|
|
64
|
+
if (!resolved) return;
|
|
65
|
+
const id = typeof resolved === "string" ? resolved : resolved.id;
|
|
66
|
+
if (!id.includes("\0")) state.clientBoundModules.add(moduleKey(id));
|
|
67
|
+
}
|
|
68
|
+
function isServerEnvironment(context) {
|
|
69
|
+
return context.environment?.config.consumer === "server";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Keeps the production/client pipeline byte-identical, while giving Gate A
|
|
73
|
+
* and Gate B a validation-only view in Vite's development SSR environment.
|
|
74
|
+
* SSR still evaluates the original source: only the gates receive the
|
|
75
|
+
* projected client view, so loader/server exports retain their legitimate
|
|
76
|
+
* server access while component-visible code is refused before evaluation.
|
|
77
|
+
*/
|
|
78
|
+
function clientEnvironmentOnly(plugin, ssrState) {
|
|
79
|
+
const validatesDevSsr = plugin.name === "warlock:gate-a-resolve" || plugin.name === "warlock:gate-b-secrets";
|
|
80
|
+
const originalTransform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
|
|
81
|
+
const originalResolveId = typeof plugin.resolveId === "function" ? plugin.resolveId : plugin.resolveId?.handler;
|
|
82
|
+
const originalBuildStart = typeof plugin.buildStart === "function" ? plugin.buildStart : plugin.buildStart?.handler;
|
|
11
83
|
return {
|
|
12
84
|
...plugin,
|
|
13
85
|
applyToEnvironment(environment) {
|
|
14
|
-
return environment.config.consumer === "client";
|
|
15
|
-
}
|
|
86
|
+
return environment.config.consumer === "client" || validatesDevSsr && environment.config.consumer === "server";
|
|
87
|
+
},
|
|
88
|
+
buildStart: originalBuildStart ? function(...args) {
|
|
89
|
+
if (isServerEnvironment(this)) return;
|
|
90
|
+
return originalBuildStart.apply(this, args);
|
|
91
|
+
} : void 0,
|
|
92
|
+
transform: originalTransform ? async function(code, id, options) {
|
|
93
|
+
if (!isServerEnvironment(this)) return originalTransform.call(this, code, id, options);
|
|
94
|
+
const clientCode = clientViewOf(ssrState, code, id);
|
|
95
|
+
if (clientCode === void 0) return null;
|
|
96
|
+
const transformed = await originalTransform.call(this, clientCode, id, {
|
|
97
|
+
...options,
|
|
98
|
+
ssr: false
|
|
99
|
+
});
|
|
100
|
+
if (plugin.name === "warlock:gate-a-resolve" && originalResolveId) for (const source of ssrState.clientImportsByModule.get(moduleKey(id)) ?? []) markResolvedClientModule(ssrState, await originalResolveId.call(this, source, id, {
|
|
101
|
+
attributes: {},
|
|
102
|
+
isEntry: false,
|
|
103
|
+
ssr: false
|
|
104
|
+
}));
|
|
105
|
+
return transformed;
|
|
106
|
+
} : void 0,
|
|
107
|
+
resolveId: originalResolveId ? async function(source, importer, options) {
|
|
108
|
+
if (!isServerEnvironment(this)) return originalResolveId.call(this, source, importer, options);
|
|
109
|
+
if (!importer) return null;
|
|
110
|
+
const importerKey = moduleKey(importer);
|
|
111
|
+
if (!(ssrState.clientBoundModules.has(importerKey) || isStatelessClientSurface(importerKey, ssrState.appRoot))) return null;
|
|
112
|
+
const survivingImports = ssrState.clientImportsByModule.get(importerKey);
|
|
113
|
+
if (survivingImports && !survivingImports.has(source)) return null;
|
|
114
|
+
const resolved = await originalResolveId.call(this, source, importer, {
|
|
115
|
+
...options,
|
|
116
|
+
ssr: false
|
|
117
|
+
});
|
|
118
|
+
markResolvedClientModule(ssrState, resolved);
|
|
119
|
+
return resolved;
|
|
120
|
+
} : void 0
|
|
16
121
|
};
|
|
17
122
|
}
|
|
18
123
|
/**
|
|
@@ -115,8 +220,16 @@ function clientEnvironmentOnly(plugin) {
|
|
|
115
220
|
*/
|
|
116
221
|
function warlockClientBoundary(options = {}) {
|
|
117
222
|
const tracker = createPublicEnvTracker();
|
|
223
|
+
const ssrState = {
|
|
224
|
+
appRoot: path.resolve(options.appRoot ?? process.cwd()),
|
|
225
|
+
clientBoundModules: /* @__PURE__ */ new Set(),
|
|
226
|
+
clientImportsByModule: /* @__PURE__ */ new Map()
|
|
227
|
+
};
|
|
118
228
|
return [
|
|
119
|
-
clientPageRegistry({
|
|
229
|
+
clientPageRegistry({
|
|
230
|
+
appRoot: options.appRoot,
|
|
231
|
+
beforePageHotUpdate: options.beforePageHotUpdate
|
|
232
|
+
}),
|
|
120
233
|
projection(),
|
|
121
234
|
gateBSecrets({ tracker }),
|
|
122
235
|
gateAResolve(options),
|
|
@@ -124,7 +237,7 @@ function warlockClientBoundary(options = {}) {
|
|
|
124
237
|
...options,
|
|
125
238
|
tracker
|
|
126
239
|
})
|
|
127
|
-
].map(clientEnvironmentOnly);
|
|
240
|
+
].map((plugin) => clientEnvironmentOnly(plugin, ssrState));
|
|
128
241
|
}
|
|
129
242
|
/**
|
|
130
243
|
* Callable production seam: callers own their exact source aliases and the
|
|
@@ -137,10 +250,10 @@ async function buildWarlockHydrationClient(options) {
|
|
|
137
250
|
outDir: options.outDir,
|
|
138
251
|
resolveAliases: options.resolveAliases,
|
|
139
252
|
external: options.external,
|
|
140
|
-
plugins: warlockClientBoundary({ appRoot: options.appRoot })
|
|
253
|
+
plugins: [...warlockClientBoundary({ appRoot: options.appRoot }), ...options.plugins ?? []]
|
|
141
254
|
});
|
|
142
255
|
}
|
|
143
256
|
|
|
144
257
|
//#endregion
|
|
145
|
-
export { CLIENT_PAGE_REGISTRY_ID, HYDRATION_CLIENT_ENTRY_NAME, ProjectionAmbiguityError, RESOLVED_CLIENT_PAGE_REGISTRY_ID, buildHydrationClient, buildPublicEnvManifest, buildWarlockHydrationClient, clientPageRegistry, createHydrationClientEntry, createPublicEnvTracker, findLeakedServerExports, findLeakedServerImportEdges, gateAResolve, gateBSecrets, gateCVerify, projection, warlockClientBoundary };
|
|
258
|
+
export { CLIENT_PAGE_REGISTRY_ID, HYDRATION_CLIENT_ENTRY_NAME, ProjectionAmbiguityError, RESOLVED_CLIENT_PAGE_REGISTRY_ID, buildHydrationClient, buildPublicEnvManifest, buildWarlockHydrationClient, clientPageRegistry, createHydrationClientEntry, createPublicEnvTracker, findLeakedServerExports, findLeakedServerImportEdges, gateAResolve, gateBSecrets, gateCVerify, invalidateClientPageRegistry, projection, warlockClientBoundary };
|
|
146
259
|
//# sourceMappingURL=index.mjs.map
|
package/esm/vite/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../web/src/vite/index.ts"],"sourcesContent":["/**\n * `@warlock.js/web/vite` — build-tooling subpath, kept separate from the\n * runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into\n * a project that doesn't build with Vite.\n */\nimport type { Plugin } from \"vite\";\nimport {\n buildHydrationClient,\n type BuildHydrationClientOptions,\n type BuildHydrationClientResult,\n} from \"./build-client\";\nimport { gateAResolve } from \"./gate-a-resolve\";\nimport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nimport { gateCVerify } from \"./gate-c-verify\";\nimport { clientPageRegistry } from \"./page-registry-plugin\";\nimport { projection } from \"./projection\";\n\nexport { buildHydrationClient } from \"./build-client\";\nexport type {\n BuildHydrationClientOptions,\n BuildHydrationClientResult,\n HydrationClientBuildOutput,\n} from \"./build-client\";\nexport { gateAResolve } from \"./gate-a-resolve\";\nexport type { EnvironmentClassifier, EnvironmentClassifierOptions, WarlockEnvironment } from \"./gate-a-resolve\";\nexport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nexport type { PublicEnvTracker } from \"./gate-b-secrets\";\nexport {\n buildPublicEnvManifest,\n findLeakedServerExports,\n findLeakedServerImportEdges,\n gateCVerify,\n} from \"./gate-c-verify\";\nexport type {\n GateCOptions,\n PublicEnvManifestEntry,\n ServerExportLeak,\n ServerImportEdgeLeak,\n} from \"./gate-c-verify\";\nexport { HYDRATION_CLIENT_ENTRY_NAME, createHydrationClientEntry } from \"./hydration-entries\";\nexport type { HydrationClientEntry } from \"./hydration-entries\";\nexport {\n CLIENT_PAGE_REGISTRY_ID,\n clientPageRegistry,\n RESOLVED_CLIENT_PAGE_REGISTRY_ID,\n} from \"./page-registry-plugin\";\nexport type { ClientPageRegistryPluginOptions } from \"./page-registry-plugin\";\nexport { projection, ProjectionAmbiguityError } from \"./projection\";\nexport type { ProjectionResult } from \"./projection\";\n\nexport type WarlockClientBoundaryOptions = Parameters<typeof gateAResolve>[0];\n\nexport type BuildWarlockHydrationClientOptions = Readonly<{\n appRoot: string;\n webRoot: string;\n /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */\n outDir: string;\n resolveAliases: BuildHydrationClientOptions[\"resolveAliases\"];\n external?: BuildHydrationClientOptions[\"external\"];\n}>;\n\nfunction clientEnvironmentOnly(plugin: Plugin): Plugin {\n return {\n ...plugin,\n applyToEnvironment(environment) {\n return environment.config.consumer === \"client\";\n },\n };\n}\n\n/**\n * The composed client-build pipeline: projection\n * strips the 5 server exports first, THEN Gate B's `transform` checks\n * whatever source remains for inline secret reads, THEN Gate A's\n * `resolveId` judges whatever imports remain. Array order here is\n * `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own\n * pipeline shape (`transform` before `resolveId`), but array order alone\n * does not guarantee this — see the hook-ordering fact below, which is what\n * actually makes the composition correct.\n *\n * Empirically observed fact (via an instrumented real `vite.build()`, not\n * assumed from plugin array order): for a given module M, Vite/Rollup calls\n * `transform(M)` BEFORE it calls `resolveId` for any of M's own import\n * specifiers — because Rollup must parse M's post-transform source to even\n * discover which specifiers to resolve next. Concretely: `transform` ran on\n * `entry.page.tsx` first, and only after that did `resolveId(\"./dep\", ...)`\n * fire for the import statement still present in the transformed code. A\n * consequence follows directly: if projection's `transform` removes an\n * import statement from a page module entirely (e.g. `loader`'s\n * `@warlock.js/core` import, stripped because `loader` itself is removed),\n * `resolveId` is never invoked for that specifier at all — Gate A doesn't\n * \"let it pass\", it never sees it. Gate A's `resolveId` only fires for\n * imports that survive projection's `transform`, which is exactly why a\n * component-level `@warlock.js/core` import (never touched by projection)\n * still reaches and is refused by Gate A.\n *\n * This is OBSERVED Rollup behavior, not a documented contract — a future\n * Vite/Rollup upgrade could invert it. Pinned by a\n * regression test (`index.spec.ts`, \"D.3 hook ordering pin\") that fails\n * loudly if the ordering ever inverts, and by the `vite` peer floor in\n * `web/package.json` (`>=7.3.5`, the version this was verified against). If\n * that test ever fails after a Vite bump: the failure mode of the ordering\n * assumption breaking is SAFE — projection would stop removing an import\n * statement Gate A still sees, so Gate A would refuse an import it used to\n * silently let a stripped server export take with it. That is a loud build\n * failure (\"Gate A refused an import\"), never a silent client-bundle leak.\n * Do NOT \"fix\" an apparent Gate A false-positive after a Vite upgrade by\n * weakening Gate A (e.g. widening what it lets through) — investigate\n * whether this ordering assumption broke instead; loosening Gate A to work\n * around it would turn a loud failure into the exact silent leak this\n * pipeline exists to prevent.\n *\n * Gate B is placed between the two for the same reason, but for a `transform`\n * hook rather than `resolveId`: within a plugin array, Rollup runs each\n * module's registered `transform` hooks in array order, each one receiving\n * the PREVIOUS plugin's output. Running Gate B after projection means it\n * inspects the POST-projection source — a `process.env.SECRET` read inside\n * `loader` (a server export, legitimately reading a real secret server-side)\n * is invisible to Gate B once projection has already removed `loader`\n * entirely, exactly as it should be: Gate B's job is to fence client-bound\n * code, and projection is what decides what counts as client-bound. A\n * component-level secret read is untouched by projection and still reaches\n * Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`\n * output at all (orthogonal concern, raw source vs. import paths), so its\n * position relative to Gate A is not load-bearing — it is placed before Gate\n * A only to keep both `transform` hooks adjacent in the array.\n *\n * Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after\n * the entire `transform`/`resolveId` build phase has completed for every\n * plugin, regardless of array position (a Rollup lifecycle fact, not\n * something this array order enforces). It verifies the EMITTED output the\n * other three produced: no server export survived as a top-level binding, no\n * import edge into a server-only package survived into the module graph, and\n * it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share\n * one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key\n * exclusion check agree by construction, not by coincidence.\n *\n * `clientPageRegistry()` is FIRST, ahead of projection. It contributes no\n * `transform` at all — only a `resolveId`/`load` pair for one synthetic id —\n * so it cannot displace or pre-empt any gate's inspection of any real file.\n * Two reasons for the position, one of which is not load-bearing and is\n * labelled as such:\n *\n * 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's\n * `resolveId` (also `enforce: \"pre\"`) reaches its `this.resolve(...)` call\n * for that specifier. Gate A's nested resolve would find it anyway, but\n * routing the id through Gate A's importer-chain bookkeeping only to have\n * it come back means a synthetic id can surface in a user-facing \"Import\n * chain:\" message. Resolving it first keeps ownership of the id in one\n * place.\n * 2. NOT load-bearing: the position relative to `projection()`. Projection is\n * `enforce: \"pre\"` and selects by file BASENAME (`projection.ts:242-248`),\n * so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that\n * enters the graph regardless of who imported it or where this plugin sits.\n * The registry emits absolute POSIX specifiers that Rollup resolves and\n * loads as ORDINARY file modules — they are not inlined into the virtual\n * module — so each one is transformed exactly as a page imported from a\n * real file would be. Projection declines the virtual module itself\n * (`\\0virtual:warlock/pages` has no matching basename), which is correct:\n * generated code has no server exports to strip.\n *\n * Point 2 is asserted, not assumed, by a real `vite.build()` in\n * `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its\n * `loader` — imports a marker module that Gate A independently PERMITS, built\n * through this exact array, with the marker proven absent from every emitted\n * chunk while the page's own component text is proven present. Inspecting this\n * array's order would prove nothing about what reaches the browser.\n */\nexport function warlockClientBoundary(options: Parameters<typeof gateAResolve>[0] = {}): Plugin[] {\n const tracker = createPublicEnvTracker();\n return [\n clientPageRegistry({ appRoot: options.appRoot }),\n projection(),\n gateBSecrets({ tracker }),\n gateAResolve(options),\n gateCVerify({ ...options, tracker }),\n ].map(clientEnvironmentOnly);\n}\n\n/**\n * Callable production seam: callers own their exact source aliases and the\n * app-root classification boundary, while this module owns the one canonical\n * projection/Gate B/Gate A/Gate C composition.\n */\nexport async function buildWarlockHydrationClient(\n options: BuildWarlockHydrationClientOptions,\n): Promise<BuildHydrationClientResult> {\n return buildHydrationClient({\n webRoot: options.webRoot,\n outDir: options.outDir,\n resolveAliases: options.resolveAliases,\n external: options.external,\n plugins: warlockClientBoundary({ appRoot: options.appRoot }),\n });\n}\n"],"mappings":";;;;;;;;;AA6DA,SAAS,sBAAsB,QAAwB;CACrD,OAAO;EACL,GAAG;EACH,mBAAmB,aAAa;GAC9B,OAAO,YAAY,OAAO,aAAa;EACzC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoGA,SAAgB,sBAAsB,UAA8C,CAAC,GAAa;CAChG,MAAM,UAAU,uBAAuB;CACvC,OAAO;EACL,mBAAmB,EAAE,SAAS,QAAQ,QAAQ,CAAC;EAC/C,WAAW;EACX,aAAa,EAAE,QAAQ,CAAC;EACxB,aAAa,OAAO;EACpB,YAAY;GAAE,GAAG;GAAS;EAAQ,CAAC;CACrC,CAAC,CAAC,IAAI,qBAAqB;AAC7B;;;;;;AAOA,eAAsB,4BACpB,SACqC;CACrC,OAAO,qBAAqB;EAC1B,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAChB,gBAAgB,QAAQ;EACxB,UAAU,QAAQ;EAClB,SAAS,sBAAsB,EAAE,SAAS,QAAQ,QAAQ,CAAC;CAC7D,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/index.ts"],"sourcesContent":["/**\n * `@warlock.js/web/vite` — build-tooling subpath, kept separate from the\n * runtime barrel (`@warlock.js/web`) so importing it never pulls `vite` into\n * a project that doesn't build with Vite.\n */\nimport { parse } from \"@babel/parser\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport {\n buildHydrationClient,\n type BuildHydrationClientOptions,\n type BuildHydrationClientResult,\n} from \"./build-client\";\nimport {\n gateAResolve,\n isAppSourcePath,\n isRecognizedUniversalSurface,\n isServerFile,\n isWithinModuleWebFolder,\n} from \"./gate-a-resolve\";\nimport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nimport { gateCVerify } from \"./gate-c-verify\";\nimport {\n clientPageRegistry,\n type ClientPageRegistryPluginOptions,\n} from \"./page-registry-plugin\";\nimport { isProjectableFile, projectModule, projection } from \"./projection\";\n\nexport { buildHydrationClient } from \"./build-client\";\nexport type {\n BuildHydrationClientOptions,\n BuildHydrationClientResult,\n HydrationClientBuildOutput,\n} from \"./build-client\";\nexport { gateAResolve } from \"./gate-a-resolve\";\nexport type {\n EnvironmentClassifier,\n EnvironmentClassifierOptions,\n WarlockEnvironment,\n} from \"./gate-a-resolve\";\nexport { createPublicEnvTracker, gateBSecrets } from \"./gate-b-secrets\";\nexport type { PublicEnvTracker } from \"./gate-b-secrets\";\nexport {\n buildPublicEnvManifest,\n findLeakedServerExports,\n findLeakedServerImportEdges,\n gateCVerify,\n} from \"./gate-c-verify\";\nexport type {\n GateCOptions,\n PublicEnvManifestEntry,\n ServerExportLeak,\n ServerImportEdgeLeak,\n} from \"./gate-c-verify\";\nexport {\n HYDRATION_CLIENT_ENTRY_NAME,\n createHydrationClientEntry,\n} from \"./hydration-entries\";\nexport type { HydrationClientEntry } from \"./hydration-entries\";\nexport {\n CLIENT_PAGE_REGISTRY_ID,\n clientPageRegistry,\n invalidateClientPageRegistry,\n RESOLVED_CLIENT_PAGE_REGISTRY_ID,\n} from \"./page-registry-plugin\";\nexport type { ClientPageRegistryPluginOptions } from \"./page-registry-plugin\";\nexport { projection, ProjectionAmbiguityError } from \"./projection\";\nexport type { ProjectionResult } from \"./projection\";\n\nexport type WarlockClientBoundaryOptions = Parameters<\n typeof gateAResolve\n>[0] & {\n beforePageHotUpdate?: ClientPageRegistryPluginOptions[\"beforePageHotUpdate\"];\n};\n\nexport type BuildWarlockHydrationClientOptions = Readonly<{\n appRoot: string;\n webRoot: string;\n /** Absolute client output dir — threaded to `buildHydrationClient` (`<outdir>/client`). */\n outDir: string;\n resolveAliases: BuildHydrationClientOptions[\"resolveAliases\"];\n external?: BuildHydrationClientOptions[\"external\"];\n /** App-configured plugins, appended after Warlock's client-boundary pipeline. */\n plugins?: BuildHydrationClientOptions[\"plugins\"];\n}>;\n\ntype SsrBoundaryState = {\n readonly appRoot: string;\n readonly clientBoundModules: Set<string>;\n readonly clientImportsByModule: Map<string, Set<string>>;\n};\n\nfunction moduleKey(id: string): string {\n return id.split(\"?\")[0].replace(/\\\\/g, \"/\");\n}\n\nconst CODE_MODULE_EXTENSION = /\\.([cm]?[jt]sx?)$/;\n\nfunction isStatelessClientSurface(id: string, appRoot: string): boolean {\n const bare = moduleKey(id);\n if (!isAppSourcePath(bare, appRoot)) return false;\n if (isServerFile(bare, appRoot)) return false;\n\n return (\n isProjectableFile(bare) ||\n isRecognizedUniversalSurface(bare) ||\n isWithinModuleWebFolder(bare, appRoot)\n );\n}\n\nfunction collectImportSpecifiers(code: string): Set<string> {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n const imports = new Set<string>();\n\n function walk(node: unknown): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const child of node) walk(child);\n return;\n }\n\n const record = node as Record<string, any>;\n if (\n (record.type === \"ImportDeclaration\" ||\n record.type === \"ExportNamedDeclaration\" ||\n record.type === \"ExportAllDeclaration\") &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n if (\n record.type === \"CallExpression\" &&\n record.callee?.type === \"Import\" &&\n record.arguments?.[0]?.type === \"StringLiteral\"\n ) {\n imports.add(record.arguments[0].value);\n }\n if (\n record.type === \"ImportExpression\" &&\n record.source?.type === \"StringLiteral\"\n ) {\n imports.add(record.source.value);\n }\n\n for (const [key, child] of Object.entries(record)) {\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\" ||\n key.endsWith(\"Comments\") ||\n key === \"extra\"\n ) {\n continue;\n }\n walk(child);\n }\n }\n\n walk(ast.program);\n return imports;\n}\n\nfunction clientViewOf(\n state: SsrBoundaryState,\n code: string,\n id: string,\n): string | undefined {\n const key = moduleKey(id);\n if (\n !state.clientBoundModules.has(key) &&\n !isStatelessClientSurface(key, state.appRoot)\n ) {\n return undefined;\n }\n\n state.clientBoundModules.add(key);\n if (!CODE_MODULE_EXTENSION.test(key)) {\n state.clientImportsByModule.set(key, new Set());\n return code;\n }\n\n const clientCode = isProjectableFile(key)\n ? projectModule(code, key).code\n : code;\n state.clientImportsByModule.set(key, collectImportSpecifiers(clientCode));\n return clientCode;\n}\n\nfunction markResolvedClientModule(\n state: SsrBoundaryState,\n resolved: { id: string } | string | null | false | void,\n): void {\n if (!resolved) return;\n const id = typeof resolved === \"string\" ? resolved : resolved.id;\n if (!id.includes(\"\\0\")) state.clientBoundModules.add(moduleKey(id));\n}\n\nfunction isServerEnvironment(context: {\n environment?: { config: { consumer?: string } };\n}): boolean {\n return context.environment?.config.consumer === \"server\";\n}\n\n/**\n * Keeps the production/client pipeline byte-identical, while giving Gate A\n * and Gate B a validation-only view in Vite's development SSR environment.\n * SSR still evaluates the original source: only the gates receive the\n * projected client view, so loader/server exports retain their legitimate\n * server access while component-visible code is refused before evaluation.\n */\nfunction clientEnvironmentOnly(\n plugin: Plugin,\n ssrState: SsrBoundaryState,\n): Plugin {\n const validatesDevSsr =\n plugin.name === \"warlock:gate-a-resolve\" ||\n plugin.name === \"warlock:gate-b-secrets\";\n const originalTransform =\n typeof plugin.transform === \"function\"\n ? plugin.transform\n : plugin.transform?.handler;\n const originalResolveId =\n typeof plugin.resolveId === \"function\"\n ? plugin.resolveId\n : plugin.resolveId?.handler;\n const originalBuildStart =\n typeof plugin.buildStart === \"function\"\n ? plugin.buildStart\n : plugin.buildStart?.handler;\n\n return {\n ...plugin,\n applyToEnvironment(environment) {\n return (\n environment.config.consumer === \"client\" ||\n (validatesDevSsr && environment.config.consumer === \"server\")\n );\n },\n buildStart: originalBuildStart\n ? function (...args) {\n if (isServerEnvironment(this)) return;\n return originalBuildStart.apply(this, args);\n }\n : undefined,\n transform: originalTransform\n ? async function (code, id, options) {\n if (!isServerEnvironment(this)) {\n return originalTransform.call(this, code, id, options);\n }\n\n const clientCode = clientViewOf(ssrState, code, id);\n if (clientCode === undefined) return null;\n\n const transformed = await originalTransform.call(\n this,\n clientCode,\n id,\n {\n ...options,\n ssr: false,\n },\n );\n\n // Vite can externalize a package in the SSR environment before its\n // normal resolver walk offers that edge to a plugin. Gate A cannot\n // wait for that walk: validate every import that survived projection\n // now, while the original TypeScript source and importer are known.\n if (plugin.name === \"warlock:gate-a-resolve\" && originalResolveId) {\n for (const source of ssrState.clientImportsByModule.get(\n moduleKey(id),\n ) ?? []) {\n const resolved = await originalResolveId.call(this, source, id, {\n attributes: {},\n isEntry: false,\n ssr: false,\n });\n markResolvedClientModule(ssrState, resolved);\n }\n }\n\n return transformed;\n }\n : undefined,\n resolveId: originalResolveId\n ? async function (source, importer, options) {\n if (!isServerEnvironment(this)) {\n return originalResolveId.call(this, source, importer, options);\n }\n\n if (!importer) return null;\n const importerKey = moduleKey(importer);\n const isClientBound =\n ssrState.clientBoundModules.has(importerKey) ||\n isStatelessClientSurface(importerKey, ssrState.appRoot);\n if (!isClientBound) return null;\n\n const survivingImports =\n ssrState.clientImportsByModule.get(importerKey);\n if (survivingImports && !survivingImports.has(source)) return null;\n\n const resolved = await originalResolveId.call(\n this,\n source,\n importer,\n {\n ...options,\n ssr: false,\n },\n );\n markResolvedClientModule(ssrState, resolved);\n return resolved;\n }\n : undefined,\n };\n}\n\n/**\n * The composed client-build pipeline: projection\n * strips the 5 server exports first, THEN Gate B's `transform` checks\n * whatever source remains for inline secret reads, THEN Gate A's\n * `resolveId` judges whatever imports remain. Array order here is\n * `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own\n * pipeline shape (`transform` before `resolveId`), but array order alone\n * does not guarantee this — see the hook-ordering fact below, which is what\n * actually makes the composition correct.\n *\n * Empirically observed fact (via an instrumented real `vite.build()`, not\n * assumed from plugin array order): for a given module M, Vite/Rollup calls\n * `transform(M)` BEFORE it calls `resolveId` for any of M's own import\n * specifiers — because Rollup must parse M's post-transform source to even\n * discover which specifiers to resolve next. Concretely: `transform` ran on\n * `entry.page.tsx` first, and only after that did `resolveId(\"./dep\", ...)`\n * fire for the import statement still present in the transformed code. A\n * consequence follows directly: if projection's `transform` removes an\n * import statement from a page module entirely (e.g. `loader`'s\n * `@warlock.js/core` import, stripped because `loader` itself is removed),\n * `resolveId` is never invoked for that specifier at all — Gate A doesn't\n * \"let it pass\", it never sees it. Gate A's `resolveId` only fires for\n * imports that survive projection's `transform`, which is exactly why a\n * component-level `@warlock.js/core` import (never touched by projection)\n * still reaches and is refused by Gate A.\n *\n * This is OBSERVED Rollup behavior, not a documented contract — a future\n * Vite/Rollup upgrade could invert it. Pinned by a\n * regression test (`index.spec.ts`, \"D.3 hook ordering pin\") that fails\n * loudly if the ordering ever inverts, and by the `vite` peer floor in\n * `web/package.json` (`>=7.3.5`, the version this was verified against). If\n * that test ever fails after a Vite bump: the failure mode of the ordering\n * assumption breaking is SAFE — projection would stop removing an import\n * statement Gate A still sees, so Gate A would refuse an import it used to\n * silently let a stripped server export take with it. That is a loud build\n * failure (\"Gate A refused an import\"), never a silent client-bundle leak.\n * Do NOT \"fix\" an apparent Gate A false-positive after a Vite upgrade by\n * weakening Gate A (e.g. widening what it lets through) — investigate\n * whether this ordering assumption broke instead; loosening Gate A to work\n * around it would turn a loud failure into the exact silent leak this\n * pipeline exists to prevent.\n *\n * Gate B is placed between the two for the same reason, but for a `transform`\n * hook rather than `resolveId`: within a plugin array, Rollup runs each\n * module's registered `transform` hooks in array order, each one receiving\n * the PREVIOUS plugin's output. Running Gate B after projection means it\n * inspects the POST-projection source — a `process.env.SECRET` read inside\n * `loader` (a server export, legitimately reading a real secret server-side)\n * is invisible to Gate B once projection has already removed `loader`\n * entirely, exactly as it should be: Gate B's job is to fence client-bound\n * code, and projection is what decides what counts as client-bound. A\n * component-level secret read is untouched by projection and still reaches\n * Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`\n * output at all (orthogonal concern, raw source vs. import paths), so its\n * position relative to Gate A is not load-bearing — it is placed before Gate\n * A only to keep both `transform` hooks adjacent in the array.\n *\n * Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after\n * the entire `transform`/`resolveId` build phase has completed for every\n * plugin, regardless of array position (a Rollup lifecycle fact, not\n * something this array order enforces). It verifies the EMITTED output the\n * other three produced: no server export survived as a top-level binding, no\n * import edge into a server-only package survived into the module graph, and\n * it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share\n * one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key\n * exclusion check agree by construction, not by coincidence.\n *\n * `clientPageRegistry()` is FIRST, ahead of projection. It contributes no\n * `transform` at all — only a `resolveId`/`load` pair for one synthetic id —\n * so it cannot displace or pre-empt any gate's inspection of any real file.\n * Two reasons for the position, one of which is not load-bearing and is\n * labelled as such:\n *\n * 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's\n * `resolveId` (also `enforce: \"pre\"`) reaches its `this.resolve(...)` call\n * for that specifier. Gate A's nested resolve would find it anyway, but\n * routing the id through Gate A's importer-chain bookkeeping only to have\n * it come back means a synthetic id can surface in a user-facing \"Import\n * chain:\" message. Resolving it first keeps ownership of the id in one\n * place.\n * 2. NOT load-bearing: the position relative to `projection()`. Projection is\n * `enforce: \"pre\"` and selects by file BASENAME (`projection.ts:242-248`),\n * so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that\n * enters the graph regardless of who imported it or where this plugin sits.\n * The registry emits absolute POSIX specifiers that Rollup resolves and\n * loads as ORDINARY file modules — they are not inlined into the virtual\n * module — so each one is transformed exactly as a page imported from a\n * real file would be. Projection declines the virtual module itself\n * (`\\0virtual:warlock/pages` has no matching basename), which is correct:\n * generated code has no server exports to strip.\n *\n * Point 2 is asserted, not assumed, by a real `vite.build()` in\n * `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its\n * `loader` — imports a marker module that Gate A independently PERMITS, built\n * through this exact array, with the marker proven absent from every emitted\n * chunk while the page's own component text is proven present. Inspecting this\n * array's order would prove nothing about what reaches the browser.\n */\nexport function warlockClientBoundary(\n options: WarlockClientBoundaryOptions = {},\n): Plugin[] {\n const tracker = createPublicEnvTracker();\n const ssrState: SsrBoundaryState = {\n appRoot: path.resolve(options.appRoot ?? process.cwd()),\n clientBoundModules: new Set(),\n clientImportsByModule: new Map(),\n };\n return [\n clientPageRegistry({\n appRoot: options.appRoot,\n beforePageHotUpdate: options.beforePageHotUpdate,\n }),\n projection(),\n gateBSecrets({ tracker }),\n gateAResolve(options),\n gateCVerify({ ...options, tracker }),\n ].map((plugin) => clientEnvironmentOnly(plugin, ssrState));\n}\n\n/**\n * Callable production seam: callers own their exact source aliases and the\n * app-root classification boundary, while this module owns the one canonical\n * projection/Gate B/Gate A/Gate C composition.\n */\nexport async function buildWarlockHydrationClient(\n options: BuildWarlockHydrationClientOptions,\n): Promise<BuildHydrationClientResult> {\n return buildHydrationClient({\n webRoot: options.webRoot,\n outDir: options.outDir,\n resolveAliases: options.resolveAliases,\n external: options.external,\n plugins: [\n ...warlockClientBoundary({ appRoot: options.appRoot }),\n ...(options.plugins ?? []),\n ],\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Plugin } from "vite";
|
|
1
|
+
import { Plugin, ViteDevServer } from "vite";
|
|
2
2
|
|
|
3
|
-
//#region
|
|
3
|
+
//#region web/src/vite/page-registry-plugin.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The specifier application code writes.
|
|
6
6
|
*
|
|
@@ -15,9 +15,28 @@ declare const CLIENT_PAGE_REGISTRY_ID = "virtual:warlock/pages";
|
|
|
15
15
|
* (and no filesystem watcher) mistakes it for a real path.
|
|
16
16
|
*/
|
|
17
17
|
declare const RESOLVED_CLIENT_PAGE_REGISTRY_ID = "\0virtual:warlock/pages";
|
|
18
|
+
/**
|
|
19
|
+
* Evicts the client registry so its next request re-runs page discovery, then
|
|
20
|
+
* reloads the document so hydration consumes that fresh registry.
|
|
21
|
+
*
|
|
22
|
+
* Vite 7 keeps separate module graphs per environment. Pages are imported by
|
|
23
|
+
* the browser, so only the resolved virtual module in the client graph is the
|
|
24
|
+
* cache entry this operation owns.
|
|
25
|
+
*/
|
|
26
|
+
declare function invalidateClientPageRegistry(vite: ViteDevServer): void;
|
|
18
27
|
type ClientPageRegistryPluginOptions = {
|
|
19
28
|
/** Absolute path to the application root. Defaults to `process.cwd()`, matching Vite's own default `root` and Gate A's `appRoot` default. */appRoot?: string; /** Source directory name under `appRoot`; forwarded verbatim to `discoverPages`, which defaults it to `"src"`. */
|
|
20
29
|
srcDir?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Optional server-side barrier run before this plugin decides how the browser
|
|
32
|
+
* receives a page update. `true` means the callback already published the
|
|
33
|
+
* new route graph and sent the required reload, so this hook emits no second
|
|
34
|
+
* update for the same filesystem event.
|
|
35
|
+
*/
|
|
36
|
+
beforePageHotUpdate?: (context: {
|
|
37
|
+
file: string;
|
|
38
|
+
type: "create" | "update" | "delete";
|
|
39
|
+
}) => boolean | Promise<boolean>;
|
|
21
40
|
};
|
|
22
41
|
/**
|
|
23
42
|
* Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.
|
|
@@ -39,5 +58,5 @@ type ClientPageRegistryPluginOptions = {
|
|
|
39
58
|
*/
|
|
40
59
|
declare function clientPageRegistry(options?: ClientPageRegistryPluginOptions): Plugin;
|
|
41
60
|
//#endregion
|
|
42
|
-
export { CLIENT_PAGE_REGISTRY_ID, ClientPageRegistryPluginOptions, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry };
|
|
61
|
+
export { CLIENT_PAGE_REGISTRY_ID, ClientPageRegistryPluginOptions, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry, invalidateClientPageRegistry };
|
|
43
62
|
//# sourceMappingURL=page-registry-plugin.d.mts.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { discoverPages, toPosix } from "../build/discover-pages.mjs";
|
|
2
|
+
import { SERVER_EXPORT_NAMES } from "./projection.mjs";
|
|
2
3
|
import { generateClientRegistry } from "../build/generate-client-registry.mjs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { parse } from "@babel/parser";
|
|
5
6
|
import MagicString from "magic-string";
|
|
6
7
|
|
|
7
|
-
//#region
|
|
8
|
+
//#region web/src/vite/page-registry-plugin.ts
|
|
8
9
|
/**
|
|
9
10
|
* The wire between the two halves that already existed and never met:
|
|
10
11
|
* `discoverPages` (the page graph, read off disk) and `generateClientRegistry`
|
|
@@ -33,6 +34,23 @@ const CLIENT_PAGE_REGISTRY_ID = "virtual:warlock/pages";
|
|
|
33
34
|
*/
|
|
34
35
|
const RESOLVED_CLIENT_PAGE_REGISTRY_ID = `\0${CLIENT_PAGE_REGISTRY_ID}`;
|
|
35
36
|
/**
|
|
37
|
+
* Evicts the client registry so its next request re-runs page discovery, then
|
|
38
|
+
* reloads the document so hydration consumes that fresh registry.
|
|
39
|
+
*
|
|
40
|
+
* Vite 7 keeps separate module graphs per environment. Pages are imported by
|
|
41
|
+
* the browser, so only the resolved virtual module in the client graph is the
|
|
42
|
+
* cache entry this operation owns.
|
|
43
|
+
*/
|
|
44
|
+
function invalidateClientPageRegistry(vite) {
|
|
45
|
+
const moduleGraph = vite.environments.client.moduleGraph;
|
|
46
|
+
const registryModule = moduleGraph.getModuleById(RESOLVED_CLIENT_PAGE_REGISTRY_ID);
|
|
47
|
+
if (registryModule) moduleGraph.invalidateModule(registryModule);
|
|
48
|
+
vite.hot.send({
|
|
49
|
+
type: "full-reload",
|
|
50
|
+
path: "*"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
36
54
|
* The import specifiers the emitted registry names must be ABSOLUTE POSIX file
|
|
37
55
|
* paths, never relative ones.
|
|
38
56
|
*
|
|
@@ -95,6 +113,184 @@ function eraseTypes(source) {
|
|
|
95
113
|
return magic.toString();
|
|
96
114
|
}
|
|
97
115
|
/**
|
|
116
|
+
* The four file shapes that carry SERVER data (`metadata` chief among them)
|
|
117
|
+
* and are therefore projected before the client graph forms — the exact set
|
|
118
|
+
* `projection.ts`'s `isProjectableFile` matches, spelled here by BASENAME so
|
|
119
|
+
* the two agree by construction on what "a server-side page module" is. A
|
|
120
|
+
* change to one of these is the only kind of change whose SERVER half
|
|
121
|
+
* (`metadata`, `loader`, …) can move without the client half moving at all.
|
|
122
|
+
*/
|
|
123
|
+
function isServerPageModule(file) {
|
|
124
|
+
const base = path.basename(file.split("?")[0]);
|
|
125
|
+
if (/\.page\.tsx?$/.test(base)) return true;
|
|
126
|
+
if (base === "layout.tsx" || base === "layout.ts") return true;
|
|
127
|
+
if (/\.layout\.tsx?$/.test(base)) return true;
|
|
128
|
+
if (base === "root.tsx") return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* What replaces a refresh-safe body in the skeleton. Its content is irrelevant —
|
|
133
|
+
* only that it is CONSTANT, so two sources that differ solely inside a masked
|
|
134
|
+
* body serialise identically.
|
|
135
|
+
*/
|
|
136
|
+
const MASKED_REFRESH_BODY = "/*warlock:refresh-body*/";
|
|
137
|
+
/** React's own convention, and the one `react-refresh` itself uses: components are PascalCase. */
|
|
138
|
+
function isComponentName(name) {
|
|
139
|
+
return typeof name === "string" && /^[A-Z]/.test(name);
|
|
140
|
+
}
|
|
141
|
+
/** The `body` node of a function-shaped expression/declaration, or `undefined` for anything else. */
|
|
142
|
+
function functionBody(node) {
|
|
143
|
+
if (!node) return void 0;
|
|
144
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") return node.body;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Generic duck-typed identifier walk, the same shape `projection.ts`'s
|
|
148
|
+
* `collectIdentifierNames` uses (it is not exported, and re-deriving one
|
|
149
|
+
* OVER-collecting walk is safe here for the same reason it is safe there).
|
|
150
|
+
*
|
|
151
|
+
* Over-collecting — counting an object property key or a shadowing parameter
|
|
152
|
+
* as a "read" — can only make the reachable set BIGGER, which can only UNMASK
|
|
153
|
+
* more component bodies, which can only produce more reloads. The safe
|
|
154
|
+
* direction.
|
|
155
|
+
*/
|
|
156
|
+
function collectIdentifierNames(node, names) {
|
|
157
|
+
if (!node || typeof node !== "object") return;
|
|
158
|
+
if (Array.isArray(node)) {
|
|
159
|
+
for (const item of node) collectIdentifierNames(item, names);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const record = node;
|
|
163
|
+
if (typeof record.type !== "string") return;
|
|
164
|
+
if (record.type === "Identifier" || record.type === "JSXIdentifier") names.add(record.name);
|
|
165
|
+
for (const key of Object.keys(record)) {
|
|
166
|
+
if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
|
|
167
|
+
if (key === "leadingComments" || key === "trailingComments" || key === "innerComments" || key === "extra") continue;
|
|
168
|
+
collectIdentifierNames(record[key], names);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** The module-scope names a top-level statement binds (the `export` wrapper looked through). */
|
|
172
|
+
function topLevelBoundNames(stmt) {
|
|
173
|
+
const names = /* @__PURE__ */ new Set();
|
|
174
|
+
const declaration = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
175
|
+
if (!declaration) return names;
|
|
176
|
+
if (declaration.type === "VariableDeclaration") {
|
|
177
|
+
for (const declarator of declaration.declarations) if (declarator.id?.type === "Identifier") names.add(declarator.id.name);
|
|
178
|
+
} else if (declaration.id?.type === "Identifier") names.add(declaration.id.name);
|
|
179
|
+
return names;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Every module-scope name reachable from one of the six server exports.
|
|
183
|
+
*
|
|
184
|
+
* Used ONLY to UNMASK: a PascalCase function that `metadata` or `loader` can
|
|
185
|
+
* reach is not a component for this purpose, it is a server-side helper that
|
|
186
|
+
* merely looks like one, and a change inside its body must reload. Seeded from
|
|
187
|
+
* any top-level statement binding a `SERVER_EXPORT_NAMES` name — deliberately
|
|
188
|
+
* looser than `projection.ts`'s own `isServerExportDeclaration` (no export
|
|
189
|
+
* requirement, no single-declarator requirement), because seeding from MORE
|
|
190
|
+
* statements can only unmask more, i.e. reload more.
|
|
191
|
+
*
|
|
192
|
+
* Fixpoint, not one pass, for the same reason projection's is: a server-only
|
|
193
|
+
* helper can be reached only through another server-only helper.
|
|
194
|
+
*/
|
|
195
|
+
function serverReachableNames(body) {
|
|
196
|
+
const reached = /* @__PURE__ */ new Set();
|
|
197
|
+
const declarations = body.filter((stmt) => stmt.type !== "ImportDeclaration").map((stmt) => ({
|
|
198
|
+
stmt,
|
|
199
|
+
names: topLevelBoundNames(stmt)
|
|
200
|
+
}));
|
|
201
|
+
for (const { stmt, names } of declarations) {
|
|
202
|
+
let isServerExport = false;
|
|
203
|
+
for (const name of names) if (SERVER_EXPORT_NAMES.has(name)) isServerExport = true;
|
|
204
|
+
if (isServerExport) collectIdentifierNames(stmt, reached);
|
|
205
|
+
}
|
|
206
|
+
for (let changed = true; changed;) {
|
|
207
|
+
changed = false;
|
|
208
|
+
for (const { stmt, names } of declarations) {
|
|
209
|
+
let isReached = false;
|
|
210
|
+
for (const name of names) if (reached.has(name)) isReached = true;
|
|
211
|
+
if (!isReached) continue;
|
|
212
|
+
const before = reached.size;
|
|
213
|
+
collectIdentifierNames(stmt, reached);
|
|
214
|
+
if (reached.size !== before) changed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return reached;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The body node to mask for a top-level statement, or `undefined` if this
|
|
221
|
+
* statement is neither a component declaration nor an exported `register`
|
|
222
|
+
* declaration.
|
|
223
|
+
*
|
|
224
|
+
* Recognised shapes, and only these:
|
|
225
|
+
* - `export default function () {…}` / `export default () => …` — the page
|
|
226
|
+
* component, whatever it is called.
|
|
227
|
+
* - `function Name() {…}` / `const Name = () => …` (PascalCase, optionally
|
|
228
|
+
* `export`ed) — a component declared alongside it.
|
|
229
|
+
* - `export function register() {…}` / `export const register = () => …` —
|
|
230
|
+
* the lifecycle hook whose replacement namespace is invoked by projection.
|
|
231
|
+
*
|
|
232
|
+
* Everything else — `memo(...)`/`forwardRef(...)` wrappers, classes,
|
|
233
|
+
* lowercase helpers, every server export — is left UNMASKED and therefore
|
|
234
|
+
* compared byte-for-byte. That costs Fast Refresh on those shapes and buys the
|
|
235
|
+
* guarantee; see this seam's header.
|
|
236
|
+
*/
|
|
237
|
+
function componentBodyToMask(stmt, serverReachable) {
|
|
238
|
+
if (stmt.type === "ExportDefaultDeclaration") return functionBody(stmt.declaration);
|
|
239
|
+
if (stmt.type === "ExportNamedDeclaration") {
|
|
240
|
+
const exported = stmt.declaration;
|
|
241
|
+
if (exported?.type === "FunctionDeclaration" && exported.id?.name === "register") return functionBody(exported);
|
|
242
|
+
if (exported?.type === "VariableDeclaration" && exported.declarations.length === 1) {
|
|
243
|
+
const declarator = exported.declarations[0];
|
|
244
|
+
if (declarator.id?.type === "Identifier" && declarator.id.name === "register") return functionBody(declarator.init);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const declaration = stmt.type === "ExportNamedDeclaration" ? stmt.declaration : stmt;
|
|
248
|
+
if (!declaration) return void 0;
|
|
249
|
+
const named = (name, node) => isComponentName(name) && !serverReachable.has(name) ? functionBody(node) : void 0;
|
|
250
|
+
if (declaration.type === "FunctionDeclaration") return named(declaration.id?.name, declaration);
|
|
251
|
+
if (declaration.type === "VariableDeclaration" && declaration.declarations.length === 1) {
|
|
252
|
+
const declarator = declaration.declarations[0];
|
|
253
|
+
if (declarator.id?.type !== "Identifier") return void 0;
|
|
254
|
+
return named(declarator.id.name, declarator.init);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The module source with every component and exported `register` body replaced
|
|
259
|
+
* by a constant — the ONE value the reload decision compares across an edit.
|
|
260
|
+
*
|
|
261
|
+
* Everything outside those bodies survives verbatim: imports, signatures,
|
|
262
|
+
* module-level declarations, all six server exports, and the comments and
|
|
263
|
+
* whitespace between them. So the skeleton is unchanged iff the save touched
|
|
264
|
+
* nothing but refresh-safe bodies.
|
|
265
|
+
*
|
|
266
|
+
* Returns `undefined` when the source does not parse — a half-typed file whose
|
|
267
|
+
* error Vite is already reporting from projection's real `transform`. The
|
|
268
|
+
* caller leaves the cache holding the last GOOD skeleton, so the next
|
|
269
|
+
* successful save is still compared against the right baseline.
|
|
270
|
+
*/
|
|
271
|
+
function captureSkeleton(code) {
|
|
272
|
+
let ast;
|
|
273
|
+
try {
|
|
274
|
+
ast = parse(code, {
|
|
275
|
+
sourceType: "module",
|
|
276
|
+
plugins: ["typescript", "jsx"]
|
|
277
|
+
});
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const body = ast.program.body;
|
|
282
|
+
const serverReachable = serverReachableNames(body);
|
|
283
|
+
const magic = new MagicString(code);
|
|
284
|
+
for (const stmt of body) {
|
|
285
|
+
const bodyNode = componentBodyToMask(stmt, serverReachable);
|
|
286
|
+
if (!bodyNode) continue;
|
|
287
|
+
const start = bodyNode.start;
|
|
288
|
+
const end = bodyNode.end;
|
|
289
|
+
if (end > start) magic.overwrite(start, end, MASKED_REFRESH_BODY);
|
|
290
|
+
}
|
|
291
|
+
return magic.toString();
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
98
294
|
* Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.
|
|
99
295
|
*
|
|
100
296
|
* Discovery runs INSIDE `load`, once per `load` call, and its result is NOT
|
|
@@ -114,6 +310,7 @@ function eraseTypes(source) {
|
|
|
114
310
|
*/
|
|
115
311
|
function clientPageRegistry(options = {}) {
|
|
116
312
|
const appRoot = path.resolve(options.appRoot ?? process.cwd());
|
|
313
|
+
const skeletonCache = /* @__PURE__ */ new Map();
|
|
117
314
|
return {
|
|
118
315
|
name: "warlock:client-page-registry",
|
|
119
316
|
enforce: "pre",
|
|
@@ -129,10 +326,58 @@ function clientPageRegistry(options = {}) {
|
|
|
129
326
|
}),
|
|
130
327
|
toImportSpecifier
|
|
131
328
|
}));
|
|
329
|
+
},
|
|
330
|
+
/**
|
|
331
|
+
* Capture-only spy. Records the SKELETON of every server page module the
|
|
332
|
+
* CLIENT environment transforms, and returns nothing so projection's own
|
|
333
|
+
* `transform` still does the real work. SERVE-ONLY:
|
|
334
|
+
* `this.environment.mode !== "dev"` skips it during `vite build`, where
|
|
335
|
+
* there is no `hotUpdate` to feed and the extra parse would be pure cost.
|
|
336
|
+
*/
|
|
337
|
+
transform(code, id) {
|
|
338
|
+
if (this.environment?.mode !== "dev") return void 0;
|
|
339
|
+
if (!isServerPageModule(id)) return void 0;
|
|
340
|
+
const skeleton = captureSkeleton(code);
|
|
341
|
+
if (skeleton !== void 0) skeletonCache.set(id, skeleton);
|
|
342
|
+
},
|
|
343
|
+
/**
|
|
344
|
+
* Applies the ruling (canon `6b240682`): Fast Refresh ONLY when the only
|
|
345
|
+
* changes are inside component or exported `register` bodies.
|
|
346
|
+
*
|
|
347
|
+
* - Skeleton moved (an import, a module-level declaration, ANY server
|
|
348
|
+
* export — with or without a simultaneous JSX change) → full reload.
|
|
349
|
+
* - Skeleton unchanged → defer to Fast Refresh, zero reloads.
|
|
350
|
+
*
|
|
351
|
+
* Note what is NOT here: no attempt to name which half a shared import or
|
|
352
|
+
* local belongs to. That question is what produced the two previous stale
|
|
353
|
+
* `<head>` bugs; this seam refuses to answer it and reloads instead.
|
|
354
|
+
* `hotUpdate` exists only on the dev server, so this is serve-only by
|
|
355
|
+
* construction.
|
|
356
|
+
*/
|
|
357
|
+
async hotUpdate(context) {
|
|
358
|
+
if (isServerPageModule(context.file)) {
|
|
359
|
+
if (await options.beforePageHotUpdate?.({
|
|
360
|
+
file: context.file,
|
|
361
|
+
type: context.type
|
|
362
|
+
})) return [];
|
|
363
|
+
}
|
|
364
|
+
if (context.type !== "update") return void 0;
|
|
365
|
+
if (!isServerPageModule(context.file)) return void 0;
|
|
366
|
+
const next = captureSkeleton(await context.read());
|
|
367
|
+
const prev = skeletonCache.get(context.file);
|
|
368
|
+
if (next !== void 0) skeletonCache.set(context.file, next);
|
|
369
|
+
if (next === void 0 || prev === void 0) return void 0;
|
|
370
|
+
if (prev !== next) {
|
|
371
|
+
this.environment.hot.send({
|
|
372
|
+
type: "full-reload",
|
|
373
|
+
path: "*"
|
|
374
|
+
});
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
132
377
|
}
|
|
133
378
|
};
|
|
134
379
|
}
|
|
135
380
|
|
|
136
381
|
//#endregion
|
|
137
|
-
export { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry };
|
|
382
|
+
export { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry, invalidateClientPageRegistry };
|
|
138
383
|
//# sourceMappingURL=page-registry-plugin.mjs.map
|