@warlock.js/web 5.2.3 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +5 -7
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs.map +1 -1
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +4 -3
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +2 -2
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +5 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/hydration-payload.mjs +6 -2
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +2 -1
- package/esm/localization.d.mts +21 -0
- package/esm/localization.mjs +28 -0
- package/esm/localization.mjs.map +1 -0
- package/esm/routing/data-request.mjs +5 -3
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +36 -7
- package/esm/routing/filesystem-route.mjs.map +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/page-file-segment.mjs +66 -0
- package/esm/routing/page-file-segment.mjs.map +1 -0
- package/esm/routing/page-route-grammar.mjs +79 -0
- package/esm/routing/page-route-grammar.mjs.map +1 -0
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.d.mts +69 -0
- package/esm/routing/route-identity.mjs +100 -44
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/server/build-hydration-payload.mjs +3 -2
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +34 -1
- package/esm/server/create-page-route-handler.mjs +35 -4
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/framework-default-not-found-stylesheet.mjs +102 -0
- package/esm/server/framework-default-not-found-stylesheet.mjs.map +1 -0
- package/esm/server/install-page-routes-from-manifest.mjs +13 -16
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +3 -1
- package/esm/server/install-page-routes.mjs +11 -11
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -13
- package/esm/server/not-found-page.mjs +50 -5
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +16 -1
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +1 -8
- package/esm/server/render-page.mjs +24 -21
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/response-cache-floor.mjs +79 -0
- package/esm/server/response-cache-floor.mjs.map +1 -0
- package/esm/server/set-cookie-cache-floor-hook.mjs +41 -0
- package/esm/server/set-cookie-cache-floor-hook.mjs.map +1 -0
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs.map +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 +23 -4
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.mjs.map +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +62 -33
- package/llms.txt +1 -1
- package/package.json +4 -3
- package/skills/create-a-page/SKILL.md +59 -28
- package/skills/navigate-on-the-client/SKILL.md +16 -11
- package/skills/serve-styles/SKILL.md +2 -1
- package/skills/use-layouts/SKILL.md +3 -6
- package/skills/write-the-root/SKILL.md +0 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.mjs","names":[],"sources":["../../../../../../../web/src/vite/projection.ts"],"sourcesContent":["/**\n * Projection — the compile-time AST transform that strips a page module's\n * six server exports before the CLIENT graph forms.\n *\n * Removes `export const route/middleware/validation/loader/metadata/prefix = ...`\n * (const-arrow form) and `export async function loader(...) {...}`\n * (function-declaration form — a page declares these as separate named\n * exports, not one fused object, so both forms are real), plus any import\n * OR top-level declaration that becomes\n * unreferenced ONLY as a result of that removal. The default export (the\n * page component) and every other non-server-named export — including the\n * synchronous, no-argument `register()` lifecycle hook — survive\n * unconditionally, regardless of what they reference — classification is by\n * FILE, not by what an export does with data (`c604f0bc` §9).\n *\n * ATTRIBUTION APPLIES TO LOCAL DECLARATIONS, NOT JUST IMPORTS. A page or\n * layout hoists things to module scope for two ordinary reasons, and the\n * reference graph tells them apart without guessing:\n *\n * `const publishCart: Middleware = ...` + `export const middleware =\n * [publishCart]` (`v5/app/.../products/web/layout.tsx:30,43`) — the only\n * reader is a server export being removed, so the binding goes with it.\n *\n * `const COMMON_TIMEZONES = [...]` read by the default export\n * (`v5/app/.../account/settings.page.tsx:118,129`) — a surviving reader, so\n * it survives.\n *\n * That is the SAME rule already applied to import bindings, extended to the\n * other binding kind. It is deliberately not a widening of\n * `SERVER_EXPORT_NAMES`, and not \"accept what I don't recognise\": a\n * declaration is kept when something the client keeps reads it, dropped when\n * nothing does AND dropping it cannot delete a side effect, and REFUSED\n * otherwise — see `isDefinitionShapedInit`.\n *\n * This is NOT Gate A (`resolveId` path refusal), Gate B (inline secret\n * reads) or Gate C (emitted-output verification) — those are separate,\n * later slices. Projection runs first; the gates enforce after.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\n\n/**\n * Exported so Gate C (`gate-c-verify.ts`) can re-derive \"does the emitted\n * bundle contain a server export as a top-level binding\" from this exact set\n * rather than hand-typing a second copy that could drift from projection's\n * own list.\n */\nexport const SERVER_EXPORT_NAMES = new Set([\n \"route\",\n \"middleware\",\n \"validation\",\n \"loader\",\n \"metadata\",\n \"prefix\",\n]);\n\n/**\n * Recognized client-safe assets that always survive projection untouched,\n * whether imported bare (`import \"./x.css\"`) or with specifiers\n * (`import styles from \"./x.module.css\"`) — `c604f0bc` §3 names CSS\n * explicitly; the rest of this list is the same \"never guess, but a known\n * asset extension is not ambiguous\" reasoning extended to the other static\n * asset kinds Vite treats as URL/asset imports, not executable code.\n */\nconst ASSET_EXTENSION_RE =\n /\\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|otf)(\\?.*)?$/i;\n\n/**\n * Top-level statement types that need no ambiguity check and are never\n * touched by removal: import declarations are handled by their own\n * survives/orphaned logic below, and every export (other than the 6 server\n * names) plus type-only declarations survive unconditionally per\n * `c604f0bc` §9 (\"classify FILES, not the data they touch\").\n *\n * `ExportAllDeclaration` (`export * from \"./x\"` / `export * as ns from\n * \"./x\"`) is deliberately NOT in this set — it can forward ANY name from its\n * source module, including a server export, and is refused explicitly below\n * rather than assumed safe.\n */\nconst ALWAYS_SAFE_STATEMENT_TYPES = new Set([\n \"ExportNamedDeclaration\",\n \"ExportDefaultDeclaration\",\n \"TSInterfaceDeclaration\",\n \"TSTypeAliasDeclaration\",\n \"EmptyStatement\",\n]);\n\n/**\n * Thrown when projection encounters an attribution-ambiguous top-level\n * statement (`c604f0bc` §3: \"the compiler must not guess\"). Carries the\n * file/statement/fix fields the plugin's `transform` hook formats into the\n * build-failure message — never silently kept or silently dropped.\n */\nexport class ProjectionAmbiguityError extends Error {\n constructor(\n public readonly file: string,\n public readonly statement: string,\n public readonly line: number,\n public readonly explanation: string,\n public readonly fix: string,\n ) {\n super(\n [\n `Projection refused to guess: an ambiguous top-level statement in the client build.`,\n ``,\n `File: ${file}:${line}`,\n `Statement: ${statement}`,\n `Cause: ${explanation}`,\n `Fix: ${fix}`,\n ].join(\"\\n\"),\n );\n this.name = \"ProjectionAmbiguityError\";\n }\n}\n\nexport interface ProjectionResult {\n code: string;\n map: ReturnType<MagicString[\"generateMap\"]>;\n}\n\n/** A top-level `const`/`function`/`class` awaiting attribution by reference. */\ninterface LocalDeclaration {\n stmt: any;\n /** The module-scope names it binds. */\n names: Set<string>;\n /** Whether removing it could delete a side effect — see `isDefinitionShapedInit`. */\n definitionShaped: boolean;\n removed: boolean;\n}\n\nfunction hasSurvivingReader(\n local: LocalDeclaration,\n survivingNames: Set<string>,\n): boolean {\n for (const name of local.names) {\n if (survivingNames.has(name)) return true;\n }\n return false;\n}\n\nfunction isKnownSafeAsset(source: string): boolean {\n return ASSET_EXTENSION_RE.test(source);\n}\n\n/**\n * Matches `export const <name> = ...` only when the declaration has exactly\n * one declarator — every server export in every fixture and v5/app page is\n * written one-const-per-export (`product-details.page.tsx:15-18,42-69,71-74`);\n * a multi-declarator `export const a = 1, b = 2` is left to the ambiguity\n * path below rather than guessing which half is server-only.\n */\nfunction isServerExportDeclaration(stmt: any): boolean {\n if (stmt.type !== \"ExportNamedDeclaration\" || !stmt.declaration) return false;\n const decl = stmt.declaration;\n if (decl.type === \"VariableDeclaration\" && decl.declarations.length === 1) {\n const id = decl.declarations[0].id;\n return id?.type === \"Identifier\" && SERVER_EXPORT_NAMES.has(id.name);\n }\n if (decl.type === \"FunctionDeclaration\") {\n return !!decl.id && SERVER_EXPORT_NAMES.has(decl.id.name);\n }\n return false;\n}\n\n/**\n * Generic duck-typed AST walk (no `@babel/traverse` dependency — this\n * package only needs `@babel/parser` + `@babel/types`-shaped nodes).\n * Collects every `Identifier`/`JSXIdentifier` name reachable from `node`,\n * used to decide whether an import binding still has a reader once the 6\n * server exports are gone. Over-collecting (e.g. counting an object\n * property key as a \"use\") only ever biases toward KEEPING an import, never\n * toward dropping one that is still needed — the safe direction for a\n * heuristic that must not guess in the removal direction.\n */\nfunction collectIdentifierNames(node: unknown, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectIdentifierNames(item, names);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"Identifier\" || record.type === \"JSXIdentifier\") {\n names.add((record as any).name);\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 collectIdentifierNames(record[key], names);\n }\n}\n\n/**\n * Top-level statements that BIND a name, and are therefore attributable by the\n * reference graph rather than by guessing. Everything outside this set and\n * `ALWAYS_SAFE_STATEMENT_TYPES` declares nothing — a bare `console.log(\"boot\")`\n * has no binding to trace a reader from, which is why it stays a hard refusal.\n */\nconst DECLARATION_STATEMENT_TYPES = new Set([\n \"VariableDeclaration\",\n \"FunctionDeclaration\",\n \"ClassDeclaration\",\n]);\n\nfunction collectPatternNames(node: any, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n switch (node.type) {\n case \"Identifier\":\n names.add(node.name);\n return;\n case \"ObjectPattern\":\n for (const property of node.properties) {\n collectPatternNames(\n property.type === \"RestElement\" ? property.argument : property.value,\n names,\n );\n }\n return;\n case \"ArrayPattern\":\n for (const element of node.elements) collectPatternNames(element, names);\n return;\n case \"AssignmentPattern\":\n collectPatternNames(node.left, names);\n return;\n case \"RestElement\":\n collectPatternNames(node.argument, names);\n return;\n }\n}\n\n/** The names a top-level declaration introduces into module scope. */\nfunction declaredNames(stmt: any): Set<string> {\n const names = new Set<string>();\n if (stmt.type === \"VariableDeclaration\") {\n for (const declarator of stmt.declarations)\n collectPatternNames(declarator.id, names);\n } else if (stmt.id?.type === \"Identifier\") {\n names.add(stmt.id.name);\n }\n return names;\n}\n\n/**\n * Whether EVALUATING this initializer can run anything.\n *\n * This is the whole safety argument for removing an unreferenced declaration.\n * A function definition or a literal only creates a value, so dropping it can\n * only drop a binding nothing reads. A call, a `new`, an `await`, a member\n * access (a getter) — those can do work, and \"was that work for the server or\n * for the client?\" is precisely the question projection must not answer by\n * guessing (`c604f0bc` §3). `const _ = installPolyfill()` with no reader at all\n * is the shape this predicate exists to refuse rather than silently delete.\n *\n * Conservative by construction: an unrecognized node type is NOT\n * definition-shaped, so a new syntax form arrives as a refusal with a message,\n * never as a silent removal.\n */\nfunction isDefinitionShapedInit(node: any): boolean {\n // `let x;` — a bare binding with nothing to evaluate.\n if (!node) return true;\n\n switch (node.type) {\n case \"ArrowFunctionExpression\":\n case \"FunctionExpression\":\n case \"StringLiteral\":\n case \"NumericLiteral\":\n case \"BooleanLiteral\":\n case \"NullLiteral\":\n case \"BigIntLiteral\":\n case \"RegExpLiteral\":\n // A bare identifier read is a binding lookup, not a computation.\n case \"Identifier\":\n return true;\n case \"TemplateLiteral\":\n return node.expressions.every((expression: any) =>\n isDefinitionShapedInit(expression),\n );\n case \"UnaryExpression\":\n return (\n node.operator !== \"delete\" && isDefinitionShapedInit(node.argument)\n );\n case \"ArrayExpression\":\n return node.elements.every(\n (element: any) =>\n element === null ||\n (element.type !== \"SpreadElement\" && isDefinitionShapedInit(element)),\n );\n case \"ObjectExpression\":\n // Spread and computed keys both evaluate arbitrary expressions; a getter\n // or setter defines a body that runs on ACCESS, which the surviving half\n // could still trigger — none of them are definitions.\n return node.properties.every(\n (property: any) =>\n property.type === \"ObjectProperty\" &&\n !property.computed &&\n isDefinitionShapedInit(property.value),\n );\n // TS-only wrappers erase at compile time; look through them.\n case \"TSAsExpression\":\n case \"TSSatisfiesExpression\":\n case \"TSNonNullExpression\":\n case \"TSTypeAssertion\":\n case \"TSInstantiationExpression\":\n case \"ParenthesizedExpression\":\n return isDefinitionShapedInit(node.expression);\n default:\n return false;\n }\n}\n\n/**\n * Statement-level form of the above. A `class` is excluded on purpose: static\n * blocks, decorators and computed member keys all run at class-definition time,\n * so a class is only ever kept or refused, never silently removed.\n */\nfunction isDefinitionShapedStatement(stmt: any): boolean {\n if (stmt.type === \"FunctionDeclaration\") return true;\n if (stmt.type !== \"VariableDeclaration\") return false;\n return stmt.declarations.every((declarator: any) =>\n isDefinitionShapedInit(declarator.init),\n );\n}\n\nfunction removeStatement(s: MagicString, code: string, node: any): void {\n let end = node.end as number;\n // Swallow one trailing newline so a removed statement doesn't leave a\n // blank line behind — cosmetic only, the output's correctness never\n // depends on it.\n if (code[end] === \"\\r\" && code[end + 1] === \"\\n\") end += 2;\n else if (code[end] === \"\\n\") end += 1;\n s.remove(node.start as number, end);\n}\n\nfunction statementSnippet(code: string, node: any): string {\n return code\n .slice(node.start as number, node.end as number)\n .split(\"\\n\")[0]\n .trim();\n}\n\n/**\n * The transform itself: parse, remove the 6 server exports and every import\n * orphaned only by that removal, fail closed on anything attribution-\n * ambiguous. `filePath` is only used for error messages (`c604f0bc` §7 —\n * fence errors must name the file).\n */\nexport function projectModule(\n code: string,\n filePath: string,\n): ProjectionResult {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n\n const s = new MagicString(code);\n const body = ast.program.body as any[];\n\n const removedServerExports: any[] = [];\n const importDeclarations: any[] = [];\n const localDeclarations: LocalDeclaration[] = [];\n\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") {\n importDeclarations.push(stmt);\n continue;\n }\n const isNamespaceReexport =\n stmt.type === \"ExportNamedDeclaration\" &&\n stmt.source != null &&\n (stmt.specifiers as any[] | undefined)?.some(\n (specifier) => specifier.type === \"ExportNamespaceSpecifier\",\n );\n\n if (stmt.type === \"ExportAllDeclaration\" || isNamespaceReexport) {\n // `export * from \"./source\"` (and `export * as ns from \"./source\"`,\n // which Babel parses as an `ExportNamedDeclaration` carrying an\n // `ExportNamespaceSpecifier` rather than as `ExportAllDeclaration` —\n // hence the second check above) re-exports every name the source\n // module exports, sight unseen.\n // Projection classifies by file (`c604f0bc` §9) and never opens a\n // second file to resolve what a re-export actually forwards — doing so\n // would mean parsing and walking the source module too, i.e. a second\n // parser. Whether the source exports one of the 6 server names is\n // therefore unknowable here, so this is attribution-ambiguous the same\n // way an unrecognized top-level statement is, and gets the same\n // refusal rather than an assumption that it is safe.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `a star re-export forwards every name the source module exports, including possibly one of the 6 known server exports (route, middleware, validation, loader, metadata, prefix) — projection cannot inspect the source module's exports without parsing a second file, so it can't tell whether this leaks a server-only binding into the client bundle`,\n `replace the star re-export with explicit named re-exports (export { ComponentA, ComponentB } from \"./source\"), listing only the client-safe names`,\n );\n }\n if (isServerExportDeclaration(stmt)) {\n removedServerExports.push(stmt);\n continue;\n }\n if (ALWAYS_SAFE_STATEMENT_TYPES.has(stmt.type)) continue;\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n // Attributable by the reference graph — decided below, once it is known\n // which statements survive. NOT accepted here.\n localDeclarations.push({\n stmt,\n names: declaredNames(stmt),\n definitionShaped: isDefinitionShapedStatement(stmt),\n removed: false,\n });\n continue;\n }\n\n // Attribution-IMPOSSIBLE: not an import, not one of the 6 known server\n // exports, not another export, not a type-only declaration, and it binds\n // no name for a reader to point at. Fail closed rather than guess which\n // side of the fence it belongs on (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `top-level executable code that declares nothing — outside the 6 known server exports (route, middleware, validation, loader, metadata, prefix), and binding no name, so projection has no reader to attribute it by and can't tell whether it belongs to the server or the client`,\n `move universal static declarations and their imports into export function register(), or mark the code with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const removedLocals = new Set<any>();\n\n /**\n * Every name READ by something that survives projection. Imports are excluded\n * so an import specifier never counts as a use of itself, and a local\n * declaration does not count as a use of ITSELF either — otherwise a\n * self-recursive server-only helper would pin its own binding alive forever.\n *\n * Over-collecting (an object property key, a shadowing parameter) only ever\n * biases toward KEEPING, never toward dropping something still needed — the\n * safe direction for a heuristic that must not guess in the removal\n * direction.\n */\n function collectSurvivingNames(): Set<string> {\n const names = new Set<string>();\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") continue;\n if (removedServerExports.includes(stmt) || removedLocals.has(stmt))\n continue;\n const own = new Set<string>();\n collectIdentifierNames(stmt, own);\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n for (const name of declaredNames(stmt)) own.delete(name);\n }\n for (const name of own) names.add(name);\n }\n return names;\n }\n\n // Fixpoint, not one pass: a server-only helper can be reached only through\n // ANOTHER server-only helper, and dropping the first orphans the second.\n let survivingNames = collectSurvivingNames();\n for (let changed = true; changed;) {\n changed = false;\n for (const local of localDeclarations) {\n if (local.removed || !local.definitionShaped) continue;\n if (hasSurvivingReader(local, survivingNames)) continue;\n local.removed = true;\n removedLocals.add(local.stmt);\n changed = true;\n }\n if (changed) survivingNames = collectSurvivingNames();\n }\n\n for (const local of localDeclarations) {\n if (local.removed || hasSurvivingReader(local, survivingNames)) continue;\n\n // Nothing the client keeps reads it, so it belongs to the server exports\n // being removed — but its initializer can RUN, and a side effect is not\n // attributable by the reference graph. Keeping it ships server work to the\n // browser; dropping it deletes a side effect the client may depend on.\n // Refuse rather than pick one (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, local.stmt),\n local.stmt.loc.start.line,\n `a top-level declaration read only by the server exports being removed, but whose initializer executes code rather than just defining a value — projection can't tell whether that work is server-only or a side effect the client depends on`,\n `move universal static declarations and their imports into export function register(), move a server-only initializer inside the export that reads it, or split it into an explicit .server/.client file`,\n );\n }\n\n for (const decl of importDeclarations) {\n const source = decl.source.value as string;\n if (isKnownSafeAsset(source)) continue; // always survives, no orphan check\n\n if (decl.specifiers.length === 0) {\n // A bare side-effect import that isn't a recognized asset extension is\n // just as attribution-ambiguous as an executable statement — could be\n // a server-only side effect or something the client genuinely needs.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, decl),\n decl.loc.start.line,\n `a bare side-effect import with no recognized client-safe asset extension — projection can't tell if it belongs only to the server exports being removed or must ship to the client`,\n `move universal static declarations and their imports into export function register(), or mark it with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const isUsed = decl.specifiers.some((spec: any) =>\n survivingNames.has(spec.local.name),\n );\n if (!isUsed) removeStatement(s, code, decl);\n }\n\n for (const stmt of removedServerExports) {\n removeStatement(s, code, stmt);\n }\n\n for (const stmt of removedLocals) {\n removeStatement(s, code, stmt);\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: true, source: filePath }),\n };\n}\n\nexport function isProjectableFile(id: string): boolean {\n const base = path.basename(id.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // NAMED layouts — `dashboard.layout.tsx` and friends — are subjects too.\n //\n // Only the exact name `layout.tsx` is POSITIONAL (discovered by its folder).\n // A named layout is addressed by import instead, which is a documented part of\n // the contract: `v5/app/src/web/layouts/dashboard.layout.tsx` says so in its\n // own header, and modules opt in with two re-export lines.\n //\n // Projection did not recognise them, and the consequence was not cosmetic.\n // `dashboard.layout.tsx` calls `navService.forUser()` INSIDE its `loader` —\n // exactly where server work belongs. But because the file was not a subject,\n // the loader was never stripped, so its `navService` import survived into the\n // client graph and dragged auth, the user model and three Node builtins with\n // it. The app was right and the subject test was wrong.\n //\n // Matching `*.layout.tsx` rather than a list of known layout names is\n // deliberate: an enumerated list is the shape that has produced every other\n // boundary defect here (canon `eb0c5ee8`).\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\nconst HMR_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\n\n/**\n * The projected module shares its scope with application source, so the helper\n * import must not redeclare a name the application already owns. A suffix is\n * only needed for the deliberately unlikely collision, but making it\n * deterministic keeps the generated HMR module valid for every page shape.\n */\nfunction hmrRegisterModulesBinding(code: string): string {\n const base = \"__warlockRegisterModules\";\n let binding = base;\n let index = 2;\n\n while (new RegExp(`\\\\b${binding}\\\\b`).test(code)) {\n binding = `${base}${index++}`;\n }\n\n return binding;\n}\n\n/**\n * The client-build Vite plugin. Scoped to `*.page.tsx`/`layout.tsx`/`root.tsx`\n * and skipped entirely for the SSR build (`options.ssr`) — the server still\n * needs `route`/`middleware`/`validation`/`loader`/`metadata`/`prefix` intact.\n */\nexport function projection(): Plugin {\n return {\n name: \"warlock:projection\",\n enforce: \"pre\",\n transform(code, id, options) {\n if (options?.ssr) return null;\n if (!isProjectableFile(id)) return null;\n\n try {\n const { code: transformed, map } = projectModule(code, id);\n const registerModules = hmrRegisterModulesBinding(transformed);\n return {\n code:\n `import { registerModules as ${registerModules} } from \"${HMR_RUNTIME_SPECIFIER}\";\\n` +\n `${transformed}\\n` +\n `if (import.meta.hot) import.meta.hot.accept((replacement) => { if (replacement) ${registerModules}([replacement]); });\\n`,\n map,\n };\n } catch (error) {\n if (error instanceof ProjectionAmbiguityError) {\n this.error(error.message);\n }\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"projection.mjs","names":[],"sources":["../../../../../../../web/src/vite/projection.ts"],"sourcesContent":["/**\n * Projection — the compile-time AST transform that strips a page module's\n * six server exports before the CLIENT graph forms.\n *\n * Removes `export const route/middleware/validation/loader/metadata/prefix = ...`\n * (const-arrow form) and `export async function loader(...) {...}`\n * (function-declaration form — a page declares these as separate named\n * exports, not one fused object, so both forms are real), plus any import\n * OR top-level declaration that becomes\n * unreferenced ONLY as a result of that removal. The default export (the\n * page component) and every other non-server-named export — including the\n * synchronous, no-argument `register()` lifecycle hook — survive\n * unconditionally, regardless of what they reference — classification is by\n * FILE, not by what an export does with data (`c604f0bc` §9).\n *\n * ATTRIBUTION APPLIES TO LOCAL DECLARATIONS, NOT JUST IMPORTS. A page or\n * layout hoists things to module scope for two ordinary reasons, and the\n * reference graph tells them apart without guessing:\n *\n * `const publishCart: Middleware = ...` + `export const middleware =\n * [publishCart]` (`v5/app/.../products/web/layout.tsx:30,43`) — the only\n * reader is a server export being removed, so the binding goes with it.\n *\n * `const COMMON_TIMEZONES = [...]` read by the default export\n * (`v5/app/.../account/settings.page.tsx:118,129`) — a surviving reader, so\n * it survives.\n *\n * That is the SAME rule already applied to import bindings, extended to the\n * other binding kind. It is deliberately not a widening of\n * `SERVER_EXPORT_NAMES`, and not \"accept what I don't recognise\": a\n * declaration is kept when something the client keeps reads it, dropped when\n * nothing does AND dropping it cannot delete a side effect, and REFUSED\n * otherwise — see `isDefinitionShapedInit`.\n *\n * This is NOT Gate A (`resolveId` path refusal), Gate B (inline secret\n * reads) or Gate C (emitted-output verification) — those are separate,\n * later slices. Projection runs first; the gates enforce after.\n */\nimport { parse } from \"@babel/parser\";\nimport MagicString from \"magic-string\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\n\n/**\n * Exported so Gate C (`gate-c-verify.ts`) can re-derive \"does the emitted\n * bundle contain a server export as a top-level binding\" from this exact set\n * rather than hand-typing a second copy that could drift from projection's\n * own list.\n */\nexport const SERVER_EXPORT_NAMES = new Set([\n \"route\",\n \"middleware\",\n \"validation\",\n \"loader\",\n \"metadata\",\n \"prefix\",\n]);\n\n/**\n * Recognized client-safe assets that always survive projection untouched,\n * whether imported bare (`import \"./x.css\"`) or with specifiers\n * (`import styles from \"./x.module.css\"`) — `c604f0bc` §3 names CSS\n * explicitly; the rest of this list is the same \"never guess, but a known\n * asset extension is not ambiguous\" reasoning extended to the other static\n * asset kinds Vite treats as URL/asset imports, not executable code.\n */\nconst ASSET_EXTENSION_RE =\n /\\.(css|scss|sass|less|styl|stylus|svg|png|jpe?g|gif|webp|avif|ico|woff2?|ttf|eot|otf)(\\?.*)?$/i;\n\n/**\n * Top-level statement types that need no ambiguity check and are never\n * touched by removal: import declarations are handled by their own\n * survives/orphaned logic below, and every export (other than the 6 server\n * names) plus type-only declarations survive unconditionally per\n * `c604f0bc` §9 (\"classify FILES, not the data they touch\").\n *\n * `ExportAllDeclaration` (`export * from \"./x\"` / `export * as ns from\n * \"./x\"`) is deliberately NOT in this set — it can forward ANY name from its\n * source module, including a server export, and is refused explicitly below\n * rather than assumed safe.\n */\nconst ALWAYS_SAFE_STATEMENT_TYPES = new Set([\n \"ExportNamedDeclaration\",\n \"ExportDefaultDeclaration\",\n \"TSInterfaceDeclaration\",\n \"TSTypeAliasDeclaration\",\n \"EmptyStatement\",\n]);\n\n/**\n * Thrown when projection encounters an attribution-ambiguous top-level\n * statement (`c604f0bc` §3: \"the compiler must not guess\"). Carries the\n * file/statement/fix fields the plugin's `transform` hook formats into the\n * build-failure message — never silently kept or silently dropped.\n */\nexport class ProjectionAmbiguityError extends Error {\n constructor(\n public readonly file: string,\n public readonly statement: string,\n public readonly line: number,\n public readonly explanation: string,\n public readonly fix: string,\n ) {\n super(\n [\n `Projection refused to guess: an ambiguous top-level statement in the client build.`,\n ``,\n `File: ${file}:${line}`,\n `Statement: ${statement}`,\n `Cause: ${explanation}`,\n `Fix: ${fix}`,\n ].join(\"\\n\"),\n );\n this.name = \"ProjectionAmbiguityError\";\n }\n}\n\nexport interface ProjectionResult {\n code: string;\n map: ReturnType<MagicString[\"generateMap\"]>;\n}\n\n/** A top-level `const`/`function`/`class` awaiting attribution by reference. */\ninterface LocalDeclaration {\n stmt: any;\n /** The module-scope names it binds. */\n names: Set<string>;\n /** Whether removing it could delete a side effect — see `isDefinitionShapedInit`. */\n definitionShaped: boolean;\n removed: boolean;\n}\n\nfunction hasSurvivingReader(\n local: LocalDeclaration,\n survivingNames: Set<string>,\n): boolean {\n for (const name of local.names) {\n if (survivingNames.has(name)) return true;\n }\n return false;\n}\n\nfunction isKnownSafeAsset(source: string): boolean {\n return ASSET_EXTENSION_RE.test(source);\n}\n\n/**\n * Matches `export const <name> = ...` only when the declaration has exactly\n * one declarator — every server export in every fixture and v5/app page is\n * written one-const-per-export (`product-details.page.tsx:15-18,42-69,71-74`);\n * a multi-declarator `export const a = 1, b = 2` is left to the ambiguity\n * path below rather than guessing which half is server-only.\n */\nfunction isServerExportDeclaration(stmt: any): boolean {\n if (stmt.type !== \"ExportNamedDeclaration\" || !stmt.declaration) return false;\n const decl = stmt.declaration;\n if (decl.type === \"VariableDeclaration\" && decl.declarations.length === 1) {\n const id = decl.declarations[0].id;\n return id?.type === \"Identifier\" && SERVER_EXPORT_NAMES.has(id.name);\n }\n if (decl.type === \"FunctionDeclaration\") {\n return !!decl.id && SERVER_EXPORT_NAMES.has(decl.id.name);\n }\n return false;\n}\n\n/**\n * Generic duck-typed AST walk (no `@babel/traverse` dependency — this\n * package only needs `@babel/parser` + `@babel/types`-shaped nodes).\n * Collects every `Identifier`/`JSXIdentifier` name reachable from `node`,\n * used to decide whether an import binding still has a reader once the 6\n * server exports are gone. Over-collecting (e.g. counting an object\n * property key as a \"use\") only ever biases toward KEEPING an import, never\n * toward dropping one that is still needed — the safe direction for a\n * heuristic that must not guess in the removal direction.\n */\nfunction collectIdentifierNames(node: unknown, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n if (Array.isArray(node)) {\n for (const item of node) collectIdentifierNames(item, names);\n return;\n }\n const record = node as Record<string, unknown>;\n if (typeof record.type !== \"string\") return;\n if (record.type === \"Identifier\" || record.type === \"JSXIdentifier\") {\n names.add((record as any).name);\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 collectIdentifierNames(record[key], names);\n }\n}\n\n/**\n * Top-level statements that BIND a name, and are therefore attributable by the\n * reference graph rather than by guessing. Everything outside this set and\n * `ALWAYS_SAFE_STATEMENT_TYPES` declares nothing — a bare `console.log(\"boot\")`\n * has no binding to trace a reader from, which is why it stays a hard refusal.\n */\nconst DECLARATION_STATEMENT_TYPES = new Set([\n \"VariableDeclaration\",\n \"FunctionDeclaration\",\n \"ClassDeclaration\",\n]);\n\nfunction collectPatternNames(node: any, names: Set<string>): void {\n if (!node || typeof node !== \"object\") return;\n switch (node.type) {\n case \"Identifier\":\n names.add(node.name);\n return;\n case \"ObjectPattern\":\n for (const property of node.properties) {\n collectPatternNames(\n property.type === \"RestElement\" ? property.argument : property.value,\n names,\n );\n }\n return;\n case \"ArrayPattern\":\n for (const element of node.elements) collectPatternNames(element, names);\n return;\n case \"AssignmentPattern\":\n collectPatternNames(node.left, names);\n return;\n case \"RestElement\":\n collectPatternNames(node.argument, names);\n return;\n }\n}\n\n/** The names a top-level declaration introduces into module scope. */\nfunction declaredNames(stmt: any): Set<string> {\n const names = new Set<string>();\n if (stmt.type === \"VariableDeclaration\") {\n for (const declarator of stmt.declarations)\n collectPatternNames(declarator.id, names);\n } else if (stmt.id?.type === \"Identifier\") {\n names.add(stmt.id.name);\n }\n return names;\n}\n\n/**\n * Whether EVALUATING this initializer can run anything.\n *\n * This is the whole safety argument for removing an unreferenced declaration.\n * A function definition or a literal only creates a value, so dropping it can\n * only drop a binding nothing reads. A call, a `new`, an `await`, a member\n * access (a getter) — those can do work, and \"was that work for the server or\n * for the client?\" is precisely the question projection must not answer by\n * guessing (`c604f0bc` §3). `const _ = installPolyfill()` with no reader at all\n * is the shape this predicate exists to refuse rather than silently delete.\n *\n * Conservative by construction: an unrecognized node type is NOT\n * definition-shaped, so a new syntax form arrives as a refusal with a message,\n * never as a silent removal.\n */\nfunction isDefinitionShapedInit(node: any): boolean {\n // `let x;` — a bare binding with nothing to evaluate.\n if (!node) return true;\n\n switch (node.type) {\n case \"ArrowFunctionExpression\":\n case \"FunctionExpression\":\n case \"StringLiteral\":\n case \"NumericLiteral\":\n case \"BooleanLiteral\":\n case \"NullLiteral\":\n case \"BigIntLiteral\":\n case \"RegExpLiteral\":\n // A bare identifier read is a binding lookup, not a computation.\n case \"Identifier\":\n return true;\n case \"TemplateLiteral\":\n return node.expressions.every((expression: any) =>\n isDefinitionShapedInit(expression),\n );\n case \"UnaryExpression\":\n return (\n node.operator !== \"delete\" && isDefinitionShapedInit(node.argument)\n );\n case \"ArrayExpression\":\n return node.elements.every(\n (element: any) =>\n element === null ||\n (element.type !== \"SpreadElement\" && isDefinitionShapedInit(element)),\n );\n case \"ObjectExpression\":\n // Spread and computed keys both evaluate arbitrary expressions; a getter\n // or setter defines a body that runs on ACCESS, which the surviving half\n // could still trigger — none of them are definitions.\n return node.properties.every(\n (property: any) =>\n property.type === \"ObjectProperty\" &&\n !property.computed &&\n isDefinitionShapedInit(property.value),\n );\n // TS-only wrappers erase at compile time; look through them.\n case \"TSAsExpression\":\n case \"TSSatisfiesExpression\":\n case \"TSNonNullExpression\":\n case \"TSTypeAssertion\":\n case \"TSInstantiationExpression\":\n case \"ParenthesizedExpression\":\n return isDefinitionShapedInit(node.expression);\n default:\n return false;\n }\n}\n\n/**\n * Statement-level form of the above. A `class` is excluded on purpose: static\n * blocks, decorators and computed member keys all run at class-definition time,\n * so a class is only ever kept or refused, never silently removed.\n */\nfunction isDefinitionShapedStatement(stmt: any): boolean {\n if (stmt.type === \"FunctionDeclaration\") return true;\n if (stmt.type !== \"VariableDeclaration\") return false;\n return stmt.declarations.every((declarator: any) =>\n isDefinitionShapedInit(declarator.init),\n );\n}\n\nfunction removeStatement(s: MagicString, code: string, node: any): void {\n let end = node.end as number;\n // Swallow one trailing newline so a removed statement doesn't leave a\n // blank line behind — cosmetic only, the output's correctness never\n // depends on it.\n if (code[end] === \"\\r\" && code[end + 1] === \"\\n\") end += 2;\n else if (code[end] === \"\\n\") end += 1;\n s.remove(node.start as number, end);\n}\n\nfunction statementSnippet(code: string, node: any): string {\n return code\n .slice(node.start as number, node.end as number)\n .split(\"\\n\")[0]\n .trim();\n}\n\n/**\n * The transform itself: parse, remove the 6 server exports and every import\n * orphaned only by that removal, fail closed on anything attribution-\n * ambiguous. `filePath` is only used for error messages (`c604f0bc` §7 —\n * fence errors must name the file).\n */\nexport function projectModule(\n code: string,\n filePath: string,\n): ProjectionResult {\n const ast = parse(code, {\n sourceType: \"module\",\n plugins: [\"typescript\", \"jsx\"],\n });\n\n const s = new MagicString(code);\n const body = ast.program.body as any[];\n\n const removedServerExports: any[] = [];\n const importDeclarations: any[] = [];\n const localDeclarations: LocalDeclaration[] = [];\n\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") {\n importDeclarations.push(stmt);\n continue;\n }\n const isNamespaceReexport =\n stmt.type === \"ExportNamedDeclaration\" &&\n stmt.source != null &&\n (stmt.specifiers as any[] | undefined)?.some(\n (specifier) => specifier.type === \"ExportNamespaceSpecifier\",\n );\n\n if (stmt.type === \"ExportAllDeclaration\" || isNamespaceReexport) {\n // `export * from \"./source\"` (and `export * as ns from \"./source\"`,\n // which Babel parses as an `ExportNamedDeclaration` carrying an\n // `ExportNamespaceSpecifier` rather than as `ExportAllDeclaration` —\n // hence the second check above) re-exports every name the source\n // module exports, sight unseen.\n // Projection classifies by file (`c604f0bc` §9) and never opens a\n // second file to resolve what a re-export actually forwards — doing so\n // would mean parsing and walking the source module too, i.e. a second\n // parser. Whether the source exports one of the 6 server names is\n // therefore unknowable here, so this is attribution-ambiguous the same\n // way an unrecognized top-level statement is, and gets the same\n // refusal rather than an assumption that it is safe.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `a star re-export forwards every name the source module exports, including possibly one of the 6 known server exports (route, middleware, validation, loader, metadata, prefix) — projection cannot inspect the source module's exports without parsing a second file, so it can't tell whether this leaks a server-only binding into the client bundle`,\n `replace the star re-export with explicit named re-exports (export { ComponentA, ComponentB } from \"./source\"), listing only the client-safe names`,\n );\n }\n if (isServerExportDeclaration(stmt)) {\n removedServerExports.push(stmt);\n continue;\n }\n if (ALWAYS_SAFE_STATEMENT_TYPES.has(stmt.type)) continue;\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n // Attributable by the reference graph — decided below, once it is known\n // which statements survive. NOT accepted here.\n localDeclarations.push({\n stmt,\n names: declaredNames(stmt),\n definitionShaped: isDefinitionShapedStatement(stmt),\n removed: false,\n });\n continue;\n }\n\n // Attribution-IMPOSSIBLE: not an import, not one of the 6 known server\n // exports, not another export, not a type-only declaration, and it binds\n // no name for a reader to point at. Fail closed rather than guess which\n // side of the fence it belongs on (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, stmt),\n stmt.loc.start.line,\n `top-level executable code that declares nothing — outside the 6 known server exports (route, middleware, validation, loader, metadata, prefix), and binding no name, so projection has no reader to attribute it by and can't tell whether it belongs to the server or the client`,\n `move universal static declarations and their imports into export function register(), or mark the code with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const removedLocals = new Set<any>();\n\n /**\n * Every name READ by something that survives projection. Imports are excluded\n * so an import specifier never counts as a use of itself, and a local\n * declaration does not count as a use of ITSELF either — otherwise a\n * self-recursive server-only helper would pin its own binding alive forever.\n *\n * Over-collecting (an object property key, a shadowing parameter) only ever\n * biases toward KEEPING, never toward dropping something still needed — the\n * safe direction for a heuristic that must not guess in the removal\n * direction.\n */\n function collectSurvivingNames(): Set<string> {\n const names = new Set<string>();\n for (const stmt of body) {\n if (stmt.type === \"ImportDeclaration\") continue;\n if (removedServerExports.includes(stmt) || removedLocals.has(stmt))\n continue;\n const own = new Set<string>();\n collectIdentifierNames(stmt, own);\n if (DECLARATION_STATEMENT_TYPES.has(stmt.type)) {\n for (const name of declaredNames(stmt)) own.delete(name);\n }\n for (const name of own) names.add(name);\n }\n return names;\n }\n\n // Fixpoint, not one pass: a server-only helper can be reached only through\n // ANOTHER server-only helper, and dropping the first orphans the second.\n let survivingNames = collectSurvivingNames();\n for (let changed = true; changed;) {\n changed = false;\n for (const local of localDeclarations) {\n if (local.removed || !local.definitionShaped) continue;\n if (hasSurvivingReader(local, survivingNames)) continue;\n local.removed = true;\n removedLocals.add(local.stmt);\n changed = true;\n }\n if (changed) survivingNames = collectSurvivingNames();\n }\n\n for (const local of localDeclarations) {\n if (local.removed || hasSurvivingReader(local, survivingNames)) continue;\n\n // Nothing the client keeps reads it, so it belongs to the server exports\n // being removed — but its initializer can RUN, and a side effect is not\n // attributable by the reference graph. Keeping it ships server work to the\n // browser; dropping it deletes a side effect the client may depend on.\n // Refuse rather than pick one (`c604f0bc` §3).\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, local.stmt),\n local.stmt.loc.start.line,\n `a top-level declaration read only by the server exports being removed, but whose initializer executes code rather than just defining a value — projection can't tell whether that work is server-only or a side effect the client depends on`,\n `move universal static declarations and their imports into export function register(), move a server-only initializer inside the export that reads it, or split it into an explicit .server/.client file`,\n );\n }\n\n for (const decl of importDeclarations) {\n const source = decl.source.value as string;\n if (isKnownSafeAsset(source)) continue; // always survives, no orphan check\n\n if (decl.specifiers.length === 0) {\n // A bare side-effect import that isn't a recognized asset extension is\n // just as attribution-ambiguous as an executable statement — could be\n // a server-only side effect or something the client genuinely needs.\n throw new ProjectionAmbiguityError(\n filePath,\n statementSnippet(code, decl),\n decl.loc.start.line,\n `a bare side-effect import with no recognized client-safe asset extension — projection can't tell if it belongs only to the server exports being removed or must ship to the client`,\n `move universal static declarations and their imports into export function register(), or mark it with an explicit .server/.client file; server-only work can instead move inside one of the 6 declared server exports`,\n );\n }\n\n const isUsed = decl.specifiers.some((spec: any) =>\n survivingNames.has(spec.local.name),\n );\n if (!isUsed) removeStatement(s, code, decl);\n }\n\n for (const stmt of removedServerExports) {\n removeStatement(s, code, stmt);\n }\n\n for (const stmt of removedLocals) {\n removeStatement(s, code, stmt);\n }\n\n return {\n code: s.toString(),\n map: s.generateMap({ hires: true, source: filePath }),\n };\n}\n\nexport function isProjectableFile(id: string): boolean {\n const base = path.basename(id.split(\"?\")[0]);\n if (/\\.page\\.tsx?$/.test(base)) return true;\n if (base === \"layout.tsx\" || base === \"layout.ts\") return true;\n // NAMED layouts — `dashboard.layout.tsx` and friends — are subjects too.\n //\n // Only the exact name `layout.tsx` is POSITIONAL (discovered by its folder).\n // A named layout is addressed by import instead, which is a documented part of\n // the contract: `v5/app/src/web/layouts/dashboard.layout.tsx` says so in its\n // own header, and modules opt in with two re-export lines.\n //\n // Projection did not recognise them, and the consequence was not cosmetic.\n // `dashboard.layout.tsx` calls `navService.forUser()` INSIDE its `loader` —\n // exactly where server work belongs. But because the file was not a subject,\n // the loader was never stripped, so its `navService` import survived into the\n // client graph and dragged auth, the user model and three Node builtins with\n // it. The app was right and the subject test was wrong.\n //\n // Matching `*.layout.tsx` rather than a list of known layout names is\n // deliberate: an enumerated list is the shape that has produced every other\n // boundary defect here (canon `eb0c5ee8`).\n if (/\\.layout\\.tsx?$/.test(base)) return true;\n if (base === \"root.tsx\") return true;\n return false;\n}\n\nconst HMR_RUNTIME_SPECIFIER = \"@warlock.js/web/client/runtime\";\n\n/**\n * The projected module shares its scope with application source, so the helper\n * import must not redeclare a name the application already owns. A suffix is\n * only needed for the deliberately unlikely collision, but making it\n * deterministic keeps the generated HMR module valid for every page shape.\n */\nfunction hmrRegisterModulesBinding(code: string): string {\n const base = \"__warlockRegisterModules\";\n let binding = base;\n let index = 2;\n\n while (new RegExp(`\\\\b${binding}\\\\b`).test(code)) {\n binding = `${base}${index++}`;\n }\n\n return binding;\n}\n\n/**\n * The client-build Vite plugin. Scoped to `*.page.tsx`/`layout.tsx`/`root.tsx`\n * and skipped entirely for the SSR build (`options.ssr`) — the server still\n * needs `route`/`middleware`/`validation`/`loader`/`metadata`/`prefix` intact.\n */\nexport function projection(): Plugin {\n return {\n name: \"warlock:projection\",\n enforce: \"pre\",\n transform(code, id, options) {\n if (options?.ssr) return null;\n if (!isProjectableFile(id)) return null;\n\n try {\n const { code: transformed, map } = projectModule(code, id);\n const registerModules = hmrRegisterModulesBinding(transformed);\n return {\n code:\n `import { registerModules as ${registerModules} } from \"${HMR_RUNTIME_SPECIFIER}\";\\n` +\n `${transformed}\\n` +\n `if (import.meta.hot) import.meta.hot.accept((replacement) => { if (replacement) ${registerModules}([replacement]); });\\n`,\n map,\n };\n } catch (error) {\n if (error instanceof ProjectionAmbiguityError) {\n this.error(error.message);\n }\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
|
package/llms-full.txt
CHANGED
|
@@ -116,12 +116,12 @@ The desired result is one page route at `/` and, when the stock JSON route exist
|
|
|
116
116
|
|
|
117
117
|
---
|
|
118
118
|
name: create-a-page
|
|
119
|
-
description: 'Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "add an SSR route", "make a React page", "type page loader data", "add an error boundary", "catch-all route", "page renders blank 200", "page has no default export"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.'
|
|
119
|
+
description: 'Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, an explicit public-cache opt-in, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `route.cache`, `maxAge`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "cache a public page", "add an SSR route", "make a React page", "type page loader data", "add an error boundary", "catch-all route", "page renders blank 200", "page has no default export"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.'
|
|
120
120
|
---
|
|
121
121
|
|
|
122
122
|
# Warlock — create a page
|
|
123
123
|
|
|
124
|
-
A page is any `*.page.tsx` beneath `src/web/` — the
|
|
124
|
+
A page is any `*.page.tsx` beneath `src/web/` — the page root. Its URL is either a declared `route` or one derived from its own location; its default export renders React.
|
|
125
125
|
|
|
126
126
|
## The shape
|
|
127
127
|
|
|
@@ -131,13 +131,12 @@ import type { PageLoader, PageMetadata, PageProps } from "@warlock.js/web";
|
|
|
131
131
|
export const route = {
|
|
132
132
|
path: "/products/:id",
|
|
133
133
|
name: "products.details",
|
|
134
|
+
cache: { public: true, maxAge: 60 },
|
|
134
135
|
} as const;
|
|
135
136
|
|
|
136
|
-
export const loader = (async ({ request
|
|
137
|
+
export const loader = (async ({ request }) => {
|
|
137
138
|
const id = request.input("id");
|
|
138
139
|
|
|
139
|
-
response.header("cache-control", "private, max-age=60");
|
|
140
|
-
|
|
141
140
|
return {
|
|
142
141
|
product: {
|
|
143
142
|
id,
|
|
@@ -180,8 +179,6 @@ export default function ContactPage() {
|
|
|
180
179
|
|
|
181
180
|
`route` is optional. A page that omits it derives both its path and its name from where the file sits beneath `src/web` ([filesystem routing](#filesystem-routing), below). A page that declares `route` uses that instead — an explicit `route` always wins over the derived one, for both the path and (when it sets `name`) the name.
|
|
182
181
|
|
|
183
|
-
Changed in 5.2: through 5.1 an omitted `route` was refused — `warlock dev` and the production discovery pass threw `MissingRouteExportError`, naming the file. That error class is gone; the same file now resolves to a real, browsable URL.
|
|
184
|
-
|
|
185
182
|
### The default export is required
|
|
186
183
|
|
|
187
184
|
`route` is optional; the default export is not. A `*.page.tsx` that exports only named bindings is a **hard discovery/build failure naming the file**:
|
|
@@ -192,8 +189,6 @@ file must default-export the React component it renders. For example:
|
|
|
192
189
|
`export default function Page() { return <main />; }`
|
|
193
190
|
```
|
|
194
191
|
|
|
195
|
-
Changed in 5.2: such a file used to be discovered and registered anyway, then serve a blank `200` at its URL — a page that looked deployed and rendered nothing, with no error anywhere. It now stops the build.
|
|
196
|
-
|
|
197
192
|
Two details worth knowing:
|
|
198
193
|
|
|
199
194
|
- **`export { Page as default }` satisfies the rule** — the check is for a runtime default binding, not for the `export default` keyword form specifically.
|
|
@@ -213,6 +208,7 @@ export const route = "/products";
|
|
|
213
208
|
export const route = {
|
|
214
209
|
path: "/products/:id",
|
|
215
210
|
name: "products.details",
|
|
211
|
+
cache: { public: true, maxAge: 60 },
|
|
216
212
|
} as const;
|
|
217
213
|
```
|
|
218
214
|
|
|
@@ -222,25 +218,53 @@ Every segment of a page's URL is written down somewhere: `route.path` (or the de
|
|
|
222
218
|
|
|
223
219
|
The build reads `route` without executing application code. Declare it directly with `export const` and literal strings. Variables, function calls, computed object keys, spreads, and `export { route }` are refused.
|
|
224
220
|
|
|
221
|
+
## Page caching
|
|
222
|
+
|
|
223
|
+
Page documents and their `x-warlock-data` representations are `no-store` by
|
|
224
|
+
default. Opt a public page into shared caching on its route:
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
export const route = {
|
|
228
|
+
path: "/products",
|
|
229
|
+
name: "products.index",
|
|
230
|
+
cache: { public: true, maxAge: 60 },
|
|
231
|
+
} as const;
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`maxAge` is seconds. Both keys are required: `cache: { public: true }` is a
|
|
235
|
+
boot-time `InvalidPageCacheOptInError`, because the framework will not invent
|
|
236
|
+
a freshness window. Remove `cache` entirely to keep the safe `no-store`
|
|
237
|
+
default.
|
|
238
|
+
|
|
239
|
+
The declaration is an opt-in, not an override of request safety. A response
|
|
240
|
+
that sets or clears a cookie, or a request that used authenticated state,
|
|
241
|
+
emits `Cache-Control: private, no-store`. If Warlock cannot determine whether
|
|
242
|
+
the request used authenticated state, it revokes the opt-in and emits
|
|
243
|
+
`Cache-Control: no-store`. Only a provably unauthenticated request with no
|
|
244
|
+
`Set-Cookie` can emit `public, max-age=<maxAge>`.
|
|
245
|
+
|
|
246
|
+
This decision is applied once after loaders finish, to both representations.
|
|
247
|
+
Setting `Cache-Control` manually in a loader cannot bypass the floor.
|
|
248
|
+
|
|
225
249
|
## Filesystem routing
|
|
226
250
|
|
|
227
251
|
Omit `route` and the URL comes from the page's own path beneath `src/web`:
|
|
228
252
|
|
|
229
253
|
- Every directory contributes a segment, in order — `src/web/products/featured.page.tsx` derives `/products/featured`.
|
|
230
|
-
- A `(group)` directory — parentheses, not braces — contributes nothing to the URL, only to organization: `src/web/(marketing)/pricing.page.tsx` derives `/pricing`.
|
|
254
|
+
- A `(group)` directory — parentheses, not braces — contributes nothing to the URL, only to organization: `src/web/(marketing)/pricing.page.tsx` derives `/pricing`. Bracket syntax inside a group name is refused at boot because it can never contribute a dynamic segment; use `(marketing)/[id]/page.page.tsx`, not `(marketing[id])/page.page.tsx`.
|
|
231
255
|
- `index.page.tsx` claims its own directory rather than adding a segment: `src/web/products/index.page.tsx` derives `/products`. This is the ONLY filename with special meaning — `home.page.tsx` is not magic and derives `/home`.
|
|
232
256
|
- `[id]` becomes `:id`: `src/web/products/[id].page.tsx` derives `/products/:id`.
|
|
233
257
|
- A layout `prefix` on the page's ancestry composes in front of the derived path exactly as it does for an explicit `route.path` ([use-layouts](../use-layouts/SKILL.md)).
|
|
234
258
|
|
|
235
259
|
Two pages that derive (or declare) the same effective path is a build error naming both files.
|
|
236
260
|
|
|
237
|
-
### Catch-all segments are
|
|
238
|
-
|
|
239
|
-
**`[...slug].page.tsx` does not do what it looks like.** There is no catch-all / rest-parameter form in filesystem routing. Only `[name]` — a plain identifier in square brackets — is recognized as a dynamic segment; `[...slug]` fails that pattern and is taken as an ordinary **literal** segment.
|
|
240
|
-
|
|
241
|
-
So `src/web/docs/[...slug].page.tsx` derives the route path `/docs/[...slug]` and the route name `docs.[...slug]`. It registers, it builds, and it serves — at the literal URL `/docs/%5B...slug%5D` and nowhere else. `/docs/a/b` does not match it.
|
|
261
|
+
### Catch-all segments are refused
|
|
242
262
|
|
|
243
|
-
|
|
263
|
+
There is no catch-all / rest-parameter form in filesystem routing. Only
|
|
264
|
+
`[name]` — a plain identifier in square brackets — is recognized as a
|
|
265
|
+
dynamic segment. `src/web/docs/[...slug].page.tsx` raises
|
|
266
|
+
`PageFileSegmentNotSupportedError` at boot and names both the page file and
|
|
267
|
+
the rejected segment.
|
|
244
268
|
|
|
245
269
|
Until a catch-all exists, use the terminal wildcard with an explicit `route`:
|
|
246
270
|
|
|
@@ -257,6 +281,11 @@ Page routes deliberately accept less than API routes:
|
|
|
257
281
|
|
|
258
282
|
Write two pages for an optional segment. Validate a constrained parameter in the page instead of putting a regex in its path.
|
|
259
283
|
|
|
284
|
+
An unsupported declared path is a boot-time
|
|
285
|
+
`PageRoutePathNotSupportedError`, never a literal or normalized route.
|
|
286
|
+
Examples that fail include `/users/:id?`, `/users/:id(\\d+)`,
|
|
287
|
+
`/near/:lat-:lng`, `/a//b`, and `/users/`.
|
|
288
|
+
|
|
260
289
|
## Metadata
|
|
261
290
|
|
|
262
291
|
`metadata` may be a static object or a function of the resolved loader data and readonly `shared` payload:
|
|
@@ -296,6 +325,10 @@ export default function ErrorPage({ error, status }: ErrorPageProps) {
|
|
|
296
325
|
|
|
297
326
|
During SSR the component receives the real thrown value in `error`. After hydration it receives the JSON-safe `{ name, message, stack? }` shape instead — the original value does not survive the wire. `robots: noindex` is a framework default on this path that an app-supplied `metadata` cannot remove.
|
|
298
327
|
|
|
328
|
+
Every unhandled error response is also forced to
|
|
329
|
+
`Cache-Control: private, no-store` at the framework's shared error funnel.
|
|
330
|
+
This includes page and API-route failures.
|
|
331
|
+
|
|
299
332
|
If the failure happens before any page module could even load — a module-load or `register()` throw — there is no trustworthy server composition left to hydrate against, so Warlock renders a plain framework fallback (your `error.page.tsx` if it can still be loaded, otherwise a minimal built-in boundary) with no hydration script at all rather than risk hydrating client code against markup nothing can vouch for.
|
|
300
333
|
|
|
301
334
|
## The `register()` hook
|
|
@@ -321,16 +354,16 @@ The browser boundary is decided by the import graph, not by the file's location.
|
|
|
321
354
|
|
|
322
355
|
Keep server-only repository and service reads inside `loader`. Do not read them from module-scope initializers or the default component.
|
|
323
356
|
|
|
324
|
-
### `.client` does not isolate SSR
|
|
357
|
+
### `.client` does not isolate SSR
|
|
325
358
|
|
|
326
359
|
A `.client.tsx` suffix is a naming convention, not an SSR-isolation boundary. A
|
|
327
360
|
module statically imported by a page, layout, root, or any of their imports is
|
|
328
361
|
still evaluated by the server. Top-level browser globals such as `window`
|
|
329
|
-
therefore crash SSR boot. Warlock
|
|
362
|
+
therefore crash SSR boot. Warlock does not ship a client-only component
|
|
330
363
|
primitive; code that requires browser globals at module scope cannot be part of
|
|
331
364
|
the SSR page graph.
|
|
332
365
|
|
|
333
|
-
### Static assets use `public/`
|
|
366
|
+
### Static assets use `public/`
|
|
334
367
|
|
|
335
368
|
The production server build does not support imported non-stylesheet assets. An
|
|
336
369
|
import such as `import logo from "./logo.svg"` works under Vite in development
|
|
@@ -341,7 +374,7 @@ are the exception and remain supported.
|
|
|
341
374
|
|
|
342
375
|
## Editing a page in development
|
|
343
376
|
|
|
344
|
-
`warlock dev` decides Fast Refresh vs. a full reload by comparing the module's
|
|
377
|
+
`warlock dev` decides Fast Refresh vs. a full reload by comparing the module's _skeleton_ — its source with every component body masked out — across the edit. Everything outside a component body is part of the skeleton: imports, module-level declarations, and all server exports (`route`, `middleware`, `validation`, `loader`, `metadata`). The skeleton moving, with or without a simultaneous JSX change, forces a full reload; the skeleton holding still defers to Fast Refresh.
|
|
345
378
|
|
|
346
379
|
- **A JSX-only edit hot-updates.** The skeleton is unchanged, so Vite's Fast Refresh applies the projected client code with no reload and no lost component state.
|
|
347
380
|
- **A `metadata`-only edit reloads the document.** `metadata` sits outside the skeleton's masked region, so the edit moves it. Warlock sends a full reload, which re-runs SSR and rebuilds `<head>`. Component state is lost — that is the price of seeing the new `<title>` without touching the browser.
|
|
@@ -349,21 +382,19 @@ are the exception and remain supported.
|
|
|
349
382
|
- **A helper function used only by the JSX still reloads if it is declared at module level.** The rule does not try to prove which half of a shared declaration the edit was "really" for — it over-approximates deliberately, because a false reload only costs component state, while a missed one ships a stale `<head>` and calls it a hot update.
|
|
350
383
|
- Creating, deleting, or renaming a page file, or editing its `route` export, is page-GRAPH churn, not a skeleton edit — see below, not Fast Refresh.
|
|
351
384
|
|
|
352
|
-
Changed in 5.1: a metadata-only edit previously left a stale `<head>` until you refreshed the browser by hand. The current skeleton-comparison rule replaces that earlier, narrower "metadata-only" special case.
|
|
353
|
-
|
|
354
385
|
## Route-table changes in development
|
|
355
386
|
|
|
356
387
|
Creating a page, deleting one, or editing its `route` export's path is a different kind of dev edit from the skeleton comparison above — it changes which URLs exist, not just how one already-registered URL renders. `warlock dev` re-registers the affected route(s) in the live route table, atomically and with no dev-server restart, so the new file (or new path) is reachable on the very next request with no manual restart.
|
|
357
388
|
|
|
358
|
-
Changed in 5.2: through 5.1 the route table was built once at boot and never again. Creating a page 404'd forever; deleting one kept it reachable at its old URL; editing `route` served the OLD path from the old file and 404'd the new one — the running server silently disagreed with the source on disk. All three are now live.
|
|
359
|
-
|
|
360
389
|
## Gotchas
|
|
361
390
|
|
|
362
|
-
- **A page with no `route` is not unreachable.** It derives a real URL from its file location — see [Filesystem routing](#filesystem-routing).
|
|
391
|
+
- **A page with no `route` is not unreachable.** It derives a real URL from its file location — see [Filesystem routing](#filesystem-routing).
|
|
363
392
|
- **A page with no default export IS refused.** Named exports alone fail the build naming the file, instead of serving a blank 200.
|
|
364
|
-
- **`.client.tsx` does not prevent SSR evaluation.** It is a naming convention
|
|
365
|
-
- **Imported static assets do not build
|
|
366
|
-
- **`[...slug]` is not a catch-all.** It
|
|
393
|
+
- **`.client.tsx` does not prevent SSR evaluation.** It is a naming convention, not a client-only component primitive.
|
|
394
|
+
- **Imported static assets do not build.** Put them in `public/` and reference their root URL; CSS imports remain supported.
|
|
395
|
+
- **`[...slug]` is not a catch-all.** It fails boot with
|
|
396
|
+
`PageFileSegmentNotSupportedError`; use an explicit terminal `*` route
|
|
397
|
+
instead — see [Catch-all segments are refused](#catch-all-segments-are-refused).
|
|
367
398
|
- **`process.env` is refused in the client/universal graph, `PUBLIC_` prefix included.** Read env values in a loader and return them as page data; see [`load-page-data/SKILL.md`](../load-page-data/SKILL.md).
|
|
368
399
|
- **Keep `route` literal.** A computed route cannot be discovered without executing app code and is refused.
|
|
369
400
|
- **Do not annotate the loader with `: PageLoader`.** That erases the return type `PageProps` needs.
|
|
@@ -656,7 +687,7 @@ description: 'Navigate hydrated pages with `<Link>`, resolve named URLs with `hr
|
|
|
656
687
|
|
|
657
688
|
`<Link>` renders a real anchor for progressive enhancement and intercepts a plain in-app click after hydration. The server remains the only route matcher; client navigation fetches the page-data representation of the URL and swaps the Layout + Page tree.
|
|
658
689
|
|
|
659
|
-
Every behaviour on this page
|
|
690
|
+
Every behaviour on this page depends on hydration having mounted. See [write-the-root](../write-the-root/SKILL.md#root-is-the-hydration-boundary).
|
|
660
691
|
|
|
661
692
|
## The shape
|
|
662
693
|
|
|
@@ -926,7 +957,7 @@ export default function ProductsPage() {
|
|
|
926
957
|
|
|
927
958
|
Recognized stylesheet imports survive the page's client projection. The client boundary is determined by the import graph, not by the file living under `web/`.
|
|
928
959
|
|
|
929
|
-
That support is specific to stylesheets in the production page graph. An imported non-stylesheet asset such as `import logo from "./logo.svg"` works under Vite in development but is refused by `warlock build
|
|
960
|
+
That support is specific to stylesheets in the production page graph. An imported non-stylesheet asset such as `import logo from "./logo.svg"` works under Vite in development but is refused by `warlock build`. Put it in the application's `public/` directory and reference its root URL instead: `public/logo.svg` is `/logo.svg`.
|
|
930
961
|
|
|
931
962
|
For each matched handler, Warlock builds one ordered CSS chain: `[root, ...matched layouts, page]`. A stylesheet imported directly by any member of that chain becomes a render-blocking link in the initial document in both development and production. Unrelated pages and layouts do not contribute CSS to this response.
|
|
932
963
|
|
|
@@ -1079,7 +1110,7 @@ Non-rendering layouts may carry prefixes and middleware and may nest freely. Do
|
|
|
1079
1110
|
|
|
1080
1111
|
## `404.page.tsx` never gets a layout
|
|
1081
1112
|
|
|
1082
|
-
A `404.page.tsx` renders with no layouts, even when it sits in a directory with a rendering `layout.tsx` above it. Discovery reports an empty layout chain for the not-found page only, and both the client hydration registry and the production route table read from that same chain — the server
|
|
1113
|
+
A `404.page.tsx` renders with no layouts, even when it sits in a directory with a rendering `layout.tsx` above it. Discovery reports an empty layout chain for the not-found page only, and both the client hydration registry and the production route table read from that same chain — the server renders it with no layout wrapper, and the client hydrates it the same way, with no layout of its own. A page that exists to handle failure must not depend on app chrome that can itself throw or need data.
|
|
1083
1114
|
|
|
1084
1115
|
This is scoped to the not-found page: an ordinary page in the same directory still gets its full layout chain, and nested-layout refusal on the 404's own path is still enforced exactly as it is for any other page.
|
|
1085
1116
|
|
|
@@ -1167,8 +1198,6 @@ The browser hydrates `#root`, not the whole document. The client tree deliberate
|
|
|
1167
1198
|
|
|
1168
1199
|
Because App is outside the hydrated subtree, put client state that must survive navigation in a layout or component beneath `#root`, not in the document root.
|
|
1169
1200
|
|
|
1170
|
-
**Requires 5.1 in an installed app.** In 5.0.0–5.0.2 the dev server handed the browser `react-dom/client` as raw CommonJS, so `hydrateRoot` was not there as a named export and the hydration entry died on import — no client JavaScript ran at all. The symptoms were a page that server-rendered correctly but had dead `useState`, no HMR, and `<Link>` doing full page loads. The fix pre-bundles React through Vite's `optimizeDeps`. If you are debugging this against a checkout of the framework itself, note that the defect never reproduced there: inside the monorepo the hydration entry resolves outside `node_modules` and React was always optimized normally.
|
|
1171
|
-
|
|
1172
1201
|
If `root.tsx` (or any module it needs) fails to load or its `register()` throws, there is no trustworthy Layout+Page composition left to hydrate — Warlock falls back to a plain document with no hydration script at all rather than hydrate the browser against markup nothing can vouch for. See [create-a-page](../create-a-page/SKILL.md) for the app's own `error.page.tsx` boundary, which is tried first.
|
|
1173
1202
|
|
|
1174
1203
|
## `<Head />`
|
package/llms.txt
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
## Skills
|
|
18
18
|
|
|
19
19
|
- [add-web-to-an-app](@warlock.js/web/add-web-to-an-app/SKILL.md): Install the SSR page layer with `warlock add web`: add React/Vite peers, scaffold `src/web/root.tsx` and `src/web/index.page.tsx`, register `webConnector()`, and safely relocate the stock top-level `GET "/"` JSON route to `/welcome`. Triggers: `warlock add web`, `webConnector`, `src/web/root.tsx`, `src/web/index.page.tsx`, `GET "/welcome"`; "add web to an app", "install Warlock web", "scaffold SSR", "homepage route collision". Skip: author a page — `@warlock.js/web/create-a-page/SKILL.md`; customize the document — `@warlock.js/web/write-the-root/SKILL.md`; dev/build/start commands — `@warlock.js/core/run-app/SKILL.md`; competing installers `create-next-app`, `vite create`, `remix init`.
|
|
20
|
-
- [create-a-page](@warlock.js/web/create-a-page/SKILL.md): Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "add an SSR route", "make a React page", "type page loader data", "add an error boundary", "catch-all route", "page renders blank 200", "page has no default export"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.
|
|
20
|
+
- [create-a-page](@warlock.js/web/create-a-page/SKILL.md): Create an SSR React page under `src/web/**`, with either a literal `route` or a filesystem-derived one, an explicit public-cache opt-in, a default component, an optional typed `loader`, page `metadata`, the `error.page.tsx` boundary, and the universal `register()` hook. Triggers: `*.page.tsx`, `route`, `route.cache`, `maxAge`, `PageLoader`, `PageProps`, `PageMetadata`, `error.page.tsx`, `register`, `[...slug]`; "create a page", "cache a public page", "add an SSR route", "make a React page", "type page loader data", "add an error boundary", "catch-all route", "page renders blank 200", "page has no default export"; typical import `import type { PageLoader, PageProps } from "@warlock.js/web"`. Skip: root document shell — `@warlock.js/web/write-the-root/SKILL.md`; layout wrappers and prefixes — `@warlock.js/web/use-layouts/SKILL.md`; loader lifecycle and `shared` — `@warlock.js/web/load-page-data/SKILL.md`; competing frameworks `next`, `remix`, `react-router` file routes.
|
|
21
21
|
- [load-page-data](@warlock.js/web/load-page-data/SKILL.md): Load App, Layout, and Page data with `AppLoader`, `LayoutLoader`, and `PageLoader`; type component `data`, validate page input, short-circuit with the buffered response, and publish request-scoped browser-safe values through `shared`. Triggers: `PageLoader`, `LayoutLoader`, `AppLoader`, `PageProps`, `shared`, `useShared`, `validation`, `request.validated`, `process.env`, `env("PUBLIC_...")`, `import.meta.env.PUBLIC_`; "load page data", "pass server data to React", "share request data", "redirect from a loader", "read an environment variable in a page", "process.env refused in the client build", "loader execution order", "return a Response from a loader". Skip: page module basics — `@warlock.js/web/create-a-page/SKILL.md`; layouts — `@warlock.js/web/use-layouts/SKILL.md`; mutation follow-up — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing loaders Next data functions, Remix loaders, React Server Components.
|
|
22
22
|
- [navigate-on-the-client](@warlock.js/web/navigate-on-the-client/SKILL.md): Navigate hydrated pages with `<Link>`, resolve named URLs with `href()`, use `navigateTo` / `navigateBack`, prefetch on interaction, inspect the server match with `currentRoute()`, and re-fetch loaders after a mutation with `refresh()`. Triggers: `Link`, `href`, `navigateTo`, `navigateBack`, `refresh`, `currentRoute`, `previousRoute`; "navigate without a reload", "link to a named route", "refresh page data", "revalidate loaders", "client-side back"; typical import `import { Link, refresh } from "@warlock.js/web"`. Skip: define a page route — `@warlock.js/web/create-a-page/SKILL.md`; loader mechanics — `@warlock.js/web/load-page-data/SKILL.md`; root hydration boundary — `@warlock.js/web/write-the-root/SKILL.md`; competing routers `@mongez/react-router`, `react-router-dom`, Next navigation.
|
|
23
23
|
- [serve-styles](@warlock.js/web/serve-styles/SKILL.md): Serve CSS imported by `root.tsx` or `*.page.tsx`, with render-blocking `<link rel="stylesheet">` delivery from Vite source URLs in development and Vite manifest assets in production. Triggers: `import "./app.css"`, page CSS, `?direct`, `manifest.json`, stylesheet flash, FOUC, `<head>`; "add global styles", "style a page", "CSS missing in SSR", "page flashes unstyled", "serve CSS in production". Skip: root document markup — `@warlock.js/web/write-the-root/SKILL.md`; page authoring — `@warlock.js/web/create-a-page/SKILL.md`; client navigation — `@warlock.js/web/navigate-on-the-client/SKILL.md`; competing styling systems CSS-in-JS, Next CSS, styled-components.
|
package/package.json
CHANGED
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@babel/parser": "^7.29.7",
|
|
9
9
|
"@babel/types": "^7.29.7",
|
|
10
|
+
"@mongez/localization": "^3.4.7",
|
|
10
11
|
"magic-string": "^0.30.21"
|
|
11
12
|
},
|
|
12
13
|
"peerDependencies": {
|
|
13
14
|
"@vitejs/plugin-react": "^5.2.0",
|
|
14
|
-
"@warlock.js/core": "5.
|
|
15
|
-
"@warlock.js/seal": "5.
|
|
15
|
+
"@warlock.js/core": "5.3.0",
|
|
16
|
+
"@warlock.js/seal": "5.3.0",
|
|
16
17
|
"react": "*",
|
|
17
18
|
"react-dom": "*",
|
|
18
19
|
"vite": ">=7.3.5 <8"
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
],
|
|
38
39
|
"author": "hassanzohdy",
|
|
39
40
|
"license": "MIT",
|
|
40
|
-
"version": "5.
|
|
41
|
+
"version": "5.3.0",
|
|
41
42
|
"type": "module",
|
|
42
43
|
"main": "./esm/index.mjs",
|
|
43
44
|
"module": "./esm/index.mjs",
|