@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.
Files changed (182) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +125 -0
  4. package/esm/build/contribution.d.mts +55 -0
  5. package/esm/build/contribution.mjs +132 -0
  6. package/esm/build/contribution.mjs.map +1 -0
  7. package/esm/build/discover-pages.mjs +294 -0
  8. package/esm/build/discover-pages.mjs.map +1 -0
  9. package/esm/build/generate-client-registry.mjs +123 -0
  10. package/esm/build/generate-client-registry.mjs.map +1 -0
  11. package/esm/build/generate-pages-barrel.mjs +330 -0
  12. package/esm/build/generate-pages-barrel.mjs.map +1 -0
  13. package/esm/build/read-route-exports.mjs +212 -0
  14. package/esm/build/read-route-exports.mjs.map +1 -0
  15. package/esm/client/build-hydrated-tree.mjs +113 -0
  16. package/esm/client/build-hydrated-tree.mjs.map +1 -0
  17. package/esm/client/hydrate-page.mjs +73 -0
  18. package/esm/client/hydrate-page.mjs.map +1 -0
  19. package/esm/client/navigation/current-route.d.mts +91 -0
  20. package/esm/client/navigation/current-route.mjs +66 -0
  21. package/esm/client/navigation/current-route.mjs.map +1 -0
  22. package/esm/client/navigation/fetch-page-data.mjs +97 -0
  23. package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
  24. package/esm/client/navigation/navigation-root.mjs +196 -0
  25. package/esm/client/navigation/navigation-root.mjs.map +1 -0
  26. package/esm/client/navigation/prefetch.mjs +110 -0
  27. package/esm/client/navigation/prefetch.mjs.map +1 -0
  28. package/esm/client/navigation/refresh.d.mts +40 -0
  29. package/esm/client/navigation/refresh.mjs +146 -0
  30. package/esm/client/navigation/refresh.mjs.map +1 -0
  31. package/esm/client/navigation/verbs.d.mts +55 -0
  32. package/esm/client/navigation/verbs.mjs +66 -0
  33. package/esm/client/navigation/verbs.mjs.map +1 -0
  34. package/esm/client/runtime/index.d.mts +4 -0
  35. package/esm/client/runtime/index.mjs +4 -0
  36. package/esm/client/runtime/manifest.d.mts +8 -0
  37. package/esm/client/runtime/manifest.mjs +77 -0
  38. package/esm/client/runtime/manifest.mjs.map +1 -0
  39. package/esm/client/runtime/matcher.d.mts +7 -0
  40. package/esm/client/runtime/matcher.mjs +169 -0
  41. package/esm/client/runtime/matcher.mjs.map +1 -0
  42. package/esm/client/runtime/types.d.mts +22 -0
  43. package/esm/components/default-app.mjs +32 -0
  44. package/esm/components/default-app.mjs.map +1 -0
  45. package/esm/components/document-context.d.mts +1 -0
  46. package/esm/components/document-context.mjs +24 -0
  47. package/esm/components/document-context.mjs.map +1 -0
  48. package/esm/components/head.d.mts +14 -0
  49. package/esm/components/head.mjs +63 -0
  50. package/esm/components/head.mjs.map +1 -0
  51. package/esm/components/link.d.mts +157 -0
  52. package/esm/components/link.mjs +194 -0
  53. package/esm/components/link.mjs.map +1 -0
  54. package/esm/components/scripts.d.mts +14 -0
  55. package/esm/components/scripts.mjs +21 -0
  56. package/esm/components/scripts.mjs.map +1 -0
  57. package/esm/connector/index.d.mts +6 -0
  58. package/esm/connector/index.mjs +7 -0
  59. package/esm/context.d.mts +107 -0
  60. package/esm/hydration/index.d.mts +1 -0
  61. package/esm/hydration/index.mjs +33 -0
  62. package/esm/hydration/index.mjs.map +1 -0
  63. package/esm/hydration-payload.d.mts +1 -0
  64. package/esm/hydration-payload.mjs +72 -0
  65. package/esm/hydration-payload.mjs.map +1 -0
  66. package/esm/index.d.mts +35 -0
  67. package/esm/index.mjs +12 -0
  68. package/esm/loaders.d.mts +47 -0
  69. package/esm/metadata.d.mts +54 -0
  70. package/esm/props.d.mts +42 -0
  71. package/esm/route.d.mts +30 -0
  72. package/esm/routing/compose-route-path.mjs +33 -0
  73. package/esm/routing/compose-route-path.mjs.map +1 -0
  74. package/esm/routing/data-request.mjs +52 -0
  75. package/esm/routing/data-request.mjs.map +1 -0
  76. package/esm/routing/layout-policy.mjs +78 -0
  77. package/esm/routing/layout-policy.mjs.map +1 -0
  78. package/esm/routing/navigator.mjs +21 -0
  79. package/esm/routing/navigator.mjs.map +1 -0
  80. package/esm/routing/query-string.d.mts +250 -0
  81. package/esm/routing/query-string.mjs +340 -0
  82. package/esm/routing/query-string.mjs.map +1 -0
  83. package/esm/routing/route-identity.mjs +68 -0
  84. package/esm/routing/route-identity.mjs.map +1 -0
  85. package/esm/routing/route-table.d.mts +34 -0
  86. package/esm/routing/route-table.mjs +182 -0
  87. package/esm/routing/route-table.mjs.map +1 -0
  88. package/esm/routing/router-events.d.mts +159 -0
  89. package/esm/routing/router-events.mjs +83 -0
  90. package/esm/routing/router-events.mjs.map +1 -0
  91. package/esm/server/buffered-response.mjs +115 -0
  92. package/esm/server/buffered-response.mjs.map +1 -0
  93. package/esm/server/build-hydration-payload.mjs +27 -0
  94. package/esm/server/build-hydration-payload.mjs.map +1 -0
  95. package/esm/server/client-asset-url-prefix.d.mts +22 -0
  96. package/esm/server/client-asset-url-prefix.mjs +23 -0
  97. package/esm/server/client-asset-url-prefix.mjs.map +1 -0
  98. package/esm/server/create-page-module-loader.mjs +49 -0
  99. package/esm/server/create-page-module-loader.mjs.map +1 -0
  100. package/esm/server/create-page-route-handler.mjs +107 -0
  101. package/esm/server/create-page-route-handler.mjs.map +1 -0
  102. package/esm/server/dev-server.mjs +167 -0
  103. package/esm/server/dev-server.mjs.map +1 -0
  104. package/esm/server/execute-page-request.d.mts +1 -0
  105. package/esm/server/execute-page-request.mjs +166 -0
  106. package/esm/server/execute-page-request.mjs.map +1 -0
  107. package/esm/server/execute-page-request.types.d.mts +1 -0
  108. package/esm/server/hydration-client-url.d.mts +37 -0
  109. package/esm/server/hydration-client-url.mjs +101 -0
  110. package/esm/server/hydration-client-url.mjs.map +1 -0
  111. package/esm/server/index.mjs +13 -0
  112. package/esm/server/install-page-routes-from-manifest.mjs +133 -0
  113. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
  114. package/esm/server/install-page-routes.d.mts +1 -0
  115. package/esm/server/install-page-routes.mjs +6 -0
  116. package/esm/server/install-production-page-routes.mjs +55 -0
  117. package/esm/server/install-production-page-routes.mjs.map +1 -0
  118. package/esm/server/match-page-route.mjs +48 -0
  119. package/esm/server/match-page-route.mjs.map +1 -0
  120. package/esm/server/page-context.d.mts +1 -0
  121. package/esm/server/page-context.mjs +33 -0
  122. package/esm/server/page-context.mjs.map +1 -0
  123. package/esm/server/page-manifest.d.mts +102 -0
  124. package/esm/server/page-manifest.mjs +31 -0
  125. package/esm/server/page-manifest.mjs.map +1 -0
  126. package/esm/server/render-page.mjs +214 -0
  127. package/esm/server/render-page.mjs.map +1 -0
  128. package/esm/server/resolve-page-metadata.mjs +33 -0
  129. package/esm/server/resolve-page-metadata.mjs.map +1 -0
  130. package/esm/server/resolve-validation-data.mjs +27 -0
  131. package/esm/server/resolve-validation-data.mjs.map +1 -0
  132. package/esm/server/settle-page-response.d.mts +1 -0
  133. package/esm/server/settle-page-response.mjs +90 -0
  134. package/esm/server/settle-page-response.mjs.map +1 -0
  135. package/esm/server/stylesheet-urls.mjs +65 -0
  136. package/esm/server/stylesheet-urls.mjs.map +1 -0
  137. package/esm/server/web-connector-factory.d.mts +25 -0
  138. package/esm/server/web-connector-factory.mjs +121 -0
  139. package/esm/server/web-connector-factory.mjs.map +1 -0
  140. package/esm/server/web-connector.d.mts +34 -0
  141. package/esm/server/web-connector.mjs +516 -0
  142. package/esm/server/web-connector.mjs.map +1 -0
  143. package/esm/shared.d.mts +22 -0
  144. package/esm/shared.mjs +223 -0
  145. package/esm/shared.mjs.map +1 -0
  146. package/esm/validation.d.mts +31 -0
  147. package/esm/vite/app-convention-aliases.mjs +43 -0
  148. package/esm/vite/app-convention-aliases.mjs.map +1 -0
  149. package/esm/vite/build-client.d.mts +38 -0
  150. package/esm/vite/build-client.mjs +72 -0
  151. package/esm/vite/build-client.mjs.map +1 -0
  152. package/esm/vite/gate-a-resolve.d.mts +57 -0
  153. package/esm/vite/gate-a-resolve.mjs +834 -0
  154. package/esm/vite/gate-a-resolve.mjs.map +1 -0
  155. package/esm/vite/gate-b-secrets.d.mts +57 -0
  156. package/esm/vite/gate-b-secrets.mjs +299 -0
  157. package/esm/vite/gate-b-secrets.mjs.map +1 -0
  158. package/esm/vite/gate-c-verify.d.mts +118 -0
  159. package/esm/vite/gate-c-verify.mjs +263 -0
  160. package/esm/vite/gate-c-verify.mjs.map +1 -0
  161. package/esm/vite/hydration-entries.d.mts +17 -0
  162. package/esm/vite/hydration-entries.mjs +45 -0
  163. package/esm/vite/hydration-entries.mjs.map +1 -0
  164. package/esm/vite/index.d.mts +126 -0
  165. package/esm/vite/index.mjs +146 -0
  166. package/esm/vite/index.mjs.map +1 -0
  167. package/esm/vite/page-registry-plugin.d.mts +43 -0
  168. package/esm/vite/page-registry-plugin.mjs +138 -0
  169. package/esm/vite/page-registry-plugin.mjs.map +1 -0
  170. package/esm/vite/projection.d.mts +31 -0
  171. package/esm/vite/projection.mjs +384 -0
  172. package/esm/vite/projection.mjs.map +1 -0
  173. package/llms-full.txt +1041 -0
  174. package/llms.txt +21 -0
  175. package/package.json +77 -0
  176. package/skills/add-web-to-an-app/SKILL.md +103 -0
  177. package/skills/create-a-page/SKILL.md +141 -0
  178. package/skills/load-page-data/SKILL.md +183 -0
  179. package/skills/navigate-on-the-client/SKILL.md +182 -0
  180. package/skills/serve-styles/SKILL.md +145 -0
  181. package/skills/use-layouts/SKILL.md +121 -0
  182. package/skills/write-the-root/SKILL.md +131 -0
