@warlock.js/web 5.2.0 → 5.2.2
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 +15 -0
- package/esm/build/contribution.d.mts +1 -1
- package/esm/build/contribution.mjs +1 -1
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +1 -1
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +1 -1
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +1 -1
- package/esm/build/page-default-export.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs +1 -1
- package/esm/build/page-routes-manifest.mjs.map +1 -1
- package/esm/build/public-files.mjs +1 -1
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +1 -1
- package/esm/client/runtime/matcher.mjs +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +1 -1
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +1 -1
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +1 -1
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +1 -1
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.mjs +1 -1
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +1 -1
- package/esm/loaders.d.mts +1 -1
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +1 -1
- package/esm/route.d.mts +1 -1
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +1 -1
- package/esm/routing/filesystem-route.mjs.map +1 -1
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +1 -1
- package/esm/runtime/register-modules.mjs +1 -1
- package/esm/runtime/register-modules.mjs.map +1 -1
- package/esm/server/build-hydration-payload.mjs +1 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +1 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +1 -1
- package/esm/server/create-page-route-handler.mjs +1 -1
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +1 -1
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +1 -1
- package/esm/server/error-page.mjs +1 -1
- package/esm/server/error-page.mjs.map +1 -1
- package/esm/server/execute-page-request.d.mts +1 -1
- package/esm/server/execute-page-request.mjs +1 -1
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +1 -1
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +1 -1
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +1 -1
- package/esm/server/install-page-routes.mjs +1 -1
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +1 -1
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +1 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +1 -1
- package/esm/server/page-context.mjs +1 -1
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-manifest.d.mts +1 -1
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +1 -1
- package/esm/server/page-render-bundle.mjs.map +1 -1
- package/esm/server/page-route-reload.mjs +1 -1
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +1 -1
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +1 -1
- package/esm/server/render-page.mjs +1 -1
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +1 -1
- package/esm/server/settle-page-response.mjs +1 -1
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +1 -1
- package/esm/server/stylesheet-urls.mjs +1 -1
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +1 -1
- package/esm/server/unregistered-pages.mjs.map +1 -1
- package/esm/server/web-connector-factory.d.mts +1 -1
- package/esm/server/web-connector-factory.mjs +1 -1
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +1 -1
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +1 -1
- package/esm/shared.mjs +1 -1
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +1 -1
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +1 -1
- package/esm/vite/index.mjs +1 -1
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +1 -1
- package/esm/vite/page-registry-plugin.mjs +1 -1
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +1 -1
- package/esm/vite/projection.mjs +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-a-resolve.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/gate-a-resolve.ts"],"sourcesContent":["/**\n * Gate A — `resolveId` refusal for the client build.\n *\n * Resolution fails for import PATHS that can never be client-safe:\n * 1. Node builtins (`fs`, `crypto`, `node:*`, ...) — see `isNodeBuiltin` for\n * why the set is ASKED of Node rather than written down, and\n * `externalizedBuiltins` for the one route that reaches a chunk\n * without passing through `resolveId` at all. This rule is judged for\n * EVERY importer, in or out of `GOVERNED_SCOPES` — see its note below.\n * 2. `@warlock.js/*` / `@mongez/*` packages whose `package.json` carries\n * `\"warlock\": { \"environment\": \"server\" }`. An ABSENT marker is NOT a\n * refusal — see the burden note on `createEnvironmentClassifier`. Judged\n * only for imports whose importer is inside the app root — see\n * `isInsideAppRoot` note below. An import statement carrying no VALUE\n * binding is not refused but ERASED — see the type-only note on\n * `recordImportKinds`.\n * 3. Server-only modules declared by FILE NAME: `*.server.ts` files,\n * anything under a `.server/` directory, and — for the app's OWN source\n * only — anything under a plain `server/` directory segment.\n * 4. Local modules outside a `$module/web/` (or `src/web/`) folder that are\n * not a recognized universal surface (`*.page.tsx`, `layout.tsx`). The app\n * root (`src/web/root.tsx`) is admitted by the `web/` FOLDER rule above,\n * not by its name — see the note in `isRecognizedUniversalSurface`. A\n * specifier that omits its file extension is judged on the file it will\n * actually load, not on the extensionless string — see\n * `completeLocalModulePath`.\n * 5. The `server-only` npm package — a module importing it has declared\n * itself server-side and cannot reach the browser. Judged by NAME, right\n * after rule 1 and ahead of rule 2's scope check; see\n * `BOUNDARY_DECLARATION_PACKAGES`.\n *\n * Gate B (inline secrets), Gate C (output verification) and the SSR mirror\n * rule for a future client-only rendering primitive is NOT this gate. Do not\n * extend this file to cover them; they are separate, later slices.\n */\nimport { parse } from \"@babel/parser\";\nimport { builtinModules } from \"node:module\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Plugin } from \"vite\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Node's OWN list of its builtins, read at runtime from `node:module`, never\n * copied into this file. A deny-list of names would be correct on the day it\n * was written and wrong on the day Node ships the next builtin — `node:sqlite`\n * and `node:test` are both recent enough to have missed any list this repo\n * could have hand-typed — and a fence that fails open on names nobody has\n * added yet is a fence with an expiry date nobody is watching. Asking the\n * runtime costs one import and cannot drift: the process doing the bundling is\n * the same process whose builtins are at issue.\n *\n * The `node:`-prefixed forms are handled separately (see `isNodeBuiltin`), so\n * this set only needs the names as `builtinModules` reports them.\n */\nconst NODE_BUILTINS = new Set(builtinModules);\n\n/**\n * Every SPELLING of every builtin — bare and `node:`-prefixed — derived from\n * the set above rather than enumerated. Used only by\n * `externalizedBuiltins`, which has to interrogate a predicate it does\n * not control and therefore needs concrete strings to ask about. `node:test`,\n * `node:sea` and friends are reported by `builtinModules` ALREADY prefixed and\n * have no bare form at all, so they are passed through unchanged.\n */\nconst NODE_BUILTIN_SPELLINGS: string[] = [...NODE_BUILTINS].flatMap((name) =>\n name.startsWith(\"node:\") ? [name] : [name, `node:${name}`],\n);\n\nexport type WarlockEnvironment = \"server\" | \"universal\" | \"client\";\n\n/**\n * Scopes whose `package.json` Gate A will read for a `warlock.environment`\n * marker. Third-party packages outside these two scopes (react, lodash, ...)\n * are never asked for this field — they are judged solely by Gate A's other,\n * import-based rules. Note that a governed package is only REFUSED when it\n * declares `\"server\"`; being in scope is not itself a refusal.\n */\nconst GOVERNED_SCOPES = [\"@warlock.js/\", \"@mongez/\"];\n\nfunction isGovernedScope(source: string): boolean {\n return GOVERNED_SCOPES.some((scope) => source.startsWith(scope));\n}\n\n/**\n * The package root of a scoped specifier, e.g. `@warlock.js/core/db` ->\n * `@warlock.js/core`, `@warlock.js/core` -> `@warlock.js/core`.\n */\nfunction governedPackageNameOf(source: string): string {\n const [scope, name] = source.split(\"/\");\n return `${scope}/${name}`;\n}\n\ninterface WorkspaceIndex {\n /** Governed-scope workspace package name -> absolute `package.json` path. */\n packageJsonByName: Map<string, string>;\n}\n\n/**\n * Indexes every `@warlock.js/*` / `@mongez/*` workspace member's\n * `package.json` path from the monorepo root's `package.json` `workspaces`\n * array (`48cb4ae7` discipline — enumerated, never hand-typed). Walks up from\n * this file looking for the workspace root (identified by\n * `name: \"warlock-workspace\"`) so it works whether this module runs from\n * `web/src/vite/` (dev) or a compiled `web/esm/vite/` output. Returns\n * `undefined` when no monorepo root is reachable — e.g. this package\n * installed standalone in a consuming app — in which case governed packages\n * are located via `node_modules` instead (see `findNodeModulesPackageJson`).\n */\nfunction findWorkspaceIndex(startDir: string): WorkspaceIndex | undefined {\n let dir = startDir;\n for (let depth = 0; depth < 6; depth++) {\n const candidate = path.join(dir, \"package.json\");\n if (existsSync(candidate)) {\n try {\n const pkg = JSON.parse(readFileSync(candidate, \"utf-8\"));\n if (\n pkg?.name === \"warlock-workspace\" &&\n Array.isArray(pkg.workspaces)\n ) {\n const packageJsonByName = new Map<string, string>();\n for (const workspace of pkg.workspaces as string[]) {\n const workspacePkgPath = path.join(dir, workspace, \"package.json\");\n if (!existsSync(workspacePkgPath)) continue;\n try {\n const workspacePkg = JSON.parse(\n readFileSync(workspacePkgPath, \"utf-8\"),\n );\n if (\n typeof workspacePkg.name === \"string\" &&\n isGovernedScope(`${workspacePkg.name}/`)\n ) {\n packageJsonByName.set(workspacePkg.name, workspacePkgPath);\n }\n } catch {\n // Unreadable workspace package.json — node_modules fallback still applies.\n }\n }\n return { packageJsonByName };\n }\n } catch {\n // Not the workspace root (or unreadable) — keep walking up.\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/**\n * Locates a governed-scope package's `package.json` the ordinary Node\n * resolution way — walking up `node_modules` directories from `startDir` —\n * for packages that are not monorepo workspace members (published\n * dependencies, e.g. `@mongez/reinforcements`, or any governed package when\n * this module runs standalone outside the monorepo).\n */\nfunction findNodeModulesPackageJson(\n pkgName: string,\n startDir: string,\n): string | undefined {\n let dir = startDir;\n for (let depth = 0; depth < 20; depth++) {\n const candidate = path.join(dir, \"node_modules\", pkgName, \"package.json\");\n if (existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/**\n * The governed-scope package name owning `normalized` (an already-`/`-\n * normalized absolute path), found by taking the LAST `node_modules/`\n * segment — nested `node_modules` (a dependency's own vendored copy) must\n * resolve to the nearest enclosing package, not the outermost one. Returns\n * `undefined` for paths with no `node_modules` segment, or whose owning\n * package isn't in a governed scope.\n */\nfunction packageNameFromNodeModulesPath(\n normalized: string,\n): string | undefined {\n const segments = normalized.split(\"node_modules/\");\n if (segments.length < 2) return undefined;\n const afterLast = segments[segments.length - 1];\n const parts = afterLast.split(\"/\");\n const name = afterLast.startsWith(\"@\") ? `${parts[0]}/${parts[1]}` : parts[0];\n return isGovernedScope(`${name}/`) ? name : undefined;\n}\n\n/**\n * The two npm packages whose entire published API is a declaration of which\n * side of the client/server boundary their importer belongs to: importing\n * `server-only` says \"this module must never reach the browser\", importing\n * `client-only` says the mirror. Mapped to the environment each one declares\n * of its IMPORTER.\n *\n * Recognized by NAME, ahead of rule 2's scope check, for a reason that is not\n * a special case: both sit outside `GOVERNED_SCOPES`, so `environmentOf` is\n * never consulted for them under ANY default — no marker resolution, current\n * or future, can ever classify them. Their name IS the marker, and reading it\n * is the same move rule 3 makes when it reads `.server.ts` off a file name.\n *\n * Only a `\"server\"` declaration is a violation HERE. `client-only` is\n * recognized and deliberately permitted: this is the client build, so a module\n * declaring itself client-side is exactly where it belongs. Judging it is the\n * SSR mirror rule's job, and that gate is not this file (see the header note).\n */\nconst BOUNDARY_DECLARATION_PACKAGES: Record<string, WarlockEnvironment> = {\n \"server-only\": \"server\",\n \"client-only\": \"client\",\n};\n\n/**\n * The boundary-declaration package a specifier reaches, if any — matching the\n * package itself or a subpath of it (`server-only/empty` is a real published\n * entry point), never a merely similar name like `server-only-utils`.\n */\nfunction boundaryDeclarationOf(\n source: string,\n): { name: string; declares: WarlockEnvironment } | undefined {\n for (const [name, declares] of Object.entries(\n BOUNDARY_DECLARATION_PACKAGES,\n )) {\n if (source === name || source.startsWith(`${name}/`))\n return { name, declares };\n }\n return undefined;\n}\n\n/**\n * Whether a specifier names a Node builtin, in either spelling.\n *\n * The `node:` prefix is answered by SHAPE, not by membership: the prefix is\n * reserved by Node for builtins and nothing else can ever legally claim it, so\n * `node:anything` is refused even when `builtinModules` has never heard of it.\n * That covers the prefix-only builtins (`node:test`, `node:sqlite`, `node:sea`)\n * on Node versions whose `builtinModules` omits them, and every builtin Node\n * has not shipped yet, without this file knowing a single one of their names.\n *\n * Bare names fall through to Node's own set, which already carries the subpath\n * entries (`fs/promises`, `stream/web`, `timers/promises`, ...) as first-class\n * members — they need no prefix matching of their own.\n */\nfunction isNodeBuiltin(source: string): boolean {\n if (source.startsWith(\"node:\")) return true;\n return NODE_BUILTINS.has(source);\n}\n\nfunction normalize(filePath: string): string {\n return filePath.replace(/\\\\/g, \"/\");\n}\n\n/**\n * A plain `server/` path segment, tested against a path ALREADY made relative\n * to the app root — never against the absolute path. An app checked out at\n * `D:/work/server/shop` would otherwise have every one of its own files match.\n */\nconst APP_SERVER_SEGMENT = /(^|\\/)server(\\/|$)/;\n\nexport function isServerFile(resolvedPath: string, appRoot: string): boolean {\n // A Vite query suffix (`?raw`, `?worker`, `?url`, ...) changes how the\n // resolved id is CONSUMED, never which file on disk it names — the two\n // anchored patterns below test the END of the string, so a suffix still\n // attached at judgement time slides `.server`/`.server.ts` out from under\n // the `$` anchor and the file reads as clean. Stripped here, once, rather\n // than trusted to every caller: `bare` is what every check below judges,\n // matching `completeLocalModulePath`'s own bare/query split for the same\n // specifier (see its note).\n const bare = resolvedPath.split(\"?\")[0];\n const normalized = normalize(bare);\n // `.server` may or may not carry an extension by the time Gate A judges it\n // — a bare specifier like `./blog.server` (extension resolved later by\n // Vite) must be caught just as `./blog.server.ts` is.\n if (/\\.server(\\.[jt]sx?)?$/.test(normalized)) return true;\n if (/(^|\\/)\\.server(\\/|$)/.test(normalized)) return true;\n // A plain `server/` directory is judged for APP SOURCE ONLY, unlike the two\n // patterns above. `.server` in a filename is an unambiguous declaration\n // wherever it appears; a directory literally called `server/` is ordinary\n // internal layout for a dependency — `@warlock.js/web` keeps its own\n // sources in `web/src/server/` — so judging it everywhere would refuse the\n // framework itself. `isAppSourcePath` is the same app-source/dependency\n // distinction rule 4 draws, not a second one.\n if (isAppSourcePath(bare, appRoot)) {\n if (APP_SERVER_SEGMENT.test(normalize(path.relative(appRoot, bare))))\n return true;\n }\n return false;\n}\n\nexport function isRecognizedUniversalSurface(resolvedPath: string): boolean {\n const normalized = normalize(resolvedPath);\n const base = path.basename(normalized);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // There is deliberately NO app-root (`src/web/root.tsx`) case here, and\n // adding one would be dead code. The app root lives directly inside a\n // `web/` folder, so `isWithinModuleWebFolder` — which the only caller,\n // `isOutsideUniversalScope`, tests on the line BEFORE this function is\n // reached — already admits it by folder, whatever the file is named. A name\n // test here could never fire; the root's name is not what makes it\n // universal. (Verified: removing such a test leaves the app-root and\n // ordinary-sibling cases in `gate-a-resolve.spec.ts` passing.)\n return false;\n}\n\n/**\n * A `web/` path segment, tested — like `APP_SERVER_SEGMENT` above and for the\n * same reason — against a path ALREADY made relative to the app root.\n *\n * Never against the absolute path, and this repo is its own proof: Gate A's\n * fixtures live at `@warlock.js/web/__tests__/vite/fixtures/gate-a/...`, so\n * EVERY absolute path under them contains a `/web/` segment belonging to the\n * package directory rather than to any app module. Matched absolutely, rule 4\n * would admit the whole fixture tree and cases 9 and 25-28 would all go green\n * for the wrong reason.\n */\nconst MODULE_WEB_SEGMENT = /(^|\\/)web(\\/|$)/;\n\n/**\n * Whether a judged path is anywhere INSIDE a module's `web/` folder — at any\n * depth, not merely directly inside one.\n *\n * The depth matters and used to be wrong: this asked for `/web/<one-segment>`,\n * so `src/web/middleware/base.middleware.ts` — two segments in — did not count\n * as being within `src/web/` at all. Nothing had noticed because the only way\n * to reach such a file is a relative import, and rule 4 was not judging those\n * (see `completeLocalModulePath`): a nested universal file escaped the rule\n * rather than being admitted by it, and the two are indistinguishable until the\n * escape is closed. Closing it without this would have turned the v5 app's own\n * `src/web/root.tsx` — which imports exactly that middleware — into a refusal.\n *\n * Rule 4's premise is a FOLDER (\"outside a `$module/web/` folder\"), and a\n * subfolder of `web/` is inside it. `web/components/`, `web/layouts/` and\n * `web/utils/` are ordinary app layout, not a way around the fence.\n */\nexport function isWithinModuleWebFolder(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n return MODULE_WEB_SEGMENT.test(\n normalize(path.relative(appRoot, resolvedPath)),\n );\n}\n\n/**\n * Rule 4's subject set: local files that CARRY CODE. Deliberately the same\n * shape as `PARSEABLE_MODULE_EXTENSIONS` below — one notion of \"this is a\n * JS/TS module\" for the whole file — which also picks up `.mts`/`.cts`, absent\n * from the earlier hand-spelled `/\\.(tsx?|jsx?|mjs|cjs)$/`. `.mts` is a real\n * module extension Vite resolves, so a file wearing it was walking past rule 4\n * with its extension written out in full.\n *\n * Every extension `IMPLICIT_MODULE_EXTENSIONS` can append MUST match this —\n * see the note there.\n */\nconst LOCAL_MODULE_EXTENSIONS = /\\.([cm]?[jt]sx?)$/;\n\n/**\n * The extensions a local specifier may leave off, in Vite's own\n * `resolve.extensions` order.\n *\n * `.json` is deliberately absent: it is not code-carrying, so completing to it\n * would only hand rule 4 a path it exempts one line later, and the exemption is\n * the correct answer for a data file.\n *\n * The list and `LOCAL_MODULE_EXTENSIONS` have to agree in one direction: an\n * extension this list can PRODUCE but that regex does not RECOGNIZE is a fresh\n * escape hatch of exactly the shape this completion exists to close — the\n * judged path would gain a suffix and still be waved through. Pinned by case 32\n * in the spec. The order only decides which of two same-named files gets\n * judged, and every rule downstream (`isServerFile`, `isWithinModuleWebFolder`,\n * `isRecognizedUniversalSurface`) answers identically for `x.ts` and `x.js`, so\n * matching Vite is for least surprise rather than for correctness.\n */\nconst IMPLICIT_MODULE_EXTENSIONS = [\n \".mjs\",\n \".js\",\n \".mts\",\n \".ts\",\n \".jsx\",\n \".tsx\",\n \".cjs\",\n \".cts\",\n];\n\nfunction isFile(candidate: string): boolean {\n try {\n return statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * The file a local specifier will ACTUALLY load, given the path `resolveId`\n * derived from it with a raw `path.resolve`.\n *\n * Without this, rule 4 was escapable by writing the import the ordinary way.\n * `resolveId` runs before anything appends an extension, so `./services/helper`\n * arrives as a path ending in `helper`; rule 4 asks for a code extension —\n * rightly, since it has no business judging `./logo.css` — finds none, and\n * declines to judge at all. `./x.middleware` slipped the same way while LOOKING\n * like it had an extension, and so did `./services` resolving to\n * `services/index.ts`. Measured: all three built clean (spec cases 25-28, 32).\n *\n * Asked of the FILESYSTEM rather than answered from the string, because the\n * string cannot tell an omitted extension from a real one: `.middleware` is not\n * a code extension and not an asset extension either, and only the disk knows\n * whether `thing.middleware.ts` is sitting there. Probing `<path><ext>` and\n * then `<path>/index<ext>` is what Vite is about to do anyway, so the rule\n * judges the file that will really be bundled.\n *\n * Deliberately NOT cached. A handful of `stat` calls per local specifier is\n * what every resolver in the pipeline already spends, and a cache would go\n * stale in watch mode exactly when a file is created or deleted — which is the\n * moment a fence must not be answering from memory.\n *\n * Two early exits, each a case where completion has nothing to add or no\n * business guessing:\n * - a non-absolute path (a bare package specifier — rules 1/2's business)\n * or a virtual `\\0` id;\n * - a path that already carries a code extension, or that already exists as a\n * real non-code file (an asset — `./theme.css`), where the path on hand is\n * the answer.\n *\n * A Vite query suffix (`./helper?raw`, `./thing?worker`, `./thing?url`) is\n * split off BEFORE any of that: the query changes what the id MEANS to Vite,\n * never which file on disk answers to it, so completion and every downstream\n * policy check (`isServerFile`) judge the BARE path — `./blog.server?raw`\n * must be caught exactly as `./blog.server` is, and used to slip through\n * here because the whole path, query included, was handed back unexamined.\n * The suffix is reattached to whatever this returns, because that IS the id\n * Vite will actually load (`resolve.extensions` completion happens first,\n * the query is consumed after) — nothing downstream should have to re-derive\n * it from the original specifier.\n */\nfunction completeLocalModulePath(judgedPath: string): string {\n if (!path.isAbsolute(judgedPath)) return judgedPath;\n if (judgedPath.includes(\"\\0\")) return judgedPath;\n\n const queryIndex = judgedPath.indexOf(\"?\");\n const barePath =\n queryIndex === -1 ? judgedPath : judgedPath.slice(0, queryIndex);\n const querySuffix = queryIndex === -1 ? \"\" : judgedPath.slice(queryIndex);\n\n return `${completeBareLocalModulePath(barePath)}${querySuffix}`;\n}\n\nfunction completeBareLocalModulePath(judgedPath: string): string {\n if (LOCAL_MODULE_EXTENSIONS.test(judgedPath)) return judgedPath;\n if (path.extname(judgedPath) !== \"\" && isFile(judgedPath)) return judgedPath;\n\n for (const extension of IMPLICIT_MODULE_EXTENSIONS) {\n const candidate = `${judgedPath}${extension}`;\n if (isFile(candidate)) return candidate;\n }\n // A directory only after every file candidate has failed — that is Node's and\n // Vite's order too, so `x.ts` beside `x/index.ts` is judged as `x.ts`.\n for (const extension of IMPLICIT_MODULE_EXTENSIONS) {\n const candidate = path.join(judgedPath, `index${extension}`);\n if (isFile(candidate)) return candidate;\n }\n\n // Nothing on disk answers to it. Left exactly as it arrived: resolution is\n // about to fail on its own, and a fence should not be inventing a verdict\n // about a file that is not there.\n return judgedPath;\n}\n\n/**\n * Rule 4 only judges local, code-carrying files INSIDE the app's own project\n * root — package specifiers are already covered by rules 1/2, non-code assets\n * (css, images, ...) are not the \"server/client leak\" shape this rule exists to\n * catch, and a dependency's own internal file layout (e.g. `@warlock.js/seal`'s\n * `src/rules/**`) is not organized by the `$module/web/` convention at all,\n * so judging it by that convention would fence out every third-party import.\n *\n * NOTHING HERE READS THE SPECIFIER, and that is the fix for the escape this\n * rule shipped with. It used to open with\n * `source.startsWith(\".\") || path.isAbsolute(source)` and return `false` for\n * anything else — so an import written in ALIAS form (`app/users/...`,\n * `web/...`, the shape `v5/app/tsconfig.json`'s `paths` and\n * `web-connector.ts`'s `resolve.alias` give every v5 app) was never judged at\n * all, whatever file it reached. Rule 4's subject is a FILE; the string the\n * author happened to type to name that file is not evidence about it. See the\n * spelling-equivalence suite in the spec — one target file, four spellings, one\n * judgement.\n *\n * Dropping that test narrows nothing and widens nothing for the specifiers it\n * used to admit, because `isAppSourcePath` below already requires an ABSOLUTE\n * path: a bare specifier's \"judged path\" is still the specifier string itself\n * (see `resolveId`), which is never absolute, so it falls out one line later\n * exactly as before. What changed is that a CALLER may now hand this rule a\n * resolved absolute path for a specifier that was not written relatively —\n * which is what `resolveId`'s alias pass does.\n *\n * `resolvedPath` must have been through `completeLocalModulePath` before it\n * gets here, or the extension test below is a way OUT of this rule rather than\n * a narrowing of it — see that function.\n */\nfunction isOutsideUniversalScope(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n if (!LOCAL_MODULE_EXTENSIONS.test(resolvedPath)) return false;\n if (!isAppSourcePath(resolvedPath, appRoot)) return false;\n // Order matters: `isAppSourcePath` above is what makes the app-root-relative\n // test in `isWithinModuleWebFolder` meaningful — it has already established\n // the path is absolute and inside `appRoot`.\n if (isWithinModuleWebFolder(resolvedPath, appRoot)) return false;\n if (isRecognizedUniversalSurface(resolvedPath)) return false;\n return true;\n}\n\n/**\n * Whether a judged path is the APP'S OWN SOURCE, as opposed to a dependency's\n * internals. This is Gate A's single definition of that boundary — rules 3\n * and 4 both call it, and `packageNameForFilePath` draws the same line for\n * Gate C. Two conditions, both already required by rule 4 before this was\n * extracted:\n *\n * - inside `appRoot` (a monorepo sibling or any other path resolution has\n * left the app root for is that package's own business), and\n * - not under a `node_modules/` segment. Nested `node_modules` inside\n * `appRoot` is the ORDINARY case for a dependency, and Vite's optimized\n * dependency chunks (`<appRoot>/node_modules/.vite/deps`) even import one\n * another relatively.\n *\n * Bare package specifiers never reach here as app source: for those the\n * \"judged path\" is still the specifier string itself (see `resolveId`), which\n * is not absolute. They are rule 2's business.\n */\nexport function isAppSourcePath(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n if (!path.isAbsolute(resolvedPath)) return false;\n if (!isInsideAppRoot(resolvedPath, appRoot)) return false;\n return !normalize(resolvedPath).includes(\"/node_modules/\");\n}\n\nfunction isInsideAppRoot(resolvedPath: string, appRoot: string): boolean {\n const relative = path.relative(appRoot, resolvedPath);\n return !!relative && !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n}\n\n/* ------------------------------------------------------------------------ *\n * Type-only import edges (rule 2 only)\n * ------------------------------------------------------------------------ */\n\n/**\n * How an importer's source text binds a specifier: `\"value\"` if ANY statement\n * mentioning that specifier can produce a runtime binding or a runtime edge,\n * `\"type\"` only when every one of them is erased by the type system.\n */\ntype ImportKind = \"value\" | \"type\";\n\n/** Files whose source Gate A will parse for import kinds. */\nconst PARSEABLE_MODULE_EXTENSIONS = /\\.([cm]?[jt]sx?)$/;\n\n/** Cheap pre-filter: no governed scope in the text, nothing to classify. */\nconst GOVERNED_SCOPE_MENTION = /@warlock\\.js\\/|@mongez\\//;\n\n/**\n * The id Gate A resolves a type-only edge into a server-only package to. `\\0`\n * is the virtual-module convention, which also keeps Gate C from trying to\n * attribute this id to a package on disk (`packageNameForFilePath` returns\n * `undefined` for any id containing `\\0` — see its note).\n */\nconst TYPE_ONLY_ERASED_ID = \"\\0warlock:type-only-erased\";\n\n/**\n * Rollup module ids carry query suffixes (`?v=`, `?used`, ...) that differ\n * between the `transform` id and a later `importer`. Both sides key through\n * here so they agree.\n */\nfunction moduleKey(id: string): string {\n return normalize(id.split(\"?\")[0]);\n}\n\nfunction parserPluginsFor(id: string): (\"typescript\" | \"jsx\")[] {\n // `jsx` on a `.ts` file mis-parses the type-assertion form `<T>value`, which\n // is legal there and only there. Everything else (including plain `.js`) is\n // parsed with both, since JSX in a `.js` file is ordinary in this ecosystem.\n return /\\.[cm]?ts$/.test(moduleKey(id))\n ? [\"typescript\"]\n : [\"typescript\", \"jsx\"];\n}\n\n/**\n * Whether an `ImportDeclaration`'s specifier list is entirely type-only.\n *\n * Both spellings count, because both are erased: the statement form\n * (`import type { X } from \"P\"`, `decl.importKind === \"type\"`) and the inline\n * form (`import { type X } from \"P\"`, every specifier's own `importKind`).\n * The inline form is the one that matters in practice — under\n * `verbatimModuleSyntax` TypeScript/esbuild emit it VERBATIM as\n * `import {} from \"P\"`, so it is the only one that survives to `resolveId` at\n * all.\n *\n * An EMPTY specifier list (`import \"P\"`) is deliberately `\"value\"`: a bare\n * side-effect import is a real runtime edge that the author wrote on purpose,\n * and it is indistinguishable — post-esbuild — from the erased form. Treating\n * it as type-only would turn this carve-out into a hole big enough to smuggle\n * a whole server package through.\n */\nfunction importDeclarationKind(decl: any): ImportKind {\n if (decl.importKind === \"type\") return \"type\";\n if (decl.specifiers.length === 0) return \"value\";\n return decl.specifiers.every((spec: any) => spec.importKind === \"type\")\n ? \"type\"\n : \"value\";\n}\n\nfunction exportDeclarationKind(stmt: any): ImportKind {\n if (stmt.exportKind === \"type\") return \"type\";\n const specifiers = stmt.specifiers ?? [];\n if (specifiers.length === 0) return \"value\"; // `export * from \"P\"` — a runtime edge.\n return specifiers.every((spec: any) => spec.exportKind === \"type\")\n ? \"type\"\n : \"value\";\n}\n\n/**\n * Every specifier reached by a RUNTIME expression rather than a static\n * import/export statement — `import(\"P\")` and `require(\"P\")` — found by a\n * duck-typed walk of the whole program, not just its top level, because both\n * are ordinary expressions and can sit anywhere.\n *\n * These are unconditionally `\"value\"`. A dynamic import of a server-only\n * package is exactly the edge rule 2 exists to refuse, and it reaches\n * `resolveId` with the same specifier string a static import would, so\n * without this walk a file could hold `import { type X } from \"P\"` beside\n * `await import(\"P\")` and have the static statement's kind exempt the dynamic\n * one.\n */\nfunction collectRuntimeSpecifiers(node: unknown, into: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectRuntimeSpecifiers(item, into);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"CallExpression\" || record.type === \"ImportExpression\") {\n const callee = (record as any).callee;\n const isRuntimeLoad =\n record.type === \"ImportExpression\" ||\n callee?.type === \"Import\" ||\n (callee?.type === \"Identifier\" && callee.name === \"require\");\n if (isRuntimeLoad) {\n const arg = (record as any).source ?? (record as any).arguments?.[0];\n if (arg?.type === \"StringLiteral\" && typeof arg.value === \"string\")\n into.add(arg.value);\n }\n }\n for (const key of Object.keys(record)) {\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\"\n )\n continue;\n if (\n key === \"leadingComments\" ||\n key === \"trailingComments\" ||\n key === \"innerComments\" ||\n key === \"extra\"\n ) {\n continue;\n }\n collectRuntimeSpecifiers(record[key], into);\n }\n}\n\n/**\n * Classifies every specifier `code` imports as `\"value\"` or `\"type\"`. Returns\n * `undefined` when the source could not be parsed — the caller reads that as\n * \"no information\", which leaves rule 2 refusing exactly as it does today.\n */\nfunction classifyImportKinds(\n code: string,\n id: string,\n): Map<string, ImportKind> | undefined {\n let ast;\n try {\n ast = parse(code, {\n sourceType: \"module\",\n plugins: parserPluginsFor(id),\n errorRecovery: false,\n });\n } catch {\n return undefined;\n }\n\n const kinds = new Map<string, ImportKind>();\n // `\"value\"` always wins a merge: one value edge anywhere in the file is\n // enough to make the specifier a runtime dependency, however many type-only\n // statements sit beside it.\n const record = (source: string, kind: ImportKind) => {\n if (kind === \"value\" || !kinds.has(source)) kinds.set(source, kind);\n };\n\n for (const stmt of ast.program.body as any[]) {\n if (stmt.type === \"ImportDeclaration\") {\n record(stmt.source.value as string, importDeclarationKind(stmt));\n continue;\n }\n if (\n (stmt.type === \"ExportNamedDeclaration\" ||\n stmt.type === \"ExportAllDeclaration\") &&\n stmt.source\n ) {\n record(stmt.source.value as string, exportDeclarationKind(stmt));\n }\n }\n\n const runtimeSpecifiers = new Set<string>();\n collectRuntimeSpecifiers(ast.program.body, runtimeSpecifiers);\n for (const source of runtimeSpecifiers) kinds.set(source, \"value\");\n\n return kinds;\n}\n\ninterface FenceViolation {\n cause: string;\n fix: string;\n}\n\n/**\n * Rule 2's predicate on its own, extracted so `resolveId` can ask the question\n * a SECOND time after `ruleViolation` has declined to refuse: a type-only edge\n * into a server-only package is not a violation, but it is not an ordinary\n * import either — it is erased (see `TYPE_ONLY_ERASED_ID`). Asking separately,\n * rather than short-circuiting inside `ruleViolation`, is what keeps rules 3\n * and 4 authoritative over a type-only specifier: `@warlock.js/core/db.server`\n * imported for its types alone is still refused, by rule 3, because\n * `ruleViolation` still runs to completion first.\n */\nfunction isServerMarkedGovernedPackage(\n source: string,\n importer: string | undefined,\n environmentOf: (pkgName: string) => WarlockEnvironment,\n appRoot: string,\n): boolean {\n if (!isGovernedScope(source)) return false;\n if (importer && !isInsideAppRoot(importer, appRoot)) return false;\n return environmentOf(governedPackageNameOf(source)) === \"server\";\n}\n\nfunction ruleViolation(\n source: string,\n resolvedPath: string,\n importer: string | undefined,\n environmentOf: (pkgName: string) => WarlockEnvironment,\n appRoot: string,\n isTypeOnlyEdge: boolean,\n): FenceViolation | undefined {\n // Rule 1 takes NO importer into account, and that is deliberate rather than\n // an omission. Rule 2's `GOVERNED_SCOPES` / `isInsideAppRoot` narrowing is\n // right for the environment MARKER, which is a claim we only make about\n // packages we publish; we do not get to judge whether Radix or Mantine is\n // server-side. A Node builtin is not that kind of judgement. `node:fs` has no\n // browser implementation whoever wrote the import — the app's own source, a\n // governed package, or a transitive dependency six hops down — so this rule\n // is judged for every importer and, unlike rule 2, is never exempted by a\n // type-only edge (see the `isTypeOnlyEdge` note below).\n if (isNodeBuiltin(source)) {\n return {\n cause: `\"${source}\" is a Node.js builtin module and cannot run in the browser.`,\n 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.`,\n };\n }\n\n const boundaryDeclaration = boundaryDeclarationOf(source);\n if (boundaryDeclaration?.declares === \"server\") {\n const declarer = importer ? displayName(importer) : source;\n return {\n 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.`,\n 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.`,\n };\n }\n\n // Rule 2 is judged only for imports whose importer is app-authored code\n // (inside `appRoot`) — mirroring rule 4's existing dependency-internals\n // exemption below. A governed package's OWN internal composition (e.g.\n // `@warlock.js/seal` importing `@mongez/reinforcements`) is that package's\n // business, not the app's; what Gate A polices is the app reaching INTO a\n // server-only package, not what a package it's already allowed to use does\n // internally.\n // `isTypeOnlyEdge` exempts rule 2 and NOTHING else. A refusal here is a\n // statement about what the app will EXECUTE, and an import statement with no\n // value binding executes nothing from the package — refusing it would be a\n // false positive on code that is already correct, which is how people learn\n // to switch a fence off. Rules 1, 3, 4 and 5 are untouched by this flag on\n // purpose: they judge the importer's own nature or a file name, neither of\n // which a type-only spelling changes.\n if (\n !isTypeOnlyEdge &&\n isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)\n ) {\n const pkgName = governedPackageNameOf(source);\n const scopeLabel = pkgName.startsWith(\"@warlock.js/\")\n ? \"@warlock.js\"\n : \"@mongez\";\n return {\n cause: `\"${source}\" resolves into ${pkgName}, a server-only ${scopeLabel} package — it declares \"warlock\": { \"environment\": \"server\" } in its package.json.`,\n 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\").`,\n };\n }\n\n if (isServerFile(resolvedPath, appRoot)) {\n return {\n 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.`,\n 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.`,\n };\n }\n\n // RULE 4 IS GONE, DELIBERATELY. Owner ruling 2026-08-24 (canon `10f6041c`):\n // the client boundary is decided by the import GRAPH, not by file location.\n //\n // It used to refuse any app file living outside `$module/web/`. That is a\n // PROXY for danger, and it produced exactly the false positives you would\n // expect from a proxy:\n //\n // - `app/auth/schema/login.schema.ts` imports only `@warlock.js/seal`\n // (marked universal) and was refused for its address.\n // - `app/access/permissions.ts` imports NOTHING — two frozen objects of\n // name constants — and was refused for its address.\n //\n // Both are genuinely universal: the server validates and authorises with\n // them, the client renders and validates with them. Every location-based fix\n // for that (move them, rename them, add a folder) was rejected by the owner\n // because it forces app structure to serve the fence rather than the app.\n //\n // WHAT STILL REFUSES, and why this is not a weakening:\n // rule 1 — a Node builtin anywhere in the graph, at any depth, any importer\n // rule 1b — a `server-only` boundary declaration\n // rule 2 — a governed package MARKED `environment: \"server\"`\n // rule 3 — a file that names itself server (`*.server.ts`, `server/`)\n //\n // Server-ness now PROPAGATES: a file is refused when something it reaches is\n // server-only, and the chain is printed so the refusal names the real cause\n // instead of an address. `nav.service` is still refused — not for sitting\n // outside `web/`, but for reaching auth and the user model.\n //\n // THE KNOWN GAP, recorded rather than hidden: a file that is secret but\n // imports nothing suspicious (a hardcoded key) is invisible to the graph.\n // `*.server.ts` above is the manual escape hatch for exactly that case.\n return undefined;\n}\n\n/**\n * The label one hop of an import chain carries.\n *\n * A basename is enough for the app's own source, where file names are chosen\n * and distinct. It is not enough for a dependency: a large share of\n * node_modules resolves to a file literally called `index.js`, and a chain\n * reading `page.tsx → index.js → node:fs` names the culprit for nobody — which\n * is the failure mode where people switch the fence off instead of fixing the\n * import. Anything under a `node_modules/` segment is therefore labelled with\n * its path from the LAST such segment (`vendor-node-lib/index.js`), so a\n * vendored nested copy reports the package that actually holds the file rather\n * than the one at the top of the tree.\n */\nfunction displayName(id: string): string {\n const normalized = normalize(id);\n const segments = normalized.split(\"node_modules/\");\n if (segments.length < 2) return path.basename(normalized);\n return segments[segments.length - 1];\n}\n\n/** Rollup's normalized `external`, as `buildStart` receives it. */\ntype ExternalPredicate = (\n id: string,\n importer: string | undefined,\n isResolved: boolean,\n) => unknown;\n\n/**\n * Every Node builtin the Rollup config would externalize — empty when none,\n * which is the only shape that lets a build proceed past `buildStart`.\n *\n * This is the one route a builtin can take into a browser chunk without ever\n * being offered to `resolveId`: Rollup consults `external` FIRST, and an\n * externalized specifier is emitted into the output verbatim — measured, on\n * this gate's own fixture, as a clean build whose chunk still opens with\n * `import { readFileSync } from \"node:fs\"`. No `resolveId` rule can close that,\n * because the hook is never called; the check has to happen against the config\n * itself, before any module is loaded.\n *\n * Asked as a QUESTION of the normalized predicate rather than by reading an\n * array, because `external` may legitimately be a string, a RegExp, an array of\n * either, or a function — Rollup normalizes all of them into the function\n * handed to `buildStart`, and asking it about each builtin spelling is the only\n * form of the check that covers every shape. A predicate that throws on an\n * unexpected shape of argument is read as \"not external\": this guard exists to\n * catch a config that says yes, and inventing a refusal out of someone else's\n * exception would be a false positive with no evidence behind it.\n *\n * This fires on CONFIGURATION, not on an actual import — nothing need import\n * `fs` for it to trip. That is on purpose: the config has already decided the\n * fence does not apply, and a build that would silently pass today only because\n * no import happens to exist yet is not a build anyone should trust tomorrow.\n */\nfunction externalizedBuiltins(isExternal: unknown): string[] {\n if (typeof isExternal !== \"function\") return [];\n const asks = isExternal as ExternalPredicate;\n return NODE_BUILTIN_SPELLINGS.filter((specifier) => {\n try {\n return asks(specifier, undefined, false) === true;\n } catch {\n return false;\n }\n });\n}\n\nexport interface EnvironmentClassifierOptions {\n /**\n * Force these governed-scope package names (e.g. `@warlock.js/core`) to\n * classify as `\"server\"` regardless of what `warlock.environment` their\n * `package.json` declares. Defaults to the live marker resolution (workspace\n * `package.json` lookup, falling back to `node_modules`). Only meant for\n * tests that need a deterministic classification independent of the host\n * filesystem.\n */\n serverPackages?: Iterable<string>;\n /**\n * The app's project root. Defines what counts as APP SOURCE (see\n * `isAppSourcePath`), which two rules depend on: rule 4 (\"outside\n * `$module/web/`\") and rule 3's plain-`server/`-folder half. A dependency's\n * own internal file layout — reached once resolution has left the app root,\n * or through a `node_modules/` segment — is exempt from both, though rules\n * 1, 2 and the `.server` half of rule 3 still apply to it. Defaults to\n * `process.cwd()`, matching Vite's own default `root`.\n */\n appRoot?: string;\n}\n\ntype GateAOptions = EnvironmentClassifierOptions;\n\nexport interface EnvironmentClassifier {\n appRoot: string;\n environmentOf(pkgName: string): WarlockEnvironment;\n /**\n * Maps an absolute, already-resolved file path (e.g. a Rollup\n * `OutputChunk.moduleIds` entry) back to the governed-scope package name\n * that owns it — a `node_modules` dependency, or a workspace member's own\n * directory PROVIDED the path is outside `appRoot`. The `appRoot` carve-out\n * matters: a fixture/app can physically live inside a workspace package's\n * own directory tree (e.g. this repo's own `web/__tests__/` fixtures live\n * under the `web` package) without thereby \"importing\" that package — the\n * app's own source is never a dependency edge onto itself, no matter where\n * on disk it happens to sit (same distinction Gate A's rule 2 already makes\n * via `isInsideAppRoot` for the importer side). `undefined` when the path\n * isn't a governed-scope dependency at all. Exists so Gate C\n * (`gate-c-verify.ts`) can classify modules already sitting in the EMITTED\n * bundle graph using this exact same marker logic, instead of hand-rolling\n * a second classification scheme — Gate C re-derives, it does not\n * duplicate.\n */\n packageNameForFilePath(absPath: string): string | undefined;\n}\n\n/**\n * The `warlock.environment` marker classifier (rule 2). Extracted from\n * `gateAResolve` so Gate C can classify packages it finds in the EMITTED\n * bundle graph with the identical resolution logic — same workspace index,\n * same `node_modules` fallback, same default — rather than a second,\n * potentially-drifting implementation.\n *\n * An ABSENT marker classifies as `\"universal\"`, i.e. allowed. This is a\n * deliberate reversal of the earlier burden-inversion default. A marker is\n * shipped metadata: making silence mean \"refuse\" meant every browser-safe\n * package we own needed a marker AND a release before any app could boot, and\n * a package the app does not control (`@mongez/react-atom`) could not be\n * imported at all. Refusal is now driven by information that is PRESENT — an\n * explicit `\"server\"` marker, or a server-only file name (rule 3) — which the\n * app author can always supply themselves without waiting on a release. The\n * marker mechanism itself is untouched and still authoritative when set.\n */\nexport function createEnvironmentClassifier(\n options: EnvironmentClassifierOptions = {},\n): EnvironmentClassifier {\n const forcedServerPackages = options.serverPackages\n ? new Set(options.serverPackages)\n : undefined;\n const appRoot = path.resolve(options.appRoot ?? process.cwd());\n const workspaceIndex = findWorkspaceIndex(__dirname);\n const environmentCache = new Map<string, WarlockEnvironment>();\n\n function environmentOf(pkgName: string): WarlockEnvironment {\n if (forcedServerPackages?.has(pkgName)) return \"server\";\n const cached = environmentCache.get(pkgName);\n if (cached) return cached;\n\n const packageJsonPath =\n workspaceIndex?.packageJsonByName.get(pkgName) ??\n findNodeModulesPackageJson(pkgName, appRoot);\n\n let environment: WarlockEnvironment = \"universal\";\n if (packageJsonPath) {\n try {\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n const marker = pkg?.warlock?.environment;\n if (\n marker === \"server\" ||\n marker === \"universal\" ||\n marker === \"client\"\n ) {\n environment = marker;\n }\n } catch {\n // Unreadable package.json — no marker could be read, so the permissive\n // default applies, exactly as for a package that carries none.\n }\n }\n\n environmentCache.set(pkgName, environment);\n return environment;\n }\n\n function packageNameForFilePath(absPath: string): string | undefined {\n // A Rollup module id is not always a file path. `\\0` is the universal\n // virtual-module convention (Vite's own `createFilter` refuses any id\n // containing it), and a virtual module has no package on disk to attribute\n // it to. Without this guard `path.resolve` grafts the synthetic id onto\n // `process.cwd()` and the workspace-root prefix scan below then attributes\n // it to whichever package the BUILD PROCESS happens to be running inside —\n // so `\\0virtual:warlock/pages` gets classified as \"@warlock.js/web,\n // server-only\" and Gate C refuses a build over a module that never touched\n // the filesystem. That failure is loud rather than silent, but it is still\n // wrong, and it makes every virtual module unbuildable whenever cwd sits\n // inside a governed-scope package. Nothing is weakened by returning\n // `undefined` here: a virtual module's own IMPORTS resolve to real paths\n // that this function still classifies, and its emitted code is still\n // subject to Gate C's server-export scan and Gate B's transform.\n if (absPath.includes(\"\\0\")) return undefined;\n\n const normalized = normalize(path.resolve(absPath));\n\n // node_modules is unambiguous — always a dependency, even when nested\n // inside appRoot (the ordinary case: `appRoot/node_modules/...`).\n const nodeModulesMatch = packageNameFromNodeModulesPath(normalized);\n if (nodeModulesMatch) return nodeModulesMatch;\n\n // Everything else inside appRoot is the app's OWN source, never a\n // dependency edge, regardless of which workspace package's directory it\n // happens to physically sit under — see the appRoot carve-out note above.\n if (isInsideAppRoot(normalized, appRoot)) return undefined;\n\n if (workspaceIndex) {\n for (const [name, pkgJsonPath] of workspaceIndex.packageJsonByName) {\n const root = `${normalize(path.dirname(pkgJsonPath))}/`;\n if (normalized.startsWith(root)) return name;\n }\n }\n return undefined;\n }\n\n return { appRoot, environmentOf, packageNameForFilePath };\n}\n\n/**\n * The client-build Vite plugin. `vite` is only imported for its types\n * (`import type`), so this module carries no runtime dependency on `vite`\n * being installed — it is a `peerDependenciesMeta.optional` peer.\n */\nexport function gateAResolve(options: GateAOptions = {}): Plugin {\n const { appRoot, environmentOf } = createEnvironmentClassifier(options);\n\n // Maps a resolved module id to the id that imported it, rebuilt per plugin\n // instance (i.e. per build) via our own `this.resolve` calls below — this\n // does not depend on Rollup's internal module-graph bookkeeping being\n // populated in any particular order.\n const importerOf = new Map<string, string>();\n\n /**\n * Module key -> that module's specifier classification, filled by the\n * `transform` hook below and read by `resolveId`.\n *\n * This is the answer to the one structural problem rule 2 has: `resolveId`\n * receives a specifier STRING and an importer path, never an AST, so it\n * cannot see for itself whether an import was type-only. The information\n * exists exactly once in the pipeline — in the module's PRE-esbuild\n * TypeScript source — so it is captured there and carried forward.\n *\n * Three routes were available; this is why the other two lost:\n *\n * - Recognizing the POST-esbuild shape (`import {} from \"P\"`) from inside\n * `resolveId`. Rejected as unsound, not merely awkward: esbuild emits the\n * identical `import {} from \"P\"` for a genuine bare side-effect import\n * under some settings, so the shape cannot separate \"erased type import\"\n * from \"the author asked for this package's side effects\". That is a\n * false NEGATIVE in a security fence — the one direction of error this\n * gate must never make — and it would additionally depend on\n * `getModuleInfo(importer).code` being populated during a dependency's\n * `resolveId`, which is undocumented Rollup ordering.\n * - Moving rule 2 wholesale into a `transform` pass. Rejected as too wide\n * for this change: `transform` never runs for entry, virtual or\n * externalized importers, so rule 2 would silently stop judging edges it\n * judges today, and the import-chain machinery (`importerOf`) is built\n * out of `resolveId`'s importer argument. Gate A is a `resolveId` fence\n * by identity; only the type-only QUESTION moves, never the refusal.\n * - Reusing `projection.ts`'s existing parse (`projectModule`). Rejected as\n * a coupling, though it is the right TECHNIQUE and is copied here:\n * projection parses only `*.page.tsx`/`layout`/`root`, records binding\n * READERS rather than import KINDS, and rewrites code. Gate A must work\n * standalone — every case in `gate-a-resolve.spec.ts` runs\n * `gateAResolve()` with no projection in the array — so it cannot depend\n * on another plugin having run.\n *\n * A module absent from this map (unparseable, never transformed, not JS/TS)\n * yields no `\"type\"` answer, so rule 2 refuses exactly as it does today.\n * Missing information fails CLOSED.\n */\n const importKindsByModule = new Map<string, Map<string, ImportKind>>();\n\n function recordImportKinds(code: string, id: string): void {\n const key = moduleKey(id);\n if (key.includes(\"\\0\")) return;\n if (!PARSEABLE_MODULE_EXTENSIONS.test(key)) return;\n if (!GOVERNED_SCOPE_MENTION.test(code)) return;\n\n const kinds = classifyImportKinds(code, key);\n if (!kinds) return;\n\n const existing = importKindsByModule.get(key);\n if (!existing) {\n importKindsByModule.set(key, kinds);\n return;\n }\n // Same path re-transformed (a query variant, or a watch rebuild): merge in\n // the same fail-closed direction `classifyImportKinds` uses internally.\n for (const [source, kind] of kinds) {\n if (kind === \"value\" || !existing.has(source)) existing.set(source, kind);\n }\n }\n\n function isTypeOnlyEdge(\n source: string,\n importer: string | undefined,\n ): boolean {\n if (!importer) return false;\n return importKindsByModule.get(moduleKey(importer))?.get(source) === \"type\";\n }\n\n /**\n * How many `resolve.alias` entries this build declares, captured from the\n * RESOLVED Vite config (`configResolved`) rather than guessed.\n *\n * Purely diagnostic, and it is the one thing the alias table can tell Gate A\n * that the resolver cannot. The gate does not match aliases itself — see the\n * note on the alias pass in `resolveId` for why asking Vite's own resolver\n * beats re-deriving `@rollup/plugin-alias`'s find/replacement semantics. But\n * when a specifier resolves to NOTHING, the count is the difference between\n * \"a dependency is missing\" and \"this build never installed the alias table\n * its own tsconfig `paths` describe\" — which is exactly the v5 production\n * client build's state, and the actionable half of that warning.\n *\n * `undefined` when `configResolved` never fired (plain Rollup, or a unit\n * harness), which the message reports as unknown rather than as zero.\n */\n let declaredAliasCount: number | undefined;\n\n function buildChain(importer: string | undefined): string[] {\n const chain: string[] = [];\n let current = importer;\n const seen = new Set<string>();\n while (current && !seen.has(current)) {\n seen.add(current);\n chain.unshift(displayName(current));\n current = importerOf.get(current);\n }\n return chain;\n }\n\n return {\n name: \"warlock:gate-a-resolve\",\n // Must run before Vite's own core resolver: Vite silently externalizes\n // Node builtins into a `__vite-browser-external` stub for browser\n // builds, and would resolve `@warlock.js/*` packages via node_modules,\n // if either ran first. \"The cheapest security mechanism wins\" (RFC\n // §1.5) means this fence goes first, not last.\n enforce: \"pre\",\n /** Records the alias-table size; see `declaredAliasCount`. Judges nothing. */\n configResolved(config) {\n const alias: unknown = config.resolve?.alias;\n if (Array.isArray(alias)) {\n declaredAliasCount = alias.length;\n } else if (alias && typeof alias === \"object\") {\n declaredAliasCount = Object.keys(alias).length;\n } else {\n declaredAliasCount = 0;\n }\n },\n /**\n * The config-level half of rule 1 — see `externalizedBuiltins` for why a\n * `resolveId` fence cannot cover it on its own. Runs once per build, before\n * the first module is loaded, so the refusal lands before anything has been\n * bundled.\n */\n buildStart(inputOptions) {\n const externalized = externalizedBuiltins(inputOptions.external);\n if (externalized.length === 0) return;\n const shown = externalized.slice(0, 5).join(\", \");\n const rest =\n externalized.length > 5 ? `, and ${externalized.length - 5} more` : \"\";\n this.error(\n [\n `Gate A refused this build: its Rollup \"external\" config would let Node builtins through the fence.`,\n ``,\n `Externalized builtins: ${shown}${rest}`,\n `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.`,\n `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.`,\n ].join(\"\\n\"),\n );\n },\n /**\n * Records import kinds; never rewrites a byte (`null` return). `enforce:\n * \"pre\"` is what makes this correct as well as early — it puts this hook\n * ahead of Vite's own `vite:esbuild` TypeScript transform, so `code` here\n * is the ORIGINAL source with `type` modifiers still on it. After esbuild\n * the distinction no longer exists in the text.\n *\n * Runs before `resolveId` fires for any of this module's own specifiers:\n * Rollup must parse a module's post-transform source to discover what to\n * resolve next. That ordering is an OBSERVED Rollup behaviour, pinned by\n * `index.spec.ts`'s \"D.3 hook ordering pin\" and documented at length in\n * `index.ts`. If it ever inverts, this map is simply empty when\n * `resolveId` reads it and rule 2 goes back to refusing type-only imports\n * — a loud false positive, never a silent leak.\n */\n transform(code, id) {\n recordImportKinds(code, id);\n return null;\n },\n /**\n * The erased stand-in a type-only edge into a server-only package resolves\n * to. It must exist rather than the import simply being permitted, and the\n * fixture measures why: `import { type Request } from \"@warlock.js/core\"`\n * under `verbatimModuleSyntax` is emitted as `import {} from\n * \"@warlock.js/core\"`, and Rollup honours that as a side-effect edge — the\n * package's module-level code lands in the client chunk even though not one\n * binding is read from it. Permitting the import would therefore trade a\n * false positive for the exact silent leak the `server` marker exists to\n * close. Resolving to an empty module gives both: a clean build, and\n * nothing of the server package in the browser.\n */\n load(id) {\n return id === TYPE_ONLY_ERASED_ID ? \"export {};\" : null;\n },\n async resolveId(source, importer, resolveOptions) {\n // Gate A is a CLIENT-build fence — see the header note and Gate B/Gate\n // C's identical carve-outs (`gate-b-secrets.ts`'s `options?.ssr` guard\n // in `transform`, Gate C's `this.environment?.config?.consumer ===\n // \"server\"` in `generateBundle`). The SSR/server build resolves the\n // same specifiers this app's client build does — a server-only import\n // is exactly what that build is FOR — so refusing them there would\n // refuse the server its own legitimate dependencies. Judged first and\n // unconditionally: nothing below this line has a file to weigh in on\n // an SSR resolution before this returns.\n if (resolveOptions?.ssr) return null;\n\n // Compute the path Gate A judges: for relative/absolute specifiers,\n // resolve against the importer's directory ourselves (filename-only\n // check, zero AST parsing, per `c604f0bc` §4) rather than delegating\n // to Vite's resolver first, then complete the extension the author was\n // free to leave off — `path.resolve` alone yields `.../helper` for\n // `./helper`, which rule 4 does not recognize as code and therefore\n // never judges. `completeLocalModulePath` is a no-op for bare\n // specifiers, virtual ids and paths that already carry an extension.\n const isRelativeOrAbsolute =\n source.startsWith(\".\") || path.isAbsolute(source);\n const judgedPath = completeLocalModulePath(\n isRelativeOrAbsolute && importer\n ? path.resolve(path.dirname(importer), source)\n : source,\n );\n\n if (source === TYPE_ONLY_ERASED_ID) return source;\n\n const typeOnly = isTypeOnlyEdge(source, importer);\n\n const violation = ruleViolation(\n source,\n judgedPath,\n importer,\n environmentOf,\n appRoot,\n typeOnly,\n );\n if (violation) {\n const chain = [...buildChain(importer), source].join(\" → \");\n this.error(\n [\n `Gate A refused an import: forbidden dependency reaches the client bundle.`,\n ``,\n `Import chain: ${chain}`,\n `File: ${importer ?? source}`,\n `Cause: ${violation.cause}`,\n `Fix: ${violation.fix}`,\n ].join(\"\\n\"),\n );\n }\n\n // Not refused, but not an ordinary import either: rule 2's predicate\n // holds and only the missing value binding spared it. Asked here, AFTER\n // `ruleViolation` has run to completion, so rules 3 and 4 keep the last\n // word over a type-only specifier (`@warlock.js/core/db.server` is still\n // refused by rule 3, whatever the import kind).\n if (\n typeOnly &&\n isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)\n ) {\n return TYPE_ONLY_ERASED_ID;\n }\n\n // Not forbidden — resolve for real so the chain map reflects Rollup's\n // actual resolved ids, then hand that resolution back so we don't do\n // the work twice.\n const resolved = await this.resolve(source, importer, {\n ...resolveOptions,\n skipSelf: true,\n });\n if (resolved && importer) {\n importerOf.set(resolved.id, importer);\n }\n\n /*\n THE ALIAS PASS — rules 3 and 4, judged a SECOND time on the file the\n specifier actually reached rather than on the string that named it.\n\n Everything above judges `judgedPath`, which for a specifier that is\n neither relative nor absolute is the specifier STRING itself. That is\n the right answer for rules 1, 2 and 5, which judge names. It is the\n wrong answer for rules 3 and 4, which judge files — and it is why an\n import written in alias form (`app/users/...`) walked past both.\n\n Resolution, not re-derivation. The card that raised this asked for the\n alias TABLE to be read out of the config and applied here. Asking\n `this.resolve` is the same answer arrived at more cheaply and with\n strictly less to get wrong: it IS Vite's resolver, configured by that\n very table, so it already covers string-vs-RegExp `find` semantics,\n entry order, `customResolver`, extension completion and `index` files —\n every one of which is a place a second implementation of\n `@rollup/plugin-alias` could drift from the first. It also covers routes\n the table does not describe at all (a `vite-tsconfig-paths` plugin, a\n `customResolver`), which a table reader would miss.\n\n The `isAppSourcePath` pre-filter is what keeps this from being a second,\n broader rule: everything in `node_modules` and everything outside\n `appRoot` is out before `ruleViolation` is asked anything, exactly as\n rules 3 and 4 already require of their own subjects.\n\n WARNING, NOT REFUSAL — and this is the deliberate, temporary half. The\n judgement above is now spelling-independent; the consequence is not yet.\n Unlike the other fences in this file, turning this one on has a measured\n non-zero blast radius in application source that BUILDS TODAY, so a\n refusal would break working builds at sites nobody has surveyed. The\n warning names the file, the specifier and the resolved path so the\n survey can be taken; when it comes back empty, this `this.warn` becomes\n `this.error` and the alias pass folds into the pass above.\n */\n const isBareSpecifier = !isRelativeOrAbsolute && !source.includes(\"\\0\");\n if (isBareSpecifier && resolved && !resolved.external) {\n const aliasPath = completeLocalModulePath(moduleKey(resolved.id));\n if (isAppSourcePath(aliasPath, appRoot)) {\n const aliasViolation = ruleViolation(\n source,\n aliasPath,\n importer,\n environmentOf,\n appRoot,\n typeOnly,\n );\n if (aliasViolation) {\n this.warn({\n message: [\n `Gate A WOULD refuse this import — reported as a WARNING, not enforced yet.`,\n ``,\n `Import chain: ${[...buildChain(importer), source].join(\" → \")}`,\n `File: ${importer ?? source}`,\n `Specifier: \"${source}\"`,\n `Resolves to: ${aliasPath}`,\n `Cause: ${aliasViolation.cause}`,\n `Fix: ${aliasViolation.fix}`,\n `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.`,\n ].join(\"\\n\"),\n id: importer,\n });\n }\n }\n }\n\n /*\n THE UNJUDGEABLE SPECIFIER — the shape that was actually measured on\n `v5/app`, and the one no amount of rule work inside this hook can judge.\n\n When nothing in the build resolves a bare specifier, Rollup does not\n fail: it warns `UNRESOLVED_IMPORT` and treats it as EXTERNAL, writing\n `import { x } from \"app/users/resources/user.resource\"` verbatim into\n the browser chunk. Measured on this gate's own fixture with an empty\n `resolve.alias`: build SUCCEEDED, emitted chunk carried the specifier\n untouched. That is the same failure mode `externalizedBuiltins` exists\n to close for rule 1, reached through a different door — an import that\n crosses into the client bundle without any rule ever having a file to\n judge.\n\n And it is not hypothetical: the v5 app declares `app/*` and `web/*` in\n its tsconfig `paths`, `web-connector.ts` installs the matching\n `resolve.alias` pair for the DEV server, and the production client build\n (`build-client.ts`, fed by `contribution.ts`'s `aliases ?? {}`) installs\n NOTHING — so in that build every alias-form import is unresolvable and\n externalized. The fix for that is a config wiring change in a file this\n gate does not own; what this gate can do is stop it being silent.\n\n Deliberately NOT a refusal and deliberately NOT a guess. Gate A will not\n invent `app/*` -> `<appRoot>/src/app/*` from the app convention: those\n prefixes are the application's to choose, and a fence that hard-codes\n them is wrong for the first app that names its trees differently.\n */\n if (isBareSpecifier && !resolved) {\n const aliasTable =\n declaredAliasCount === undefined\n ? `this build's resolve.alias was never reported to the gate`\n : `this build's resolve.alias declares ${declaredAliasCount} ${declaredAliasCount === 1 ? \"entry\" : \"entries\"}`;\n this.warn({\n message: [\n `Gate A could not judge this import — and it is going into the browser chunk unjudged.`,\n ``,\n `Import chain: ${[...buildChain(importer), source].join(\" → \")}`,\n `File: ${importer ?? source}`,\n `Specifier: \"${source}\"`,\n `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}).`,\n `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.`,\n ].join(\"\\n\"),\n id: importer,\n });\n }\n\n return resolved ?? null;\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gate-a-resolve.mjs","names":[],"sources":["../../../../../../../web/src/vite/gate-a-resolve.ts"],"sourcesContent":["/**\n * Gate A — `resolveId` refusal for the client build.\n *\n * Resolution fails for import PATHS that can never be client-safe:\n * 1. Node builtins (`fs`, `crypto`, `node:*`, ...) — see `isNodeBuiltin` for\n * why the set is ASKED of Node rather than written down, and\n * `externalizedBuiltins` for the one route that reaches a chunk\n * without passing through `resolveId` at all. This rule is judged for\n * EVERY importer, in or out of `GOVERNED_SCOPES` — see its note below.\n * 2. `@warlock.js/*` / `@mongez/*` packages whose `package.json` carries\n * `\"warlock\": { \"environment\": \"server\" }`. An ABSENT marker is NOT a\n * refusal — see the burden note on `createEnvironmentClassifier`. Judged\n * only for imports whose importer is inside the app root — see\n * `isInsideAppRoot` note below. An import statement carrying no VALUE\n * binding is not refused but ERASED — see the type-only note on\n * `recordImportKinds`.\n * 3. Server-only modules declared by FILE NAME: `*.server.ts` files,\n * anything under a `.server/` directory, and — for the app's OWN source\n * only — anything under a plain `server/` directory segment.\n * 4. Local modules outside a `$module/web/` (or `src/web/`) folder that are\n * not a recognized universal surface (`*.page.tsx`, `layout.tsx`). The app\n * root (`src/web/root.tsx`) is admitted by the `web/` FOLDER rule above,\n * not by its name — see the note in `isRecognizedUniversalSurface`. A\n * specifier that omits its file extension is judged on the file it will\n * actually load, not on the extensionless string — see\n * `completeLocalModulePath`.\n * 5. The `server-only` npm package — a module importing it has declared\n * itself server-side and cannot reach the browser. Judged by NAME, right\n * after rule 1 and ahead of rule 2's scope check; see\n * `BOUNDARY_DECLARATION_PACKAGES`.\n *\n * Gate B (inline secrets), Gate C (output verification) and the SSR mirror\n * rule for a future client-only rendering primitive is NOT this gate. Do not\n * extend this file to cover them; they are separate, later slices.\n */\nimport { parse } from \"@babel/parser\";\nimport { builtinModules } from \"node:module\";\nimport { existsSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Plugin } from \"vite\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n/**\n * Node's OWN list of its builtins, read at runtime from `node:module`, never\n * copied into this file. A deny-list of names would be correct on the day it\n * was written and wrong on the day Node ships the next builtin — `node:sqlite`\n * and `node:test` are both recent enough to have missed any list this repo\n * could have hand-typed — and a fence that fails open on names nobody has\n * added yet is a fence with an expiry date nobody is watching. Asking the\n * runtime costs one import and cannot drift: the process doing the bundling is\n * the same process whose builtins are at issue.\n *\n * The `node:`-prefixed forms are handled separately (see `isNodeBuiltin`), so\n * this set only needs the names as `builtinModules` reports them.\n */\nconst NODE_BUILTINS = new Set(builtinModules);\n\n/**\n * Every SPELLING of every builtin — bare and `node:`-prefixed — derived from\n * the set above rather than enumerated. Used only by\n * `externalizedBuiltins`, which has to interrogate a predicate it does\n * not control and therefore needs concrete strings to ask about. `node:test`,\n * `node:sea` and friends are reported by `builtinModules` ALREADY prefixed and\n * have no bare form at all, so they are passed through unchanged.\n */\nconst NODE_BUILTIN_SPELLINGS: string[] = [...NODE_BUILTINS].flatMap((name) =>\n name.startsWith(\"node:\") ? [name] : [name, `node:${name}`],\n);\n\nexport type WarlockEnvironment = \"server\" | \"universal\" | \"client\";\n\n/**\n * Scopes whose `package.json` Gate A will read for a `warlock.environment`\n * marker. Third-party packages outside these two scopes (react, lodash, ...)\n * are never asked for this field — they are judged solely by Gate A's other,\n * import-based rules. Note that a governed package is only REFUSED when it\n * declares `\"server\"`; being in scope is not itself a refusal.\n */\nconst GOVERNED_SCOPES = [\"@warlock.js/\", \"@mongez/\"];\n\nfunction isGovernedScope(source: string): boolean {\n return GOVERNED_SCOPES.some((scope) => source.startsWith(scope));\n}\n\n/**\n * The package root of a scoped specifier, e.g. `@warlock.js/core/db` ->\n * `@warlock.js/core`, `@warlock.js/core` -> `@warlock.js/core`.\n */\nfunction governedPackageNameOf(source: string): string {\n const [scope, name] = source.split(\"/\");\n return `${scope}/${name}`;\n}\n\ninterface WorkspaceIndex {\n /** Governed-scope workspace package name -> absolute `package.json` path. */\n packageJsonByName: Map<string, string>;\n}\n\n/**\n * Indexes every `@warlock.js/*` / `@mongez/*` workspace member's\n * `package.json` path from the monorepo root's `package.json` `workspaces`\n * array (`48cb4ae7` discipline — enumerated, never hand-typed). Walks up from\n * this file looking for the workspace root (identified by\n * `name: \"warlock-workspace\"`) so it works whether this module runs from\n * `web/src/vite/` (dev) or a compiled `web/esm/vite/` output. Returns\n * `undefined` when no monorepo root is reachable — e.g. this package\n * installed standalone in a consuming app — in which case governed packages\n * are located via `node_modules` instead (see `findNodeModulesPackageJson`).\n */\nfunction findWorkspaceIndex(startDir: string): WorkspaceIndex | undefined {\n let dir = startDir;\n for (let depth = 0; depth < 6; depth++) {\n const candidate = path.join(dir, \"package.json\");\n if (existsSync(candidate)) {\n try {\n const pkg = JSON.parse(readFileSync(candidate, \"utf-8\"));\n if (\n pkg?.name === \"warlock-workspace\" &&\n Array.isArray(pkg.workspaces)\n ) {\n const packageJsonByName = new Map<string, string>();\n for (const workspace of pkg.workspaces as string[]) {\n const workspacePkgPath = path.join(dir, workspace, \"package.json\");\n if (!existsSync(workspacePkgPath)) continue;\n try {\n const workspacePkg = JSON.parse(\n readFileSync(workspacePkgPath, \"utf-8\"),\n );\n if (\n typeof workspacePkg.name === \"string\" &&\n isGovernedScope(`${workspacePkg.name}/`)\n ) {\n packageJsonByName.set(workspacePkg.name, workspacePkgPath);\n }\n } catch {\n // Unreadable workspace package.json — node_modules fallback still applies.\n }\n }\n return { packageJsonByName };\n }\n } catch {\n // Not the workspace root (or unreadable) — keep walking up.\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/**\n * Locates a governed-scope package's `package.json` the ordinary Node\n * resolution way — walking up `node_modules` directories from `startDir` —\n * for packages that are not monorepo workspace members (published\n * dependencies, e.g. `@mongez/reinforcements`, or any governed package when\n * this module runs standalone outside the monorepo).\n */\nfunction findNodeModulesPackageJson(\n pkgName: string,\n startDir: string,\n): string | undefined {\n let dir = startDir;\n for (let depth = 0; depth < 20; depth++) {\n const candidate = path.join(dir, \"node_modules\", pkgName, \"package.json\");\n if (existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) break;\n dir = parent;\n }\n return undefined;\n}\n\n/**\n * The governed-scope package name owning `normalized` (an already-`/`-\n * normalized absolute path), found by taking the LAST `node_modules/`\n * segment — nested `node_modules` (a dependency's own vendored copy) must\n * resolve to the nearest enclosing package, not the outermost one. Returns\n * `undefined` for paths with no `node_modules` segment, or whose owning\n * package isn't in a governed scope.\n */\nfunction packageNameFromNodeModulesPath(\n normalized: string,\n): string | undefined {\n const segments = normalized.split(\"node_modules/\");\n if (segments.length < 2) return undefined;\n const afterLast = segments[segments.length - 1];\n const parts = afterLast.split(\"/\");\n const name = afterLast.startsWith(\"@\") ? `${parts[0]}/${parts[1]}` : parts[0];\n return isGovernedScope(`${name}/`) ? name : undefined;\n}\n\n/**\n * The two npm packages whose entire published API is a declaration of which\n * side of the client/server boundary their importer belongs to: importing\n * `server-only` says \"this module must never reach the browser\", importing\n * `client-only` says the mirror. Mapped to the environment each one declares\n * of its IMPORTER.\n *\n * Recognized by NAME, ahead of rule 2's scope check, for a reason that is not\n * a special case: both sit outside `GOVERNED_SCOPES`, so `environmentOf` is\n * never consulted for them under ANY default — no marker resolution, current\n * or future, can ever classify them. Their name IS the marker, and reading it\n * is the same move rule 3 makes when it reads `.server.ts` off a file name.\n *\n * Only a `\"server\"` declaration is a violation HERE. `client-only` is\n * recognized and deliberately permitted: this is the client build, so a module\n * declaring itself client-side is exactly where it belongs. Judging it is the\n * SSR mirror rule's job, and that gate is not this file (see the header note).\n */\nconst BOUNDARY_DECLARATION_PACKAGES: Record<string, WarlockEnvironment> = {\n \"server-only\": \"server\",\n \"client-only\": \"client\",\n};\n\n/**\n * The boundary-declaration package a specifier reaches, if any — matching the\n * package itself or a subpath of it (`server-only/empty` is a real published\n * entry point), never a merely similar name like `server-only-utils`.\n */\nfunction boundaryDeclarationOf(\n source: string,\n): { name: string; declares: WarlockEnvironment } | undefined {\n for (const [name, declares] of Object.entries(\n BOUNDARY_DECLARATION_PACKAGES,\n )) {\n if (source === name || source.startsWith(`${name}/`))\n return { name, declares };\n }\n return undefined;\n}\n\n/**\n * Whether a specifier names a Node builtin, in either spelling.\n *\n * The `node:` prefix is answered by SHAPE, not by membership: the prefix is\n * reserved by Node for builtins and nothing else can ever legally claim it, so\n * `node:anything` is refused even when `builtinModules` has never heard of it.\n * That covers the prefix-only builtins (`node:test`, `node:sqlite`, `node:sea`)\n * on Node versions whose `builtinModules` omits them, and every builtin Node\n * has not shipped yet, without this file knowing a single one of their names.\n *\n * Bare names fall through to Node's own set, which already carries the subpath\n * entries (`fs/promises`, `stream/web`, `timers/promises`, ...) as first-class\n * members — they need no prefix matching of their own.\n */\nfunction isNodeBuiltin(source: string): boolean {\n if (source.startsWith(\"node:\")) return true;\n return NODE_BUILTINS.has(source);\n}\n\nfunction normalize(filePath: string): string {\n return filePath.replace(/\\\\/g, \"/\");\n}\n\n/**\n * A plain `server/` path segment, tested against a path ALREADY made relative\n * to the app root — never against the absolute path. An app checked out at\n * `D:/work/server/shop` would otherwise have every one of its own files match.\n */\nconst APP_SERVER_SEGMENT = /(^|\\/)server(\\/|$)/;\n\nexport function isServerFile(resolvedPath: string, appRoot: string): boolean {\n // A Vite query suffix (`?raw`, `?worker`, `?url`, ...) changes how the\n // resolved id is CONSUMED, never which file on disk it names — the two\n // anchored patterns below test the END of the string, so a suffix still\n // attached at judgement time slides `.server`/`.server.ts` out from under\n // the `$` anchor and the file reads as clean. Stripped here, once, rather\n // than trusted to every caller: `bare` is what every check below judges,\n // matching `completeLocalModulePath`'s own bare/query split for the same\n // specifier (see its note).\n const bare = resolvedPath.split(\"?\")[0];\n const normalized = normalize(bare);\n // `.server` may or may not carry an extension by the time Gate A judges it\n // — a bare specifier like `./blog.server` (extension resolved later by\n // Vite) must be caught just as `./blog.server.ts` is.\n if (/\\.server(\\.[jt]sx?)?$/.test(normalized)) return true;\n if (/(^|\\/)\\.server(\\/|$)/.test(normalized)) return true;\n // A plain `server/` directory is judged for APP SOURCE ONLY, unlike the two\n // patterns above. `.server` in a filename is an unambiguous declaration\n // wherever it appears; a directory literally called `server/` is ordinary\n // internal layout for a dependency — `@warlock.js/web` keeps its own\n // sources in `web/src/server/` — so judging it everywhere would refuse the\n // framework itself. `isAppSourcePath` is the same app-source/dependency\n // distinction rule 4 draws, not a second one.\n if (isAppSourcePath(bare, appRoot)) {\n if (APP_SERVER_SEGMENT.test(normalize(path.relative(appRoot, bare))))\n return true;\n }\n return false;\n}\n\nexport function isRecognizedUniversalSurface(resolvedPath: string): boolean {\n const normalized = normalize(resolvedPath);\n const base = path.basename(normalized);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // There is deliberately NO app-root (`src/web/root.tsx`) case here, and\n // adding one would be dead code. The app root lives directly inside a\n // `web/` folder, so `isWithinModuleWebFolder` — which the only caller,\n // `isOutsideUniversalScope`, tests on the line BEFORE this function is\n // reached — already admits it by folder, whatever the file is named. A name\n // test here could never fire; the root's name is not what makes it\n // universal. (Verified: removing such a test leaves the app-root and\n // ordinary-sibling cases in `gate-a-resolve.spec.ts` passing.)\n return false;\n}\n\n/**\n * A `web/` path segment, tested — like `APP_SERVER_SEGMENT` above and for the\n * same reason — against a path ALREADY made relative to the app root.\n *\n * Never against the absolute path, and this repo is its own proof: Gate A's\n * fixtures live at `@warlock.js/web/__tests__/vite/fixtures/gate-a/...`, so\n * EVERY absolute path under them contains a `/web/` segment belonging to the\n * package directory rather than to any app module. Matched absolutely, rule 4\n * would admit the whole fixture tree and cases 9 and 25-28 would all go green\n * for the wrong reason.\n */\nconst MODULE_WEB_SEGMENT = /(^|\\/)web(\\/|$)/;\n\n/**\n * Whether a judged path is anywhere INSIDE a module's `web/` folder — at any\n * depth, not merely directly inside one.\n *\n * The depth matters and used to be wrong: this asked for `/web/<one-segment>`,\n * so `src/web/middleware/base.middleware.ts` — two segments in — did not count\n * as being within `src/web/` at all. Nothing had noticed because the only way\n * to reach such a file is a relative import, and rule 4 was not judging those\n * (see `completeLocalModulePath`): a nested universal file escaped the rule\n * rather than being admitted by it, and the two are indistinguishable until the\n * escape is closed. Closing it without this would have turned the v5 app's own\n * `src/web/root.tsx` — which imports exactly that middleware — into a refusal.\n *\n * Rule 4's premise is a FOLDER (\"outside a `$module/web/` folder\"), and a\n * subfolder of `web/` is inside it. `web/components/`, `web/layouts/` and\n * `web/utils/` are ordinary app layout, not a way around the fence.\n */\nexport function isWithinModuleWebFolder(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n return MODULE_WEB_SEGMENT.test(\n normalize(path.relative(appRoot, resolvedPath)),\n );\n}\n\n/**\n * Rule 4's subject set: local files that CARRY CODE. Deliberately the same\n * shape as `PARSEABLE_MODULE_EXTENSIONS` below — one notion of \"this is a\n * JS/TS module\" for the whole file — which also picks up `.mts`/`.cts`, absent\n * from the earlier hand-spelled `/\\.(tsx?|jsx?|mjs|cjs)$/`. `.mts` is a real\n * module extension Vite resolves, so a file wearing it was walking past rule 4\n * with its extension written out in full.\n *\n * Every extension `IMPLICIT_MODULE_EXTENSIONS` can append MUST match this —\n * see the note there.\n */\nconst LOCAL_MODULE_EXTENSIONS = /\\.([cm]?[jt]sx?)$/;\n\n/**\n * The extensions a local specifier may leave off, in Vite's own\n * `resolve.extensions` order.\n *\n * `.json` is deliberately absent: it is not code-carrying, so completing to it\n * would only hand rule 4 a path it exempts one line later, and the exemption is\n * the correct answer for a data file.\n *\n * The list and `LOCAL_MODULE_EXTENSIONS` have to agree in one direction: an\n * extension this list can PRODUCE but that regex does not RECOGNIZE is a fresh\n * escape hatch of exactly the shape this completion exists to close — the\n * judged path would gain a suffix and still be waved through. Pinned by case 32\n * in the spec. The order only decides which of two same-named files gets\n * judged, and every rule downstream (`isServerFile`, `isWithinModuleWebFolder`,\n * `isRecognizedUniversalSurface`) answers identically for `x.ts` and `x.js`, so\n * matching Vite is for least surprise rather than for correctness.\n */\nconst IMPLICIT_MODULE_EXTENSIONS = [\n \".mjs\",\n \".js\",\n \".mts\",\n \".ts\",\n \".jsx\",\n \".tsx\",\n \".cjs\",\n \".cts\",\n];\n\nfunction isFile(candidate: string): boolean {\n try {\n return statSync(candidate).isFile();\n } catch {\n return false;\n }\n}\n\n/**\n * The file a local specifier will ACTUALLY load, given the path `resolveId`\n * derived from it with a raw `path.resolve`.\n *\n * Without this, rule 4 was escapable by writing the import the ordinary way.\n * `resolveId` runs before anything appends an extension, so `./services/helper`\n * arrives as a path ending in `helper`; rule 4 asks for a code extension —\n * rightly, since it has no business judging `./logo.css` — finds none, and\n * declines to judge at all. `./x.middleware` slipped the same way while LOOKING\n * like it had an extension, and so did `./services` resolving to\n * `services/index.ts`. Measured: all three built clean (spec cases 25-28, 32).\n *\n * Asked of the FILESYSTEM rather than answered from the string, because the\n * string cannot tell an omitted extension from a real one: `.middleware` is not\n * a code extension and not an asset extension either, and only the disk knows\n * whether `thing.middleware.ts` is sitting there. Probing `<path><ext>` and\n * then `<path>/index<ext>` is what Vite is about to do anyway, so the rule\n * judges the file that will really be bundled.\n *\n * Deliberately NOT cached. A handful of `stat` calls per local specifier is\n * what every resolver in the pipeline already spends, and a cache would go\n * stale in watch mode exactly when a file is created or deleted — which is the\n * moment a fence must not be answering from memory.\n *\n * Two early exits, each a case where completion has nothing to add or no\n * business guessing:\n * - a non-absolute path (a bare package specifier — rules 1/2's business)\n * or a virtual `\\0` id;\n * - a path that already carries a code extension, or that already exists as a\n * real non-code file (an asset — `./theme.css`), where the path on hand is\n * the answer.\n *\n * A Vite query suffix (`./helper?raw`, `./thing?worker`, `./thing?url`) is\n * split off BEFORE any of that: the query changes what the id MEANS to Vite,\n * never which file on disk answers to it, so completion and every downstream\n * policy check (`isServerFile`) judge the BARE path — `./blog.server?raw`\n * must be caught exactly as `./blog.server` is, and used to slip through\n * here because the whole path, query included, was handed back unexamined.\n * The suffix is reattached to whatever this returns, because that IS the id\n * Vite will actually load (`resolve.extensions` completion happens first,\n * the query is consumed after) — nothing downstream should have to re-derive\n * it from the original specifier.\n */\nfunction completeLocalModulePath(judgedPath: string): string {\n if (!path.isAbsolute(judgedPath)) return judgedPath;\n if (judgedPath.includes(\"\\0\")) return judgedPath;\n\n const queryIndex = judgedPath.indexOf(\"?\");\n const barePath =\n queryIndex === -1 ? judgedPath : judgedPath.slice(0, queryIndex);\n const querySuffix = queryIndex === -1 ? \"\" : judgedPath.slice(queryIndex);\n\n return `${completeBareLocalModulePath(barePath)}${querySuffix}`;\n}\n\nfunction completeBareLocalModulePath(judgedPath: string): string {\n if (LOCAL_MODULE_EXTENSIONS.test(judgedPath)) return judgedPath;\n if (path.extname(judgedPath) !== \"\" && isFile(judgedPath)) return judgedPath;\n\n for (const extension of IMPLICIT_MODULE_EXTENSIONS) {\n const candidate = `${judgedPath}${extension}`;\n if (isFile(candidate)) return candidate;\n }\n // A directory only after every file candidate has failed — that is Node's and\n // Vite's order too, so `x.ts` beside `x/index.ts` is judged as `x.ts`.\n for (const extension of IMPLICIT_MODULE_EXTENSIONS) {\n const candidate = path.join(judgedPath, `index${extension}`);\n if (isFile(candidate)) return candidate;\n }\n\n // Nothing on disk answers to it. Left exactly as it arrived: resolution is\n // about to fail on its own, and a fence should not be inventing a verdict\n // about a file that is not there.\n return judgedPath;\n}\n\n/**\n * Rule 4 only judges local, code-carrying files INSIDE the app's own project\n * root — package specifiers are already covered by rules 1/2, non-code assets\n * (css, images, ...) are not the \"server/client leak\" shape this rule exists to\n * catch, and a dependency's own internal file layout (e.g. `@warlock.js/seal`'s\n * `src/rules/**`) is not organized by the `$module/web/` convention at all,\n * so judging it by that convention would fence out every third-party import.\n *\n * NOTHING HERE READS THE SPECIFIER, and that is the fix for the escape this\n * rule shipped with. It used to open with\n * `source.startsWith(\".\") || path.isAbsolute(source)` and return `false` for\n * anything else — so an import written in ALIAS form (`app/users/...`,\n * `web/...`, the shape `v5/app/tsconfig.json`'s `paths` and\n * `web-connector.ts`'s `resolve.alias` give every v5 app) was never judged at\n * all, whatever file it reached. Rule 4's subject is a FILE; the string the\n * author happened to type to name that file is not evidence about it. See the\n * spelling-equivalence suite in the spec — one target file, four spellings, one\n * judgement.\n *\n * Dropping that test narrows nothing and widens nothing for the specifiers it\n * used to admit, because `isAppSourcePath` below already requires an ABSOLUTE\n * path: a bare specifier's \"judged path\" is still the specifier string itself\n * (see `resolveId`), which is never absolute, so it falls out one line later\n * exactly as before. What changed is that a CALLER may now hand this rule a\n * resolved absolute path for a specifier that was not written relatively —\n * which is what `resolveId`'s alias pass does.\n *\n * `resolvedPath` must have been through `completeLocalModulePath` before it\n * gets here, or the extension test below is a way OUT of this rule rather than\n * a narrowing of it — see that function.\n */\nfunction isOutsideUniversalScope(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n if (!LOCAL_MODULE_EXTENSIONS.test(resolvedPath)) return false;\n if (!isAppSourcePath(resolvedPath, appRoot)) return false;\n // Order matters: `isAppSourcePath` above is what makes the app-root-relative\n // test in `isWithinModuleWebFolder` meaningful — it has already established\n // the path is absolute and inside `appRoot`.\n if (isWithinModuleWebFolder(resolvedPath, appRoot)) return false;\n if (isRecognizedUniversalSurface(resolvedPath)) return false;\n return true;\n}\n\n/**\n * Whether a judged path is the APP'S OWN SOURCE, as opposed to a dependency's\n * internals. This is Gate A's single definition of that boundary — rules 3\n * and 4 both call it, and `packageNameForFilePath` draws the same line for\n * Gate C. Two conditions, both already required by rule 4 before this was\n * extracted:\n *\n * - inside `appRoot` (a monorepo sibling or any other path resolution has\n * left the app root for is that package's own business), and\n * - not under a `node_modules/` segment. Nested `node_modules` inside\n * `appRoot` is the ORDINARY case for a dependency, and Vite's optimized\n * dependency chunks (`<appRoot>/node_modules/.vite/deps`) even import one\n * another relatively.\n *\n * Bare package specifiers never reach here as app source: for those the\n * \"judged path\" is still the specifier string itself (see `resolveId`), which\n * is not absolute. They are rule 2's business.\n */\nexport function isAppSourcePath(\n resolvedPath: string,\n appRoot: string,\n): boolean {\n if (!path.isAbsolute(resolvedPath)) return false;\n if (!isInsideAppRoot(resolvedPath, appRoot)) return false;\n return !normalize(resolvedPath).includes(\"/node_modules/\");\n}\n\nfunction isInsideAppRoot(resolvedPath: string, appRoot: string): boolean {\n const relative = path.relative(appRoot, resolvedPath);\n return !!relative && !relative.startsWith(\"..\") && !path.isAbsolute(relative);\n}\n\n/* ------------------------------------------------------------------------ *\n * Type-only import edges (rule 2 only)\n * ------------------------------------------------------------------------ */\n\n/**\n * How an importer's source text binds a specifier: `\"value\"` if ANY statement\n * mentioning that specifier can produce a runtime binding or a runtime edge,\n * `\"type\"` only when every one of them is erased by the type system.\n */\ntype ImportKind = \"value\" | \"type\";\n\n/** Files whose source Gate A will parse for import kinds. */\nconst PARSEABLE_MODULE_EXTENSIONS = /\\.([cm]?[jt]sx?)$/;\n\n/** Cheap pre-filter: no governed scope in the text, nothing to classify. */\nconst GOVERNED_SCOPE_MENTION = /@warlock\\.js\\/|@mongez\\//;\n\n/**\n * The id Gate A resolves a type-only edge into a server-only package to. `\\0`\n * is the virtual-module convention, which also keeps Gate C from trying to\n * attribute this id to a package on disk (`packageNameForFilePath` returns\n * `undefined` for any id containing `\\0` — see its note).\n */\nconst TYPE_ONLY_ERASED_ID = \"\\0warlock:type-only-erased\";\n\n/**\n * Rollup module ids carry query suffixes (`?v=`, `?used`, ...) that differ\n * between the `transform` id and a later `importer`. Both sides key through\n * here so they agree.\n */\nfunction moduleKey(id: string): string {\n return normalize(id.split(\"?\")[0]);\n}\n\nfunction parserPluginsFor(id: string): (\"typescript\" | \"jsx\")[] {\n // `jsx` on a `.ts` file mis-parses the type-assertion form `<T>value`, which\n // is legal there and only there. Everything else (including plain `.js`) is\n // parsed with both, since JSX in a `.js` file is ordinary in this ecosystem.\n return /\\.[cm]?ts$/.test(moduleKey(id))\n ? [\"typescript\"]\n : [\"typescript\", \"jsx\"];\n}\n\n/**\n * Whether an `ImportDeclaration`'s specifier list is entirely type-only.\n *\n * Both spellings count, because both are erased: the statement form\n * (`import type { X } from \"P\"`, `decl.importKind === \"type\"`) and the inline\n * form (`import { type X } from \"P\"`, every specifier's own `importKind`).\n * The inline form is the one that matters in practice — under\n * `verbatimModuleSyntax` TypeScript/esbuild emit it VERBATIM as\n * `import {} from \"P\"`, so it is the only one that survives to `resolveId` at\n * all.\n *\n * An EMPTY specifier list (`import \"P\"`) is deliberately `\"value\"`: a bare\n * side-effect import is a real runtime edge that the author wrote on purpose,\n * and it is indistinguishable — post-esbuild — from the erased form. Treating\n * it as type-only would turn this carve-out into a hole big enough to smuggle\n * a whole server package through.\n */\nfunction importDeclarationKind(decl: any): ImportKind {\n if (decl.importKind === \"type\") return \"type\";\n if (decl.specifiers.length === 0) return \"value\";\n return decl.specifiers.every((spec: any) => spec.importKind === \"type\")\n ? \"type\"\n : \"value\";\n}\n\nfunction exportDeclarationKind(stmt: any): ImportKind {\n if (stmt.exportKind === \"type\") return \"type\";\n const specifiers = stmt.specifiers ?? [];\n if (specifiers.length === 0) return \"value\"; // `export * from \"P\"` — a runtime edge.\n return specifiers.every((spec: any) => spec.exportKind === \"type\")\n ? \"type\"\n : \"value\";\n}\n\n/**\n * Every specifier reached by a RUNTIME expression rather than a static\n * import/export statement — `import(\"P\")` and `require(\"P\")` — found by a\n * duck-typed walk of the whole program, not just its top level, because both\n * are ordinary expressions and can sit anywhere.\n *\n * These are unconditionally `\"value\"`. A dynamic import of a server-only\n * package is exactly the edge rule 2 exists to refuse, and it reaches\n * `resolveId` with the same specifier string a static import would, so\n * without this walk a file could hold `import { type X } from \"P\"` beside\n * `await import(\"P\")` and have the static statement's kind exempt the dynamic\n * one.\n */\nfunction collectRuntimeSpecifiers(node: unknown, into: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectRuntimeSpecifiers(item, into);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"CallExpression\" || record.type === \"ImportExpression\") {\n const callee = (record as any).callee;\n const isRuntimeLoad =\n record.type === \"ImportExpression\" ||\n callee?.type === \"Import\" ||\n (callee?.type === \"Identifier\" && callee.name === \"require\");\n if (isRuntimeLoad) {\n const arg = (record as any).source ?? (record as any).arguments?.[0];\n if (arg?.type === \"StringLiteral\" && typeof arg.value === \"string\")\n into.add(arg.value);\n }\n }\n for (const key of Object.keys(record)) {\n if (\n key === \"type\" ||\n key === \"start\" ||\n key === \"end\" ||\n key === \"loc\" ||\n key === \"range\"\n )\n continue;\n if (\n key === \"leadingComments\" ||\n key === \"trailingComments\" ||\n key === \"innerComments\" ||\n key === \"extra\"\n ) {\n continue;\n }\n collectRuntimeSpecifiers(record[key], into);\n }\n}\n\n/**\n * Classifies every specifier `code` imports as `\"value\"` or `\"type\"`. Returns\n * `undefined` when the source could not be parsed — the caller reads that as\n * \"no information\", which leaves rule 2 refusing exactly as it does today.\n */\nfunction classifyImportKinds(\n code: string,\n id: string,\n): Map<string, ImportKind> | undefined {\n let ast;\n try {\n ast = parse(code, {\n sourceType: \"module\",\n plugins: parserPluginsFor(id),\n errorRecovery: false,\n });\n } catch {\n return undefined;\n }\n\n const kinds = new Map<string, ImportKind>();\n // `\"value\"` always wins a merge: one value edge anywhere in the file is\n // enough to make the specifier a runtime dependency, however many type-only\n // statements sit beside it.\n const record = (source: string, kind: ImportKind) => {\n if (kind === \"value\" || !kinds.has(source)) kinds.set(source, kind);\n };\n\n for (const stmt of ast.program.body as any[]) {\n if (stmt.type === \"ImportDeclaration\") {\n record(stmt.source.value as string, importDeclarationKind(stmt));\n continue;\n }\n if (\n (stmt.type === \"ExportNamedDeclaration\" ||\n stmt.type === \"ExportAllDeclaration\") &&\n stmt.source\n ) {\n record(stmt.source.value as string, exportDeclarationKind(stmt));\n }\n }\n\n const runtimeSpecifiers = new Set<string>();\n collectRuntimeSpecifiers(ast.program.body, runtimeSpecifiers);\n for (const source of runtimeSpecifiers) kinds.set(source, \"value\");\n\n return kinds;\n}\n\ninterface FenceViolation {\n cause: string;\n fix: string;\n}\n\n/**\n * Rule 2's predicate on its own, extracted so `resolveId` can ask the question\n * a SECOND time after `ruleViolation` has declined to refuse: a type-only edge\n * into a server-only package is not a violation, but it is not an ordinary\n * import either — it is erased (see `TYPE_ONLY_ERASED_ID`). Asking separately,\n * rather than short-circuiting inside `ruleViolation`, is what keeps rules 3\n * and 4 authoritative over a type-only specifier: `@warlock.js/core/db.server`\n * imported for its types alone is still refused, by rule 3, because\n * `ruleViolation` still runs to completion first.\n */\nfunction isServerMarkedGovernedPackage(\n source: string,\n importer: string | undefined,\n environmentOf: (pkgName: string) => WarlockEnvironment,\n appRoot: string,\n): boolean {\n if (!isGovernedScope(source)) return false;\n if (importer && !isInsideAppRoot(importer, appRoot)) return false;\n return environmentOf(governedPackageNameOf(source)) === \"server\";\n}\n\nfunction ruleViolation(\n source: string,\n resolvedPath: string,\n importer: string | undefined,\n environmentOf: (pkgName: string) => WarlockEnvironment,\n appRoot: string,\n isTypeOnlyEdge: boolean,\n): FenceViolation | undefined {\n // Rule 1 takes NO importer into account, and that is deliberate rather than\n // an omission. Rule 2's `GOVERNED_SCOPES` / `isInsideAppRoot` narrowing is\n // right for the environment MARKER, which is a claim we only make about\n // packages we publish; we do not get to judge whether Radix or Mantine is\n // server-side. A Node builtin is not that kind of judgement. `node:fs` has no\n // browser implementation whoever wrote the import — the app's own source, a\n // governed package, or a transitive dependency six hops down — so this rule\n // is judged for every importer and, unlike rule 2, is never exempted by a\n // type-only edge (see the `isTypeOnlyEdge` note below).\n if (isNodeBuiltin(source)) {\n return {\n cause: `\"${source}\" is a Node.js builtin module and cannot run in the browser.`,\n 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.`,\n };\n }\n\n const boundaryDeclaration = boundaryDeclarationOf(source);\n if (boundaryDeclaration?.declares === \"server\") {\n const declarer = importer ? displayName(importer) : source;\n return {\n 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.`,\n 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.`,\n };\n }\n\n // Rule 2 is judged only for imports whose importer is app-authored code\n // (inside `appRoot`) — mirroring rule 4's existing dependency-internals\n // exemption below. A governed package's OWN internal composition (e.g.\n // `@warlock.js/seal` importing `@mongez/reinforcements`) is that package's\n // business, not the app's; what Gate A polices is the app reaching INTO a\n // server-only package, not what a package it's already allowed to use does\n // internally.\n // `isTypeOnlyEdge` exempts rule 2 and NOTHING else. A refusal here is a\n // statement about what the app will EXECUTE, and an import statement with no\n // value binding executes nothing from the package — refusing it would be a\n // false positive on code that is already correct, which is how people learn\n // to switch a fence off. Rules 1, 3, 4 and 5 are untouched by this flag on\n // purpose: they judge the importer's own nature or a file name, neither of\n // which a type-only spelling changes.\n if (\n !isTypeOnlyEdge &&\n isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)\n ) {\n const pkgName = governedPackageNameOf(source);\n const scopeLabel = pkgName.startsWith(\"@warlock.js/\")\n ? \"@warlock.js\"\n : \"@mongez\";\n return {\n cause: `\"${source}\" resolves into ${pkgName}, a server-only ${scopeLabel} package — it declares \"warlock\": { \"environment\": \"server\" } in its package.json.`,\n 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\").`,\n };\n }\n\n if (isServerFile(resolvedPath, appRoot)) {\n return {\n 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.`,\n 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.`,\n };\n }\n\n // RULE 4 IS GONE, DELIBERATELY. Owner ruling 2026-08-24 (canon `10f6041c`):\n // the client boundary is decided by the import GRAPH, not by file location.\n //\n // It used to refuse any app file living outside `$module/web/`. That is a\n // PROXY for danger, and it produced exactly the false positives you would\n // expect from a proxy:\n //\n // - `app/auth/schema/login.schema.ts` imports only `@warlock.js/seal`\n // (marked universal) and was refused for its address.\n // - `app/access/permissions.ts` imports NOTHING — two frozen objects of\n // name constants — and was refused for its address.\n //\n // Both are genuinely universal: the server validates and authorises with\n // them, the client renders and validates with them. Every location-based fix\n // for that (move them, rename them, add a folder) was rejected by the owner\n // because it forces app structure to serve the fence rather than the app.\n //\n // WHAT STILL REFUSES, and why this is not a weakening:\n // rule 1 — a Node builtin anywhere in the graph, at any depth, any importer\n // rule 1b — a `server-only` boundary declaration\n // rule 2 — a governed package MARKED `environment: \"server\"`\n // rule 3 — a file that names itself server (`*.server.ts`, `server/`)\n //\n // Server-ness now PROPAGATES: a file is refused when something it reaches is\n // server-only, and the chain is printed so the refusal names the real cause\n // instead of an address. `nav.service` is still refused — not for sitting\n // outside `web/`, but for reaching auth and the user model.\n //\n // THE KNOWN GAP, recorded rather than hidden: a file that is secret but\n // imports nothing suspicious (a hardcoded key) is invisible to the graph.\n // `*.server.ts` above is the manual escape hatch for exactly that case.\n return undefined;\n}\n\n/**\n * The label one hop of an import chain carries.\n *\n * A basename is enough for the app's own source, where file names are chosen\n * and distinct. It is not enough for a dependency: a large share of\n * node_modules resolves to a file literally called `index.js`, and a chain\n * reading `page.tsx → index.js → node:fs` names the culprit for nobody — which\n * is the failure mode where people switch the fence off instead of fixing the\n * import. Anything under a `node_modules/` segment is therefore labelled with\n * its path from the LAST such segment (`vendor-node-lib/index.js`), so a\n * vendored nested copy reports the package that actually holds the file rather\n * than the one at the top of the tree.\n */\nfunction displayName(id: string): string {\n const normalized = normalize(id);\n const segments = normalized.split(\"node_modules/\");\n if (segments.length < 2) return path.basename(normalized);\n return segments[segments.length - 1];\n}\n\n/** Rollup's normalized `external`, as `buildStart` receives it. */\ntype ExternalPredicate = (\n id: string,\n importer: string | undefined,\n isResolved: boolean,\n) => unknown;\n\n/**\n * Every Node builtin the Rollup config would externalize — empty when none,\n * which is the only shape that lets a build proceed past `buildStart`.\n *\n * This is the one route a builtin can take into a browser chunk without ever\n * being offered to `resolveId`: Rollup consults `external` FIRST, and an\n * externalized specifier is emitted into the output verbatim — measured, on\n * this gate's own fixture, as a clean build whose chunk still opens with\n * `import { readFileSync } from \"node:fs\"`. No `resolveId` rule can close that,\n * because the hook is never called; the check has to happen against the config\n * itself, before any module is loaded.\n *\n * Asked as a QUESTION of the normalized predicate rather than by reading an\n * array, because `external` may legitimately be a string, a RegExp, an array of\n * either, or a function — Rollup normalizes all of them into the function\n * handed to `buildStart`, and asking it about each builtin spelling is the only\n * form of the check that covers every shape. A predicate that throws on an\n * unexpected shape of argument is read as \"not external\": this guard exists to\n * catch a config that says yes, and inventing a refusal out of someone else's\n * exception would be a false positive with no evidence behind it.\n *\n * This fires on CONFIGURATION, not on an actual import — nothing need import\n * `fs` for it to trip. That is on purpose: the config has already decided the\n * fence does not apply, and a build that would silently pass today only because\n * no import happens to exist yet is not a build anyone should trust tomorrow.\n */\nfunction externalizedBuiltins(isExternal: unknown): string[] {\n if (typeof isExternal !== \"function\") return [];\n const asks = isExternal as ExternalPredicate;\n return NODE_BUILTIN_SPELLINGS.filter((specifier) => {\n try {\n return asks(specifier, undefined, false) === true;\n } catch {\n return false;\n }\n });\n}\n\nexport interface EnvironmentClassifierOptions {\n /**\n * Force these governed-scope package names (e.g. `@warlock.js/core`) to\n * classify as `\"server\"` regardless of what `warlock.environment` their\n * `package.json` declares. Defaults to the live marker resolution (workspace\n * `package.json` lookup, falling back to `node_modules`). Only meant for\n * tests that need a deterministic classification independent of the host\n * filesystem.\n */\n serverPackages?: Iterable<string>;\n /**\n * The app's project root. Defines what counts as APP SOURCE (see\n * `isAppSourcePath`), which two rules depend on: rule 4 (\"outside\n * `$module/web/`\") and rule 3's plain-`server/`-folder half. A dependency's\n * own internal file layout — reached once resolution has left the app root,\n * or through a `node_modules/` segment — is exempt from both, though rules\n * 1, 2 and the `.server` half of rule 3 still apply to it. Defaults to\n * `process.cwd()`, matching Vite's own default `root`.\n */\n appRoot?: string;\n}\n\ntype GateAOptions = EnvironmentClassifierOptions;\n\nexport interface EnvironmentClassifier {\n appRoot: string;\n environmentOf(pkgName: string): WarlockEnvironment;\n /**\n * Maps an absolute, already-resolved file path (e.g. a Rollup\n * `OutputChunk.moduleIds` entry) back to the governed-scope package name\n * that owns it — a `node_modules` dependency, or a workspace member's own\n * directory PROVIDED the path is outside `appRoot`. The `appRoot` carve-out\n * matters: a fixture/app can physically live inside a workspace package's\n * own directory tree (e.g. this repo's own `web/__tests__/` fixtures live\n * under the `web` package) without thereby \"importing\" that package — the\n * app's own source is never a dependency edge onto itself, no matter where\n * on disk it happens to sit (same distinction Gate A's rule 2 already makes\n * via `isInsideAppRoot` for the importer side). `undefined` when the path\n * isn't a governed-scope dependency at all. Exists so Gate C\n * (`gate-c-verify.ts`) can classify modules already sitting in the EMITTED\n * bundle graph using this exact same marker logic, instead of hand-rolling\n * a second classification scheme — Gate C re-derives, it does not\n * duplicate.\n */\n packageNameForFilePath(absPath: string): string | undefined;\n}\n\n/**\n * The `warlock.environment` marker classifier (rule 2). Extracted from\n * `gateAResolve` so Gate C can classify packages it finds in the EMITTED\n * bundle graph with the identical resolution logic — same workspace index,\n * same `node_modules` fallback, same default — rather than a second,\n * potentially-drifting implementation.\n *\n * An ABSENT marker classifies as `\"universal\"`, i.e. allowed. This is a\n * deliberate reversal of the earlier burden-inversion default. A marker is\n * shipped metadata: making silence mean \"refuse\" meant every browser-safe\n * package we own needed a marker AND a release before any app could boot, and\n * a package the app does not control (`@mongez/react-atom`) could not be\n * imported at all. Refusal is now driven by information that is PRESENT — an\n * explicit `\"server\"` marker, or a server-only file name (rule 3) — which the\n * app author can always supply themselves without waiting on a release. The\n * marker mechanism itself is untouched and still authoritative when set.\n */\nexport function createEnvironmentClassifier(\n options: EnvironmentClassifierOptions = {},\n): EnvironmentClassifier {\n const forcedServerPackages = options.serverPackages\n ? new Set(options.serverPackages)\n : undefined;\n const appRoot = path.resolve(options.appRoot ?? process.cwd());\n const workspaceIndex = findWorkspaceIndex(__dirname);\n const environmentCache = new Map<string, WarlockEnvironment>();\n\n function environmentOf(pkgName: string): WarlockEnvironment {\n if (forcedServerPackages?.has(pkgName)) return \"server\";\n const cached = environmentCache.get(pkgName);\n if (cached) return cached;\n\n const packageJsonPath =\n workspaceIndex?.packageJsonByName.get(pkgName) ??\n findNodeModulesPackageJson(pkgName, appRoot);\n\n let environment: WarlockEnvironment = \"universal\";\n if (packageJsonPath) {\n try {\n const pkg = JSON.parse(readFileSync(packageJsonPath, \"utf-8\"));\n const marker = pkg?.warlock?.environment;\n if (\n marker === \"server\" ||\n marker === \"universal\" ||\n marker === \"client\"\n ) {\n environment = marker;\n }\n } catch {\n // Unreadable package.json — no marker could be read, so the permissive\n // default applies, exactly as for a package that carries none.\n }\n }\n\n environmentCache.set(pkgName, environment);\n return environment;\n }\n\n function packageNameForFilePath(absPath: string): string | undefined {\n // A Rollup module id is not always a file path. `\\0` is the universal\n // virtual-module convention (Vite's own `createFilter` refuses any id\n // containing it), and a virtual module has no package on disk to attribute\n // it to. Without this guard `path.resolve` grafts the synthetic id onto\n // `process.cwd()` and the workspace-root prefix scan below then attributes\n // it to whichever package the BUILD PROCESS happens to be running inside —\n // so `\\0virtual:warlock/pages` gets classified as \"@warlock.js/web,\n // server-only\" and Gate C refuses a build over a module that never touched\n // the filesystem. That failure is loud rather than silent, but it is still\n // wrong, and it makes every virtual module unbuildable whenever cwd sits\n // inside a governed-scope package. Nothing is weakened by returning\n // `undefined` here: a virtual module's own IMPORTS resolve to real paths\n // that this function still classifies, and its emitted code is still\n // subject to Gate C's server-export scan and Gate B's transform.\n if (absPath.includes(\"\\0\")) return undefined;\n\n const normalized = normalize(path.resolve(absPath));\n\n // node_modules is unambiguous — always a dependency, even when nested\n // inside appRoot (the ordinary case: `appRoot/node_modules/...`).\n const nodeModulesMatch = packageNameFromNodeModulesPath(normalized);\n if (nodeModulesMatch) return nodeModulesMatch;\n\n // Everything else inside appRoot is the app's OWN source, never a\n // dependency edge, regardless of which workspace package's directory it\n // happens to physically sit under — see the appRoot carve-out note above.\n if (isInsideAppRoot(normalized, appRoot)) return undefined;\n\n if (workspaceIndex) {\n for (const [name, pkgJsonPath] of workspaceIndex.packageJsonByName) {\n const root = `${normalize(path.dirname(pkgJsonPath))}/`;\n if (normalized.startsWith(root)) return name;\n }\n }\n return undefined;\n }\n\n return { appRoot, environmentOf, packageNameForFilePath };\n}\n\n/**\n * The client-build Vite plugin. `vite` is only imported for its types\n * (`import type`), so this module carries no runtime dependency on `vite`\n * being installed — it is a `peerDependenciesMeta.optional` peer.\n */\nexport function gateAResolve(options: GateAOptions = {}): Plugin {\n const { appRoot, environmentOf } = createEnvironmentClassifier(options);\n\n // Maps a resolved module id to the id that imported it, rebuilt per plugin\n // instance (i.e. per build) via our own `this.resolve` calls below — this\n // does not depend on Rollup's internal module-graph bookkeeping being\n // populated in any particular order.\n const importerOf = new Map<string, string>();\n\n /**\n * Module key -> that module's specifier classification, filled by the\n * `transform` hook below and read by `resolveId`.\n *\n * This is the answer to the one structural problem rule 2 has: `resolveId`\n * receives a specifier STRING and an importer path, never an AST, so it\n * cannot see for itself whether an import was type-only. The information\n * exists exactly once in the pipeline — in the module's PRE-esbuild\n * TypeScript source — so it is captured there and carried forward.\n *\n * Three routes were available; this is why the other two lost:\n *\n * - Recognizing the POST-esbuild shape (`import {} from \"P\"`) from inside\n * `resolveId`. Rejected as unsound, not merely awkward: esbuild emits the\n * identical `import {} from \"P\"` for a genuine bare side-effect import\n * under some settings, so the shape cannot separate \"erased type import\"\n * from \"the author asked for this package's side effects\". That is a\n * false NEGATIVE in a security fence — the one direction of error this\n * gate must never make — and it would additionally depend on\n * `getModuleInfo(importer).code` being populated during a dependency's\n * `resolveId`, which is undocumented Rollup ordering.\n * - Moving rule 2 wholesale into a `transform` pass. Rejected as too wide\n * for this change: `transform` never runs for entry, virtual or\n * externalized importers, so rule 2 would silently stop judging edges it\n * judges today, and the import-chain machinery (`importerOf`) is built\n * out of `resolveId`'s importer argument. Gate A is a `resolveId` fence\n * by identity; only the type-only QUESTION moves, never the refusal.\n * - Reusing `projection.ts`'s existing parse (`projectModule`). Rejected as\n * a coupling, though it is the right TECHNIQUE and is copied here:\n * projection parses only `*.page.tsx`/`layout`/`root`, records binding\n * READERS rather than import KINDS, and rewrites code. Gate A must work\n * standalone — every case in `gate-a-resolve.spec.ts` runs\n * `gateAResolve()` with no projection in the array — so it cannot depend\n * on another plugin having run.\n *\n * A module absent from this map (unparseable, never transformed, not JS/TS)\n * yields no `\"type\"` answer, so rule 2 refuses exactly as it does today.\n * Missing information fails CLOSED.\n */\n const importKindsByModule = new Map<string, Map<string, ImportKind>>();\n\n function recordImportKinds(code: string, id: string): void {\n const key = moduleKey(id);\n if (key.includes(\"\\0\")) return;\n if (!PARSEABLE_MODULE_EXTENSIONS.test(key)) return;\n if (!GOVERNED_SCOPE_MENTION.test(code)) return;\n\n const kinds = classifyImportKinds(code, key);\n if (!kinds) return;\n\n const existing = importKindsByModule.get(key);\n if (!existing) {\n importKindsByModule.set(key, kinds);\n return;\n }\n // Same path re-transformed (a query variant, or a watch rebuild): merge in\n // the same fail-closed direction `classifyImportKinds` uses internally.\n for (const [source, kind] of kinds) {\n if (kind === \"value\" || !existing.has(source)) existing.set(source, kind);\n }\n }\n\n function isTypeOnlyEdge(\n source: string,\n importer: string | undefined,\n ): boolean {\n if (!importer) return false;\n return importKindsByModule.get(moduleKey(importer))?.get(source) === \"type\";\n }\n\n /**\n * How many `resolve.alias` entries this build declares, captured from the\n * RESOLVED Vite config (`configResolved`) rather than guessed.\n *\n * Purely diagnostic, and it is the one thing the alias table can tell Gate A\n * that the resolver cannot. The gate does not match aliases itself — see the\n * note on the alias pass in `resolveId` for why asking Vite's own resolver\n * beats re-deriving `@rollup/plugin-alias`'s find/replacement semantics. But\n * when a specifier resolves to NOTHING, the count is the difference between\n * \"a dependency is missing\" and \"this build never installed the alias table\n * its own tsconfig `paths` describe\" — which is exactly the v5 production\n * client build's state, and the actionable half of that warning.\n *\n * `undefined` when `configResolved` never fired (plain Rollup, or a unit\n * harness), which the message reports as unknown rather than as zero.\n */\n let declaredAliasCount: number | undefined;\n\n function buildChain(importer: string | undefined): string[] {\n const chain: string[] = [];\n let current = importer;\n const seen = new Set<string>();\n while (current && !seen.has(current)) {\n seen.add(current);\n chain.unshift(displayName(current));\n current = importerOf.get(current);\n }\n return chain;\n }\n\n return {\n name: \"warlock:gate-a-resolve\",\n // Must run before Vite's own core resolver: Vite silently externalizes\n // Node builtins into a `__vite-browser-external` stub for browser\n // builds, and would resolve `@warlock.js/*` packages via node_modules,\n // if either ran first. \"The cheapest security mechanism wins\" (RFC\n // §1.5) means this fence goes first, not last.\n enforce: \"pre\",\n /** Records the alias-table size; see `declaredAliasCount`. Judges nothing. */\n configResolved(config) {\n const alias: unknown = config.resolve?.alias;\n if (Array.isArray(alias)) {\n declaredAliasCount = alias.length;\n } else if (alias && typeof alias === \"object\") {\n declaredAliasCount = Object.keys(alias).length;\n } else {\n declaredAliasCount = 0;\n }\n },\n /**\n * The config-level half of rule 1 — see `externalizedBuiltins` for why a\n * `resolveId` fence cannot cover it on its own. Runs once per build, before\n * the first module is loaded, so the refusal lands before anything has been\n * bundled.\n */\n buildStart(inputOptions) {\n const externalized = externalizedBuiltins(inputOptions.external);\n if (externalized.length === 0) return;\n const shown = externalized.slice(0, 5).join(\", \");\n const rest =\n externalized.length > 5 ? `, and ${externalized.length - 5} more` : \"\";\n this.error(\n [\n `Gate A refused this build: its Rollup \"external\" config would let Node builtins through the fence.`,\n ``,\n `Externalized builtins: ${shown}${rest}`,\n `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.`,\n `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.`,\n ].join(\"\\n\"),\n );\n },\n /**\n * Records import kinds; never rewrites a byte (`null` return). `enforce:\n * \"pre\"` is what makes this correct as well as early — it puts this hook\n * ahead of Vite's own `vite:esbuild` TypeScript transform, so `code` here\n * is the ORIGINAL source with `type` modifiers still on it. After esbuild\n * the distinction no longer exists in the text.\n *\n * Runs before `resolveId` fires for any of this module's own specifiers:\n * Rollup must parse a module's post-transform source to discover what to\n * resolve next. That ordering is an OBSERVED Rollup behaviour, pinned by\n * `index.spec.ts`'s \"D.3 hook ordering pin\" and documented at length in\n * `index.ts`. If it ever inverts, this map is simply empty when\n * `resolveId` reads it and rule 2 goes back to refusing type-only imports\n * — a loud false positive, never a silent leak.\n */\n transform(code, id) {\n recordImportKinds(code, id);\n return null;\n },\n /**\n * The erased stand-in a type-only edge into a server-only package resolves\n * to. It must exist rather than the import simply being permitted, and the\n * fixture measures why: `import { type Request } from \"@warlock.js/core\"`\n * under `verbatimModuleSyntax` is emitted as `import {} from\n * \"@warlock.js/core\"`, and Rollup honours that as a side-effect edge — the\n * package's module-level code lands in the client chunk even though not one\n * binding is read from it. Permitting the import would therefore trade a\n * false positive for the exact silent leak the `server` marker exists to\n * close. Resolving to an empty module gives both: a clean build, and\n * nothing of the server package in the browser.\n */\n load(id) {\n return id === TYPE_ONLY_ERASED_ID ? \"export {};\" : null;\n },\n async resolveId(source, importer, resolveOptions) {\n // Gate A is a CLIENT-build fence — see the header note and Gate B/Gate\n // C's identical carve-outs (`gate-b-secrets.ts`'s `options?.ssr` guard\n // in `transform`, Gate C's `this.environment?.config?.consumer ===\n // \"server\"` in `generateBundle`). The SSR/server build resolves the\n // same specifiers this app's client build does — a server-only import\n // is exactly what that build is FOR — so refusing them there would\n // refuse the server its own legitimate dependencies. Judged first and\n // unconditionally: nothing below this line has a file to weigh in on\n // an SSR resolution before this returns.\n if (resolveOptions?.ssr) return null;\n\n // Compute the path Gate A judges: for relative/absolute specifiers,\n // resolve against the importer's directory ourselves (filename-only\n // check, zero AST parsing, per `c604f0bc` §4) rather than delegating\n // to Vite's resolver first, then complete the extension the author was\n // free to leave off — `path.resolve` alone yields `.../helper` for\n // `./helper`, which rule 4 does not recognize as code and therefore\n // never judges. `completeLocalModulePath` is a no-op for bare\n // specifiers, virtual ids and paths that already carry an extension.\n const isRelativeOrAbsolute =\n source.startsWith(\".\") || path.isAbsolute(source);\n const judgedPath = completeLocalModulePath(\n isRelativeOrAbsolute && importer\n ? path.resolve(path.dirname(importer), source)\n : source,\n );\n\n if (source === TYPE_ONLY_ERASED_ID) return source;\n\n const typeOnly = isTypeOnlyEdge(source, importer);\n\n const violation = ruleViolation(\n source,\n judgedPath,\n importer,\n environmentOf,\n appRoot,\n typeOnly,\n );\n if (violation) {\n const chain = [...buildChain(importer), source].join(\" → \");\n this.error(\n [\n `Gate A refused an import: forbidden dependency reaches the client bundle.`,\n ``,\n `Import chain: ${chain}`,\n `File: ${importer ?? source}`,\n `Cause: ${violation.cause}`,\n `Fix: ${violation.fix}`,\n ].join(\"\\n\"),\n );\n }\n\n // Not refused, but not an ordinary import either: rule 2's predicate\n // holds and only the missing value binding spared it. Asked here, AFTER\n // `ruleViolation` has run to completion, so rules 3 and 4 keep the last\n // word over a type-only specifier (`@warlock.js/core/db.server` is still\n // refused by rule 3, whatever the import kind).\n if (\n typeOnly &&\n isServerMarkedGovernedPackage(source, importer, environmentOf, appRoot)\n ) {\n return TYPE_ONLY_ERASED_ID;\n }\n\n // Not forbidden — resolve for real so the chain map reflects Rollup's\n // actual resolved ids, then hand that resolution back so we don't do\n // the work twice.\n const resolved = await this.resolve(source, importer, {\n ...resolveOptions,\n skipSelf: true,\n });\n if (resolved && importer) {\n importerOf.set(resolved.id, importer);\n }\n\n /*\n THE ALIAS PASS — rules 3 and 4, judged a SECOND time on the file the\n specifier actually reached rather than on the string that named it.\n\n Everything above judges `judgedPath`, which for a specifier that is\n neither relative nor absolute is the specifier STRING itself. That is\n the right answer for rules 1, 2 and 5, which judge names. It is the\n wrong answer for rules 3 and 4, which judge files — and it is why an\n import written in alias form (`app/users/...`) walked past both.\n\n Resolution, not re-derivation. The card that raised this asked for the\n alias TABLE to be read out of the config and applied here. Asking\n `this.resolve` is the same answer arrived at more cheaply and with\n strictly less to get wrong: it IS Vite's resolver, configured by that\n very table, so it already covers string-vs-RegExp `find` semantics,\n entry order, `customResolver`, extension completion and `index` files —\n every one of which is a place a second implementation of\n `@rollup/plugin-alias` could drift from the first. It also covers routes\n the table does not describe at all (a `vite-tsconfig-paths` plugin, a\n `customResolver`), which a table reader would miss.\n\n The `isAppSourcePath` pre-filter is what keeps this from being a second,\n broader rule: everything in `node_modules` and everything outside\n `appRoot` is out before `ruleViolation` is asked anything, exactly as\n rules 3 and 4 already require of their own subjects.\n\n WARNING, NOT REFUSAL — and this is the deliberate, temporary half. The\n judgement above is now spelling-independent; the consequence is not yet.\n Unlike the other fences in this file, turning this one on has a measured\n non-zero blast radius in application source that BUILDS TODAY, so a\n refusal would break working builds at sites nobody has surveyed. The\n warning names the file, the specifier and the resolved path so the\n survey can be taken; when it comes back empty, this `this.warn` becomes\n `this.error` and the alias pass folds into the pass above.\n */\n const isBareSpecifier = !isRelativeOrAbsolute && !source.includes(\"\\0\");\n if (isBareSpecifier && resolved && !resolved.external) {\n const aliasPath = completeLocalModulePath(moduleKey(resolved.id));\n if (isAppSourcePath(aliasPath, appRoot)) {\n const aliasViolation = ruleViolation(\n source,\n aliasPath,\n importer,\n environmentOf,\n appRoot,\n typeOnly,\n );\n if (aliasViolation) {\n this.warn({\n message: [\n `Gate A WOULD refuse this import — reported as a WARNING, not enforced yet.`,\n ``,\n `Import chain: ${[...buildChain(importer), source].join(\" → \")}`,\n `File: ${importer ?? source}`,\n `Specifier: \"${source}\"`,\n `Resolves to: ${aliasPath}`,\n `Cause: ${aliasViolation.cause}`,\n `Fix: ${aliasViolation.fix}`,\n `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.`,\n ].join(\"\\n\"),\n id: importer,\n });\n }\n }\n }\n\n /*\n THE UNJUDGEABLE SPECIFIER — the shape that was actually measured on\n `v5/app`, and the one no amount of rule work inside this hook can judge.\n\n When nothing in the build resolves a bare specifier, Rollup does not\n fail: it warns `UNRESOLVED_IMPORT` and treats it as EXTERNAL, writing\n `import { x } from \"app/users/resources/user.resource\"` verbatim into\n the browser chunk. Measured on this gate's own fixture with an empty\n `resolve.alias`: build SUCCEEDED, emitted chunk carried the specifier\n untouched. That is the same failure mode `externalizedBuiltins` exists\n to close for rule 1, reached through a different door — an import that\n crosses into the client bundle without any rule ever having a file to\n judge.\n\n And it is not hypothetical: the v5 app declares `app/*` and `web/*` in\n its tsconfig `paths`, `web-connector.ts` installs the matching\n `resolve.alias` pair for the DEV server, and the production client build\n (`build-client.ts`, fed by `contribution.ts`'s `aliases ?? {}`) installs\n NOTHING — so in that build every alias-form import is unresolvable and\n externalized. The fix for that is a config wiring change in a file this\n gate does not own; what this gate can do is stop it being silent.\n\n Deliberately NOT a refusal and deliberately NOT a guess. Gate A will not\n invent `app/*` -> `<appRoot>/src/app/*` from the app convention: those\n prefixes are the application's to choose, and a fence that hard-codes\n them is wrong for the first app that names its trees differently.\n */\n if (isBareSpecifier && !resolved) {\n const aliasTable =\n declaredAliasCount === undefined\n ? `this build's resolve.alias was never reported to the gate`\n : `this build's resolve.alias declares ${declaredAliasCount} ${declaredAliasCount === 1 ? \"entry\" : \"entries\"}`;\n this.warn({\n message: [\n `Gate A could not judge this import — and it is going into the browser chunk unjudged.`,\n ``,\n `Import chain: ${[...buildChain(importer), source].join(\" → \")}`,\n `File: ${importer ?? source}`,\n `Specifier: \"${source}\"`,\n `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}).`,\n `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.`,\n ].join(\"\\n\"),\n id: importer,\n });\n }\n\n return resolved ?? null;\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
|
-
//#region web/src/vite/gate-b-secrets.d.ts
|
|
3
|
+
//#region ../web/src/vite/gate-b-secrets.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Tracks which declared `PUBLIC_*` keys were actually read anywhere in the
|
|
6
6
|
* client build, and the resolved env values Vite would inline for them.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate-b-secrets.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/vite/gate-b-secrets.ts"],"sourcesContent":["/**\r\n * Gate B — an AST transform gate for inline secrets.\r\n *\r\n * `resolveId` (Gate A) refuses import PATHS; it cannot see a bare property\r\n * read like `process.env.SECRET` or `import.meta.env.SOME_KEY` — there is no\r\n * import to refuse, just a member expression. Gate B is the orthogonal,\r\n * transform-time check that catches exactly that class. It is applied to\r\n * EVERY module Vite processes in the client build — NOT scoped to\r\n * `*.page.tsx` like projection, because a secret can leak from any helper,\r\n * component, or shared util, not only from a page file.\r\n *\r\n * The PUBLIC_ env-var convention this gate enforces is DEFINED here and\r\n * nowhere else: a client module may read\r\n * `import.meta.env.X` only when `X` starts with `PUBLIC_`. `process.env` is\r\n * never readable client-side at all — Node's `process` object does not exist\r\n * in the browser, so any static or computed read of `process.env.X`, or of\r\n * `process.env` itself as a bare whole-object value (destructured, spread,\r\n * assigned, or passed as an argument), is forbidden regardless of `X`.\r\n *\r\n * Gate A, Gate C and the SSR mirror rule are NOT this gate. Do not extend\r\n * this file to cover them; they are separate slices.\r\n *\r\n * A bare `import.meta.env` reference — used as a value directly (passed as\r\n * an argument, spread, destructured, or aliased to a variable) rather than\r\n * narrowed to a single static `.KEY` access — is caught here too: it leaks\r\n * the WHOLE env object, not one var, so it is the most\r\n * severe violation this gate judges, and is caught at the exact source line\r\n * like every other Gate B violation, not only as a whole-build\r\n * `generateBundle` failure. See `findViolation`'s `consumedEnvBases` note.\r\n *\r\n * A bare `process.env` reference is caught the same way, for the same\r\n * reason — `findViolation`'s `consumedEnvBases` check applies to both bases,\r\n * and unlike `import.meta.env` there is no allowed key at all for\r\n * `process.env`, narrowed or bare.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\n\r\nconst PUBLIC_ENV_PREFIX = \"PUBLIC_\";\r\n\r\n/**\r\n * Vite injects these five `import.meta.env` keys itself (vite's own\r\n * `resolveConfig`: `env: { ...userEnv, BASE_URL, MODE, DEV, PROD }`, plus\r\n * `SSR` set per-environment by vite's `vite:define` plugin) — they are\r\n * framework metadata, not app secrets, and are legitimately readable\r\n * client-side with no `PUBLIC_` prefix. Named explicitly here so a future\r\n * false positive against these doesn't get\r\n * \"fixed\" by widening the PUBLIC_ rule instead of consulting this allowlist.\r\n */\r\nconst VITE_BUILTIN_ENV_KEYS = new Set([\"MODE\", \"DEV\", \"PROD\", \"BASE_URL\", \"SSR\"]);\r\n\r\nconst JS_MODULE_EXTENSIONS = /\\.(tsx?|jsx?|mjs|cjs)$/;\r\n\r\nfunction isJsModule(id: string): boolean {\r\n return JS_MODULE_EXTENSIONS.test(id.split(\"?\")[0]);\r\n}\r\n\r\n/**\r\n * Third-party dependencies (React among them) commonly guard dev-only code\r\n * with `process.env.NODE_ENV` checks that Vite's own dependency\r\n * pre-bundling/define step is responsible for handling — Gate B judges this\r\n * app's own source, not a vendor package's internal layout, the same\r\n * \"dependency internals are out of scope\" call Gate A's rule 4 makes for\r\n * `$module/web/` classification.\r\n */\r\nfunction isNodeModulesFile(id: string): boolean {\r\n return /(^|[\\\\/])node_modules([\\\\/]|$)/.test(id);\r\n}\r\n\r\n/**\r\n * Generic duck-typed AST walk (mirrors `projection.ts`'s `collectIdentifierNames`\r\n * — no `@babel/traverse` dependency, this package only needs `@babel/parser`).\r\n * Visits every node in the tree, including nested function bodies and JSX\r\n * expression containers, so a secret read buried inside a callback or a\r\n * conditional is caught just as one at the top level is.\r\n */\r\nfunction walk(node: unknown, visit: (node: Record<string, any>) => void): void {\r\n if (!node || typeof node !== \"object\") return;\r\n if (Array.isArray(node)) {\r\n for (const item of node) walk(item, visit);\r\n return;\r\n }\r\n const record = node as Record<string, any>;\r\n if (typeof record.type !== \"string\") return;\r\n visit(record);\r\n for (const key of Object.keys(record)) {\r\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\r\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\r\n continue;\r\n }\r\n walk(record[key], visit);\r\n }\r\n}\r\n\r\nfunction isIdentifierNamed(node: any, name: string): boolean {\r\n return node?.type === \"Identifier\" && node.name === name;\r\n}\r\n\r\nfunction isMemberExpression(node: any): boolean {\r\n return node?.type === \"MemberExpression\" || node?.type === \"OptionalMemberExpression\";\r\n}\r\n\r\nfunction isProcessObject(node: any): boolean {\r\n if (isIdentifierNamed(node, \"process\")) return true;\r\n if (!isMemberExpression(node)) return false;\r\n\r\n const host = node.object;\r\n if (\r\n !isIdentifierNamed(host, \"globalThis\") &&\r\n !isIdentifierNamed(host, \"window\") &&\r\n !isIdentifierNamed(host, \"self\")\r\n ) {\r\n return false;\r\n }\r\n\r\n if (!node.computed) return isIdentifierNamed(node.property, \"process\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"process\";\r\n}\r\n\r\n/** Matches the `process.env` member expression itself (dot or static-bracket). */\r\nfunction isProcessEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n if (!isProcessObject(node.object)) return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\n/** Matches the `import.meta.env` member expression itself (dot or static-bracket). */\r\nfunction isImportMetaEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n const object = node.object;\r\n if (object?.type !== \"MetaProperty\") return false;\r\n if (object.meta?.name !== \"import\" || object.property?.name !== \"meta\") return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\ntype KeyResolution = { readonly static: true; readonly key: string } | { readonly static: false };\r\n\r\n/**\r\n * Resolves the key an outer `<base>.<key>` / `<base>[<key>]` member\r\n * expression reads off `process.env` or `import.meta.env`. Only a literal\r\n * dot-property or a static string-literal bracket key counts as static —\r\n * anything else (a variable, a template literal, a call expression, ...) is\r\n * a computed key and must fail closed (`c604f0bc` §5 / §3 \"never guess\").\r\n */\r\nfunction resolveKey(outer: any): KeyResolution {\r\n if (!outer.computed) {\r\n if (outer.property?.type === \"Identifier\") return { static: true, key: outer.property.name };\r\n return { static: false };\r\n }\r\n if (outer.property?.type === \"StringLiteral\") return { static: true, key: outer.property.value };\r\n return { static: false };\r\n}\r\n\r\nfunction expressionText(code: string, node: any): string {\r\n return code.slice(node.start as number, node.end as number);\r\n}\r\n\r\n/**\r\n * Tracks which declared `PUBLIC_*` keys were actually read anywhere in the\r\n * client build, and the resolved env values Vite would inline for them.\r\n * Shared, by construction, between `gateBSecrets` (which owns the writes —\r\n * `transform`'s `onPublicKeyRead` callback, `configResolved`'s declared-env\r\n * snapshot) and Gate C's inlined-value manifest (`gate-c-verify.ts`), so the\r\n * manifest's \"was this key actually inlined\" list and Gate B's own\r\n * unread-key exclusion (`generateBundle` below) can never drift apart into\r\n * two different answers — they read the same `Set`/object, not two\r\n * independently-recomputed ones.\r\n */\r\nexport interface PublicEnvTracker {\r\n readonly referencedKeys: Set<string>;\r\n declaredEnv: Record<string, unknown>;\r\n}\r\n\r\nexport function createPublicEnvTracker(): PublicEnvTracker {\r\n return { referencedKeys: new Set(), declaredEnv: {} };\r\n}\r\n\r\ninterface Violation {\r\n line: number;\r\n expression: string;\r\n cause: string;\r\n fix: string;\r\n}\r\n\r\n/**\r\n * Walks a module's AST looking for `process.env.*` and `import.meta.env.*`\r\n * reads — narrowed member accesses and bare whole-object references alike —\r\n * and returns the first violation found, or `undefined` if the module is\r\n * clean. Only the first violation is reported per module, fail-fast, so a\r\n * build failure always points at one concrete fix.\r\n *\r\n * `onPublicKeyRead` fires for every statically-resolved `PUBLIC_*` key\r\n * allowed through, so the caller can track which declared `PUBLIC_*` env\r\n * vars are actually referenced anywhere in the client build (see\r\n * `gateBSecrets`'s `generateBundle` check below).\r\n *\r\n * `consumedEnvBases` tracks every `process.env` and `import.meta.env`\r\n * MemberExpression node that was already judged as the `.object` of a\r\n * further static/computed `.KEY` access (the narrowed-read checks above) —\r\n * i.e. a NARROWED read, whether allowed or refused. Because `walk` visits a\r\n * parent node before its children, an outer `<base>.KEY` access always marks\r\n * its `base` sub-node as consumed BEFORE that same sub-node is visited on\r\n * its own. Any `process.env` or `import.meta.env` MemberExpression NOT found\r\n * in this set when visited on its own is therefore a bare reference used as\r\n * a value — `const env = process.env`, `fn(process.env)`,\r\n * `fn({...process.env})`, `const { X } = process.env` (and the same four\r\n * shapes for `import.meta.env`) all take this exact shape (the base is a\r\n * direct child of a declarator/argument/spread, never wrapped in one more\r\n * `.KEY` MemberExpression) — and leaks the WHOLE env object, not one key, so\r\n * it fails regardless of what it's assigned to.\r\n */\r\nfunction findViolation(\r\n code: string,\r\n ast: any,\r\n onPublicKeyRead: (key: string) => void,\r\n): Violation | undefined {\r\n let found: Violation | undefined;\r\n const consumedEnvBases = new WeakSet<object>();\r\n\r\n walk(ast.program, (node) => {\r\n if (found || !isMemberExpression(node)) return;\r\n const outer = node;\r\n\r\n if (isProcessEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"process.env.${key.key}\" is read in client-bound code. process.env does not exist in the browser — there is no \"public\" process.env key, static or computed.`\r\n : `a computed key is read off process.env in client-bound code. process.env does not exist in the browser, and the compiler cannot guess whether a computed key is safe.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), or — if the client genuinely needs this value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isProcessEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"process.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"process.env.<KEY>\" access) — process.env does not exist in the browser, so reading it as a value like this (assigned, destructured, spread, or passed as an argument) is never safe: there is no \"public\" process.env key, static or computed.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), reading only the specific \"process.env.<KEY>\" value you actually need, or — if the client genuinely needs a value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n if (key.static && VITE_BUILTIN_ENV_KEYS.has(key.key)) return; // Vite built-in, not a secret\r\n if (key.static && key.key.startsWith(PUBLIC_ENV_PREFIX)) {\r\n onPublicKeyRead(key.key);\r\n return; // allowed\r\n }\r\n\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"import.meta.env.${key.key}\" is read in client-bound code, but its name does not start with \"${PUBLIC_ENV_PREFIX}\". Only import.meta.env keys prefixed \"${PUBLIC_ENV_PREFIX}\" are allowed in the client build — everything else is assumed to be a secret.`\r\n : `a computed key is read off import.meta.env in client-bound code. The compiler cannot guess whether a computed key resolves to a \"${PUBLIC_ENV_PREFIX}\"-prefixed name, so it fails closed rather than assume the key is public.`,\r\n fix: key.static\r\n ? `Rename the env var to start with \"${PUBLIC_ENV_PREFIX}\" (e.g. \"${PUBLIC_ENV_PREFIX}${key.key}\") if it is genuinely safe to ship to the browser, or move the code that reads it into a *.server.ts file / server export otherwise.`\r\n : `Use a static \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" literal key instead of a computed one, or move the code that reads it into a *.server.ts file / server export if the key resolves to a secret.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"import.meta.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"${PUBLIC_ENV_PREFIX}*\" key access) — used as a value like this (assigned, destructured, spread, or passed as an argument), it leaks every declared env var, public or not, to the client.`,\r\n fix: `Read only the specific \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" key(s) you actually need, one at a time, instead of referencing the whole \"import.meta.env\" object.`,\r\n };\r\n }\r\n });\r\n\r\n return found;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. `vite` is only imported for its types\r\n * (`import type`), matching Gate A / projection's peer-dependency shape.\r\n *\r\n * Beyond the `transform` gate itself, this plugin also owns two pieces of\r\n * Vite config surface, both load-bearing for\r\n * the `PUBLIC_` convention this file defines:\r\n *\r\n * 1. `config()` sets `envPrefix: \"PUBLIC_\"`. Vite's own `import.meta.env.*`\r\n * inlining (its `vite:define` plugin) only loads env vars from\r\n * `process.env`/`.env` files that match `envPrefix`, which defaults to\r\n * `\"VITE_\"` — NOT `\"PUBLIC_\"`. Verified against a real build: without this\r\n * line, `import.meta.env.PUBLIC_API_URL` silently compiles to `void 0` in\r\n * the emitted bundle (Vite's own definePlugin doesn't know the var\r\n * exists, so it replaces the read with its \"undefined\" fallback), even\r\n * though Gate B's `transform` gate allows the read through. This is not\r\n * just an optimization — without it, every `PUBLIC_*` read Gate B\r\n * approves is a silent runtime bug.\r\n * 2. `configResolved`/`generateBundle` track which declared `PUBLIC_*` keys\r\n * were actually read (via `findViolation`'s `onPublicKeyRead` callback)\r\n * and, once the bundle is emitted, scan the real output for any\r\n * unread key's value leaking in anyway (e.g. via `{...import.meta.env}`\r\n * or another whole-object read, which Vite serializes in full,\r\n * independent of Gate B's own per-key checks) — verified on the actual\r\n * emitted chunk code, not assumed from the transform/define config alone.\r\n *\r\n * `options.tracker` (default: a private, unshared one) is where that\r\n * read/declared-env state lives — pass the SAME `PublicEnvTracker` instance\r\n * used to build Gate C's inlined-value manifest (`warlockClientBoundary`\r\n * does this) so the manifest and this plugin's unread-key exclusion read off\r\n * one shared Set, not two.\r\n */\r\nexport function gateBSecrets(gateBOptions: { tracker?: PublicEnvTracker } = {}): Plugin {\r\n const tracker = gateBOptions.tracker ?? createPublicEnvTracker();\r\n\r\n return {\r\n name: \"warlock:gate-b-secrets\",\r\n enforce: \"pre\",\r\n config() {\r\n return { envPrefix: PUBLIC_ENV_PREFIX };\r\n },\r\n configResolved(config) {\r\n tracker.declaredEnv = config.env;\r\n },\r\n transform(code, id, options) {\r\n if (options?.ssr) return null;\r\n if (!isJsModule(id)) return null;\r\n if (isNodeModulesFile(id)) return null;\r\n\r\n const ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n\r\n const violation = findViolation(code, ast, (key) => tracker.referencedKeys.add(key));\r\n if (violation) {\r\n this.error(\r\n [\r\n `Gate B refused a module: forbidden inline env read in the client build.`,\r\n ``,\r\n `File: ${id}:${violation.line}`,\r\n `Expression: ${violation.expression}`,\r\n `Cause: ${violation.cause}`,\r\n `Fix: ${violation.fix}`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n return null;\r\n },\r\n generateBundle(_options, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const unreadKeys = Object.keys(tracker.declaredEnv).filter(\r\n (key) => key.startsWith(PUBLIC_ENV_PREFIX) && !tracker.referencedKeys.has(key),\r\n );\r\n if (unreadKeys.length === 0) return;\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (file.type !== \"chunk\") continue;\r\n\r\n for (const key of unreadKeys) {\r\n const inlinedValue = JSON.stringify(tracker.declaredEnv[key]);\r\n if (!file.code.includes(inlinedValue)) continue;\r\n\r\n this.error(\r\n [\r\n `Gate B refused a build: an unread PUBLIC_ env var was inlined into the emitted bundle.`,\r\n ``,\r\n `File: ${file.fileName}`,\r\n `Key: import.meta.env.${key}`,\r\n `Cause: \"${key}\" is never read as \"import.meta.env.${key}\" anywhere in the client build, but its value was still inlined into the emitted output — most likely a whole-object \"import.meta.env\" read (e.g. a spread or destructure), which Vite serializes in full regardless of which individual keys are actually used.`,\r\n `Fix: read \"import.meta.env.${key}\" directly wherever its value is needed instead of spreading/destructuring the whole \"import.meta.env\" object, or remove the unused \"${key}\" declaration if the client build doesn't need it.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n }\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gate-b-secrets.mjs","names":[],"sources":["../../../../../../../web/src/vite/gate-b-secrets.ts"],"sourcesContent":["/**\r\n * Gate B — an AST transform gate for inline secrets.\r\n *\r\n * `resolveId` (Gate A) refuses import PATHS; it cannot see a bare property\r\n * read like `process.env.SECRET` or `import.meta.env.SOME_KEY` — there is no\r\n * import to refuse, just a member expression. Gate B is the orthogonal,\r\n * transform-time check that catches exactly that class. It is applied to\r\n * EVERY module Vite processes in the client build — NOT scoped to\r\n * `*.page.tsx` like projection, because a secret can leak from any helper,\r\n * component, or shared util, not only from a page file.\r\n *\r\n * The PUBLIC_ env-var convention this gate enforces is DEFINED here and\r\n * nowhere else: a client module may read\r\n * `import.meta.env.X` only when `X` starts with `PUBLIC_`. `process.env` is\r\n * never readable client-side at all — Node's `process` object does not exist\r\n * in the browser, so any static or computed read of `process.env.X`, or of\r\n * `process.env` itself as a bare whole-object value (destructured, spread,\r\n * assigned, or passed as an argument), is forbidden regardless of `X`.\r\n *\r\n * Gate A, Gate C and the SSR mirror rule are NOT this gate. Do not extend\r\n * this file to cover them; they are separate slices.\r\n *\r\n * A bare `import.meta.env` reference — used as a value directly (passed as\r\n * an argument, spread, destructured, or aliased to a variable) rather than\r\n * narrowed to a single static `.KEY` access — is caught here too: it leaks\r\n * the WHOLE env object, not one var, so it is the most\r\n * severe violation this gate judges, and is caught at the exact source line\r\n * like every other Gate B violation, not only as a whole-build\r\n * `generateBundle` failure. See `findViolation`'s `consumedEnvBases` note.\r\n *\r\n * A bare `process.env` reference is caught the same way, for the same\r\n * reason — `findViolation`'s `consumedEnvBases` check applies to both bases,\r\n * and unlike `import.meta.env` there is no allowed key at all for\r\n * `process.env`, narrowed or bare.\r\n */\r\nimport { parse } from \"@babel/parser\";\r\nimport type { Plugin } from \"vite\";\r\n\r\nconst PUBLIC_ENV_PREFIX = \"PUBLIC_\";\r\n\r\n/**\r\n * Vite injects these five `import.meta.env` keys itself (vite's own\r\n * `resolveConfig`: `env: { ...userEnv, BASE_URL, MODE, DEV, PROD }`, plus\r\n * `SSR` set per-environment by vite's `vite:define` plugin) — they are\r\n * framework metadata, not app secrets, and are legitimately readable\r\n * client-side with no `PUBLIC_` prefix. Named explicitly here so a future\r\n * false positive against these doesn't get\r\n * \"fixed\" by widening the PUBLIC_ rule instead of consulting this allowlist.\r\n */\r\nconst VITE_BUILTIN_ENV_KEYS = new Set([\"MODE\", \"DEV\", \"PROD\", \"BASE_URL\", \"SSR\"]);\r\n\r\nconst JS_MODULE_EXTENSIONS = /\\.(tsx?|jsx?|mjs|cjs)$/;\r\n\r\nfunction isJsModule(id: string): boolean {\r\n return JS_MODULE_EXTENSIONS.test(id.split(\"?\")[0]);\r\n}\r\n\r\n/**\r\n * Third-party dependencies (React among them) commonly guard dev-only code\r\n * with `process.env.NODE_ENV` checks that Vite's own dependency\r\n * pre-bundling/define step is responsible for handling — Gate B judges this\r\n * app's own source, not a vendor package's internal layout, the same\r\n * \"dependency internals are out of scope\" call Gate A's rule 4 makes for\r\n * `$module/web/` classification.\r\n */\r\nfunction isNodeModulesFile(id: string): boolean {\r\n return /(^|[\\\\/])node_modules([\\\\/]|$)/.test(id);\r\n}\r\n\r\n/**\r\n * Generic duck-typed AST walk (mirrors `projection.ts`'s `collectIdentifierNames`\r\n * — no `@babel/traverse` dependency, this package only needs `@babel/parser`).\r\n * Visits every node in the tree, including nested function bodies and JSX\r\n * expression containers, so a secret read buried inside a callback or a\r\n * conditional is caught just as one at the top level is.\r\n */\r\nfunction walk(node: unknown, visit: (node: Record<string, any>) => void): void {\r\n if (!node || typeof node !== \"object\") return;\r\n if (Array.isArray(node)) {\r\n for (const item of node) walk(item, visit);\r\n return;\r\n }\r\n const record = node as Record<string, any>;\r\n if (typeof record.type !== \"string\") return;\r\n visit(record);\r\n for (const key of Object.keys(record)) {\r\n if (key === \"type\" || key === \"start\" || key === \"end\" || key === \"loc\" || key === \"range\") continue;\r\n if (key === \"leadingComments\" || key === \"trailingComments\" || key === \"innerComments\" || key === \"extra\") {\r\n continue;\r\n }\r\n walk(record[key], visit);\r\n }\r\n}\r\n\r\nfunction isIdentifierNamed(node: any, name: string): boolean {\r\n return node?.type === \"Identifier\" && node.name === name;\r\n}\r\n\r\nfunction isMemberExpression(node: any): boolean {\r\n return node?.type === \"MemberExpression\" || node?.type === \"OptionalMemberExpression\";\r\n}\r\n\r\nfunction isProcessObject(node: any): boolean {\r\n if (isIdentifierNamed(node, \"process\")) return true;\r\n if (!isMemberExpression(node)) return false;\r\n\r\n const host = node.object;\r\n if (\r\n !isIdentifierNamed(host, \"globalThis\") &&\r\n !isIdentifierNamed(host, \"window\") &&\r\n !isIdentifierNamed(host, \"self\")\r\n ) {\r\n return false;\r\n }\r\n\r\n if (!node.computed) return isIdentifierNamed(node.property, \"process\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"process\";\r\n}\r\n\r\n/** Matches the `process.env` member expression itself (dot or static-bracket). */\r\nfunction isProcessEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n if (!isProcessObject(node.object)) return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\n/** Matches the `import.meta.env` member expression itself (dot or static-bracket). */\r\nfunction isImportMetaEnvBase(node: any): boolean {\r\n if (!isMemberExpression(node)) return false;\r\n const object = node.object;\r\n if (object?.type !== \"MetaProperty\") return false;\r\n if (object.meta?.name !== \"import\" || object.property?.name !== \"meta\") return false;\r\n if (!node.computed) return isIdentifierNamed(node.property, \"env\");\r\n return node.property?.type === \"StringLiteral\" && node.property.value === \"env\";\r\n}\r\n\r\ntype KeyResolution = { readonly static: true; readonly key: string } | { readonly static: false };\r\n\r\n/**\r\n * Resolves the key an outer `<base>.<key>` / `<base>[<key>]` member\r\n * expression reads off `process.env` or `import.meta.env`. Only a literal\r\n * dot-property or a static string-literal bracket key counts as static —\r\n * anything else (a variable, a template literal, a call expression, ...) is\r\n * a computed key and must fail closed (`c604f0bc` §5 / §3 \"never guess\").\r\n */\r\nfunction resolveKey(outer: any): KeyResolution {\r\n if (!outer.computed) {\r\n if (outer.property?.type === \"Identifier\") return { static: true, key: outer.property.name };\r\n return { static: false };\r\n }\r\n if (outer.property?.type === \"StringLiteral\") return { static: true, key: outer.property.value };\r\n return { static: false };\r\n}\r\n\r\nfunction expressionText(code: string, node: any): string {\r\n return code.slice(node.start as number, node.end as number);\r\n}\r\n\r\n/**\r\n * Tracks which declared `PUBLIC_*` keys were actually read anywhere in the\r\n * client build, and the resolved env values Vite would inline for them.\r\n * Shared, by construction, between `gateBSecrets` (which owns the writes —\r\n * `transform`'s `onPublicKeyRead` callback, `configResolved`'s declared-env\r\n * snapshot) and Gate C's inlined-value manifest (`gate-c-verify.ts`), so the\r\n * manifest's \"was this key actually inlined\" list and Gate B's own\r\n * unread-key exclusion (`generateBundle` below) can never drift apart into\r\n * two different answers — they read the same `Set`/object, not two\r\n * independently-recomputed ones.\r\n */\r\nexport interface PublicEnvTracker {\r\n readonly referencedKeys: Set<string>;\r\n declaredEnv: Record<string, unknown>;\r\n}\r\n\r\nexport function createPublicEnvTracker(): PublicEnvTracker {\r\n return { referencedKeys: new Set(), declaredEnv: {} };\r\n}\r\n\r\ninterface Violation {\r\n line: number;\r\n expression: string;\r\n cause: string;\r\n fix: string;\r\n}\r\n\r\n/**\r\n * Walks a module's AST looking for `process.env.*` and `import.meta.env.*`\r\n * reads — narrowed member accesses and bare whole-object references alike —\r\n * and returns the first violation found, or `undefined` if the module is\r\n * clean. Only the first violation is reported per module, fail-fast, so a\r\n * build failure always points at one concrete fix.\r\n *\r\n * `onPublicKeyRead` fires for every statically-resolved `PUBLIC_*` key\r\n * allowed through, so the caller can track which declared `PUBLIC_*` env\r\n * vars are actually referenced anywhere in the client build (see\r\n * `gateBSecrets`'s `generateBundle` check below).\r\n *\r\n * `consumedEnvBases` tracks every `process.env` and `import.meta.env`\r\n * MemberExpression node that was already judged as the `.object` of a\r\n * further static/computed `.KEY` access (the narrowed-read checks above) —\r\n * i.e. a NARROWED read, whether allowed or refused. Because `walk` visits a\r\n * parent node before its children, an outer `<base>.KEY` access always marks\r\n * its `base` sub-node as consumed BEFORE that same sub-node is visited on\r\n * its own. Any `process.env` or `import.meta.env` MemberExpression NOT found\r\n * in this set when visited on its own is therefore a bare reference used as\r\n * a value — `const env = process.env`, `fn(process.env)`,\r\n * `fn({...process.env})`, `const { X } = process.env` (and the same four\r\n * shapes for `import.meta.env`) all take this exact shape (the base is a\r\n * direct child of a declarator/argument/spread, never wrapped in one more\r\n * `.KEY` MemberExpression) — and leaks the WHOLE env object, not one key, so\r\n * it fails regardless of what it's assigned to.\r\n */\r\nfunction findViolation(\r\n code: string,\r\n ast: any,\r\n onPublicKeyRead: (key: string) => void,\r\n): Violation | undefined {\r\n let found: Violation | undefined;\r\n const consumedEnvBases = new WeakSet<object>();\r\n\r\n walk(ast.program, (node) => {\r\n if (found || !isMemberExpression(node)) return;\r\n const outer = node;\r\n\r\n if (isProcessEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"process.env.${key.key}\" is read in client-bound code. process.env does not exist in the browser — there is no \"public\" process.env key, static or computed.`\r\n : `a computed key is read off process.env in client-bound code. process.env does not exist in the browser, and the compiler cannot guess whether a computed key is safe.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), or — if the client genuinely needs this value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isProcessEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"process.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"process.env.<KEY>\" access) — process.env does not exist in the browser, so reading it as a value like this (assigned, destructured, spread, or passed as an argument) is never safe: there is no \"public\" process.env key, static or computed.`,\r\n fix: `Move the code that needs this value into a *.server.ts file or a server export (loader/route/middleware/validation/metadata), reading only the specific \"process.env.<KEY>\" value you actually need, or — if the client genuinely needs a value — expose it via import.meta.env.${PUBLIC_ENV_PREFIX}* instead.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer.object)) {\r\n consumedEnvBases.add(outer.object);\r\n const key = resolveKey(outer);\r\n if (key.static && VITE_BUILTIN_ENV_KEYS.has(key.key)) return; // Vite built-in, not a secret\r\n if (key.static && key.key.startsWith(PUBLIC_ENV_PREFIX)) {\r\n onPublicKeyRead(key.key);\r\n return; // allowed\r\n }\r\n\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: key.static\r\n ? `\"import.meta.env.${key.key}\" is read in client-bound code, but its name does not start with \"${PUBLIC_ENV_PREFIX}\". Only import.meta.env keys prefixed \"${PUBLIC_ENV_PREFIX}\" are allowed in the client build — everything else is assumed to be a secret.`\r\n : `a computed key is read off import.meta.env in client-bound code. The compiler cannot guess whether a computed key resolves to a \"${PUBLIC_ENV_PREFIX}\"-prefixed name, so it fails closed rather than assume the key is public.`,\r\n fix: key.static\r\n ? `Rename the env var to start with \"${PUBLIC_ENV_PREFIX}\" (e.g. \"${PUBLIC_ENV_PREFIX}${key.key}\") if it is genuinely safe to ship to the browser, or move the code that reads it into a *.server.ts file / server export otherwise.`\r\n : `Use a static \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" literal key instead of a computed one, or move the code that reads it into a *.server.ts file / server export if the key resolves to a secret.`,\r\n };\r\n return;\r\n }\r\n\r\n if (isImportMetaEnvBase(outer) && !consumedEnvBases.has(outer)) {\r\n found = {\r\n line: outer.loc.start.line,\r\n expression: expressionText(code, outer),\r\n cause: `\"import.meta.env\" is referenced as a whole object in client-bound code (not narrowed to one static \"${PUBLIC_ENV_PREFIX}*\" key access) — used as a value like this (assigned, destructured, spread, or passed as an argument), it leaks every declared env var, public or not, to the client.`,\r\n fix: `Read only the specific \"import.meta.env.${PUBLIC_ENV_PREFIX}*\" key(s) you actually need, one at a time, instead of referencing the whole \"import.meta.env\" object.`,\r\n };\r\n }\r\n });\r\n\r\n return found;\r\n}\r\n\r\n/**\r\n * The client-build Vite plugin. `vite` is only imported for its types\r\n * (`import type`), matching Gate A / projection's peer-dependency shape.\r\n *\r\n * Beyond the `transform` gate itself, this plugin also owns two pieces of\r\n * Vite config surface, both load-bearing for\r\n * the `PUBLIC_` convention this file defines:\r\n *\r\n * 1. `config()` sets `envPrefix: \"PUBLIC_\"`. Vite's own `import.meta.env.*`\r\n * inlining (its `vite:define` plugin) only loads env vars from\r\n * `process.env`/`.env` files that match `envPrefix`, which defaults to\r\n * `\"VITE_\"` — NOT `\"PUBLIC_\"`. Verified against a real build: without this\r\n * line, `import.meta.env.PUBLIC_API_URL` silently compiles to `void 0` in\r\n * the emitted bundle (Vite's own definePlugin doesn't know the var\r\n * exists, so it replaces the read with its \"undefined\" fallback), even\r\n * though Gate B's `transform` gate allows the read through. This is not\r\n * just an optimization — without it, every `PUBLIC_*` read Gate B\r\n * approves is a silent runtime bug.\r\n * 2. `configResolved`/`generateBundle` track which declared `PUBLIC_*` keys\r\n * were actually read (via `findViolation`'s `onPublicKeyRead` callback)\r\n * and, once the bundle is emitted, scan the real output for any\r\n * unread key's value leaking in anyway (e.g. via `{...import.meta.env}`\r\n * or another whole-object read, which Vite serializes in full,\r\n * independent of Gate B's own per-key checks) — verified on the actual\r\n * emitted chunk code, not assumed from the transform/define config alone.\r\n *\r\n * `options.tracker` (default: a private, unshared one) is where that\r\n * read/declared-env state lives — pass the SAME `PublicEnvTracker` instance\r\n * used to build Gate C's inlined-value manifest (`warlockClientBoundary`\r\n * does this) so the manifest and this plugin's unread-key exclusion read off\r\n * one shared Set, not two.\r\n */\r\nexport function gateBSecrets(gateBOptions: { tracker?: PublicEnvTracker } = {}): Plugin {\r\n const tracker = gateBOptions.tracker ?? createPublicEnvTracker();\r\n\r\n return {\r\n name: \"warlock:gate-b-secrets\",\r\n enforce: \"pre\",\r\n config() {\r\n return { envPrefix: PUBLIC_ENV_PREFIX };\r\n },\r\n configResolved(config) {\r\n tracker.declaredEnv = config.env;\r\n },\r\n transform(code, id, options) {\r\n if (options?.ssr) return null;\r\n if (!isJsModule(id)) return null;\r\n if (isNodeModulesFile(id)) return null;\r\n\r\n const ast = parse(code, { sourceType: \"module\", plugins: [\"typescript\", \"jsx\"] });\r\n\r\n const violation = findViolation(code, ast, (key) => tracker.referencedKeys.add(key));\r\n if (violation) {\r\n this.error(\r\n [\r\n `Gate B refused a module: forbidden inline env read in the client build.`,\r\n ``,\r\n `File: ${id}:${violation.line}`,\r\n `Expression: ${violation.expression}`,\r\n `Cause: ${violation.cause}`,\r\n `Fix: ${violation.fix}`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n\r\n return null;\r\n },\r\n generateBundle(_options, bundle) {\r\n if (this.environment?.config?.consumer === \"server\") return;\r\n\r\n const unreadKeys = Object.keys(tracker.declaredEnv).filter(\r\n (key) => key.startsWith(PUBLIC_ENV_PREFIX) && !tracker.referencedKeys.has(key),\r\n );\r\n if (unreadKeys.length === 0) return;\r\n\r\n for (const file of Object.values(bundle)) {\r\n if (file.type !== \"chunk\") continue;\r\n\r\n for (const key of unreadKeys) {\r\n const inlinedValue = JSON.stringify(tracker.declaredEnv[key]);\r\n if (!file.code.includes(inlinedValue)) continue;\r\n\r\n this.error(\r\n [\r\n `Gate B refused a build: an unread PUBLIC_ env var was inlined into the emitted bundle.`,\r\n ``,\r\n `File: ${file.fileName}`,\r\n `Key: import.meta.env.${key}`,\r\n `Cause: \"${key}\" is never read as \"import.meta.env.${key}\" anywhere in the client build, but its value was still inlined into the emitted output — most likely a whole-object \"import.meta.env\" read (e.g. a spread or destructure), which Vite serializes in full regardless of which individual keys are actually used.`,\r\n `Fix: read \"import.meta.env.${key}\" directly wherever its value is needed instead of spreading/destructuring the whole \"import.meta.env\" object, or remove the unused \"${key}\" declaration if the client build doesn't need it.`,\r\n ].join(\"\\n\"),\r\n );\r\n }\r\n }\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { EnvironmentClassifier, EnvironmentClassifierOptions } from "./gate-a-re
|
|
|
2
2
|
import { PublicEnvTracker } from "./gate-b-secrets.mjs";
|
|
3
3
|
import { Plugin } from "vite";
|
|
4
4
|
|
|
5
|
-
//#region web/src/vite/gate-c-verify.d.ts
|
|
5
|
+
//#region ../web/src/vite/gate-c-verify.d.ts
|
|
6
6
|
interface ServerExportLeak {
|
|
7
7
|
fileName: string;
|
|
8
8
|
exportName: string;
|
|
@@ -3,7 +3,7 @@ import { createPublicEnvTracker } from "./gate-b-secrets.mjs";
|
|
|
3
3
|
import { SERVER_EXPORT_NAMES } from "./projection.mjs";
|
|
4
4
|
import { parse } from "@babel/parser";
|
|
5
5
|
|
|
6
|
-
//#region web/src/vite/gate-c-verify.ts
|
|
6
|
+
//#region ../web/src/vite/gate-c-verify.ts
|
|
7
7
|
/**
|
|
8
8
|
* Gate C — emitted-output verification.
|
|
9
9
|
*
|