@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,146 @@
|
|
|
1
|
+
import { HYDRATION_CLIENT_ENTRY_NAME, createHydrationClientEntry } from "./hydration-entries.mjs";
|
|
2
|
+
import { buildHydrationClient } from "./build-client.mjs";
|
|
3
|
+
import { gateAResolve } from "./gate-a-resolve.mjs";
|
|
4
|
+
import { createPublicEnvTracker, gateBSecrets } from "./gate-b-secrets.mjs";
|
|
5
|
+
import { ProjectionAmbiguityError, projection } from "./projection.mjs";
|
|
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";
|
|
8
|
+
|
|
9
|
+
//#region ../web/src/vite/index.ts
|
|
10
|
+
function clientEnvironmentOnly(plugin) {
|
|
11
|
+
return {
|
|
12
|
+
...plugin,
|
|
13
|
+
applyToEnvironment(environment) {
|
|
14
|
+
return environment.config.consumer === "client";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The composed client-build pipeline: projection
|
|
20
|
+
* strips the 5 server exports first, THEN Gate B's `transform` checks
|
|
21
|
+
* whatever source remains for inline secret reads, THEN Gate A's
|
|
22
|
+
* `resolveId` judges whatever imports remain. Array order here is
|
|
23
|
+
* `[projection(), gateBSecrets(), gateAResolve()]` to match Vite's own
|
|
24
|
+
* pipeline shape (`transform` before `resolveId`), but array order alone
|
|
25
|
+
* does not guarantee this — see the hook-ordering fact below, which is what
|
|
26
|
+
* actually makes the composition correct.
|
|
27
|
+
*
|
|
28
|
+
* Empirically observed fact (via an instrumented real `vite.build()`, not
|
|
29
|
+
* assumed from plugin array order): for a given module M, Vite/Rollup calls
|
|
30
|
+
* `transform(M)` BEFORE it calls `resolveId` for any of M's own import
|
|
31
|
+
* specifiers — because Rollup must parse M's post-transform source to even
|
|
32
|
+
* discover which specifiers to resolve next. Concretely: `transform` ran on
|
|
33
|
+
* `entry.page.tsx` first, and only after that did `resolveId("./dep", ...)`
|
|
34
|
+
* fire for the import statement still present in the transformed code. A
|
|
35
|
+
* consequence follows directly: if projection's `transform` removes an
|
|
36
|
+
* import statement from a page module entirely (e.g. `loader`'s
|
|
37
|
+
* `@warlock.js/core` import, stripped because `loader` itself is removed),
|
|
38
|
+
* `resolveId` is never invoked for that specifier at all — Gate A doesn't
|
|
39
|
+
* "let it pass", it never sees it. Gate A's `resolveId` only fires for
|
|
40
|
+
* imports that survive projection's `transform`, which is exactly why a
|
|
41
|
+
* component-level `@warlock.js/core` import (never touched by projection)
|
|
42
|
+
* still reaches and is refused by Gate A.
|
|
43
|
+
*
|
|
44
|
+
* This is OBSERVED Rollup behavior, not a documented contract — a future
|
|
45
|
+
* Vite/Rollup upgrade could invert it. Pinned by a
|
|
46
|
+
* regression test (`index.spec.ts`, "D.3 hook ordering pin") that fails
|
|
47
|
+
* loudly if the ordering ever inverts, and by the `vite` peer floor in
|
|
48
|
+
* `web/package.json` (`>=7.3.5`, the version this was verified against). If
|
|
49
|
+
* that test ever fails after a Vite bump: the failure mode of the ordering
|
|
50
|
+
* assumption breaking is SAFE — projection would stop removing an import
|
|
51
|
+
* statement Gate A still sees, so Gate A would refuse an import it used to
|
|
52
|
+
* silently let a stripped server export take with it. That is a loud build
|
|
53
|
+
* failure ("Gate A refused an import"), never a silent client-bundle leak.
|
|
54
|
+
* Do NOT "fix" an apparent Gate A false-positive after a Vite upgrade by
|
|
55
|
+
* weakening Gate A (e.g. widening what it lets through) — investigate
|
|
56
|
+
* whether this ordering assumption broke instead; loosening Gate A to work
|
|
57
|
+
* around it would turn a loud failure into the exact silent leak this
|
|
58
|
+
* pipeline exists to prevent.
|
|
59
|
+
*
|
|
60
|
+
* Gate B is placed between the two for the same reason, but for a `transform`
|
|
61
|
+
* hook rather than `resolveId`: within a plugin array, Rollup runs each
|
|
62
|
+
* module's registered `transform` hooks in array order, each one receiving
|
|
63
|
+
* the PREVIOUS plugin's output. Running Gate B after projection means it
|
|
64
|
+
* inspects the POST-projection source — a `process.env.SECRET` read inside
|
|
65
|
+
* `loader` (a server export, legitimately reading a real secret server-side)
|
|
66
|
+
* is invisible to Gate B once projection has already removed `loader`
|
|
67
|
+
* entirely, exactly as it should be: Gate B's job is to fence client-bound
|
|
68
|
+
* code, and projection is what decides what counts as client-bound. A
|
|
69
|
+
* component-level secret read is untouched by projection and still reaches
|
|
70
|
+
* Gate B, which refuses it. Gate B does not depend on Gate A's `resolveId`
|
|
71
|
+
* output at all (orthogonal concern, raw source vs. import paths), so its
|
|
72
|
+
* position relative to Gate A is not load-bearing — it is placed before Gate
|
|
73
|
+
* A only to keep both `transform` hooks adjacent in the array.
|
|
74
|
+
*
|
|
75
|
+
* Gate C (`gate-c-verify.ts`) runs last and only at `generateBundle` — after
|
|
76
|
+
* the entire `transform`/`resolveId` build phase has completed for every
|
|
77
|
+
* plugin, regardless of array position (a Rollup lifecycle fact, not
|
|
78
|
+
* something this array order enforces). It verifies the EMITTED output the
|
|
79
|
+
* other three produced: no server export survived as a top-level binding, no
|
|
80
|
+
* import edge into a server-only package survived into the module graph, and
|
|
81
|
+
* it emits the reviewable `PUBLIC_*` inlined-value manifest. `gateBSecrets` and `gateCVerify` share
|
|
82
|
+
* one `PublicEnvTracker` instance so the manifest and Gate B's own unread-key
|
|
83
|
+
* exclusion check agree by construction, not by coincidence.
|
|
84
|
+
*
|
|
85
|
+
* `clientPageRegistry()` is FIRST, ahead of projection. It contributes no
|
|
86
|
+
* `transform` at all — only a `resolveId`/`load` pair for one synthetic id —
|
|
87
|
+
* so it cannot displace or pre-empt any gate's inspection of any real file.
|
|
88
|
+
* Two reasons for the position, one of which is not load-bearing and is
|
|
89
|
+
* labelled as such:
|
|
90
|
+
*
|
|
91
|
+
* 1. Load-bearing: it must own `virtual:warlock/pages` before Gate A's
|
|
92
|
+
* `resolveId` (also `enforce: "pre"`) reaches its `this.resolve(...)` call
|
|
93
|
+
* for that specifier. Gate A's nested resolve would find it anyway, but
|
|
94
|
+
* routing the id through Gate A's importer-chain bookkeeping only to have
|
|
95
|
+
* it come back means a synthetic id can surface in a user-facing "Import
|
|
96
|
+
* chain:" message. Resolving it first keeps ownership of the id in one
|
|
97
|
+
* place.
|
|
98
|
+
* 2. NOT load-bearing: the position relative to `projection()`. Projection is
|
|
99
|
+
* `enforce: "pre"` and selects by file BASENAME (`projection.ts:242-248`),
|
|
100
|
+
* so it transforms every `*.page.tsx` / `layout.tsx` / `root.tsx` that
|
|
101
|
+
* enters the graph regardless of who imported it or where this plugin sits.
|
|
102
|
+
* The registry emits absolute POSIX specifiers that Rollup resolves and
|
|
103
|
+
* loads as ORDINARY file modules — they are not inlined into the virtual
|
|
104
|
+
* module — so each one is transformed exactly as a page imported from a
|
|
105
|
+
* real file would be. Projection declines the virtual module itself
|
|
106
|
+
* (`\0virtual:warlock/pages` has no matching basename), which is correct:
|
|
107
|
+
* generated code has no server exports to strip.
|
|
108
|
+
*
|
|
109
|
+
* Point 2 is asserted, not assumed, by a real `vite.build()` in
|
|
110
|
+
* `page-registry-plugin.spec.ts`: a fixture page whose `loader` — and only its
|
|
111
|
+
* `loader` — imports a marker module that Gate A independently PERMITS, built
|
|
112
|
+
* through this exact array, with the marker proven absent from every emitted
|
|
113
|
+
* chunk while the page's own component text is proven present. Inspecting this
|
|
114
|
+
* array's order would prove nothing about what reaches the browser.
|
|
115
|
+
*/
|
|
116
|
+
function warlockClientBoundary(options = {}) {
|
|
117
|
+
const tracker = createPublicEnvTracker();
|
|
118
|
+
return [
|
|
119
|
+
clientPageRegistry({ appRoot: options.appRoot }),
|
|
120
|
+
projection(),
|
|
121
|
+
gateBSecrets({ tracker }),
|
|
122
|
+
gateAResolve(options),
|
|
123
|
+
gateCVerify({
|
|
124
|
+
...options,
|
|
125
|
+
tracker
|
|
126
|
+
})
|
|
127
|
+
].map(clientEnvironmentOnly);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Callable production seam: callers own their exact source aliases and the
|
|
131
|
+
* app-root classification boundary, while this module owns the one canonical
|
|
132
|
+
* projection/Gate B/Gate A/Gate C composition.
|
|
133
|
+
*/
|
|
134
|
+
async function buildWarlockHydrationClient(options) {
|
|
135
|
+
return buildHydrationClient({
|
|
136
|
+
webRoot: options.webRoot,
|
|
137
|
+
outDir: options.outDir,
|
|
138
|
+
resolveAliases: options.resolveAliases,
|
|
139
|
+
external: options.external,
|
|
140
|
+
plugins: warlockClientBoundary({ appRoot: options.appRoot })
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//#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 };
|
|
146
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/vite/page-registry-plugin.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The specifier application code writes.
|
|
6
|
+
*
|
|
7
|
+
* Exported so the client runtime imports this constant instead of retyping the
|
|
8
|
+
* string: a constant two sides must agree on is a guard, and a guard duplicated
|
|
9
|
+
* at a second site fails open at the third — a typo'd re-spelling doesn't fail
|
|
10
|
+
* loudly, it resolves to "no such module" or, worse, to a stale real file.
|
|
11
|
+
*/
|
|
12
|
+
declare const CLIENT_PAGE_REGISTRY_ID = "virtual:warlock/pages";
|
|
13
|
+
/**
|
|
14
|
+
* The resolved id, `\0`-prefixed per Vite/Rollup convention so no other plugin
|
|
15
|
+
* (and no filesystem watcher) mistakes it for a real path.
|
|
16
|
+
*/
|
|
17
|
+
declare const RESOLVED_CLIENT_PAGE_REGISTRY_ID = "\0virtual:warlock/pages";
|
|
18
|
+
type ClientPageRegistryPluginOptions = {
|
|
19
|
+
/** 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
|
+
srcDir?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.
|
|
24
|
+
*
|
|
25
|
+
* Discovery runs INSIDE `load`, once per `load` call, and its result is NOT
|
|
26
|
+
* cached across builds — the plugin holds no state at all. A registry cached
|
|
27
|
+
* past the moment a page file appears is a page that silently 404s until
|
|
28
|
+
* someone restarts the dev server, which is a far more expensive bug than
|
|
29
|
+
* re-walking a source tree. Rollup calls `load` once per module per build, and
|
|
30
|
+
* in dev Vite's module graph caches the transformed result until the module is
|
|
31
|
+
* invalidated, so the walk is not per-request either way. (Invalidating that
|
|
32
|
+
* dev-server cache when a page file is ADDED needs a `handleHotUpdate`/watcher
|
|
33
|
+
* hook that belongs with the dev provider slice — see the followup.)
|
|
34
|
+
*
|
|
35
|
+
* `enforce: "pre"` and placed FIRST in `warlockClientBoundary`'s array — see
|
|
36
|
+
* that function's comment in `index.ts` for why position is what it is, and
|
|
37
|
+
* `page-registry-plugin.spec.ts` for the real-build proof that the pages this
|
|
38
|
+
* module names still reach `projection()`.
|
|
39
|
+
*/
|
|
40
|
+
declare function clientPageRegistry(options?: ClientPageRegistryPluginOptions): Plugin;
|
|
41
|
+
//#endregion
|
|
42
|
+
export { CLIENT_PAGE_REGISTRY_ID, ClientPageRegistryPluginOptions, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry };
|
|
43
|
+
//# sourceMappingURL=page-registry-plugin.d.mts.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { discoverPages, toPosix } from "../build/discover-pages.mjs";
|
|
2
|
+
import { generateClientRegistry } from "../build/generate-client-registry.mjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse } from "@babel/parser";
|
|
5
|
+
import MagicString from "magic-string";
|
|
6
|
+
|
|
7
|
+
//#region ../web/src/vite/page-registry-plugin.ts
|
|
8
|
+
/**
|
|
9
|
+
* The wire between the two halves that already existed and never met:
|
|
10
|
+
* `discoverPages` (the page graph, read off disk) and `generateClientRegistry`
|
|
11
|
+
* (the module SOURCE that carries that graph into the browser). Neither one
|
|
12
|
+
* touches Vite; this plugin is the only place they are joined, and it joins
|
|
13
|
+
* them as a VIRTUAL module so nothing is ever written to the user's tree.
|
|
14
|
+
*
|
|
15
|
+
* Identical in dev and build — no `apply`/`command` gating, matching the rest
|
|
16
|
+
* of `warlockClientBoundary`'s composition (`index.ts`), which is also
|
|
17
|
+
* mode-agnostic. A registry that differed between `vite dev` and `vite build`
|
|
18
|
+
* would make every dev-only or prod-only page bug unreproducible in the other
|
|
19
|
+
* mode.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The specifier application code writes.
|
|
23
|
+
*
|
|
24
|
+
* Exported so the client runtime imports this constant instead of retyping the
|
|
25
|
+
* string: a constant two sides must agree on is a guard, and a guard duplicated
|
|
26
|
+
* at a second site fails open at the third — a typo'd re-spelling doesn't fail
|
|
27
|
+
* loudly, it resolves to "no such module" or, worse, to a stale real file.
|
|
28
|
+
*/
|
|
29
|
+
const CLIENT_PAGE_REGISTRY_ID = "virtual:warlock/pages";
|
|
30
|
+
/**
|
|
31
|
+
* The resolved id, `\0`-prefixed per Vite/Rollup convention so no other plugin
|
|
32
|
+
* (and no filesystem watcher) mistakes it for a real path.
|
|
33
|
+
*/
|
|
34
|
+
const RESOLVED_CLIENT_PAGE_REGISTRY_ID = `\0${CLIENT_PAGE_REGISTRY_ID}`;
|
|
35
|
+
/**
|
|
36
|
+
* The import specifiers the emitted registry names must be ABSOLUTE POSIX file
|
|
37
|
+
* paths, never relative ones.
|
|
38
|
+
*
|
|
39
|
+
* A relative specifier resolves against its IMPORTER, and the importer here is
|
|
40
|
+
* `\0virtual:warlock/pages` — a synthetic id whose `dirname` is not a real
|
|
41
|
+
* directory. `./blog.page.tsx` from that importer resolves to nonsense that
|
|
42
|
+
* fails at bundle time with a path no user authored and no user can act on.
|
|
43
|
+
*
|
|
44
|
+
* Separator normalization is `hydration-entries.ts`'s
|
|
45
|
+
* (`hydration-entries.ts:12-14`) and `discover-pages.ts`'s single
|
|
46
|
+
* `.replace(/\\/g, "/")` rule, reused via the already-exported `toPosix` rather
|
|
47
|
+
* than spelled a third time — keeping the drive colon (`D:/...`) is exactly
|
|
48
|
+
* what Vite's resolver wants on Windows.
|
|
49
|
+
*/
|
|
50
|
+
function toImportSpecifier(absoluteFilePath) {
|
|
51
|
+
return toPosix(path.resolve(absoluteFilePath));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Erases the generated module's TypeScript down to plain JavaScript.
|
|
55
|
+
*
|
|
56
|
+
* NOT optional, and not a style choice. Vite's `vite:esbuild` transform is
|
|
57
|
+
* gated behind `createFilter`, which refuses ANY id containing a NUL byte
|
|
58
|
+
* (`node_modules/vite/dist/node/chunks/config.js:1512` — `if
|
|
59
|
+
* (id.includes("\0")) return false`). So the one module in this build that is
|
|
60
|
+
* `\0`-prefixed by convention is precisely the one module esbuild will never
|
|
61
|
+
* transform, while `generateClientRegistry` always emits TypeScript (a
|
|
62
|
+
* type-only `ClientPageEntry` import plus the array's type annotation). Handed
|
|
63
|
+
* to Rollup verbatim, `import type { ClientPageEntry } from ...` is a
|
|
64
|
+
* JavaScript syntax error.
|
|
65
|
+
*
|
|
66
|
+
* Done with the AST rather than a regex, using the same `@babel/parser` +
|
|
67
|
+
* `MagicString` pair `projection.ts` already uses in this directory — a regex
|
|
68
|
+
* over generated source is a second grammar that drifts from the generator's
|
|
69
|
+
* silently. If the generator ever emits a TS construct outside these two
|
|
70
|
+
* shapes, the result is a Rollup parse error naming the virtual module: loud,
|
|
71
|
+
* not silent. `page-registry-plugin.spec.ts` pins that the erased output
|
|
72
|
+
* re-parses as plain JavaScript with the TypeScript plugin switched OFF.
|
|
73
|
+
*/
|
|
74
|
+
function eraseTypes(source) {
|
|
75
|
+
const ast = parse(source, {
|
|
76
|
+
sourceType: "module",
|
|
77
|
+
plugins: ["typescript"]
|
|
78
|
+
});
|
|
79
|
+
const magic = new MagicString(source);
|
|
80
|
+
for (const statement of ast.program.body) {
|
|
81
|
+
if (statement.type === "ImportDeclaration" && statement.importKind === "type") {
|
|
82
|
+
let end = statement.end;
|
|
83
|
+
if (source[end] === "\r" && source[end + 1] === "\n") end += 2;
|
|
84
|
+
else if (source[end] === "\n") end += 1;
|
|
85
|
+
magic.remove(statement.start, end);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
|
|
89
|
+
if (declaration?.type !== "VariableDeclaration") continue;
|
|
90
|
+
for (const declarator of declaration.declarations) {
|
|
91
|
+
const annotation = declarator.id?.typeAnnotation;
|
|
92
|
+
if (annotation) magic.remove(annotation.start, annotation.end);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return magic.toString();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.
|
|
99
|
+
*
|
|
100
|
+
* Discovery runs INSIDE `load`, once per `load` call, and its result is NOT
|
|
101
|
+
* cached across builds — the plugin holds no state at all. A registry cached
|
|
102
|
+
* past the moment a page file appears is a page that silently 404s until
|
|
103
|
+
* someone restarts the dev server, which is a far more expensive bug than
|
|
104
|
+
* re-walking a source tree. Rollup calls `load` once per module per build, and
|
|
105
|
+
* in dev Vite's module graph caches the transformed result until the module is
|
|
106
|
+
* invalidated, so the walk is not per-request either way. (Invalidating that
|
|
107
|
+
* dev-server cache when a page file is ADDED needs a `handleHotUpdate`/watcher
|
|
108
|
+
* hook that belongs with the dev provider slice — see the followup.)
|
|
109
|
+
*
|
|
110
|
+
* `enforce: "pre"` and placed FIRST in `warlockClientBoundary`'s array — see
|
|
111
|
+
* that function's comment in `index.ts` for why position is what it is, and
|
|
112
|
+
* `page-registry-plugin.spec.ts` for the real-build proof that the pages this
|
|
113
|
+
* module names still reach `projection()`.
|
|
114
|
+
*/
|
|
115
|
+
function clientPageRegistry(options = {}) {
|
|
116
|
+
const appRoot = path.resolve(options.appRoot ?? process.cwd());
|
|
117
|
+
return {
|
|
118
|
+
name: "warlock:client-page-registry",
|
|
119
|
+
enforce: "pre",
|
|
120
|
+
resolveId(source) {
|
|
121
|
+
if (source === "virtual:warlock/pages") return RESOLVED_CLIENT_PAGE_REGISTRY_ID;
|
|
122
|
+
},
|
|
123
|
+
load(id) {
|
|
124
|
+
if (id !== RESOLVED_CLIENT_PAGE_REGISTRY_ID) return void 0;
|
|
125
|
+
return eraseTypes(generateClientRegistry({
|
|
126
|
+
pages: discoverPages({
|
|
127
|
+
appRoot,
|
|
128
|
+
srcDir: options.srcDir
|
|
129
|
+
}),
|
|
130
|
+
toImportSpecifier
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//#endregion
|
|
137
|
+
export { CLIENT_PAGE_REGISTRY_ID, RESOLVED_CLIENT_PAGE_REGISTRY_ID, clientPageRegistry };
|
|
138
|
+
//# sourceMappingURL=page-registry-plugin.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-registry-plugin.mjs","names":[],"sources":["../../../../../../../web/src/vite/page-registry-plugin.ts"],"sourcesContent":["/**\n * The wire between the two halves that already existed and never met:\n * `discoverPages` (the page graph, read off disk) and `generateClientRegistry`\n * (the module SOURCE that carries that graph into the browser). Neither one\n * touches Vite; this plugin is the only place they are joined, and it joins\n * them as a VIRTUAL module so nothing is ever written to the user's tree.\n *\n * Identical in dev and build — no `apply`/`command` gating, matching the rest\n * of `warlockClientBoundary`'s composition (`index.ts`), which is also\n * mode-agnostic. A registry that differed between `vite dev` and `vite build`\n * would make every dev-only or prod-only page bug unreproducible in the other\n * mode.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { discoverPages, toPosix } from \"../build/discover-pages\";\nimport { generateClientRegistry } from \"../build/generate-client-registry\";\n\n/**\n * The specifier application code writes.\n *\n * Exported so the client runtime imports this constant instead of retyping the\n * string: a constant two sides must agree on is a guard, and a guard duplicated\n * at a second site fails open at the third — a typo'd re-spelling doesn't fail\n * loudly, it resolves to \"no such module\" or, worse, to a stale real file.\n */\nexport const CLIENT_PAGE_REGISTRY_ID = \"virtual:warlock/pages\";\n\n/**\n * The resolved id, `\\0`-prefixed per Vite/Rollup convention so no other plugin\n * (and no filesystem watcher) mistakes it for a real path.\n */\nexport const RESOLVED_CLIENT_PAGE_REGISTRY_ID = `\\0${CLIENT_PAGE_REGISTRY_ID}`;\n\nexport type ClientPageRegistryPluginOptions = {\n /** Absolute path to the application root. Defaults to `process.cwd()`, matching Vite's own default `root` and Gate A's `appRoot` default. */\n appRoot?: string;\n /** Source directory name under `appRoot`; forwarded verbatim to `discoverPages`, which defaults it to `\"src\"`. */\n srcDir?: string;\n};\n\n/**\n * The import specifiers the emitted registry names must be ABSOLUTE POSIX file\n * paths, never relative ones.\n *\n * A relative specifier resolves against its IMPORTER, and the importer here is\n * `\\0virtual:warlock/pages` — a synthetic id whose `dirname` is not a real\n * directory. `./blog.page.tsx` from that importer resolves to nonsense that\n * fails at bundle time with a path no user authored and no user can act on.\n *\n * Separator normalization is `hydration-entries.ts`'s\n * (`hydration-entries.ts:12-14`) and `discover-pages.ts`'s single\n * `.replace(/\\\\/g, \"/\")` rule, reused via the already-exported `toPosix` rather\n * than spelled a third time — keeping the drive colon (`D:/...`) is exactly\n * what Vite's resolver wants on Windows.\n */\nfunction toImportSpecifier(absoluteFilePath: string): string {\n return toPosix(path.resolve(absoluteFilePath));\n}\n\n/**\n * Erases the generated module's TypeScript down to plain JavaScript.\n *\n * NOT optional, and not a style choice. Vite's `vite:esbuild` transform is\n * gated behind `createFilter`, which refuses ANY id containing a NUL byte\n * (`node_modules/vite/dist/node/chunks/config.js:1512` — `if\n * (id.includes(\"\\0\")) return false`). So the one module in this build that is\n * `\\0`-prefixed by convention is precisely the one module esbuild will never\n * transform, while `generateClientRegistry` always emits TypeScript (a\n * type-only `ClientPageEntry` import plus the array's type annotation). Handed\n * to Rollup verbatim, `import type { ClientPageEntry } from ...` is a\n * JavaScript syntax error.\n *\n * Done with the AST rather than a regex, using the same `@babel/parser` +\n * `MagicString` pair `projection.ts` already uses in this directory — a regex\n * over generated source is a second grammar that drifts from the generator's\n * silently. If the generator ever emits a TS construct outside these two\n * shapes, the result is a Rollup parse error naming the virtual module: loud,\n * not silent. `page-registry-plugin.spec.ts` pins that the erased output\n * re-parses as plain JavaScript with the TypeScript plugin switched OFF.\n */\nfunction eraseTypes(source: string): string {\n const ast = parse(source, { sourceType: \"module\", plugins: [\"typescript\"] });\n const magic = new MagicString(source);\n\n for (const statement of ast.program.body as any[]) {\n if (statement.type === \"ImportDeclaration\" && statement.importKind === \"type\") {\n let end = statement.end as number;\n if (source[end] === \"\\r\" && source[end + 1] === \"\\n\") end += 2;\n else if (source[end] === \"\\n\") end += 1;\n magic.remove(statement.start as number, end);\n continue;\n }\n\n const declaration =\n statement.type === \"ExportNamedDeclaration\" ? statement.declaration : statement;\n\n if (declaration?.type !== \"VariableDeclaration\") continue;\n\n for (const declarator of declaration.declarations as any[]) {\n const annotation = declarator.id?.typeAnnotation;\n if (annotation) magic.remove(annotation.start as number, annotation.end as number);\n }\n }\n\n return magic.toString();\n}\n\n/**\n * Serves the client page registry at {@link CLIENT_PAGE_REGISTRY_ID}.\n *\n * Discovery runs INSIDE `load`, once per `load` call, and its result is NOT\n * cached across builds — the plugin holds no state at all. A registry cached\n * past the moment a page file appears is a page that silently 404s until\n * someone restarts the dev server, which is a far more expensive bug than\n * re-walking a source tree. Rollup calls `load` once per module per build, and\n * in dev Vite's module graph caches the transformed result until the module is\n * invalidated, so the walk is not per-request either way. (Invalidating that\n * dev-server cache when a page file is ADDED needs a `handleHotUpdate`/watcher\n * hook that belongs with the dev provider slice — see the followup.)\n *\n * `enforce: \"pre\"` and placed FIRST in `warlockClientBoundary`'s array — see\n * that function's comment in `index.ts` for why position is what it is, and\n * `page-registry-plugin.spec.ts` for the real-build proof that the pages this\n * module names still reach `projection()`.\n */\nexport function clientPageRegistry(options: ClientPageRegistryPluginOptions = {}): Plugin {\n const appRoot = path.resolve(options.appRoot ?? process.cwd());\n\n return {\n name: \"warlock:client-page-registry\",\n enforce: \"pre\",\n resolveId(source) {\n if (source === CLIENT_PAGE_REGISTRY_ID) return RESOLVED_CLIENT_PAGE_REGISTRY_ID;\n return undefined;\n },\n load(id) {\n if (id !== RESOLVED_CLIENT_PAGE_REGISTRY_ID) return undefined;\n\n const pages = discoverPages({ appRoot, srcDir: options.srcDir });\n\n return eraseTypes(generateClientRegistry({ pages, toImportSpecifier }));\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,0BAA0B;;;;;AAMvC,MAAa,mCAAmC,KAAK;;;;;;;;;;;;;;;;AAwBrD,SAAS,kBAAkB,kBAAkC;CAC3D,OAAO,QAAQ,KAAK,QAAQ,gBAAgB,CAAC;AAC/C;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAS,WAAW,QAAwB;CAC1C,MAAM,MAAM,MAAM,QAAQ;EAAE,YAAY;EAAU,SAAS,CAAC,YAAY;CAAE,CAAC;CAC3E,MAAM,QAAQ,IAAI,YAAY,MAAM;CAEpC,KAAK,MAAM,aAAa,IAAI,QAAQ,MAAe;EACjD,IAAI,UAAU,SAAS,uBAAuB,UAAU,eAAe,QAAQ;GAC7E,IAAI,MAAM,UAAU;GACpB,IAAI,OAAO,SAAS,QAAQ,OAAO,MAAM,OAAO,MAAM,OAAO;QACxD,IAAI,OAAO,SAAS,MAAM,OAAO;GACtC,MAAM,OAAO,UAAU,OAAiB,GAAG;GAC3C;EACF;EAEA,MAAM,cACJ,UAAU,SAAS,2BAA2B,UAAU,cAAc;EAExE,IAAI,aAAa,SAAS,uBAAuB;EAEjD,KAAK,MAAM,cAAc,YAAY,cAAuB;GAC1D,MAAM,aAAa,WAAW,IAAI;GAClC,IAAI,YAAY,MAAM,OAAO,WAAW,OAAiB,WAAW,GAAa;EACnF;CACF;CAEA,OAAO,MAAM,SAAS;AACxB;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,UAA2C,CAAC,GAAW;CACxF,MAAM,UAAU,KAAK,QAAQ,QAAQ,WAAW,QAAQ,IAAI,CAAC;CAE7D,OAAO;EACL,MAAM;EACN,SAAS;EACT,UAAU,QAAQ;GAChB,IAAI,oCAAoC,OAAO;EAEjD;EACA,KAAK,IAAI;GACP,IAAI,OAAO,kCAAkC,OAAO;GAIpD,OAAO,WAAW,uBAAuB;IAAE,OAF7B,cAAc;KAAE;KAAS,QAAQ,QAAQ;IAAO,CAEf;IAAG;GAAkB,CAAC,CAAC;EACxE;CACF;AACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import MagicString from "magic-string";
|
|
2
|
+
import { Plugin } from "vite";
|
|
3
|
+
|
|
4
|
+
//#region ../web/src/vite/projection.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when projection encounters an attribution-ambiguous top-level
|
|
7
|
+
* statement (`c604f0bc` §3: "the compiler must not guess"). Carries the
|
|
8
|
+
* file/statement/fix fields the plugin's `transform` hook formats into the
|
|
9
|
+
* build-failure message — never silently kept or silently dropped.
|
|
10
|
+
*/
|
|
11
|
+
declare class ProjectionAmbiguityError extends Error {
|
|
12
|
+
readonly file: string;
|
|
13
|
+
readonly statement: string;
|
|
14
|
+
readonly line: number;
|
|
15
|
+
readonly explanation: string;
|
|
16
|
+
readonly fix: string;
|
|
17
|
+
constructor(file: string, statement: string, line: number, explanation: string, fix: string);
|
|
18
|
+
}
|
|
19
|
+
interface ProjectionResult {
|
|
20
|
+
code: string;
|
|
21
|
+
map: ReturnType<MagicString["generateMap"]>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The client-build Vite plugin. Scoped to `*.page.tsx`/`layout.tsx`/`root.tsx`
|
|
25
|
+
* and skipped entirely for the SSR build (`options.ssr`) — the server still
|
|
26
|
+
* needs `route`/`middleware`/`validation`/`loader`/`metadata` intact.
|
|
27
|
+
*/
|
|
28
|
+
declare function projection(): Plugin;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ProjectionAmbiguityError, ProjectionResult, projection };
|
|
31
|
+
//# sourceMappingURL=projection.d.mts.map
|