@@ -0,0 +1,834 @@
1
+ import { builtinModules } from "node:module";
2
+ import { existsSync, readFileSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parse } from "@babel/parser";
6
+
7
+ //#region ../web/src/vite/gate-a-resolve.ts
8
+ /**
9
+ * Gate A — `resolveId` refusal for the client build.
10
+ *
11
+ * Resolution fails for import PATHS that can never be client-safe:
12
+ * 1. Node builtins (`fs`, `crypto`, `node:*`, ...) — see `isNodeBuiltin` for
13
+ * why the set is ASKED of Node rather than written down, and
14
+ * `externalizedBuiltins` for the one route that reaches a chunk
15
+ * without passing through `resolveId` at all. This rule is judged for
16
+ * EVERY importer, in or out of `GOVERNED_SCOPES` — see its note below.
17
+ * 2. `@warlock.js/*` / `@mongez/*` packages whose `package.json` carries
18
+ * `"warlock": { "environment": "server" }`. An ABSENT marker is NOT a
19
+ * refusal — see the burden note on `createEnvironmentClassifier`. Judged
20
+ * only for imports whose importer is inside the app root — see
21
+ * `isInsideAppRoot` note below. An import statement carrying no VALUE
22
+ * binding is not refused but ERASED — see the type-only note on
23
+ * `recordImportKinds`.
24
+ * 3. Server-only modules declared by FILE NAME: `*.server.ts` files,
25
+ * anything under a `.server/` directory, and — for the app's OWN source
26
+ * only — anything under a plain `server/` directory segment.
27
+ * 4. Local modules outside a `$module/web/` (or `src/web/`) folder that are
28
+ * not a recognized universal surface (`*.page.tsx`, `layout.tsx`). The app
29
+ * root (`src/web/root.tsx`) is admitted by the `web/` FOLDER rule above,
30
+ * not by its name — see the note in `isRecognizedUniversalSurface`. A
31
+ * specifier that omits its file extension is judged on the file it will
32
+ * actually load, not on the extensionless string — see
33
+ * `completeLocalModulePath`.
34
+ * 5. The `server-only` npm package — a module importing it has declared
35
+ * itself server-side and cannot reach the browser. Judged by NAME, right
36
+ * after rule 1 and ahead of rule 2's scope check; see
37
+ * `BOUNDARY_DECLARATION_PACKAGES`.
38
+ *
39
+ * Gate B (inline secrets), Gate C (output verification) and the SSR mirror
40
+ * rule (`*.client.*` refusal / `clientOnly()`) are NOT this gate. Do not
41
+ * extend this file to cover them; they are separate, later slices.
42
+ */
43
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
44
+ /**
45
+ * Node's OWN list of its builtins, read at runtime from `node:module`, never
46
+ * copied into this file. A deny-list of names would be correct on the day it
47
+ * was written and wrong on the day Node ships the next builtin — `node:sqlite`
48
+ * and `node:test` are both recent enough to have missed any list this repo
49
+ * could have hand-typed — and a fence that fails open on names nobody has
50
+ * added yet is a fence with an expiry date nobody is watching. Asking the
51
+ * runtime costs one import and cannot drift: the process doing the bundling is
52
+ * the same process whose builtins are at issue.
53
+ *
54
+ * The `node:`-prefixed forms are handled separately (see `isNodeBuiltin`), so
55
+ * this set only needs the names as `builtinModules` reports them.
56
+ */
57
+ const NODE_BUILTINS = new Set(builtinModules);
58
+ /**
59
+ * Every SPELLING of every builtin — bare and `node:`-prefixed — derived from
60
+ * the set above rather than enumerated. Used only by
61
+ * `externalizedBuiltins`, which has to interrogate a predicate it does
62
+ * not control and therefore needs concrete strings to ask about. `node:test`,
63
+ * `node:sea` and friends are reported by `builtinModules` ALREADY prefixed and
64
+ * have no bare form at all, so they are passed through unchanged.
65
+ */
66
+ const NODE_BUILTIN_SPELLINGS = [...NODE_BUILTINS].flatMap((name) => name.startsWith("node:") ? [name] : [name, `node:${name}`]);
67
+ /**
68
+ * Scopes whose `package.json` Gate A will read for a `warlock.environment`
69
+ * marker. Third-party packages outside these two scopes (react, lodash, ...)
70
+ * are never asked for this field — they are judged solely by Gate A's other,
71
+ * import-based rules. Note that a governed package is only REFUSED when it
72
+ * declares `"server"`; being in scope is not itself a refusal.
73
+ */
74
+ const GOVERNED_SCOPES = ["@warlock.js/", "@mongez/"];
75
+ function isGovernedScope(source) {
76
+ return GOVERNED_SCOPES.some((scope) => source.startsWith(scope));
77
+ }
78
+ /**
79
+ * The package root of a scoped specifier, e.g. `@warlock.js/core/db` ->
80
+ * `@warlock.js/core`, `@warlock.js/core` -> `@warlock.js/core`.
81
+ */
82
+ function governedPackageNameOf(source) {
83
+ const [scope, name] = source.split("/");
84
+ return `${scope}/${name}`;
85
+ }
86
+ /**
87
+ * Indexes every `@warlock.js/*` / `@mongez/*` workspace member's
88
+ * `package.json` path from the monorepo root's `package.json` `workspaces`
89
+ * array (`48cb4ae7` discipline — enumerated, never hand-typed). Walks up from
90
+ * this file looking for the workspace root (identified by
91
+ * `name: "warlock-workspace"`) so it works whether this module runs from
92
+ * `web/src/vite/` (dev) or a compiled `web/esm/vite/` output. Returns
93
+ * `undefined` when no monorepo root is reachable — e.g. this package
94
+ * installed standalone in a consuming app — in which case governed packages
95
+ * are located via `node_modules` instead (see `findNodeModulesPackageJson`).
96
+ */
97
+ function findWorkspaceIndex(startDir) {
98
+ let dir = startDir;
99
+ for (let depth = 0; depth < 6; depth++) {
100
+ const candidate = path.join(dir, "package.json");
101
+ if (existsSync(candidate)) try {
102
+ const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
103
+ if (pkg?.name === "warlock-workspace" && Array.isArray(pkg.workspaces)) {
104
+ const packageJsonByName = /* @__PURE__ */ new Map();
105
+ for (const workspace of pkg.workspaces) {
106
+ const workspacePkgPath = path.join(dir, workspace, "package.json");
107
+ if (!existsSync(workspacePkgPath)) continue;
108
+ try {
109
+ const workspacePkg = JSON.parse(readFileSync(workspacePkgPath, "utf-8"));
110
+ if (typeof workspacePkg.name === "string" && isGovernedScope(`${workspacePkg.name}/`)) packageJsonByName.set(workspacePkg.name, workspacePkgPath);
111
+ } catch {}
112
+ }
113
+ return { packageJsonByName };
114
+ }
115
+ } catch {}
116
+ const parent = path.dirname(dir);
117
+ if (parent === dir) break;
118
+ dir = parent;
119
+ }
120
+ }
121
+ /**
122
+ * Locates a governed-scope package's `package.json` the ordinary Node
123
+ * resolution way — walking up `node_modules` directories from `startDir` —
124
+ * for packages that are not monorepo workspace members (published
125
+ * dependencies, e.g. `@mongez/reinforcements`, or any governed package when
126
+ * this module runs standalone outside the monorepo).
127
+ */
128
+ function findNodeModulesPackageJson(pkgName, startDir) {
129
+ let dir = startDir;
130
+ for (let depth = 0; depth < 20; depth++) {
131
+ const candidate = path.join(dir, "node_modules", pkgName, "package.json");
132
+ if (existsSync(candidate)) return candidate;
133
+ const parent = path.dirname(dir);
134
+ if (parent === dir) break;
135
+ dir = parent;
136
+ }
137
+ }
138
+ /**
139
+ * The governed-scope package name owning `normalized` (an already-`/`-
140
+ * normalized absolute path), found by taking the LAST `node_modules/`
141
+ * segment — nested `node_modules` (a dependency's own vendored copy) must
142
+ * resolve to the nearest enclosing package, not the outermost one. Returns
143
+ * `undefined` for paths with no `node_modules` segment, or whose owning
144
+ * package isn't in a governed scope.
145
+ */
146
+ function packageNameFromNodeModulesPath(normalized) {
147
+ const segments = normalized.split("node_modules/");
148
+ if (segments.length < 2) return void 0;
149
+ const afterLast = segments[segments.length - 1];
150
+ const parts = afterLast.split("/");
151
+ const name = afterLast.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
152
+ return isGovernedScope(`${name}/`) ? name : void 0;
153
+ }
154
+ /**
155
+ * The two npm packages whose entire published API is a declaration of which
156
+ * side of the client/server boundary their importer belongs to: importing
157
+ * `server-only` says "this module must never reach the browser", importing
158
+ * `client-only` says the mirror. Mapped to the environment each one declares
159
+ * of its IMPORTER.
160
+ *
161
+ * Recognized by NAME, ahead of rule 2's scope check, for a reason that is not
162
+ * a special case: both sit outside `GOVERNED_SCOPES`, so `environmentOf` is
163
+ * never consulted for them under ANY default — no marker resolution, current
164
+ * or future, can ever classify them. Their name IS the marker, and reading it
165
+ * is the same move rule 3 makes when it reads `.server.ts` off a file name.
166
+ *
167
+ * Only a `"server"` declaration is a violation HERE. `client-only` is
168
+ * recognized and deliberately permitted: this is the client build, so a module
169
+ * declaring itself client-side is exactly where it belongs. Judging it is the
170
+ * SSR mirror rule's job, and that gate is not this file (see the header note).
171
+ */
172
+ const BOUNDARY_DECLARATION_PACKAGES = {
173
+ "server-only": "server",
174
+ "client-only": "client"
175
+ };
176
+ /**
177
+ * The boundary-declaration package a specifier reaches, if any — matching the
178
+ * package itself or a subpath of it (`server-only/empty` is a real published
179
+ * entry point), never a merely similar name like `server-only-utils`.
180
+ */
181
+ function boundaryDeclarationOf(source) {
182
+ for (const [name, declares] of Object.entries(BOUNDARY_DECLARATION_PACKAGES)) if (source === name || source.startsWith(`${name}/`)) return {
183
+ name,
184
+ declares
185
+ };
186
+ }
187
+ /**
188
+ * Whether a specifier names a Node builtin, in either spelling.
189
+ *
190
+ * The `node:` prefix is answered by SHAPE, not by membership: the prefix is
191
+ * reserved by Node for builtins and nothing else can ever legally claim it, so
192
+ * `node:anything` is refused even when `builtinModules` has never heard of it.
193
+ * That covers the prefix-only builtins (`node:test`, `node:sqlite`, `node:sea`)
194
+ * on Node versions whose `builtinModules` omits them, and every builtin Node
195
+ * has not shipped yet, without this file knowing a single one of their names.
196
+ *
197
+ * Bare names fall through to Node's own set, which already carries the subpath
198
+ * entries (`fs/promises`, `stream/web`, `timers/promises`, ...) as first-class
199
+ * members — they need no prefix matching of their own.
200
+ */
201
+ function isNodeBuiltin(source) {
202
+ if (source.startsWith("node:")) return true;
203
+ return NODE_BUILTINS.has(source);
204
+ }
205
+ function normalize(filePath) {
206
+ return filePath.replace(/\\/g, "/");
207
+ }
208
+ /**
209
+ * A plain `server/` path segment, tested against a path ALREADY made relative
210
+ * to the app root — never against the absolute path. An app checked out at
211
+ * `D:/work/server/shop` would otherwise have every one of its own files match.
212
+ */
213
+ const APP_SERVER_SEGMENT = /(^|\/)server(\/|$)/;
214
+ function isServerFile(resolvedPath, appRoot) {
215
+ const normalized = normalize(resolvedPath);
216
+ if (/\.server(\.[jt]sx?)?$/.test(normalized)) return true;
217
+ if (/(^|\/)\.server(\/|$)/.test(normalized)) return true;
218
+ if (isAppSourcePath(resolvedPath, appRoot)) {
219
+ if (APP_SERVER_SEGMENT.test(normalize(path.relative(appRoot, resolvedPath)))) return true;
220
+ }
221
+ return false;
222
+ }
223
+ /**
224
+ * Rule 4's subject set: local files that CARRY CODE. Deliberately the same
225
+ * shape as `PARSEABLE_MODULE_EXTENSIONS` below — one notion of "this is a
226
+ * JS/TS module" for the whole file — which also picks up `.mts`/`.cts`, absent
227
+ * from the earlier hand-spelled `/\.(tsx?|jsx?|mjs|cjs)$/`. `.mts` is a real
228
+ * module extension Vite resolves, so a file wearing it was walking past rule 4
229
+ * with its extension written out in full.
230
+ *
231
+ * Every extension `IMPLICIT_MODULE_EXTENSIONS` can append MUST match this —
232
+ * see the note there.
233
+ */
234
+ const LOCAL_MODULE_EXTENSIONS = /\.([cm]?[jt]sx?)$/;
235
+ /**
236
+ * The extensions a local specifier may leave off, in Vite's own
237
+ * `resolve.extensions` order.
238
+ *
239
+ * `.json` is deliberately absent: it is not code-carrying, so completing to it
240
+ * would only hand rule 4 a path it exempts one line later, and the exemption is
241
+ * the correct answer for a data file.
242
+ *
243
+ * The list and `LOCAL_MODULE_EXTENSIONS` have to agree in one direction: an
244
+ * extension this list can PRODUCE but that regex does not RECOGNIZE is a fresh
245
+ * escape hatch of exactly the shape this completion exists to close — the
246
+ * judged path would gain a suffix and still be waved through. Pinned by case 32
247
+ * in the spec. The order only decides which of two same-named files gets
248
+ * judged, and every rule downstream (`isServerFile`, `isWithinModuleWebFolder`,
249
+ * `isRecognizedUniversalSurface`) answers identically for `x.ts` and `x.js`, so
250
+ * matching Vite is for least surprise rather than for correctness.
251
+ */
252
+ const IMPLICIT_MODULE_EXTENSIONS = [
253
+ ".mjs",
254
+ ".js",
255
+ ".mts",
256
+ ".ts",
257
+ ".jsx",
258
+ ".tsx",
259
+ ".cjs",
260
+ ".cts"
261
+ ];
262
+ function isFile(candidate) {
263
+ try {
264
+ return statSync(candidate).isFile();
265
+ } catch {
266
+ return false;
267
+ }
268
+ }
269
+ /**
270
+ * The file a local specifier will ACTUALLY load, given the path `resolveId`
271
+ * derived from it with a raw `path.resolve`.
272
+ *
273
+ * Without this, rule 4 was escapable by writing the import the ordinary way.
274
+ * `resolveId` runs before anything appends an extension, so `./services/helper`
275
+ * arrives as a path ending in `helper`; rule 4 asks for a code extension —
276
+ * rightly, since it has no business judging `./logo.css` — finds none, and
277
+ * declines to judge at all. `./x.middleware` slipped the same way while LOOKING
278
+ * like it had an extension, and so did `./services` resolving to
279
+ * `services/index.ts`. Measured: all three built clean (spec cases 25-28, 32).
280
+ *
281
+ * Asked of the FILESYSTEM rather than answered from the string, because the
282
+ * string cannot tell an omitted extension from a real one: `.middleware` is not
283
+ * a code extension and not an asset extension either, and only the disk knows
284
+ * whether `thing.middleware.ts` is sitting there. Probing `<path><ext>` and
285
+ * then `<path>/index<ext>` is what Vite is about to do anyway, so the rule
286
+ * judges the file that will really be bundled.
287
+ *
288
+ * Deliberately NOT cached. A handful of `stat` calls per local specifier is
289
+ * what every resolver in the pipeline already spends, and a cache would go
290
+ * stale in watch mode exactly when a file is created or deleted — which is the
291
+ * moment a fence must not be answering from memory.
292
+ *
293
+ * Three early exits, each a case where completion has nothing to add or no
294
+ * business guessing:
295
+ * - a non-absolute path (a bare package specifier — rules 1/2's business)
296
+ * or a virtual `\0` id;
297
+ * - a query suffix (`./helper?raw`, `./thing?worker`). The query changes what
298
+ * the id MEANS to Vite, and inventing a file for it is a separate decision
299
+ * from this one. Rule 4 still does not judge these — a known gap, narrower
300
+ * than the one being closed here, and not widened by guesswork;
301
+ * - a path that already carries a code extension, or that already exists as a
302
+ * real non-code file (an asset — `./theme.css`), where the path on hand is
303
+ * the answer.
304
+ */
305
+ function completeLocalModulePath(judgedPath) {
306
+ if (!path.isAbsolute(judgedPath)) return judgedPath;
307
+ if (judgedPath.includes("?") || judgedPath.includes("\0")) return judgedPath;
308
+ if (LOCAL_MODULE_EXTENSIONS.test(judgedPath)) return judgedPath;
309
+ if (path.extname(judgedPath) !== "" && isFile(judgedPath)) return judgedPath;
310
+ for (const extension of IMPLICIT_MODULE_EXTENSIONS) {
311
+ const candidate = `${judgedPath}${extension}`;
312
+ if (isFile(candidate)) return candidate;
313
+ }
314
+ for (const extension of IMPLICIT_MODULE_EXTENSIONS) {
315
+ const candidate = path.join(judgedPath, `index${extension}`);
316
+ if (isFile(candidate)) return candidate;
317
+ }
318
+ return judgedPath;
319
+ }
320
+ /**
321
+ * Whether a judged path is the APP'S OWN SOURCE, as opposed to a dependency's
322
+ * internals. This is Gate A's single definition of that boundary — rules 3
323
+ * and 4 both call it, and `packageNameForFilePath` draws the same line for
324
+ * Gate C. Two conditions, both already required by rule 4 before this was
325
+ * extracted:
326
+ *
327
+ * - inside `appRoot` (a monorepo sibling or any other path resolution has
328
+ * left the app root for is that package's own business), and
329
+ * - not under a `node_modules/` segment. Nested `node_modules` inside
330
+ * `appRoot` is the ORDINARY case for a dependency, and Vite's optimized
331
+ * dependency chunks (`<appRoot>/node_modules/.vite/deps`) even import one
332
+ * another relatively.
333
+ *
334
+ * Bare package specifiers never reach here as app source: for those the
335
+ * "judged path" is still the specifier string itself (see `resolveId`), which
336
+ * is not absolute. They are rule 2's business.
337
+ */
338
+ function isAppSourcePath(resolvedPath, appRoot) {
339
+ if (!path.isAbsolute(resolvedPath)) return false;
340
+ if (!isInsideAppRoot(resolvedPath, appRoot)) return false;
341
+ return !normalize(resolvedPath).includes("/node_modules/");
342
+ }
343
+ function isInsideAppRoot(resolvedPath, appRoot) {
344
+ const relative = path.relative(appRoot, resolvedPath);
345
+ return !!relative && !relative.startsWith("..") && !path.isAbsolute(relative);
346
+ }
347
+ /** Files whose source Gate A will parse for import kinds. */
348
+ const PARSEABLE_MODULE_EXTENSIONS = /\.([cm]?[jt]sx?)$/;
349
+ /** Cheap pre-filter: no governed scope in the text, nothing to classify. */
350
+ const GOVERNED_SCOPE_MENTION = /@warlock\.js\/|@mongez\//;
351
+ /**
352
+ * The id Gate A resolves a type-only edge into a server-only package to. `\0`
353
+ * is the virtual-module convention, which also keeps Gate C from trying to
354
+ * attribute this id to a package on disk (`packageNameForFilePath` returns
355
+ * `undefined` for any id containing `\0` — see its note).
356
+ */
357
+ const TYPE_ONLY_ERASED_ID = "\0warlock:type-only-erased";
358
+ /**
359
+ * Rollup module ids carry query suffixes (`?v=`, `?used`, ...) that differ
360
+ * between the `transform` id and a later `importer`. Both sides key through
361
+ * here so they agree.
362
+ */
363
+ function moduleKey(id) {
364
+ return normalize(id.split("?")[0]);
365
+ }
366
+ function parserPluginsFor(id) {
367
+ return /\.[cm]?ts$/.test(moduleKey(id)) ? ["typescript"] : ["typescript", "jsx"];
368
+ }
369
+ /**
370
+ * Whether an `ImportDeclaration`'s specifier list is entirely type-only.
371
+ *
372
+ * Both spellings count, because both are erased: the statement form
373
+ * (`import type { X } from "P"`, `decl.importKind === "type"`) and the inline
374
+ * form (`import { type X } from "P"`, every specifier's own `importKind`).
375
+ * The inline form is the one that matters in practice — under
376
+ * `verbatimModuleSyntax` TypeScript/esbuild emit it VERBATIM as
377
+ * `import {} from "P"`, so it is the only one that survives to `resolveId` at
378
+ * all.
379
+ *
380
+ * An EMPTY specifier list (`import "P"`) is deliberately `"value"`: a bare
381
+ * side-effect import is a real runtime edge that the author wrote on purpose,
382
+ * and it is indistinguishable — post-esbuild — from the erased form. Treating
383
+ * it as type-only would turn this carve-out into a hole big enough to smuggle
384
+ * a whole server package through.
385
+ */
386
+ function importDeclarationKind(decl) {
387
+ if (decl.importKind === "type") return "type";
388
+ if (decl.specifiers.length === 0) return "value";
389
+ return decl.specifiers.every((spec) => spec.importKind === "type") ? "type" : "value";
390
+ }
391
+ function exportDeclarationKind(stmt) {
392
+ if (stmt.exportKind === "type") return "type";
393
+ const specifiers = stmt.specifiers ?? [];
394
+ if (specifiers.length === 0) return "value";
395
+ return specifiers.every((spec) => spec.exportKind === "type") ? "type" : "value";
396
+ }
397
+ /**
398
+ * Every specifier reached by a RUNTIME expression rather than a static
399
+ * import/export statement — `import("P")` and `require("P")` — found by a
400
+ * duck-typed walk of the whole program, not just its top level, because both
401
+ * are ordinary expressions and can sit anywhere.
402
+ *
403
+ * These are unconditionally `"value"`. A dynamic import of a server-only
404
+ * package is exactly the edge rule 2 exists to refuse, and it reaches
405
+ * `resolveId` with the same specifier string a static import would, so
406
+ * without this walk a file could hold `import { type X } from "P"` beside
407
+ * `await import("P")` and have the static statement's kind exempt the dynamic
408
+ * one.
409
+ */
410
+ function collectRuntimeSpecifiers(node, into) {
411
+ if (!node || typeof node !== "object") return;
412
+ if (Array.isArray(node)) {
413
+ for (const item of node) collectRuntimeSpecifiers(item, into);
414
+ return;
415
+ }
416
+ const record = node;
417
+ if (typeof record.type !== "string") return;
418
+ if (record.type === "CallExpression" || record.type === "ImportExpression") {
419
+ const callee = record.callee;
420
+ if (record.type === "ImportExpression" || callee?.type === "Import" || callee?.type === "Identifier" && callee.name === "require") {
421
+ const arg = record.source ?? record.arguments?.[0];
422
+ if (arg?.type === "StringLiteral" && typeof arg.value === "string") into.add(arg.value);
423
+ }
424
+ }
425
+ for (const key of Object.keys(record)) {
426
+ if (key === "type" || key === "start" || key === "end" || key === "loc" || key === "range") continue;
427
+ if (key === "leadingComments" || key === "trailingComments" || key === "innerComments" || key === "extra") continue;
428
+ collectRuntimeSpecifiers(record[key], into);
429
+ }
430
+ }
431
+ /**
432
+ * Classifies every specifier `code` imports as `"value"` or `"type"`. Returns
433
+ * `undefined` when the source could not be parsed — the caller reads that as
434
+ * "no information", which leaves rule 2 refusing exactly as it does today.
435
+ */
436
+ function classifyImportKinds(code, id) {
437
+ let ast;
438
+ try {
439
+ ast = parse(code, {
440
+ sourceType: "module",
441
+ plugins: parserPluginsFor(id),
442
+ errorRecovery: false
443
+ });
444
+ } catch {
445
+ return;
446
+ }
447
+ const kinds = /* @__PURE__ */ new Map();
448
+ const record = (source, kind) => {
449
+ if (kind === "value" || !kinds.has(source)) kinds.set(source, kind);
450
+ };
451
+ for (const stmt of ast.program.body) {
452
+ if (stmt.type === "ImportDeclaration") {
453
+ record(stmt.source.value, importDeclarationKind(stmt));
454
+ continue;
455
+ }
456
+ if ((stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportAllDeclaration") && stmt.source) record(stmt.source.value, exportDeclarationKind(stmt));
457
+ }
458
+ const runtimeSpecifiers = /* @__PURE__ */ new Set();
459
+ collectRuntimeSpecifiers(ast.program.body, runtimeSpecifiers);
460
+ for (const source of runtimeSpecifiers) kinds.set(source, "value");
461
+ return kinds;
462
+ }
463
+ /**
464
+ * Rule 2's predicate on its own, extracted so `resolveId` can ask the question
465
+ * a SECOND time after `ruleViolation` has declined to refuse: a type-only edge
466
+ * into a server-only package is not a violation, but it is not an ordinary
467
+ * import either — it is erased (see `TYPE_ONLY_ERASED_ID`). Asking separately,
468
+ * rather than short-circuiting inside `ruleViolation`, is what keeps rules 3
469
+ * and 4 authoritative over a type-only specifier: `@warlock.js/core/db.server`
470
+ * imported for its types alone is still refused, by rule 3, because
471
+ * `ruleViolation` still runs to completion first.
472
+ */
473
+ function isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot) {
474
+ if (!isGovernedScope(source)) return false;
475
+ if (importer && !isInsideAppRoot(importer, appRoot)) return false;
476
+ return environmentOf(governedPackageNameOf(source)) === "server";
477
+ }
478
+ function ruleViolation(source, resolvedPath, importer, environmentOf, appRoot, isTypeOnlyEdge) {
479
+ if (isNodeBuiltin(source)) return {
480
+ cause: `"${source}" is a Node.js builtin module and cannot run in the browser.`,
481
+ fix: `Move the code that needs "${source}" into a *.server.ts file or a server-only loader/controller, and pass only serializable data to the client.`
482
+ };
483
+ const boundaryDeclaration = boundaryDeclarationOf(source);
484
+ if (boundaryDeclaration?.declares === "server") {
485
+ const declarer = importer ? displayName(importer) : source;
486
+ return {
487
+ cause: `"${declarer}" imports "${boundaryDeclaration.name}" — that package publishes no API at all, only a declaration: any module importing it is server-side and must never be bundled for the browser.`,
488
+ fix: `Move the server work in "${declarer}" into a *.server.ts file, a server/ folder, a loader, or a controller — the client only needs the serialized data it returns — or, if this module is genuinely browser-safe, remove its "${boundaryDeclaration.name}" import.`
489
+ };
490
+ }
491
+ if (!isTypeOnlyEdge && isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)) {
492
+ const pkgName = governedPackageNameOf(source);
493
+ return {
494
+ cause: `"${source}" resolves into ${pkgName}, a server-only ${pkgName.startsWith("@warlock.js/") ? "@warlock.js" : "@mongez"} package — it declares "warlock": { "environment": "server" } in its package.json.`,
495
+ fix: `Move this import behind a *.server.ts file, a server/ folder, a loader, or a controller — the client only needs the serialized data it returns — or, if ${pkgName} is genuinely universal/client-safe, change its marker to "warlock": { "environment": "universal" } (or "client").`
496
+ };
497
+ }
498
+ if (isServerFile(resolvedPath, appRoot)) return {
499
+ cause: `"${source}" is a server-only file — its name declares it: it matches *.server.ts, or it lives under a .server/ directory, or under a server/ folder in this app's source.`,
500
+ fix: `Import the data this file produces through a loader instead of importing the server file directly, or move the shared logic into a universal helper under $module/web/ — outside any server/ folder and without a .server suffix.`
501
+ };
502
+ }
503
+ /**
504
+ * The label one hop of an import chain carries.
505
+ *
506
+ * A basename is enough for the app's own source, where file names are chosen
507
+ * and distinct. It is not enough for a dependency: a large share of
508
+ * node_modules resolves to a file literally called `index.js`, and a chain
509
+ * reading `page.tsx → index.js → node:fs` names the culprit for nobody — which
510
+ * is the failure mode where people switch the fence off instead of fixing the
511
+ * import. Anything under a `node_modules/` segment is therefore labelled with
512
+ * its path from the LAST such segment (`vendor-node-lib/index.js`), so a
513
+ * vendored nested copy reports the package that actually holds the file rather
514
+ * than the one at the top of the tree.
515
+ */
516
+ function displayName(id) {
517
+ const normalized = normalize(id);
518
+ const segments = normalized.split("node_modules/");
519
+ if (segments.length < 2) return path.basename(normalized);
520
+ return segments[segments.length - 1];
521
+ }
522
+ /**
523
+ * Every Node builtin the Rollup config would externalize — empty when none,
524
+ * which is the only shape that lets a build proceed past `buildStart`.
525
+ *
526
+ * This is the one route a builtin can take into a browser chunk without ever
527
+ * being offered to `resolveId`: Rollup consults `external` FIRST, and an
528
+ * externalized specifier is emitted into the output verbatim — measured, on
529
+ * this gate's own fixture, as a clean build whose chunk still opens with
530
+ * `import { readFileSync } from "node:fs"`. No `resolveId` rule can close that,
531
+ * because the hook is never called; the check has to happen against the config
532
+ * itself, before any module is loaded.
533
+ *
534
+ * Asked as a QUESTION of the normalized predicate rather than by reading an
535
+ * array, because `external` may legitimately be a string, a RegExp, an array of
536
+ * either, or a function — Rollup normalizes all of them into the function
537
+ * handed to `buildStart`, and asking it about each builtin spelling is the only
538
+ * form of the check that covers every shape. A predicate that throws on an
539
+ * unexpected shape of argument is read as "not external": this guard exists to
540
+ * catch a config that says yes, and inventing a refusal out of someone else's
541
+ * exception would be a false positive with no evidence behind it.
542
+ *
543
+ * This fires on CONFIGURATION, not on an actual import — nothing need import
544
+ * `fs` for it to trip. That is on purpose: the config has already decided the
545
+ * fence does not apply, and a build that would silently pass today only because
546
+ * no import happens to exist yet is not a build anyone should trust tomorrow.
547
+ */
548
+ function externalizedBuiltins(isExternal) {
549
+ if (typeof isExternal !== "function") return [];
550
+ const asks = isExternal;
551
+ return NODE_BUILTIN_SPELLINGS.filter((specifier) => {
552
+ try {
553
+ return asks(specifier, void 0, false) === true;
554
+ } catch {
555
+ return false;
556
+ }
557
+ });
558
+ }
559
+ /**
560
+ * The `warlock.environment` marker classifier (rule 2). Extracted from
561
+ * `gateAResolve` so Gate C can classify packages it finds in the EMITTED
562
+ * bundle graph with the identical resolution logic — same workspace index,
563
+ * same `node_modules` fallback, same default — rather than a second,
564
+ * potentially-drifting implementation.
565
+ *
566
+ * An ABSENT marker classifies as `"universal"`, i.e. allowed. This is a
567
+ * deliberate reversal of the earlier burden-inversion default. A marker is
568
+ * shipped metadata: making silence mean "refuse" meant every browser-safe
569
+ * package we own needed a marker AND a release before any app could boot, and
570
+ * a package the app does not control (`@mongez/react-atom`) could not be
571
+ * imported at all. Refusal is now driven by information that is PRESENT — an
572
+ * explicit `"server"` marker, or a server-only file name (rule 3) — which the
573
+ * app author can always supply themselves without waiting on a release. The
574
+ * marker mechanism itself is untouched and still authoritative when set.
575
+ */
576
+ function createEnvironmentClassifier(options = {}) {
577
+ const forcedServerPackages = options.serverPackages ? new Set(options.serverPackages) : void 0;
578
+ const appRoot = path.resolve(options.appRoot ?? process.cwd());
579
+ const workspaceIndex = findWorkspaceIndex(__dirname);
580
+ const environmentCache = /* @__PURE__ */ new Map();
581
+ function environmentOf(pkgName) {
582
+ if (forcedServerPackages?.has(pkgName)) return "server";
583
+ const cached = environmentCache.get(pkgName);
584
+ if (cached) return cached;
585
+ const packageJsonPath = workspaceIndex?.packageJsonByName.get(pkgName) ?? findNodeModulesPackageJson(pkgName, appRoot);
586
+ let environment = "universal";
587
+ if (packageJsonPath) try {
588
+ const marker = JSON.parse(readFileSync(packageJsonPath, "utf-8"))?.warlock?.environment;
589
+ if (marker === "server" || marker === "universal" || marker === "client") environment = marker;
590
+ } catch {}
591
+ environmentCache.set(pkgName, environment);
592
+ return environment;
593
+ }
594
+ function packageNameForFilePath(absPath) {
595
+ if (absPath.includes("\0")) return void 0;
596
+ const normalized = normalize(path.resolve(absPath));
597
+ const nodeModulesMatch = packageNameFromNodeModulesPath(normalized);
598
+ if (nodeModulesMatch) return nodeModulesMatch;
599
+ if (isInsideAppRoot(normalized, appRoot)) return void 0;
600
+ if (workspaceIndex) for (const [name, pkgJsonPath] of workspaceIndex.packageJsonByName) {
601
+ const root = `${normalize(path.dirname(pkgJsonPath))}/`;
602
+ if (normalized.startsWith(root)) return name;
603
+ }
604
+ }
605
+ return {
606
+ appRoot,
607
+ environmentOf,
608
+ packageNameForFilePath
609
+ };
610
+ }
611
+ /**
612
+ * The client-build Vite plugin. `vite` is only imported for its types
613
+ * (`import type`), so this module carries no runtime dependency on `vite`
614
+ * being installed — it is a `peerDependenciesMeta.optional` peer.
615
+ */
616
+ function gateAResolve(options = {}) {
617
+ const { appRoot, environmentOf } = createEnvironmentClassifier(options);
618
+ const importerOf = /* @__PURE__ */ new Map();
619
+ /**
620
+ * Module key -> that module's specifier classification, filled by the
621
+ * `transform` hook below and read by `resolveId`.
622
+ *
623
+ * This is the answer to the one structural problem rule 2 has: `resolveId`
624
+ * receives a specifier STRING and an importer path, never an AST, so it
625
+ * cannot see for itself whether an import was type-only. The information
626
+ * exists exactly once in the pipeline — in the module's PRE-esbuild
627
+ * TypeScript source — so it is captured there and carried forward.
628
+ *
629
+ * Three routes were available; this is why the other two lost:
630
+ *
631
+ * - Recognizing the POST-esbuild shape (`import {} from "P"`) from inside
632
+ * `resolveId`. Rejected as unsound, not merely awkward: esbuild emits the
633
+ * identical `import {} from "P"` for a genuine bare side-effect import
634
+ * under some settings, so the shape cannot separate "erased type import"
635
+ * from "the author asked for this package's side effects". That is a
636
+ * false NEGATIVE in a security fence — the one direction of error this
637
+ * gate must never make — and it would additionally depend on
638
+ * `getModuleInfo(importer).code` being populated during a dependency's
639
+ * `resolveId`, which is undocumented Rollup ordering.
640
+ * - Moving rule 2 wholesale into a `transform` pass. Rejected as too wide
641
+ * for this change: `transform` never runs for entry, virtual or
642
+ * externalized importers, so rule 2 would silently stop judging edges it
643
+ * judges today, and the import-chain machinery (`importerOf`) is built
644
+ * out of `resolveId`'s importer argument. Gate A is a `resolveId` fence
645
+ * by identity; only the type-only QUESTION moves, never the refusal.
646
+ * - Reusing `projection.ts`'s existing parse (`projectModule`). Rejected as
647
+ * a coupling, though it is the right TECHNIQUE and is copied here:
648
+ * projection parses only `*.page.tsx`/`layout`/`root`, records binding
649
+ * READERS rather than import KINDS, and rewrites code. Gate A must work
650
+ * standalone — every case in `gate-a-resolve.spec.ts` runs
651
+ * `gateAResolve()` with no projection in the array — so it cannot depend
652
+ * on another plugin having run.
653
+ *
654
+ * A module absent from this map (unparseable, never transformed, not JS/TS)
655
+ * yields no `"type"` answer, so rule 2 refuses exactly as it does today.
656
+ * Missing information fails CLOSED.
657
+ */
658
+ const importKindsByModule = /* @__PURE__ */ new Map();
659
+ function recordImportKinds(code, id) {
660
+ const key = moduleKey(id);
661
+ if (key.includes("\0")) return;
662
+ if (!PARSEABLE_MODULE_EXTENSIONS.test(key)) return;
663
+ if (!GOVERNED_SCOPE_MENTION.test(code)) return;
664
+ const kinds = classifyImportKinds(code, key);
665
+ if (!kinds) return;
666
+ const existing = importKindsByModule.get(key);
667
+ if (!existing) {
668
+ importKindsByModule.set(key, kinds);
669
+ return;
670
+ }
671
+ for (const [source, kind] of kinds) if (kind === "value" || !existing.has(source)) existing.set(source, kind);
672
+ }
673
+ function isTypeOnlyEdge(source, importer) {
674
+ if (!importer) return false;
675
+ return importKindsByModule.get(moduleKey(importer))?.get(source) === "type";
676
+ }
677
+ /**
678
+ * How many `resolve.alias` entries this build declares, captured from the
679
+ * RESOLVED Vite config (`configResolved`) rather than guessed.
680
+ *
681
+ * Purely diagnostic, and it is the one thing the alias table can tell Gate A
682
+ * that the resolver cannot. The gate does not match aliases itself — see the
683
+ * note on the alias pass in `resolveId` for why asking Vite's own resolver
684
+ * beats re-deriving `@rollup/plugin-alias`'s find/replacement semantics. But
685
+ * when a specifier resolves to NOTHING, the count is the difference between
686
+ * "a dependency is missing" and "this build never installed the alias table
687
+ * its own tsconfig `paths` describe" — which is exactly the v5 production
688
+ * client build's state, and the actionable half of that warning.
689
+ *
690
+ * `undefined` when `configResolved` never fired (plain Rollup, or a unit
691
+ * harness), which the message reports as unknown rather than as zero.
692
+ */
693
+ let declaredAliasCount;
694
+ function buildChain(importer) {
695
+ const chain = [];
696
+ let current = importer;
697
+ const seen = /* @__PURE__ */ new Set();
698
+ while (current && !seen.has(current)) {
699
+ seen.add(current);
700
+ chain.unshift(displayName(current));
701
+ current = importerOf.get(current);
702
+ }
703
+ return chain;
704
+ }
705
+ return {
706
+ name: "warlock:gate-a-resolve",
707
+ enforce: "pre",
708
+ /** Records the alias-table size; see `declaredAliasCount`. Judges nothing. */
709
+ configResolved(config) {
710
+ const alias = config.resolve?.alias;
711
+ if (Array.isArray(alias)) declaredAliasCount = alias.length;
712
+ else if (alias && typeof alias === "object") declaredAliasCount = Object.keys(alias).length;
713
+ else declaredAliasCount = 0;
714
+ },
715
+ /**
716
+ * The config-level half of rule 1 — see `externalizedBuiltins` for why a
717
+ * `resolveId` fence cannot cover it on its own. Runs once per build, before
718
+ * the first module is loaded, so the refusal lands before anything has been
719
+ * bundled.
720
+ */
721
+ buildStart(inputOptions) {
722
+ const externalized = externalizedBuiltins(inputOptions.external);
723
+ if (externalized.length === 0) return;
724
+ const shown = externalized.slice(0, 5).join(", ");
725
+ const rest = externalized.length > 5 ? `, and ${externalized.length - 5} more` : "";
726
+ this.error([
727
+ `Gate A refused this build: its Rollup "external" config would let Node builtins through the fence.`,
728
+ ``,
729
+ `Externalized builtins: ${shown}${rest}`,
730
+ `Cause: Rollup consults "external" BEFORE any resolveId hook, so an externalized builtin is never offered to Gate A and is emitted into the browser chunk verbatim (e.g. import { readFileSync } from "node:fs"). No import of one has to exist yet — the configuration alone has already disabled rule 1.`,
731
+ `Fix: remove the Node builtins from build.rollupOptions.external (keeping any non-builtin entries), and move the code that needs them into a *.server.ts file, a server/ folder, a loader, or a controller. If this build targets Node rather than the browser, it should not be running Gate A at all.`
732
+ ].join("\n"));
733
+ },
734
+ /**
735
+ * Records import kinds; never rewrites a byte (`null` return). `enforce:
736
+ * "pre"` is what makes this correct as well as early — it puts this hook
737
+ * ahead of Vite's own `vite:esbuild` TypeScript transform, so `code` here
738
+ * is the ORIGINAL source with `type` modifiers still on it. After esbuild
739
+ * the distinction no longer exists in the text.
740
+ *
741
+ * Runs before `resolveId` fires for any of this module's own specifiers:
742
+ * Rollup must parse a module's post-transform source to discover what to
743
+ * resolve next. That ordering is an OBSERVED Rollup behaviour, pinned by
744
+ * `index.spec.ts`'s "D.3 hook ordering pin" and documented at length in
745
+ * `index.ts`. If it ever inverts, this map is simply empty when
746
+ * `resolveId` reads it and rule 2 goes back to refusing type-only imports
747
+ * — a loud false positive, never a silent leak.
748
+ */
749
+ transform(code, id) {
750
+ recordImportKinds(code, id);
751
+ return null;
752
+ },
753
+ /**
754
+ * The erased stand-in a type-only edge into a server-only package resolves
755
+ * to. It must exist rather than the import simply being permitted, and the
756
+ * fixture measures why: `import { type Request } from "@warlock.js/core"`
757
+ * under `verbatimModuleSyntax` is emitted as `import {} from
758
+ * "@warlock.js/core"`, and Rollup honours that as a side-effect edge — the
759
+ * package's module-level code lands in the client chunk even though not one
760
+ * binding is read from it. Permitting the import would therefore trade a
761
+ * false positive for the exact silent leak the `server` marker exists to
762
+ * close. Resolving to an empty module gives both: a clean build, and
763
+ * nothing of the server package in the browser.
764
+ */
765
+ load(id) {
766
+ return id === TYPE_ONLY_ERASED_ID ? "export {};" : null;
767
+ },
768
+ async resolveId(source, importer, resolveOptions) {
769
+ const isRelativeOrAbsolute = source.startsWith(".") || path.isAbsolute(source);
770
+ const judgedPath = completeLocalModulePath(isRelativeOrAbsolute && importer ? path.resolve(path.dirname(importer), source) : source);
771
+ if (source === TYPE_ONLY_ERASED_ID) return source;
772
+ const typeOnly = isTypeOnlyEdge(source, importer);
773
+ const violation = ruleViolation(source, judgedPath, importer, environmentOf, appRoot, typeOnly);
774
+ if (violation) {
775
+ const chain = [...buildChain(importer), source].join(" → ");
776
+ this.error([
777
+ `Gate A refused an import: forbidden dependency reaches the client bundle.`,
778
+ ``,
779
+ `Import chain: ${chain}`,
780
+ `File: ${importer ?? source}`,
781
+ `Cause: ${violation.cause}`,
782
+ `Fix: ${violation.fix}`
783
+ ].join("\n"));
784
+ }
785
+ if (typeOnly && isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)) return TYPE_ONLY_ERASED_ID;
786
+ const resolved = await this.resolve(source, importer, {
787
+ ...resolveOptions,
788
+ skipSelf: true
789
+ });
790
+ if (resolved && importer) importerOf.set(resolved.id, importer);
791
+ const isBareSpecifier = !isRelativeOrAbsolute && !source.includes("\0");
792
+ if (isBareSpecifier && resolved && !resolved.external) {
793
+ const aliasPath = completeLocalModulePath(moduleKey(resolved.id));
794
+ if (isAppSourcePath(aliasPath, appRoot)) {
795
+ const aliasViolation = ruleViolation(source, aliasPath, importer, environmentOf, appRoot, typeOnly);
796
+ if (aliasViolation) this.warn({
797
+ message: [
798
+ `Gate A WOULD refuse this import — reported as a WARNING, not enforced yet.`,
799
+ ``,
800
+ `Import chain: ${[...buildChain(importer), source].join(" → ")}`,
801
+ `File: ${importer ?? source}`,
802
+ `Specifier: "${source}"`,
803
+ `Resolves to: ${aliasPath}`,
804
+ `Cause: ${aliasViolation.cause}`,
805
+ `Fix: ${aliasViolation.fix}`,
806
+ `Why only a warning: this specifier is written in ALIAS form. The identical import spelled relatively or absolutely is REFUSED today — the spelling was the only difference — and rules 3 and 4 have only just started judging this form. Every line like this is either an import to fix or a rule to correct; when there are none left, this becomes a refusal.`
807
+ ].join("\n"),
808
+ id: importer
809
+ });
810
+ }
811
+ }
812
+ if (isBareSpecifier && !resolved) {
813
+ const aliasTable = declaredAliasCount === void 0 ? `this build's resolve.alias was never reported to the gate` : `this build's resolve.alias declares ${declaredAliasCount} ${declaredAliasCount === 1 ? "entry" : "entries"}`;
814
+ this.warn({
815
+ message: [
816
+ `Gate A could not judge this import — and it is going into the browser chunk unjudged.`,
817
+ ``,
818
+ `Import chain: ${[...buildChain(importer), source].join(" → ")}`,
819
+ `File: ${importer ?? source}`,
820
+ `Specifier: "${source}"`,
821
+ `Cause: nothing in this build resolves "${source}", so Rollup treats it as an external dependency and writes the import statement into the client chunk verbatim. Rules 3 and 4 judge a FILE; with no resolution there is no file, so this import crossed the fence unexamined rather than being allowed by it (${aliasTable}).`,
822
+ `Fix: if this is an app-convention alias specifier (a tsconfig "paths" entry such as app/* or web/*), give the CLIENT build the matching resolve.alias entry the dev server already installs, so the specifier resolves to a real file and Gate A can judge it. Otherwise install the missing package, or write the import relatively.`
823
+ ].join("\n"),
824
+ id: importer
825
+ });
826
+ }
827
+ return resolved ?? null;
828
+ }
829
+ };
830
+ }
831
+
832
+ //#endregion
833
+ export { createEnvironmentClassifier, gateAResolve };
834
+ //# sourceMappingURL=gate-a-resolve.mjs.map