@warlock.js/web 5.6.0 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/esm/build/contribution.mjs +1 -1
  3. package/esm/build/discover-pages.mjs +3 -3
  4. package/esm/build/generate-pages-barrel.mjs +2 -2
  5. package/esm/build/page-routes-manifest.mjs +1 -1
  6. package/esm/build/public-files.mjs +1 -1
  7. package/esm/client/navigation/fetch-page-data.mjs +3 -10
  8. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  9. package/esm/client/navigation/navigation-root.mjs +1 -1
  10. package/esm/hydration-payload.mjs +19 -10
  11. package/esm/hydration-payload.mjs.map +1 -1
  12. package/esm/loaders.d.mts +12 -7
  13. package/esm/route.d.mts +8 -32
  14. package/esm/server/create-page-route-handler.mjs +12 -6
  15. package/esm/server/create-page-route-handler.mjs.map +1 -1
  16. package/esm/server/execute-page-request.mjs +29 -37
  17. package/esm/server/execute-page-request.mjs.map +1 -1
  18. package/esm/server/execute-page-request.types.d.mts +15 -4
  19. package/esm/server/hydration-client-url.mjs +1 -1
  20. package/esm/server/index.d.mts +2 -1
  21. package/esm/server/index.mjs +4 -4
  22. package/esm/server/install-page-routes-from-manifest.d.mts +9 -0
  23. package/esm/server/install-page-routes-from-manifest.mjs +20 -20
  24. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  25. package/esm/server/install-page-routes.d.mts +2 -24
  26. package/esm/server/install-page-routes.mjs +48 -25
  27. package/esm/server/install-page-routes.mjs.map +1 -1
  28. package/esm/server/match-page-route.mjs +6 -10
  29. package/esm/server/match-page-route.mjs.map +1 -1
  30. package/esm/server/not-found-page.d.mts +1 -0
  31. package/esm/server/not-found-page.mjs +8 -1
  32. package/esm/server/not-found-page.mjs.map +1 -1
  33. package/esm/server/page-file-change.mjs +1 -1
  34. package/esm/server/page-module-shapes.d.mts +24 -0
  35. package/esm/server/page-route-reload.mjs +1 -1
  36. package/esm/server/register-production-public-files.mjs +1 -1
  37. package/esm/server/render-page.d.mts +3 -2
  38. package/esm/server/render-page.mjs +1 -0
  39. package/esm/server/render-page.mjs.map +1 -1
  40. package/esm/server/resolve-route-validation-input.mjs +2 -2
  41. package/esm/server/resolve-route-validation-input.mjs.map +1 -1
  42. package/esm/server/resolve-validation-data.mjs +1 -1
  43. package/esm/server/resolve-validation-data.mjs.map +1 -1
  44. package/esm/server/stylesheet-urls.mjs +78 -19
  45. package/esm/server/stylesheet-urls.mjs.map +1 -1
  46. package/esm/server/web-connector.mjs +2 -2
  47. package/esm/server/web-connector.mjs.map +1 -1
  48. package/esm/validation.d.mts +12 -1
  49. package/esm/vite/build-client.mjs +1 -1
  50. package/esm/vite/gate-a-resolve.mjs +1 -1
  51. package/esm/vite/hydration-entries.mjs +1 -1
  52. package/llms-full.txt +25 -19
  53. package/package.json +3 -3
  54. package/skills/create-a-page/SKILL.md +25 -19
  55. package/esm/server/route-validation-error.mjs +0 -32
  56. package/esm/server/route-validation-error.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"web-connector.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector.ts"],"sourcesContent":["/**\r\n * `WebConnector` — the SSR page surface as a first-class Warlock connector.\r\n *\r\n * It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React\r\n * pages on one port, and `web` no longer owns a private copy of the HTTP\r\n * lifecycle. Everything this file does used to live in `startDevServer()`\r\n * (`web/src/server/dev-error-transport.ts`), which created its own Fastify instance,\r\n * scanned the router and called `listen()` itself — three responsibilities core\r\n * already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.\r\n *\r\n * WHY A `Late` CONNECTOR IS THE RIGHT SEAM, in ordering terms:\r\n * `ConnectorsManager.startPhase` runs **every** `boot()` in a phase before\r\n * **any** `start()` (`core/src/connectors/connectors-manager.ts:87-93`).\r\n * `HttpConnector` is itself `Late` (`core/src/connectors/http-connector.ts:41`)\r\n * and publishes its Fastify instance during its own `boot()`\r\n * (`container.set(\"http.server\", …)`, `core/src/connectors/http-connector.ts:74`).\r\n * So by the time this connector's `boot()` runs, Fastify and its plugins exist,\r\n * the raw node server exists, and NOTHING has been scanned or bound yet — page\r\n * routes registered here are picked up by `HttpConnector.start()`'s\r\n * `router.scanDevServer(…)` (`core/src/connectors/http-connector.ts:133`) before\r\n * `listen()` (`:147`). `SocketConnector.boot()` reads the same container key the\r\n * same way (`core/src/connectors/socket-connector.ts:78-80`) — this file is\r\n * deliberately shaped after it.\r\n *\r\n * What it can NOT do, and why that is fine: route COLLECTION happens earlier\r\n * (`core/src/dev-server/development-server.ts:57` precedes `:66`), so pages are\r\n * not discovered by the framework's file scanner. They are discovered here, by\r\n * `installPageRoutes` (`./install-page-routes.ts:189`), and registered through\r\n * the ordinary `router.get(…)` API — there is no second server matcher.\r\n *\r\n * DELIBERATE EXCEPTION to A.3 §2 (\"web has no core dependency\"), the same one\r\n * `./dev-error-transport.ts` and `./install-page-routes.ts` record in their own headers:\r\n * this module is not re-exported from any package barrel (`web/src/index.ts`,\r\n * `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of\r\n * `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only\r\n * ever imported by tooling that already depends on core.\r\n *\r\n * `@warlock.js/web/connector` reaches this class ONLY through\r\n * `./web-connector-factory.ts`'s `await import(\"./web-connector\")` — a\r\n * deliberate seam, because a static edge from that barrel to this file would\r\n * put `../vite`, core's router and `./dev-error-transport` into the import graph of\r\n * every consuming app's `warlock.config.ts`.\r\n */\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport type { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from \"fastify\";\r\nimport type { Alias, PluginOption, ViteDevServer } from \"vite\";\r\nimport {\r\n Application,\r\n BaseConnector,\r\n ConnectorLifecyclePhase,\r\n type ConnectorName,\r\n container,\r\n type FastifyInstance,\r\n requestContext,\r\n router,\r\n} from \"@warlock.js/core\";\r\nimport { resolveWebPackageRoot } from \"../build/contribution\";\r\nimport { createHydrationClientEntry, invalidateClientPageRegistry } from \"../vite\";\r\nimport { createWebConnectorViteConfig } from \"../vite/dev-server-config\";\r\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\r\nimport { devErrorTransportPlugin, sendCapturedDevError } from \"./dev-error-transport\";\r\nimport { resolveHydrationClientUrl } from \"./hydration-client-url\";\r\nimport type { InstalledPageRoute } from \"./install-page-routes\";\r\nimport { installProductionPageRoutes } from \"./install-production-page-routes\";\r\nimport {\r\n classifyPageFileChanges,\r\n hasPageFileChanges,\r\n type PageFileChanges,\r\n} from \"./page-file-change\";\r\nimport {\r\n pageRouteSourceFiles,\r\n pageRoutesNeedReplacement,\r\n registeredPageFiles,\r\n} from \"./page-route-reload\";\r\nimport { consumePageManifest, type PageManifest } from \"./page-manifest\";\r\nimport { registerProductionPublicFiles } from \"./register-production-public-files\";\r\nimport { createUnregisteredPageReporter } from \"./unregistered-pages\";\r\nimport { WEB_CONNECTOR_PRIORITY } from \"./web-connector-factory\";\r\n\r\n/**\r\n * Boot/shutdown position relative to core's own connectors.\r\n *\r\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\r\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\r\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\r\n * is \"immediately after http, before everything else\". Two consequences, both\r\n * wanted:\r\n *\r\n * - `boot()` sees a Fastify instance that already has core's plugins and\r\n * health routes on it (`core/src/connectors/http-connector.ts:76`, `:85`).\r\n * - teardown is reverse-priority (`core/src/connectors/connectors-manager.ts:118`),\r\n * so Vite closes BEFORE the HTTP server does, not after.\r\n *\r\n * Note this is a magic number, not a declared dependency: core has no\r\n * `after`/`dependsOn` on the connector interface (`core/src/connectors/types.ts:8-71`).\r\n * Ordering only *needs* to be right for shutdown — `boot()` correctness is\r\n * guaranteed by the phase's boot-all-then-start-all pass regardless of priority.\r\n *\r\n * Declared in `./web-connector-factory` and re-exported here: the lazy delegate\r\n * that `webConnector()` returns must publish `priority` without loading this\r\n * (heavy) module. Importers keep the specifier they already use.\r\n */\r\nexport { WEB_CONNECTOR_PRIORITY };\r\n\r\n/**\r\n * Production boot ran with no page manifest in the registry.\r\n *\r\n * `undefined` from `consumePageManifest()` is a FACT, not an error — the registry never throws\r\n * on absence. The connector supplies the meaning, and it does so from MODE, not\r\n * from the value: in dev the absence is normal because Vite supplies the\r\n * modules and no build has run; in production it means the app was not built\r\n * with web, and a prod server that boots anyway serves 404s while looking\r\n * healthy — the exact silent failure this error exists to prevent.\r\n */\r\n/**\r\n * Which half of the handoff is live: Vite serving from source, or a bundle\r\n * produced by `warlock build`.\r\n *\r\n * RUNTIME STRATEGY, NOT `Application.environment`. The two are explicitly a\r\n * \"separate axis\" (`core/src/utils/environment.ts:4-7`), and the question this\r\n * connector asks — \"is there a page manifest, or does Vite supply the modules?\"\r\n * — is a HOSTING question. `warlock dev` with `NODE_ENV=production` (a staging\r\n * checkout, or just an inherited shell variable) is still Vite-hosted and still\r\n * has no manifest; keying off the environment would make that app refuse to\r\n * boot. Core sets the strategy on both sides deliberately:\r\n * `core/src/cli/commands/dev-server.command.ts:12` declares `\"development\"` for\r\n * `warlock dev`, and the generated production entry declares `\"production\"`\r\n * (`core/src/production/production-builder.ts:245`).\r\n *\r\n * This is core's own connector-level idiom, not a new one:\r\n * `core/src/connectors/http-connector.ts:132` picks `scanDevServer` over `scan`\r\n * the same way.\r\n */\r\nfunction isProductionRuntime(): boolean {\r\n return Application.runtimeStrategy === \"production\";\r\n}\r\n\r\nfunction pageFileVersion(file: string): string {\r\n try {\r\n return `present:${fs.readFileSync(file, \"utf8\")}`;\r\n } catch {\r\n return \"absent\";\r\n }\r\n}\r\n\r\nfunction pageChangeVersions(changes: PageFileChanges): Map<string, string> {\r\n return new Map(\r\n [...changes.added, ...changes.removed, ...changes.inspectionNeeded].map((file) => {\r\n const absoluteFile = path.resolve(file);\r\n return [absoluteFile, pageFileVersion(absoluteFile)] as const;\r\n }),\r\n );\r\n}\r\n\r\nexport class WebPageManifestMissingError extends Error {\r\n public constructor() {\r\n super(\r\n \"WebConnector booted in production without a page manifest. The generated \" +\r\n \"`pages.ts` barrel never ran, so no page modules were handed to the connector \" +\r\n \"and there is nothing to serve. Run `warlock build` with the web connector \" +\r\n \"registered in `warlock.config.ts > connectors`, and start the artifact that \" +\r\n \"build produced.\",\r\n );\r\n this.name = \"WebPageManifestMissingError\";\r\n }\r\n}\r\n\r\n/**\r\n * The manifest carried browser artifacts but no `clientDir`.\r\n *\r\n * The build bakes that field in beside the page table, so the only way to\r\n * observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`\r\n * older than the field, started against a newer runtime. Named rather than\r\n * left to `path.resolve(cwd, undefined)`, which throws a `TypeError` naming\r\n * neither the manifest nor the rebuild that fixes it.\r\n */\r\nexport class WebClientDirMissingError extends Error {\r\n public constructor() {\r\n super(\r\n \"The page manifest carries pages or public files but no `clientDir`, so the \" +\r\n \"connector cannot \" +\r\n \"locate the browser artifacts. This artifact was built by an older @warlock.js/web \" +\r\n \"than the one now running it. Re-run `warlock build` to regenerate the \" +\r\n \"`pages.ts` barrel against the current version.\",\r\n );\r\n this.name = \"WebClientDirMissingError\";\r\n }\r\n}\r\n\r\n/**\r\n * `@fastify/static`'s `maxAge` is milliseconds (the `send` package's option,\r\n * not seconds like `Cache-Control`'s own `max-age`) — one year, matching the\r\n * header this produces: `public, max-age=31536000, immutable`. Safe forever\r\n * because every filename under {@link CLIENT_ASSET_URL_PREFIX} is content-hashed\r\n * by the client build: a changed file is a changed URL, never a changed\r\n * response at the same URL, which is the one condition `immutable` requires.\r\n */\r\nconst HASHED_ASSET_CACHE_MAX_AGE_MS = 31536000 * 1000;\r\n\r\n/**\r\n * Exported so a test can assert the real options this connector hands\r\n * `router.directory` — the same object `boot()` uses below, not a copy a spec\r\n * could drift from unnoticed.\r\n */\r\nexport function productionAssetsDirectoryOptions(clientDir: string) {\r\n return {\r\n root: path.join(clientDir, \"assets\"),\r\n prefix: `${CLIENT_ASSET_URL_PREFIX}/`,\r\n maxAge: HASHED_ASSET_CACHE_MAX_AGE_MS,\r\n immutable: true,\r\n };\r\n}\r\n\r\nexport type WebConnectorOptions = {\r\n /**\r\n * Vite's `root` — the application directory that owns `src/`, `package.json`\r\n * and `tsconfig.json`. Rooting Vite at the APP (not at the `web` package) is\r\n * what makes the app's own bare specifiers and dependency-optimizer scan\r\n * resolve correctly. Defaults to `process.cwd()`, which is where `warlock dev`\r\n * already runs.\r\n */\r\n appRoot?: string;\r\n /** `<appRoot>/src` by default. Pages are `<appSrcRoot>/app/**\\/*.page.tsx`. */\r\n appSrcRoot?: string;\r\n /** The single global app-root file. `<appSrcRoot>/web/root.tsx` by default. */\r\n appFile?: string;\r\n /**\r\n * Root of the `@warlock.js/web` package, used to locate the hydration client\r\n * entry. Derived from this module's own location by default — a caller only\r\n * sets it when the package is not laid out normally.\r\n */\r\n webRoot?: string;\r\n /**\r\n * Extra `resolve.alias` entries, prepended to the app-convention aliases\r\n * (`web/*` → `src/web`, `app/*` → `src/app`) so a caller can win a conflict.\r\n * A normal application needs none of these; a monorepo checkout with unbuilt\r\n * workspace packages does.\r\n */\r\n resolveAlias?: Alias[];\r\n /** Extra `ssr.external` entries, appended to {@link CORE_OPTIONAL_PEERS}. */\r\n ssrExternal?: string[];\r\n /** Extra Vite plugins, appended after the client-boundary gates. */\r\n plugins?: PluginOption[];\r\n};\r\n\r\n/**\r\n * Web Connector\r\n * Manages the Vite dev server and the SSR page routes, mounted on the HTTP\r\n * connector's Fastify instance.\r\n */\r\nexport class WebConnector extends BaseConnector {\r\n public readonly name: ConnectorName = \"web\";\r\n public readonly priority = WEB_CONNECTOR_PRIORITY;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Nothing. Core already supplies each watcher batch to `shouldRestart`; page\r\n * membership and route identity are classified there, while component and\r\n * layout body edits remain Vite's HMR domain.\r\n */\r\n protected readonly watchedFiles: string[] = [];\r\n\r\n protected readonly options: WebConnectorOptions;\r\n\r\n protected vite?: ViteDevServer;\r\n\r\n protected installedPages: InstalledPageRoute[] = [];\r\n\r\n /**\r\n * The build→runtime handoff table, read once at boot.\r\n *\r\n * `undefined` in dev is the normal case and carries no meaning beyond \"no\r\n * build has run\" — see {@link WebPageManifestMissingError} for why the\r\n * production reading is a hard error and why the branch is on MODE.\r\n */\r\n protected pageManifest?: PageManifest;\r\n\r\n /**\r\n * The paths the DEVELOPMENT boot resolved, kept so `shouldRestart` can decide\r\n * whether a changed file is a page without re-deriving (and re-proving) the\r\n * web package root on every watcher batch. `undefined` in production and\r\n * before boot, which is exactly when `shouldRestart` must answer `false`.\r\n */\r\n protected resolvedPaths?: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>;\r\n\r\n /** Synchronous watcher classification handed to the async reload phase. */\r\n protected pendingPageChanges?: PageFileChanges;\r\n\r\n /** Reuses Vite's pipeline-barrel module instance for every dev reinstall. */\r\n protected installDevPageRoutes?: () => Promise<InstalledPageRoute[]>;\r\n\r\n /** Serializes Vite and core watcher callbacks that can observe the same edit. */\r\n protected pageRouteReloadQueue: Promise<unknown> = Promise.resolve();\r\n\r\n /** Committed file versions awaiting the overlapping watcher callback. */\r\n protected pendingHotUpdateSuppressions = new Map<string, string>();\r\n\r\n public constructor(options: WebConnectorOptions = {}) {\r\n super();\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Boot the connector — wire the page pipeline's request context and register\r\n * every page on the router.\r\n *\r\n * Two ways of doing that, one per hosting mode, and they share the shape\r\n * rather than the mechanism. Development creates Vite in middleware mode,\r\n * mounts it on the HTTP connector's Fastify instance and discovers pages by\r\n * walking `app/`; production takes both answers from the manifest the build\r\n * handed over (`./install-production-page-routes`) and touches no Vite at all.\r\n *\r\n * Everything here happens BEFORE `HttpConnector.start()` scans and listens,\r\n * which is the entire reason this is a `Late` connector's `boot()` and not its\r\n * `start()`.\r\n */\r\n public async boot() {\r\n // THE MODE BRANCH. One `if`, and it reads the mode — never the value.\r\n // `consumePageManifest()` returning `undefined` must not mean two different\r\n // things at one call site, so the only\r\n // question asked of the value here is \"is it there\", and the only thing\r\n // that decides whether that matters is {@link isProductionRuntime}.\r\n this.pageManifest = consumePageManifest();\r\n\r\n if (isProductionRuntime() && !this.pageManifest) {\r\n throw new WebPageManifestMissingError();\r\n }\r\n\r\n // The manifest is guaranteed present by the guard above; naming it again is\r\n // what narrows the type, not a second check of the same condition.\r\n if (isProductionRuntime() && this.pageManifest) {\r\n // Gated on `clientDir`, never on `publicFiles.length`: a zero-page,\r\n // zero-public-file build legitimately carries no `clientDir` at all (see\r\n // `resolveClientDir`'s note), and calling this without one would demand\r\n // a client build that had no reason to exist. But once a `clientDir` IS\r\n // present — a client build actually ran — registering an EMPTY\r\n // `publicFiles` is free (the loop inside is a no-op) and the staleness\r\n // check this runs (`warnIfPublicBuildIsStale`) is most needed exactly\r\n // here: an app that shipped a build with NO public files still needs a\r\n // file added to `public/` afterwards to be caught, not 404 in silence\r\n // because a length check skipped the call that would have named it.\r\n if (this.pageManifest.clientDir !== undefined) {\r\n registerProductionPublicFiles(\r\n router,\r\n this.resolveClientDir(),\r\n this.pageManifest.publicFiles ?? [],\r\n );\r\n }\r\n\r\n this.installedPages = await installProductionPageRoutes({\r\n router,\r\n manifest: this.pageManifest,\r\n pageContext: requestContext,\r\n sharedStore: () => requestContext.getStore(),\r\n // The URL is resolved lazily, by the production path, only if there are\r\n // pages to hydrate — see the option's own note.\r\n resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),\r\n // The stylesheets are read from the manifest in this directory, by the\r\n // installer itself — it already imports the barrel that owns that\r\n // reader, and production has one module graph, so resolving there\r\n // rather than here avoids loading the barrel twice.\r\n clientDir: this.pageManifest.clientDir,\r\n });\r\n\r\n // SERVE THE CLIENT BUNDLE. Without this the whole production page path\r\n // completes and still ships a dead page: the SSR HTML carries\r\n // `<script type=\"module\" src=\"/assets/hydration-<hash>.js\">`, that request\r\n // 404s, and React never takes over. Nothing else in the process serves\r\n // that directory — `CLIENT_ASSET_URL_PREFIX` was, until now, only ever\r\n // read to VALIDATE the URL written into the HTML, never to mount the\r\n // files it points at.\r\n //\r\n // In `boot()` rather than `start()` because the router registers static\r\n // directories during its SCAN, and the scan is `HttpConnector.start()` —\r\n // which every `boot()` precedes. Registering in `start()` would be a\r\n // no-op that looked correct.\r\n //\r\n // Dev needs no equivalent: Vite's middleware serves the module graph\r\n // itself, which is why this sits inside the production branch and not\r\n // above it.\r\n //\r\n // Gated on the page count for the same reason the build's hydration\r\n // client step is: a zero-page build produces no `assets/` bundle to\r\n // mount. Such a build may still carry `clientDir` for copied public\r\n // files, which were registered individually above rather than exposing\r\n // this directory wholesale.\r\n if (this.pageManifest.pages.length > 0) {\r\n router.directory(productionAssetsDirectoryOptions(this.resolveClientDir()));\r\n }\r\n\r\n return;\r\n }\r\n\r\n // Everything below this line is the Vite-hosted development path. Nothing\r\n // above it touches Vite: it is an optional peer, so a production install\r\n // does not carry it, and this method is the only place the two halves meet.\r\n //\r\n // Fastify is required by the DEVELOPMENT path alone, and the guard sits\r\n // here rather than above the branch for that reason: production mounts no\r\n // middleware and needs no HMR socket, it registers page routes on the\r\n // router and `HttpConnector.start()` scans them like any other route.\r\n const fastify = this.resolveFastify();\r\n const paths = await this.resolvePaths();\r\n\r\n // Kept for `shouldRestart`, which is asked of this connector on every\r\n // watcher batch and must answer without re-resolving anything.\r\n this.resolvedPaths = paths;\r\n\r\n this.vite = await this.createViteServer(fastify, paths);\r\n\r\n // The pipeline barrel is loaded THROUGH VITE, not imported directly, and\r\n // that is load-bearing: page modules are evaluated inside Vite's SSR module\r\n // graph, so `connectSharedStore`/`connectPageContext` must be called on\r\n // VITE's instance of those modules. A plain Node `import` here would wire a\r\n // second, unrelated module instance and every page would render with an\r\n // empty shared store.\r\n const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);\r\n\r\n webServerSsr.connectSharedStore(() => requestContext.getStore());\r\n webServerSsr.connectPageContext(requestContext);\r\n\r\n // Vite's `middlewares` is a plain Connect `(req, res, next)` stack and\r\n // Fastify's `request.raw`/`reply.raw` ARE node's `req`/`res`, so an\r\n // `onRequest` hook mounts it with no plugin at all — `@fastify/middie` is\r\n // not needed and is not a core dependency (`core/package.json`). Vite never\r\n // fronts the server: it either answers its own asset request or calls\r\n // `done()` and Warlock's router owns the response.\r\n //\r\n // ONE EXCEPTION, and it is why `done` is wrapped rather than passed\r\n // straight through: vite reaches this callback for TWO different reasons in\r\n // middleware mode — \"not mine\" and \"mine, and it failed\". The second one\r\n // arrives indistinguishable from the first, because vite's own error\r\n // handler logs the failure and then calls `next()` with the error cleared\r\n // (`node_modules/vite/dist/node/chunks/config.js:9525-9527`). Handing that\r\n // to the framework produced an empty 404 on a module that exists — via the\r\n // app's catch-all page route (`./render-page.ts:604`,\r\n // `./create-page-route-handler.ts:147`) — and threw the only useful\r\n // explanation away. `devErrorTransportPlugin` captures it upstream; this\r\n // reads it back.\r\n // Dev-only on both sides — nothing below this line runs in production.\r\n fastify.addHook(\r\n \"onRequest\",\r\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n this.vite?.middlewares(request.raw, reply.raw, (error?: Error) => {\r\n if (sendCapturedDevError(request.raw, reply.raw)) return;\r\n\r\n done(error);\r\n });\r\n },\r\n );\r\n\r\n const reportUnregisteredPages = createUnregisteredPageReporter({\r\n appRoot: paths.appRoot,\r\n appSrcRoot: paths.appSrcRoot,\r\n registeredPageFiles: () => registeredPageFiles(router.list(), paths.appSrcRoot),\r\n });\r\n\r\n fastify.addHook(\r\n \"onResponse\",\r\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n if (reply.statusCode === 404) {\r\n reportUnregisteredPages({\r\n method: request.method,\r\n url: request.url,\r\n pathname: new URL(request.url, \"http://warlock.local\").pathname,\r\n });\r\n }\r\n\r\n done();\r\n },\r\n );\r\n\r\n this.installDevPageRoutes = () =>\r\n webServerSsr.installPageRoutes({\r\n router,\r\n vite: this.vite,\r\n appSrcRoot: paths.appSrcRoot,\r\n appFile: paths.appFile,\r\n hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),\r\n // Without these the first paint of every full page load is unstyled: the\r\n // client bundle imports the CSS, so JavaScript applies it only after the\r\n // module graph loads. A render-blocking <link> in <head> is what makes\r\n // the page arrive styled instead of arriving and then correcting itself.\r\n stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile),\r\n // Resolved here, on the NODE side, and forwarded — see\r\n // `InstallPageRoutesOptions.httpServer` (`install-page-routes.ts`) for\r\n // why `createPageRouteHandler` cannot read this out of the container\r\n // itself from inside Vite's SSR module graph. `fastify` is this same\r\n // request's `resolveFastify()` result, already in scope above.\r\n httpServer: fastify,\r\n });\r\n\r\n this.installedPages = await this.installDevPageRoutes();\r\n }\r\n\r\n /**\r\n * Where the browser fetches the hydration entry from — the one line that\r\n * differs between the two modes, so it is the only thing that branches.\r\n *\r\n * Dev keeps Vite's `/@fs/` URL, which Vite's own middleware transforms on\r\n * demand. Production reads the hashed filename out of the client build's\r\n * `.vite/manifest.json` exactly once, at boot, and NEVER falls back: each way\r\n * that read can fail is its own named error (`./hydration-client-url.ts:24`,\r\n * `:36`, `:53`), because \"serve without hydration\" would be a page that\r\n * renders and then does nothing.\r\n *\r\n * `webRoot` is OPTIONAL because only the dev branch has any use for it, and\r\n * asking production for it would be worse than useless: it is proved by\r\n * reading `<root>/package.json` off disk, which is exactly the kind of\r\n * source-tree lookup a built artifact must never depend on. A dev boot that\r\n * somehow reaches here without one is refused by name by the entry factory.\r\n */\r\n protected resolveHydrationClientModuleUrl(webRoot?: string): string {\r\n if (isProductionRuntime()) {\r\n return resolveHydrationClientUrl({ clientDir: this.resolveClientDir() });\r\n }\r\n\r\n return createHydrationClientEntry(webRoot ?? \"\").devUrl;\r\n }\r\n\r\n /**\r\n * `<outdir>/client` — the layout the build half writes and this half reads\r\n * at boot, taken from the value the build BAKED into the page manifest.\r\n *\r\n * It used to call `resolveBuildConfig()`, which reads `warlock.config.ts`\r\n * through `warlockConfigManager`. That works in `warlock build` and in the\r\n * `warlock start` SUPERVISOR, and it cannot work here: the supervisor spawns\r\n * a plain `node dist/app.js` CHILD, and that process never loads — and could\r\n * not load — a TypeScript build-time config. The call threw\r\n * `WarlockConfig not loaded` inside connector boot, so the production server\r\n * died before it ever listened.\r\n *\r\n * Baking it also settles the drift the old comment was worried about, and\r\n * settles it harder: `build` and `start` cannot disagree about where the\r\n * bundle lives, because `start` is no longer re-deriving the path at all —\r\n * it reads back the one string `build` wrote.\r\n */\r\n protected resolveClientDir(): string {\r\n // The manifest CACHED at boot (line ~269), not a second `consumePageManifest()`:\r\n // this runs from a callback the production branch invokes lazily, long after\r\n // that assignment, and reading the same field the mode branch already\r\n // decided on keeps one source of truth for the boot's view of the manifest.\r\n const clientDir = this.pageManifest?.clientDir;\r\n\r\n if (clientDir === undefined) {\r\n // Reached only via a manifest with browser artifacts but no `clientDir`\r\n // — i.e. a bundle built by a web version older than this field. Named here\r\n // rather than left to surface as an ENOENT on a `path.join(undefined)`\r\n // deep inside the manifest read.\r\n throw new WebClientDirMissingError();\r\n }\r\n\r\n return path.resolve(process.cwd(), clientDir);\r\n }\r\n\r\n /**\r\n * The page manifest this connector consumed at boot, or `undefined` in dev\r\n * where Vite supplies the modules instead.\r\n */\r\n public getPageManifest(): PageManifest | undefined {\r\n return this.pageManifest;\r\n }\r\n\r\n /**\r\n * Activate. There is nothing to listen on — `HttpConnector.start()` owns the\r\n * single `listen()` for the whole process — so this only marks the connector\r\n * live once `boot()` has wired everything.\r\n */\r\n public async start(): Promise<void> {\r\n if (!this.vite) return;\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Shutdown — close Vite, and drop the sockets Vite's middleware left behind.\r\n *\r\n * Reverse-priority teardown (`core/src/connectors/connectors-manager.ts:118`)\r\n * puts this BEFORE `HttpConnector.shutdown()`, which is exactly what the\r\n * second call needs: requests answered by Vite's connect stack are written\r\n * straight to `reply.raw`, so Fastify never observes them completing and their\r\n * keep-alive sockets are never counted idle. Core's default\r\n * `forceCloseConnections: \"idle\"` (`core/src/http/server.ts:34`) then waits on\r\n * them forever. A dev server has no draining obligation, and the connector\r\n * that caused the raw writes is the right one to clean up after them.\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) return;\r\n\r\n if (container.has(\"http.server\")) {\r\n container.get(\"http.server\").server.closeAllConnections();\r\n }\r\n\r\n await this.vite?.close();\r\n this.vite = undefined;\r\n this.installedPages = [];\r\n this.installDevPageRoutes = undefined;\r\n this.pendingPageChanges = undefined;\r\n this.pendingHotUpdateSuppressions.clear();\r\n this.pageManifest = undefined;\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Queue page add/remove/edit candidates for asynchronous live routing work.\r\n * Classification stays synchronous because core's connector interface is;\r\n * edited route exports are evaluated later through Vite's fresh SSR graph.\r\n */\r\n public shouldRestart(changedFiles: string[] = []): boolean {\r\n if (this.vite === undefined || this.resolvedPaths === undefined) {\r\n return false;\r\n }\r\n\r\n this.pendingPageChanges = this.classifyPageChanges(changedFiles);\r\n return this.pendingPageChanges !== undefined;\r\n }\r\n\r\n protected classifyPageChanges(changedFiles: readonly string[]): PageFileChanges | undefined {\r\n if (this.resolvedPaths === undefined) return undefined;\r\n\r\n const changes = classifyPageFileChanges(changedFiles, {\r\n appRoot: this.resolvedPaths.appRoot,\r\n appSrcRoot: this.resolvedPaths.appSrcRoot,\r\n installedPageFiles: registeredPageFiles(router.list(), this.resolvedPaths.appSrcRoot),\r\n });\r\n\r\n return hasPageFileChanges(changes) ? changes : undefined;\r\n }\r\n\r\n protected enqueuePageRouteReload(changes: PageFileChanges): Promise<boolean> {\r\n const eventVersions = pageChangeVersions(changes);\r\n const run = this.pageRouteReloadQueue\r\n .catch(() => undefined)\r\n .then(async () => {\r\n const vite = this.vite;\r\n const install = this.installDevPageRoutes;\r\n const paths = this.resolvedPaths;\r\n\r\n if (vite === undefined || install === undefined || paths === undefined) return false;\r\n\r\n const matchingCommittedFiles = new Set<string>();\r\n\r\n // This check belongs inside the queue: a matching core transaction may\r\n // commit while a Vite callback is waiting behind it. File versions are\r\n // captured when the job is queued so a later edit cannot consume an\r\n // earlier event's marker.\r\n for (const [file, eventVersion] of eventVersions) {\r\n const committedVersion = this.pendingHotUpdateSuppressions.get(file);\r\n if (committedVersion === undefined) continue;\r\n\r\n // Matching markers are consumed exactly once. A mismatched marker is\r\n // obsolete and must not survive to suppress a future reverted edit.\r\n this.pendingHotUpdateSuppressions.delete(file);\r\n if (committedVersion === eventVersion) matchingCommittedFiles.add(file);\r\n }\r\n\r\n if (eventVersions.size > 0 && matchingCommittedFiles.size === eventVersions.size) {\r\n return true;\r\n }\r\n\r\n const replace = await pageRoutesNeedReplacement(changes, {\r\n vite,\r\n appSrcRoot: paths.appSrcRoot,\r\n installedPages: this.installedPages,\r\n });\r\n\r\n if (!replace) return false;\r\n\r\n const nextInstalledPages = await router.replaceRoutesBySourceFiles(\r\n pageRouteSourceFiles(router.list()),\r\n install,\r\n );\r\n\r\n // Advance observable state only after the router transaction commits.\r\n // A rejected install keeps both the old route table and browser live.\r\n this.installedPages = nextInstalledPages;\r\n invalidateClientPageRegistry(vite);\r\n\r\n for (const [file, eventVersion] of eventVersions) {\r\n if (!matchingCommittedFiles.has(file)) {\r\n this.pendingHotUpdateSuppressions.set(file, eventVersion);\r\n }\r\n }\r\n\r\n return true;\r\n });\r\n\r\n this.pageRouteReloadQueue = run;\r\n return run;\r\n }\r\n\r\n /**\r\n * Vite-side ordering barrier. It publishes a changed route graph before the\r\n * page-registry plugin can reload the document; a matching change already\r\n * handled by core is consumed once, preventing a duplicate full reload.\r\n */\r\n protected async handlePageHotUpdate(file: string): Promise<boolean> {\r\n const absoluteFile = path.resolve(file);\r\n const changes = this.classifyPageChanges([absoluteFile]);\r\n if (changes === undefined) return false;\r\n\r\n return this.enqueuePageRouteReload(changes);\r\n }\r\n\r\n /**\r\n * Live page routing update. Despite the connector API name, this never closes\r\n * Vite: it atomically replaces page-owned routes only when membership or the\r\n * canonical route identity changed, then refreshes the client registry.\r\n */\r\n public async restart(): Promise<void> {\r\n const changes = this.pendingPageChanges;\r\n this.pendingPageChanges = undefined;\r\n\r\n if (\r\n changes === undefined ||\r\n this.resolvedPaths === undefined ||\r\n this.vite === undefined ||\r\n this.installDevPageRoutes === undefined\r\n ) {\r\n return;\r\n }\r\n\r\n await this.enqueuePageRouteReload(changes);\r\n }\r\n\r\n /** The pages this connector registered on the router, in registration order. */\r\n public getInstalledPages(): readonly InstalledPageRoute[] {\r\n return this.installedPages;\r\n }\r\n\r\n /**\r\n * The Fastify instance the HTTP connector published during its own `boot()`\r\n * (`core/src/connectors/http-connector.ts:74`).\r\n *\r\n * Absence is fatal rather than a silent no-op: unlike sockets, there is no\r\n * standalone fallback a page surface could serve from, and the failure this\r\n * guards against — an app with no `src/config/http.ts` — otherwise shows up\r\n * as every page 404ing with no explanation.\r\n */\r\n protected resolveFastify(): FastifyInstance {\r\n if (!container.has(\"http.server\")) {\r\n throw new Error(\r\n \"WebConnector requires the HTTP connector's Fastify instance, but \" +\r\n \"`http.server` is not in the container. The `http` config is missing — add `src/config/http.ts` \" +\r\n \"so `HttpConnector.boot()` runs (core/src/connectors/http-connector.ts:61-74).\",\r\n );\r\n }\r\n\r\n return container.get(\"http.server\");\r\n }\r\n\r\n /** Resolve every path this connector needs from the (optional) options. */\r\n protected async resolvePaths() {\r\n const appRoot = this.options.appRoot ?? process.cwd();\r\n const appSrcRoot = this.options.appSrcRoot ?? path.join(appRoot, \"src\");\r\n const selfPath = fileURLToPath(import.meta.url);\r\n // The web package root goes through `resolveWebPackageRoot`, which PROVES\r\n // the directory by reading `<root>/package.json` and matching its `name`,\r\n // rather than trusting a fixed number of `..` hops. A configured root is\r\n // asserted the same way. Either failure throws\r\n // `WebPackageRootResolutionError` naming the directory at boot — the\r\n // alternative was a wrong root surfacing much later as a 404 on the\r\n // hydration entry with nothing to point at.\r\n const webRoot = await resolveWebPackageRoot(this.options.webRoot);\r\n\r\n return {\r\n appRoot,\r\n appSrcRoot,\r\n appFile: this.options.appFile ?? path.join(appSrcRoot, \"web/root.tsx\"),\r\n webRoot,\r\n // Extension-agnostic on purpose: the sibling barrel is `index.ts` when\r\n // this package runs from source and `index.js` once it is built, the same\r\n // trick `registerLoader` uses when it resolves its own siblings.\r\n webServerBarrel: path.join(path.dirname(selfPath), `index${path.extname(selfPath)}`),\r\n };\r\n }\r\n\r\n protected async createViteServer(\r\n fastify: FastifyInstance,\r\n paths: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>,\r\n ): Promise<ViteDevServer> {\r\n const { createServer, buildErrorMessage, searchForWorkspaceRoot } = await import(\"vite\");\r\n\r\n // Vite's own default for `server.fs.allow`, reproduced rather than dropped:\r\n // naming the key at all REPLACES the default, and an application that\r\n // legitimately serves files from above its own root has to keep working.\r\n const workspaceRoot = searchForWorkspaceRoot(paths.appRoot);\r\n\r\n return createServer(\r\n await createWebConnectorViteConfig({\r\n appRoot: paths.appRoot,\r\n appSrcRoot: paths.appSrcRoot,\r\n webRoot: paths.webRoot,\r\n workspaceRoot,\r\n hmrServer: fastify.server,\r\n handlePageHotUpdate: (file) => this.handlePageHotUpdate(file),\r\n leadingPlugins: [devErrorTransportPlugin({ isProductionRuntime, buildErrorMessage })],\r\n resolveAlias: this.options.resolveAlias,\r\n ssrExternal: this.options.ssrExternal,\r\n plugins: this.options.plugins,\r\n }),\r\n );\r\n }\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"web-connector.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector.ts"],"sourcesContent":["/**\r\n * `WebConnector` — the SSR page surface as a first-class Warlock connector.\r\n *\r\n * It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React\r\n * pages on one port, and `web` no longer owns a private copy of the HTTP\r\n * lifecycle. Everything this file does used to live in `startDevServer()`\r\n * (`web/src/server/dev-error-transport.ts`), which created its own Fastify instance,\r\n * scanned the router and called `listen()` itself — three responsibilities core\r\n * already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.\r\n *\r\n * WHY A `Late` CONNECTOR IS THE RIGHT SEAM, in ordering terms:\r\n * `ConnectorsManager.startPhase` runs **every** `boot()` in a phase before\r\n * **any** `start()` (`core/src/connectors/connectors-manager.ts:87-93`).\r\n * `HttpConnector` is itself `Late` (`core/src/connectors/http-connector.ts:41`)\r\n * and publishes its Fastify instance during its own `boot()`\r\n * (`container.set(\"http.server\", …)`, `core/src/connectors/http-connector.ts:74`).\r\n * So by the time this connector's `boot()` runs, Fastify and its plugins exist,\r\n * the raw node server exists, and NOTHING has been scanned or bound yet — page\r\n * routes registered here are picked up by `HttpConnector.start()`'s\r\n * `router.scanDevServer(…)` (`core/src/connectors/http-connector.ts:133`) before\r\n * `listen()` (`:147`). `SocketConnector.boot()` reads the same container key the\r\n * same way (`core/src/connectors/socket-connector.ts:78-80`) — this file is\r\n * deliberately shaped after it.\r\n *\r\n * What it can NOT do, and why that is fine: route COLLECTION happens earlier\r\n * (`core/src/dev-server/development-server.ts:57` precedes `:66`), so pages are\r\n * not discovered by the framework's file scanner. They are discovered here, by\r\n * `installPageRoutes` (`./install-page-routes.ts:189`), and registered through\r\n * the ordinary `router.get(…)` API — there is no second server matcher.\r\n *\r\n * DELIBERATE EXCEPTION to A.3 §2 (\"web has no core dependency\"), the same one\r\n * `./dev-error-transport.ts` and `./install-page-routes.ts` record in their own headers:\r\n * this module is not re-exported from any package barrel (`web/src/index.ts`,\r\n * `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of\r\n * `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only\r\n * ever imported by tooling that already depends on core.\r\n *\r\n * `@warlock.js/web/connector` reaches this class ONLY through\r\n * `./web-connector-factory.ts`'s `await import(\"./web-connector\")` — a\r\n * deliberate seam, because a static edge from that barrel to this file would\r\n * put `../vite`, core's router and `./dev-error-transport` into the import graph of\r\n * every consuming app's `warlock.config.ts`.\r\n */\r\nimport fs from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport type { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from \"fastify\";\r\nimport type { Alias, PluginOption, ViteDevServer } from \"vite\";\r\nimport {\r\n Application,\r\n BaseConnector,\r\n ConnectorLifecyclePhase,\r\n type ConnectorName,\r\n container,\r\n type FastifyInstance,\r\n requestContext,\r\n router,\r\n} from \"@warlock.js/core\";\r\nimport { resolveWebPackageRoot } from \"../build/contribution\";\r\nimport { createHydrationClientEntry, invalidateClientPageRegistry } from \"../vite\";\r\nimport { createWebConnectorViteConfig } from \"../vite/dev-server-config\";\r\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\r\nimport { devErrorTransportPlugin, sendCapturedDevError } from \"./dev-error-transport\";\r\nimport { resolveHydrationClientUrl } from \"./hydration-client-url\";\r\nimport type { InstalledPageRoute } from \"./install-page-routes\";\r\nimport { installProductionPageRoutes } from \"./install-production-page-routes\";\r\nimport {\r\n classifyPageFileChanges,\r\n hasPageFileChanges,\r\n type PageFileChanges,\r\n} from \"./page-file-change\";\r\nimport {\r\n pageRouteSourceFiles,\r\n pageRoutesNeedReplacement,\r\n registeredPageFiles,\r\n} from \"./page-route-reload\";\r\nimport { consumePageManifest, type PageManifest } from \"./page-manifest\";\r\nimport { registerProductionPublicFiles } from \"./register-production-public-files\";\r\nimport { createUnregisteredPageReporter } from \"./unregistered-pages\";\r\nimport { WEB_CONNECTOR_PRIORITY } from \"./web-connector-factory\";\r\n\r\n/**\r\n * Boot/shutdown position relative to core's own connectors.\r\n *\r\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\r\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\r\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\r\n * is \"immediately after http, before everything else\". Two consequences, both\r\n * wanted:\r\n *\r\n * - `boot()` sees a Fastify instance that already has core's plugins and\r\n * health routes on it (`core/src/connectors/http-connector.ts:76`, `:85`).\r\n * - teardown is reverse-priority (`core/src/connectors/connectors-manager.ts:118`),\r\n * so Vite closes BEFORE the HTTP server does, not after.\r\n *\r\n * Note this is a magic number, not a declared dependency: core has no\r\n * `after`/`dependsOn` on the connector interface (`core/src/connectors/types.ts:8-71`).\r\n * Ordering only *needs* to be right for shutdown — `boot()` correctness is\r\n * guaranteed by the phase's boot-all-then-start-all pass regardless of priority.\r\n *\r\n * Declared in `./web-connector-factory` and re-exported here: the lazy delegate\r\n * that `webConnector()` returns must publish `priority` without loading this\r\n * (heavy) module. Importers keep the specifier they already use.\r\n */\r\nexport { WEB_CONNECTOR_PRIORITY };\r\n\r\n/**\r\n * Production boot ran with no page manifest in the registry.\r\n *\r\n * `undefined` from `consumePageManifest()` is a FACT, not an error — the registry never throws\r\n * on absence. The connector supplies the meaning, and it does so from MODE, not\r\n * from the value: in dev the absence is normal because Vite supplies the\r\n * modules and no build has run; in production it means the app was not built\r\n * with web, and a prod server that boots anyway serves 404s while looking\r\n * healthy — the exact silent failure this error exists to prevent.\r\n */\r\n/**\r\n * Which half of the handoff is live: Vite serving from source, or a bundle\r\n * produced by `warlock build`.\r\n *\r\n * RUNTIME STRATEGY, NOT `Application.environment`. The two are explicitly a\r\n * \"separate axis\" (`core/src/utils/environment.ts:4-7`), and the question this\r\n * connector asks — \"is there a page manifest, or does Vite supply the modules?\"\r\n * — is a HOSTING question. `warlock dev` with `NODE_ENV=production` (a staging\r\n * checkout, or just an inherited shell variable) is still Vite-hosted and still\r\n * has no manifest; keying off the environment would make that app refuse to\r\n * boot. Core sets the strategy on both sides deliberately:\r\n * `core/src/cli/commands/dev-server.command.ts:12` declares `\"development\"` for\r\n * `warlock dev`, and the generated production entry declares `\"production\"`\r\n * (`core/src/production/production-builder.ts:245`).\r\n *\r\n * This is core's own connector-level idiom, not a new one:\r\n * `core/src/connectors/http-connector.ts:132` picks `scanDevServer` over `scan`\r\n * the same way.\r\n */\r\nfunction isProductionRuntime(): boolean {\r\n return Application.runtimeStrategy === \"production\";\r\n}\r\n\r\nfunction pageFileVersion(file: string): string {\r\n try {\r\n return `present:${fs.readFileSync(file, \"utf8\")}`;\r\n } catch {\r\n return \"absent\";\r\n }\r\n}\r\n\r\nfunction pageChangeVersions(changes: PageFileChanges): Map<string, string> {\r\n return new Map(\r\n [...changes.added, ...changes.removed, ...changes.inspectionNeeded].map((file) => {\r\n const absoluteFile = path.resolve(file);\r\n return [absoluteFile, pageFileVersion(absoluteFile)] as const;\r\n }),\r\n );\r\n}\r\n\r\nexport class WebPageManifestMissingError extends Error {\r\n public constructor() {\r\n super(\r\n \"WebConnector booted in production without a page manifest. The generated \" +\r\n \"`pages.ts` barrel never ran, so no page modules were handed to the connector \" +\r\n \"and there is nothing to serve. Run `warlock build` with the web connector \" +\r\n \"registered in `warlock.config.ts > connectors`, and start the artifact that \" +\r\n \"build produced.\",\r\n );\r\n this.name = \"WebPageManifestMissingError\";\r\n }\r\n}\r\n\r\n/**\r\n * The manifest carried browser artifacts but no `clientDir`.\r\n *\r\n * The build bakes that field in beside the page table, so the only way to\r\n * observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`\r\n * older than the field, started against a newer runtime. Named rather than\r\n * left to `path.resolve(cwd, undefined)`, which throws a `TypeError` naming\r\n * neither the manifest nor the rebuild that fixes it.\r\n */\r\nexport class WebClientDirMissingError extends Error {\r\n public constructor() {\r\n super(\r\n \"The page manifest carries pages or public files but no `clientDir`, so the \" +\r\n \"connector cannot \" +\r\n \"locate the browser artifacts. This artifact was built by an older @warlock.js/web \" +\r\n \"than the one now running it. Re-run `warlock build` to regenerate the \" +\r\n \"`pages.ts` barrel against the current version.\",\r\n );\r\n this.name = \"WebClientDirMissingError\";\r\n }\r\n}\r\n\r\n/**\r\n * `@fastify/static`'s `maxAge` is milliseconds (the `send` package's option,\r\n * not seconds like `Cache-Control`'s own `max-age`) — one year, matching the\r\n * header this produces: `public, max-age=31536000, immutable`. Safe forever\r\n * because every filename under {@link CLIENT_ASSET_URL_PREFIX} is content-hashed\r\n * by the client build: a changed file is a changed URL, never a changed\r\n * response at the same URL, which is the one condition `immutable` requires.\r\n */\r\nconst HASHED_ASSET_CACHE_MAX_AGE_MS = 31536000 * 1000;\r\n\r\n/**\r\n * Exported so a test can assert the real options this connector hands\r\n * `router.directory` — the same object `boot()` uses below, not a copy a spec\r\n * could drift from unnoticed.\r\n */\r\nexport function productionAssetsDirectoryOptions(clientDir: string) {\r\n return {\r\n root: path.join(clientDir, \"assets\"),\r\n prefix: `${CLIENT_ASSET_URL_PREFIX}/`,\r\n maxAge: HASHED_ASSET_CACHE_MAX_AGE_MS,\r\n immutable: true,\r\n };\r\n}\r\n\r\nexport type WebConnectorOptions = {\r\n /**\r\n * Vite's `root` — the application directory that owns `src/`, `package.json`\r\n * and `tsconfig.json`. Rooting Vite at the APP (not at the `web` package) is\r\n * what makes the app's own bare specifiers and dependency-optimizer scan\r\n * resolve correctly. Defaults to `process.cwd()`, which is where `warlock dev`\r\n * already runs.\r\n */\r\n appRoot?: string;\r\n /** `<appRoot>/src` by default. Pages are `<appSrcRoot>/app/**\\/*.page.tsx`. */\r\n appSrcRoot?: string;\r\n /** The single global app-root file. `<appSrcRoot>/web/root.tsx` by default. */\r\n appFile?: string;\r\n /**\r\n * Root of the `@warlock.js/web` package, used to locate the hydration client\r\n * entry. Derived from this module's own location by default — a caller only\r\n * sets it when the package is not laid out normally.\r\n */\r\n webRoot?: string;\r\n /**\r\n * Extra `resolve.alias` entries, prepended to the app-convention aliases\r\n * (`web/*` → `src/web`, `app/*` → `src/app`) so a caller can win a conflict.\r\n * A normal application needs none of these; a monorepo checkout with unbuilt\r\n * workspace packages does.\r\n */\r\n resolveAlias?: Alias[];\r\n /** Extra `ssr.external` entries, appended to {@link CORE_OPTIONAL_PEERS}. */\r\n ssrExternal?: string[];\r\n /** Extra Vite plugins, appended after the client-boundary gates. */\r\n plugins?: PluginOption[];\r\n};\r\n\r\n/**\r\n * Web Connector\r\n * Manages the Vite dev server and the SSR page routes, mounted on the HTTP\r\n * connector's Fastify instance.\r\n */\r\nexport class WebConnector extends BaseConnector {\r\n public readonly name: ConnectorName = \"web\";\r\n public readonly priority = WEB_CONNECTOR_PRIORITY;\r\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\r\n\r\n /**\r\n * Nothing. Core already supplies each watcher batch to `shouldRestart`; page\r\n * membership and route identity are classified there, while component and\r\n * layout body edits remain Vite's HMR domain.\r\n */\r\n protected readonly watchedFiles: string[] = [];\r\n\r\n protected readonly options: WebConnectorOptions;\r\n\r\n protected vite?: ViteDevServer;\r\n\r\n protected installedPages: InstalledPageRoute[] = [];\r\n\r\n /**\r\n * The build→runtime handoff table, read once at boot.\r\n *\r\n * `undefined` in dev is the normal case and carries no meaning beyond \"no\r\n * build has run\" — see {@link WebPageManifestMissingError} for why the\r\n * production reading is a hard error and why the branch is on MODE.\r\n */\r\n protected pageManifest?: PageManifest;\r\n\r\n /**\r\n * The paths the DEVELOPMENT boot resolved, kept so `shouldRestart` can decide\r\n * whether a changed file is a page without re-deriving (and re-proving) the\r\n * web package root on every watcher batch. `undefined` in production and\r\n * before boot, which is exactly when `shouldRestart` must answer `false`.\r\n */\r\n protected resolvedPaths?: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>;\r\n\r\n /** Synchronous watcher classification handed to the async reload phase. */\r\n protected pendingPageChanges?: PageFileChanges;\r\n\r\n /** Reuses Vite's pipeline-barrel module instance for every dev reinstall. */\r\n protected installDevPageRoutes?: () => Promise<InstalledPageRoute[]>;\r\n\r\n /** Serializes Vite and core watcher callbacks that can observe the same edit. */\r\n protected pageRouteReloadQueue: Promise<unknown> = Promise.resolve();\r\n\r\n /** Committed file versions awaiting the overlapping watcher callback. */\r\n protected pendingHotUpdateSuppressions = new Map<string, string>();\r\n\r\n public constructor(options: WebConnectorOptions = {}) {\r\n super();\r\n this.options = options;\r\n }\r\n\r\n /**\r\n * Boot the connector — wire the page pipeline's request context and register\r\n * every page on the router.\r\n *\r\n * Two ways of doing that, one per hosting mode, and they share the shape\r\n * rather than the mechanism. Development creates Vite in middleware mode,\r\n * mounts it on the HTTP connector's Fastify instance and discovers pages by\r\n * walking `app/`; production takes both answers from the manifest the build\r\n * handed over (`./install-production-page-routes`) and touches no Vite at all.\r\n *\r\n * Everything here happens BEFORE `HttpConnector.start()` scans and listens,\r\n * which is the entire reason this is a `Late` connector's `boot()` and not its\r\n * `start()`.\r\n */\r\n public async boot() {\r\n // THE MODE BRANCH. One `if`, and it reads the mode — never the value.\r\n // `consumePageManifest()` returning `undefined` must not mean two different\r\n // things at one call site, so the only\r\n // question asked of the value here is \"is it there\", and the only thing\r\n // that decides whether that matters is {@link isProductionRuntime}.\r\n this.pageManifest = consumePageManifest();\r\n\r\n if (isProductionRuntime() && !this.pageManifest) {\r\n throw new WebPageManifestMissingError();\r\n }\r\n\r\n // The manifest is guaranteed present by the guard above; naming it again is\r\n // what narrows the type, not a second check of the same condition.\r\n if (isProductionRuntime() && this.pageManifest) {\r\n // Gated on `clientDir`, never on `publicFiles.length`: a zero-page,\r\n // zero-public-file build legitimately carries no `clientDir` at all (see\r\n // `resolveClientDir`'s note), and calling this without one would demand\r\n // a client build that had no reason to exist. But once a `clientDir` IS\r\n // present — a client build actually ran — registering an EMPTY\r\n // `publicFiles` is free (the loop inside is a no-op) and the staleness\r\n // check this runs (`warnIfPublicBuildIsStale`) is most needed exactly\r\n // here: an app that shipped a build with NO public files still needs a\r\n // file added to `public/` afterwards to be caught, not 404 in silence\r\n // because a length check skipped the call that would have named it.\r\n if (this.pageManifest.clientDir !== undefined) {\r\n registerProductionPublicFiles(\r\n router,\r\n this.resolveClientDir(),\r\n this.pageManifest.publicFiles ?? [],\r\n );\r\n }\r\n\r\n this.installedPages = await installProductionPageRoutes({\r\n router,\r\n manifest: this.pageManifest,\r\n pageContext: requestContext,\r\n sharedStore: () => requestContext.getStore(),\r\n // The URL is resolved lazily, by the production path, only if there are\r\n // pages to hydrate — see the option's own note.\r\n resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),\r\n // The stylesheets are read from the manifest in this directory, by the\r\n // installer itself — it already imports the barrel that owns that\r\n // reader, and production has one module graph, so resolving there\r\n // rather than here avoids loading the barrel twice.\r\n clientDir: this.pageManifest.clientDir,\r\n });\r\n\r\n // SERVE THE CLIENT BUNDLE. Without this the whole production page path\r\n // completes and still ships a dead page: the SSR HTML carries\r\n // `<script type=\"module\" src=\"/assets/hydration-<hash>.js\">`, that request\r\n // 404s, and React never takes over. Nothing else in the process serves\r\n // that directory — `CLIENT_ASSET_URL_PREFIX` was, until now, only ever\r\n // read to VALIDATE the URL written into the HTML, never to mount the\r\n // files it points at.\r\n //\r\n // In `boot()` rather than `start()` because the router registers static\r\n // directories during its SCAN, and the scan is `HttpConnector.start()` —\r\n // which every `boot()` precedes. Registering in `start()` would be a\r\n // no-op that looked correct.\r\n //\r\n // Dev needs no equivalent: Vite's middleware serves the module graph\r\n // itself, which is why this sits inside the production branch and not\r\n // above it.\r\n //\r\n // Gated on the page count for the same reason the build's hydration\r\n // client step is: a zero-page build produces no `assets/` bundle to\r\n // mount. Such a build may still carry `clientDir` for copied public\r\n // files, which were registered individually above rather than exposing\r\n // this directory wholesale.\r\n if (this.pageManifest.pages.length > 0) {\r\n router.directory(productionAssetsDirectoryOptions(this.resolveClientDir()));\r\n }\r\n\r\n return;\r\n }\r\n\r\n // Everything below this line is the Vite-hosted development path. Nothing\r\n // above it touches Vite: it is an optional peer, so a production install\r\n // does not carry it, and this method is the only place the two halves meet.\r\n //\r\n // Fastify is required by the DEVELOPMENT path alone, and the guard sits\r\n // here rather than above the branch for that reason: production mounts no\r\n // middleware and needs no HMR socket, it registers page routes on the\r\n // router and `HttpConnector.start()` scans them like any other route.\r\n const fastify = this.resolveFastify();\r\n const paths = await this.resolvePaths();\r\n\r\n // Kept for `shouldRestart`, which is asked of this connector on every\r\n // watcher batch and must answer without re-resolving anything.\r\n this.resolvedPaths = paths;\r\n\r\n this.vite = await this.createViteServer(fastify, paths);\r\n\r\n // The pipeline barrel is loaded THROUGH VITE, not imported directly, and\r\n // that is load-bearing: page modules are evaluated inside Vite's SSR module\r\n // graph, so `connectSharedStore`/`connectPageContext` must be called on\r\n // VITE's instance of those modules. A plain Node `import` here would wire a\r\n // second, unrelated module instance and every page would render with an\r\n // empty shared store.\r\n const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);\r\n\r\n webServerSsr.connectSharedStore(() => requestContext.getStore());\r\n webServerSsr.connectPageContext(requestContext);\r\n\r\n // Vite's `middlewares` is a plain Connect `(req, res, next)` stack and\r\n // Fastify's `request.raw`/`reply.raw` ARE node's `req`/`res`, so an\r\n // `onRequest` hook mounts it with no plugin at all — `@fastify/middie` is\r\n // not needed and is not a core dependency (`core/package.json`). Vite never\r\n // fronts the server: it either answers its own asset request or calls\r\n // `done()` and Warlock's router owns the response.\r\n //\r\n // ONE EXCEPTION, and it is why `done` is wrapped rather than passed\r\n // straight through: vite reaches this callback for TWO different reasons in\r\n // middleware mode — \"not mine\" and \"mine, and it failed\". The second one\r\n // arrives indistinguishable from the first, because vite's own error\r\n // handler logs the failure and then calls `next()` with the error cleared\r\n // (`node_modules/vite/dist/node/chunks/config.js:9525-9527`). Handing that\r\n // to the framework produced an empty 404 on a module that exists — via the\r\n // app's catch-all page route (`./render-page.ts:604`,\r\n // `./create-page-route-handler.ts:147`) — and threw the only useful\r\n // explanation away. `devErrorTransportPlugin` captures it upstream; this\r\n // reads it back.\r\n // Dev-only on both sides — nothing below this line runs in production.\r\n fastify.addHook(\r\n \"onRequest\",\r\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n this.vite?.middlewares(request.raw, reply.raw, (error?: Error) => {\r\n if (sendCapturedDevError(request.raw, reply.raw)) return;\r\n\r\n done(error);\r\n });\r\n },\r\n );\r\n\r\n const reportUnregisteredPages = createUnregisteredPageReporter({\r\n appRoot: paths.appRoot,\r\n appSrcRoot: paths.appSrcRoot,\r\n registeredPageFiles: () => registeredPageFiles(router.list(), paths.appSrcRoot),\r\n });\r\n\r\n fastify.addHook(\r\n \"onResponse\",\r\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\r\n if (reply.statusCode === 404) {\r\n reportUnregisteredPages({\r\n method: request.method,\r\n url: request.url,\r\n pathname: new URL(request.url, \"http://warlock.local\").pathname,\r\n });\r\n }\r\n\r\n done();\r\n },\r\n );\r\n\r\n this.installDevPageRoutes = () =>\r\n webServerSsr.installPageRoutes({\r\n router,\r\n vite: this.vite,\r\n appSrcRoot: paths.appSrcRoot,\r\n appFile: paths.appFile,\r\n appRoot: paths.appRoot,\r\n hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),\r\n // Resolved here, on the NODE side, and forwarded — see\r\n // `InstallPageRoutesOptions.httpServer` (`install-page-routes.ts`) for\r\n // why `createPageRouteHandler` cannot read this out of the container\r\n // itself from inside Vite's SSR module graph. `fastify` is this same\r\n // request's `resolveFastify()` result, already in scope above.\r\n httpServer: fastify,\r\n });\r\n\r\n this.installedPages = await this.installDevPageRoutes();\r\n }\r\n\r\n /**\r\n * Where the browser fetches the hydration entry from — the one line that\r\n * differs between the two modes, so it is the only thing that branches.\r\n *\r\n * Dev keeps Vite's `/@fs/` URL, which Vite's own middleware transforms on\r\n * demand. Production reads the hashed filename out of the client build's\r\n * `.vite/manifest.json` exactly once, at boot, and NEVER falls back: each way\r\n * that read can fail is its own named error (`./hydration-client-url.ts:24`,\r\n * `:36`, `:53`), because \"serve without hydration\" would be a page that\r\n * renders and then does nothing.\r\n *\r\n * `webRoot` is OPTIONAL because only the dev branch has any use for it, and\r\n * asking production for it would be worse than useless: it is proved by\r\n * reading `<root>/package.json` off disk, which is exactly the kind of\r\n * source-tree lookup a built artifact must never depend on. A dev boot that\r\n * somehow reaches here without one is refused by name by the entry factory.\r\n */\r\n protected resolveHydrationClientModuleUrl(webRoot?: string): string {\r\n if (isProductionRuntime()) {\r\n return resolveHydrationClientUrl({ clientDir: this.resolveClientDir() });\r\n }\r\n\r\n return createHydrationClientEntry(webRoot ?? \"\").devUrl;\r\n }\r\n\r\n /**\r\n * `<outdir>/client` — the layout the build half writes and this half reads\r\n * at boot, taken from the value the build BAKED into the page manifest.\r\n *\r\n * It used to call `resolveBuildConfig()`, which reads `warlock.config.ts`\r\n * through `warlockConfigManager`. That works in `warlock build` and in the\r\n * `warlock start` SUPERVISOR, and it cannot work here: the supervisor spawns\r\n * a plain `node dist/app.js` CHILD, and that process never loads — and could\r\n * not load — a TypeScript build-time config. The call threw\r\n * `WarlockConfig not loaded` inside connector boot, so the production server\r\n * died before it ever listened.\r\n *\r\n * Baking it also settles the drift the old comment was worried about, and\r\n * settles it harder: `build` and `start` cannot disagree about where the\r\n * bundle lives, because `start` is no longer re-deriving the path at all —\r\n * it reads back the one string `build` wrote.\r\n */\r\n protected resolveClientDir(): string {\r\n // The manifest CACHED at boot (line ~269), not a second `consumePageManifest()`:\r\n // this runs from a callback the production branch invokes lazily, long after\r\n // that assignment, and reading the same field the mode branch already\r\n // decided on keeps one source of truth for the boot's view of the manifest.\r\n const clientDir = this.pageManifest?.clientDir;\r\n\r\n if (clientDir === undefined) {\r\n // Reached only via a manifest with browser artifacts but no `clientDir`\r\n // — i.e. a bundle built by a web version older than this field. Named here\r\n // rather than left to surface as an ENOENT on a `path.join(undefined)`\r\n // deep inside the manifest read.\r\n throw new WebClientDirMissingError();\r\n }\r\n\r\n return path.resolve(process.cwd(), clientDir);\r\n }\r\n\r\n /**\r\n * The page manifest this connector consumed at boot, or `undefined` in dev\r\n * where Vite supplies the modules instead.\r\n */\r\n public getPageManifest(): PageManifest | undefined {\r\n return this.pageManifest;\r\n }\r\n\r\n /**\r\n * Activate. There is nothing to listen on — `HttpConnector.start()` owns the\r\n * single `listen()` for the whole process — so this only marks the connector\r\n * live once `boot()` has wired everything.\r\n */\r\n public async start(): Promise<void> {\r\n if (!this.vite) return;\r\n\r\n this.active = true;\r\n }\r\n\r\n /**\r\n * Shutdown — close Vite, and drop the sockets Vite's middleware left behind.\r\n *\r\n * Reverse-priority teardown (`core/src/connectors/connectors-manager.ts:118`)\r\n * puts this BEFORE `HttpConnector.shutdown()`, which is exactly what the\r\n * second call needs: requests answered by Vite's connect stack are written\r\n * straight to `reply.raw`, so Fastify never observes them completing and their\r\n * keep-alive sockets are never counted idle. Core's default\r\n * `forceCloseConnections: \"idle\"` (`core/src/http/server.ts:34`) then waits on\r\n * them forever. A dev server has no draining obligation, and the connector\r\n * that caused the raw writes is the right one to clean up after them.\r\n */\r\n public async shutdown(): Promise<void> {\r\n if (!this.active) return;\r\n\r\n if (container.has(\"http.server\")) {\r\n container.get(\"http.server\").server.closeAllConnections();\r\n }\r\n\r\n await this.vite?.close();\r\n this.vite = undefined;\r\n this.installedPages = [];\r\n this.installDevPageRoutes = undefined;\r\n this.pendingPageChanges = undefined;\r\n this.pendingHotUpdateSuppressions.clear();\r\n this.pageManifest = undefined;\r\n\r\n this.active = false;\r\n }\r\n\r\n /**\r\n * Queue page add/remove/edit candidates for asynchronous live routing work.\r\n * Classification stays synchronous because core's connector interface is;\r\n * edited route exports are evaluated later through Vite's fresh SSR graph.\r\n */\r\n public shouldRestart(changedFiles: string[] = []): boolean {\r\n if (this.vite === undefined || this.resolvedPaths === undefined) {\r\n return false;\r\n }\r\n\r\n this.pendingPageChanges = this.classifyPageChanges(changedFiles);\r\n return this.pendingPageChanges !== undefined;\r\n }\r\n\r\n protected classifyPageChanges(changedFiles: readonly string[]): PageFileChanges | undefined {\r\n if (this.resolvedPaths === undefined) return undefined;\r\n\r\n const changes = classifyPageFileChanges(changedFiles, {\r\n appRoot: this.resolvedPaths.appRoot,\r\n appSrcRoot: this.resolvedPaths.appSrcRoot,\r\n installedPageFiles: registeredPageFiles(router.list(), this.resolvedPaths.appSrcRoot),\r\n });\r\n\r\n return hasPageFileChanges(changes) ? changes : undefined;\r\n }\r\n\r\n protected enqueuePageRouteReload(changes: PageFileChanges): Promise<boolean> {\r\n const eventVersions = pageChangeVersions(changes);\r\n const run = this.pageRouteReloadQueue\r\n .catch(() => undefined)\r\n .then(async () => {\r\n const vite = this.vite;\r\n const install = this.installDevPageRoutes;\r\n const paths = this.resolvedPaths;\r\n\r\n if (vite === undefined || install === undefined || paths === undefined) return false;\r\n\r\n const matchingCommittedFiles = new Set<string>();\r\n\r\n // This check belongs inside the queue: a matching core transaction may\r\n // commit while a Vite callback is waiting behind it. File versions are\r\n // captured when the job is queued so a later edit cannot consume an\r\n // earlier event's marker.\r\n for (const [file, eventVersion] of eventVersions) {\r\n const committedVersion = this.pendingHotUpdateSuppressions.get(file);\r\n if (committedVersion === undefined) continue;\r\n\r\n // Matching markers are consumed exactly once. A mismatched marker is\r\n // obsolete and must not survive to suppress a future reverted edit.\r\n this.pendingHotUpdateSuppressions.delete(file);\r\n if (committedVersion === eventVersion) matchingCommittedFiles.add(file);\r\n }\r\n\r\n if (eventVersions.size > 0 && matchingCommittedFiles.size === eventVersions.size) {\r\n return true;\r\n }\r\n\r\n const replace = await pageRoutesNeedReplacement(changes, {\r\n vite,\r\n appSrcRoot: paths.appSrcRoot,\r\n installedPages: this.installedPages,\r\n });\r\n\r\n if (!replace) return false;\r\n\r\n const nextInstalledPages = await router.replaceRoutesBySourceFiles(\r\n pageRouteSourceFiles(router.list()),\r\n install,\r\n );\r\n\r\n // Advance observable state only after the router transaction commits.\r\n // A rejected install keeps both the old route table and browser live.\r\n this.installedPages = nextInstalledPages;\r\n invalidateClientPageRegistry(vite);\r\n\r\n for (const [file, eventVersion] of eventVersions) {\r\n if (!matchingCommittedFiles.has(file)) {\r\n this.pendingHotUpdateSuppressions.set(file, eventVersion);\r\n }\r\n }\r\n\r\n return true;\r\n });\r\n\r\n this.pageRouteReloadQueue = run;\r\n return run;\r\n }\r\n\r\n /**\r\n * Vite-side ordering barrier. It publishes a changed route graph before the\r\n * page-registry plugin can reload the document; a matching change already\r\n * handled by core is consumed once, preventing a duplicate full reload.\r\n */\r\n protected async handlePageHotUpdate(file: string): Promise<boolean> {\r\n const absoluteFile = path.resolve(file);\r\n const changes = this.classifyPageChanges([absoluteFile]);\r\n if (changes === undefined) return false;\r\n\r\n return this.enqueuePageRouteReload(changes);\r\n }\r\n\r\n /**\r\n * Live page routing update. Despite the connector API name, this never closes\r\n * Vite: it atomically replaces page-owned routes only when membership or the\r\n * canonical route identity changed, then refreshes the client registry.\r\n */\r\n public async restart(): Promise<void> {\r\n const changes = this.pendingPageChanges;\r\n this.pendingPageChanges = undefined;\r\n\r\n if (\r\n changes === undefined ||\r\n this.resolvedPaths === undefined ||\r\n this.vite === undefined ||\r\n this.installDevPageRoutes === undefined\r\n ) {\r\n return;\r\n }\r\n\r\n await this.enqueuePageRouteReload(changes);\r\n }\r\n\r\n /** The pages this connector registered on the router, in registration order. */\r\n public getInstalledPages(): readonly InstalledPageRoute[] {\r\n return this.installedPages;\r\n }\r\n\r\n /**\r\n * The Fastify instance the HTTP connector published during its own `boot()`\r\n * (`core/src/connectors/http-connector.ts:74`).\r\n *\r\n * Absence is fatal rather than a silent no-op: unlike sockets, there is no\r\n * standalone fallback a page surface could serve from, and the failure this\r\n * guards against — an app with no `src/config/http.ts` — otherwise shows up\r\n * as every page 404ing with no explanation.\r\n */\r\n protected resolveFastify(): FastifyInstance {\r\n if (!container.has(\"http.server\")) {\r\n throw new Error(\r\n \"WebConnector requires the HTTP connector's Fastify instance, but \" +\r\n \"`http.server` is not in the container. The `http` config is missing — add `src/config/http.ts` \" +\r\n \"so `HttpConnector.boot()` runs (core/src/connectors/http-connector.ts:61-74).\",\r\n );\r\n }\r\n\r\n return container.get(\"http.server\");\r\n }\r\n\r\n /** Resolve every path this connector needs from the (optional) options. */\r\n protected async resolvePaths() {\r\n const appRoot = this.options.appRoot ?? process.cwd();\r\n const appSrcRoot = this.options.appSrcRoot ?? path.join(appRoot, \"src\");\r\n const selfPath = fileURLToPath(import.meta.url);\r\n // The web package root goes through `resolveWebPackageRoot`, which PROVES\r\n // the directory by reading `<root>/package.json` and matching its `name`,\r\n // rather than trusting a fixed number of `..` hops. A configured root is\r\n // asserted the same way. Either failure throws\r\n // `WebPackageRootResolutionError` naming the directory at boot — the\r\n // alternative was a wrong root surfacing much later as a 404 on the\r\n // hydration entry with nothing to point at.\r\n const webRoot = await resolveWebPackageRoot(this.options.webRoot);\r\n\r\n return {\r\n appRoot,\r\n appSrcRoot,\r\n appFile: this.options.appFile ?? path.join(appSrcRoot, \"web/root.tsx\"),\r\n webRoot,\r\n // Extension-agnostic on purpose: the sibling barrel is `index.ts` when\r\n // this package runs from source and `index.js` once it is built, the same\r\n // trick `registerLoader` uses when it resolves its own siblings.\r\n webServerBarrel: path.join(path.dirname(selfPath), `index${path.extname(selfPath)}`),\r\n };\r\n }\r\n\r\n protected async createViteServer(\r\n fastify: FastifyInstance,\r\n paths: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>,\r\n ): Promise<ViteDevServer> {\r\n const { createServer, buildErrorMessage, searchForWorkspaceRoot } = await import(\"vite\");\r\n\r\n // Vite's own default for `server.fs.allow`, reproduced rather than dropped:\r\n // naming the key at all REPLACES the default, and an application that\r\n // legitimately serves files from above its own root has to keep working.\r\n const workspaceRoot = searchForWorkspaceRoot(paths.appRoot);\r\n\r\n return createServer(\r\n await createWebConnectorViteConfig({\r\n appRoot: paths.appRoot,\r\n appSrcRoot: paths.appSrcRoot,\r\n webRoot: paths.webRoot,\r\n workspaceRoot,\r\n hmrServer: fastify.server,\r\n handlePageHotUpdate: (file) => this.handlePageHotUpdate(file),\r\n leadingPlugins: [devErrorTransportPlugin({ isProductionRuntime, buildErrorMessage })],\r\n resolveAlias: this.options.resolveAlias,\r\n ssrExternal: this.options.ssrExternal,\r\n plugins: this.options.plugins,\r\n }),\r\n );\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuIA,SAAS,sBAA+B;CACtC,OAAO,YAAY,oBAAoB;AACzC;AAEA,SAAS,gBAAgB,MAAsB;CAC7C,IAAI;EACF,OAAO,WAAW,GAAG,aAAa,MAAM,MAAM;CAChD,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,mBAAmB,SAA+C;CACzE,OAAO,IAAI,IACT;EAAC,GAAG,QAAQ;EAAO,GAAG,QAAQ;EAAS,GAAG,QAAQ;CAAgB,CAAC,CAAC,KAAK,SAAS;EAChF,MAAM,eAAe,KAAK,QAAQ,IAAI;EACtC,OAAO,CAAC,cAAc,gBAAgB,YAAY,CAAC;CACrD,CAAC,CACH;AACF;AAEA,IAAa,8BAAb,cAAiD,MAAM;CACrD,AAAO,cAAc;EACnB,MACE,6TAKF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;;AAWA,IAAa,2BAAb,cAA8C,MAAM;CAClD,AAAO,cAAc;EACnB,MACE,oSAKF;EACA,KAAK,OAAO;CACd;AACF;;;;;;;;;AAUA,MAAM,gCAAgC,UAAW;;;;;;AAOjD,SAAgB,iCAAiC,WAAmB;CAClE,OAAO;EACL,MAAM,KAAK,KAAK,WAAW,QAAQ;EACnC,QAAQ,GAAG,wBAAwB;EACnC,QAAQ;EACR,WAAW;CACb;AACF;;;;;;AAuCA,IAAa,eAAb,cAAkC,cAAc;CAC9C,AAAgB,OAAsB;CACtC,AAAgB,WAAW;CAC3B,AAAgB,iBAAiB,wBAAwB;;;;;;CAOzD,AAAmB,eAAyB,CAAC;CAE7C,AAAmB;CAEnB,AAAU;CAEV,AAAU,iBAAuC,CAAC;;;;;;;;CASlD,AAAU;;;;;;;CAQV,AAAU;;CAGV,AAAU;;CAGV,AAAU;;CAGV,AAAU,uBAAyC,QAAQ,QAAQ;;CAGnE,AAAU,+CAA+B,IAAI,IAAoB;CAEjE,AAAO,YAAY,UAA+B,CAAC,GAAG;EACpD,MAAM;EACN,KAAK,UAAU;CACjB;;;;;;;;;;;;;;;CAgBA,MAAa,OAAO;EAMlB,KAAK,eAAe,oBAAoB;EAExC,IAAI,oBAAoB,KAAK,CAAC,KAAK,cACjC,MAAM,IAAI,4BAA4B;EAKxC,IAAI,oBAAoB,KAAK,KAAK,cAAc;GAW9C,IAAI,KAAK,aAAa,cAAc,QAClC,8BACE,QACA,KAAK,iBAAiB,GACtB,KAAK,aAAa,eAAe,CAAC,CACpC;GAGF,KAAK,iBAAiB,MAAM,4BAA4B;IACtD;IACA,UAAU,KAAK;IACf,aAAa;IACb,mBAAmB,eAAe,SAAS;IAG3C,uCAAuC,KAAK,gCAAgC;IAK5E,WAAW,KAAK,aAAa;GAC/B,CAAC;GAwBD,IAAI,KAAK,aAAa,MAAM,SAAS,GACnC,OAAO,UAAU,iCAAiC,KAAK,iBAAiB,CAAC,CAAC;GAG5E;EACF;EAUA,MAAM,UAAU,KAAK,eAAe;EACpC,MAAM,QAAQ,MAAM,KAAK,aAAa;EAItC,KAAK,gBAAgB;EAErB,KAAK,OAAO,MAAM,KAAK,iBAAiB,SAAS,KAAK;EAQtD,MAAM,eAAe,MAAM,KAAK,KAAK,cAAc,MAAM,eAAe;EAExE,aAAa,yBAAyB,eAAe,SAAS,CAAC;EAC/D,aAAa,mBAAmB,cAAc;EAqB9C,QAAQ,QACN,cACC,SAAyB,OAAqB,SAAkC;GAC/E,KAAK,MAAM,YAAY,QAAQ,KAAK,MAAM,MAAM,UAAkB;IAChE,IAAI,qBAAqB,QAAQ,KAAK,MAAM,GAAG,GAAG;IAElD,KAAK,KAAK;GACZ,CAAC;EACH,CACF;EAEA,MAAM,0BAA0B,+BAA+B;GAC7D,SAAS,MAAM;GACf,YAAY,MAAM;GAClB,2BAA2B,oBAAoB,OAAO,KAAK,GAAG,MAAM,UAAU;EAChF,CAAC;EAED,QAAQ,QACN,eACC,SAAyB,OAAqB,SAAkC;GAC/E,IAAI,MAAM,eAAe,KACvB,wBAAwB;IACtB,QAAQ,QAAQ;IAChB,KAAK,QAAQ;IACb,UAAU,IAAI,IAAI,QAAQ,KAAK,sBAAsB,CAAC,CAAC;GACzD,CAAC;GAGH,KAAK;EACP,CACF;EAEA,KAAK,6BACH,aAAa,kBAAkB;GAC7B;GACA,MAAM,KAAK;GACX,YAAY,MAAM;GAClB,SAAS,MAAM;GACf,SAAS,MAAM;GACf,0BAA0B,KAAK,gCAAgC,MAAM,OAAO;GAM5E,YAAY;EACd,CAAC;EAEH,KAAK,iBAAiB,MAAM,KAAK,qBAAqB;CACxD;;;;;;;;;;;;;;;;;;CAmBA,AAAU,gCAAgC,SAA0B;EAClE,IAAI,oBAAoB,GACtB,OAAO,0BAA0B,EAAE,WAAW,KAAK,iBAAiB,EAAE,CAAC;EAGzE,OAAO,2BAA2B,WAAW,EAAE,CAAC,CAAC;CACnD;;;;;;;;;;;;;;;;;;CAmBA,AAAU,mBAA2B;EAKnC,MAAM,YAAY,KAAK,cAAc;EAErC,IAAI,cAAc,QAKhB,MAAM,IAAI,yBAAyB;EAGrC,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,SAAS;CAC9C;;;;;CAMA,AAAO,kBAA4C;EACjD,OAAO,KAAK;CACd;;;;;;CAOA,MAAa,QAAuB;EAClC,IAAI,CAAC,KAAK,MAAM;EAEhB,KAAK,SAAS;CAChB;;;;;;;;;;;;;CAcA,MAAa,WAA0B;EACrC,IAAI,CAAC,KAAK,QAAQ;EAElB,IAAI,UAAU,IAAI,aAAa,GAC7B,UAAU,IAAI,aAAa,CAAC,CAAC,OAAO,oBAAoB;EAG1D,MAAM,KAAK,MAAM,MAAM;EACvB,KAAK,OAAO;EACZ,KAAK,iBAAiB,CAAC;EACvB,KAAK,uBAAuB;EAC5B,KAAK,qBAAqB;EAC1B,KAAK,6BAA6B,MAAM;EACxC,KAAK,eAAe;EAEpB,KAAK,SAAS;CAChB;;;;;;CAOA,AAAO,cAAc,eAAyB,CAAC,GAAY;EACzD,IAAI,KAAK,SAAS,UAAa,KAAK,kBAAkB,QACpD,OAAO;EAGT,KAAK,qBAAqB,KAAK,oBAAoB,YAAY;EAC/D,OAAO,KAAK,uBAAuB;CACrC;CAEA,AAAU,oBAAoB,cAA8D;EAC1F,IAAI,KAAK,kBAAkB,QAAW,OAAO;EAE7C,MAAM,UAAU,wBAAwB,cAAc;GACpD,SAAS,KAAK,cAAc;GAC5B,YAAY,KAAK,cAAc;GAC/B,oBAAoB,oBAAoB,OAAO,KAAK,GAAG,KAAK,cAAc,UAAU;EACtF,CAAC;EAED,OAAO,mBAAmB,OAAO,IAAI,UAAU;CACjD;CAEA,AAAU,uBAAuB,SAA4C;EAC3E,MAAM,gBAAgB,mBAAmB,OAAO;EAChD,MAAM,MAAM,KAAK,qBACd,YAAY,MAAS,CAAC,CACtB,KAAK,YAAY;GAChB,MAAM,OAAO,KAAK;GAClB,MAAM,UAAU,KAAK;GACrB,MAAM,QAAQ,KAAK;GAEnB,IAAI,SAAS,UAAa,YAAY,UAAa,UAAU,QAAW,OAAO;GAE/E,MAAM,yCAAyB,IAAI,IAAY;GAM/C,KAAK,MAAM,CAAC,MAAM,iBAAiB,eAAe;IAChD,MAAM,mBAAmB,KAAK,6BAA6B,IAAI,IAAI;IACnE,IAAI,qBAAqB,QAAW;IAIpC,KAAK,6BAA6B,OAAO,IAAI;IAC7C,IAAI,qBAAqB,cAAc,uBAAuB,IAAI,IAAI;GACxE;GAEA,IAAI,cAAc,OAAO,KAAK,uBAAuB,SAAS,cAAc,MAC1E,OAAO;GAST,IAAI,CAAC,MANiB,0BAA0B,SAAS;IACvD;IACA,YAAY,MAAM;IAClB,gBAAgB,KAAK;GACvB,CAAC,GAEa,OAAO;GAErB,MAAM,qBAAqB,MAAM,OAAO,2BACtC,qBAAqB,OAAO,KAAK,CAAC,GAClC,OACF;GAIA,KAAK,iBAAiB;GACtB,6BAA6B,IAAI;GAEjC,KAAK,MAAM,CAAC,MAAM,iBAAiB,eACjC,IAAI,CAAC,uBAAuB,IAAI,IAAI,GAClC,KAAK,6BAA6B,IAAI,MAAM,YAAY;GAI5D,OAAO;EACT,CAAC;EAEH,KAAK,uBAAuB;EAC5B,OAAO;CACT;;;;;;CAOA,MAAgB,oBAAoB,MAAgC;EAClE,MAAM,eAAe,KAAK,QAAQ,IAAI;EACtC,MAAM,UAAU,KAAK,oBAAoB,CAAC,YAAY,CAAC;EACvD,IAAI,YAAY,QAAW,OAAO;EAElC,OAAO,KAAK,uBAAuB,OAAO;CAC5C;;;;;;CAOA,MAAa,UAAyB;EACpC,MAAM,UAAU,KAAK;EACrB,KAAK,qBAAqB;EAE1B,IACE,YAAY,UACZ,KAAK,kBAAkB,UACvB,KAAK,SAAS,UACd,KAAK,yBAAyB,QAE9B;EAGF,MAAM,KAAK,uBAAuB,OAAO;CAC3C;;CAGA,AAAO,oBAAmD;EACxD,OAAO,KAAK;CACd;;;;;;;;;;CAWA,AAAU,iBAAkC;EAC1C,IAAI,CAAC,UAAU,IAAI,aAAa,GAC9B,MAAM,IAAI,MACR,+OAGF;EAGF,OAAO,UAAU,IAAI,aAAa;CACpC;;CAGA,MAAgB,eAAe;EAC7B,MAAM,UAAU,KAAK,QAAQ,WAAW,QAAQ,IAAI;EACpD,MAAM,aAAa,KAAK,QAAQ,cAAc,KAAK,KAAK,SAAS,KAAK;EACtE,MAAM,WAAW,cAAc,OAAO,KAAK,GAAG;EAQ9C,MAAM,UAAU,MAAM,sBAAsB,KAAK,QAAQ,OAAO;EAEhE,OAAO;GACL;GACA;GACA,SAAS,KAAK,QAAQ,WAAW,KAAK,KAAK,YAAY,cAAc;GACrE;GAIA,iBAAiB,KAAK,KAAK,KAAK,QAAQ,QAAQ,GAAG,QAAQ,KAAK,QAAQ,QAAQ,GAAG;EACrF;CACF;CAEA,MAAgB,iBACd,SACA,OACwB;EACxB,MAAM,EAAE,cAAc,mBAAmB,2BAA2B,MAAM,OAAO;EAKjF,MAAM,gBAAgB,uBAAuB,MAAM,OAAO;EAE1D,OAAO,aACL,MAAM,6BAA6B;GACjC,SAAS,MAAM;GACf,YAAY,MAAM;GAClB,SAAS,MAAM;GACf;GACA,WAAW,QAAQ;GACnB,sBAAsB,SAAS,KAAK,oBAAoB,IAAI;GAC5D,gBAAgB,CAAC,wBAAwB;IAAE;IAAqB;GAAkB,CAAC,CAAC;GACpF,cAAc,KAAK,QAAQ;GAC3B,aAAa,KAAK,QAAQ;GAC1B,SAAS,KAAK,QAAQ;EACxB,CAAC,CACH;CACF;AACF"}
@@ -10,7 +10,15 @@ import { Infer } from "@warlock.js/seal";
10
10
  type PageValidation = {
11
11
  schema?: unknown;
12
12
  validating?: readonly string[];
13
+ params?: never;
14
+ query?: never;
15
+ } | {
16
+ params?: unknown;
17
+ query?: unknown;
18
+ schema?: never;
19
+ validating?: never;
13
20
  };
21
+ type ValidatedPart<TValidation, TKey extends "params" | "query"> = TValidation extends Record<TKey, infer TSchema> ? { [TPart in TKey]: Infer.Output<TSchema> } : Record<string, never>;
14
22
  /**
15
23
  * What `request.validated()` hands back: `Infer.Output`, not bare `Infer`.
16
24
  * Bare `Infer<T>` is `Infer.Input<T>` — the shape a CALLER sends, where
@@ -25,7 +33,10 @@ type PageValidation = {
25
33
  */
26
34
  type ValidatedOutput<TValidation> = TValidation extends {
27
35
  schema: infer TSchema;
28
- } ? Infer.Output<TSchema> : Record<string, never>;
36
+ } ? Infer.Output<TSchema> : TValidation extends {
37
+ params?: unknown;
38
+ query?: unknown;
39
+ } ? ValidatedPart<TValidation, "params"> & ValidatedPart<TValidation, "query"> : Record<string, never>;
29
40
  //#endregion
30
41
  export { PageValidation, ValidatedOutput };
31
42
  //# sourceMappingURL=validation.d.mts.map
@@ -1,6 +1,6 @@
1
1
  import { createHydrationClientEntry } from "./hydration-entries.mjs";
2
- import { statSync } from "node:fs";
3
2
  import path from "node:path";
3
+ import { statSync } from "node:fs";
4
4
 
5
5
  //#region ../web/src/vite/build-client.ts
6
6
  function assertEntryFile(entry) {
@@ -1,8 +1,8 @@
1
1
  import { toPosix } from "../shared/to-posix.mjs";
2
2
  import { moduleKey } from "../shared/module-key.mjs";
3
3
  import { builtinModules } from "node:module";
4
- import { existsSync, readFileSync, statSync } from "node:fs";
5
4
  import path from "node:path";
5
+ import { existsSync, readFileSync, statSync } from "node:fs";
6
6
  import { parse } from "@babel/parser";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { toPosix } from "../shared/to-posix.mjs";
2
- import { existsSync } from "node:fs";
3
2
  import path from "node:path";
3
+ import { existsSync } from "node:fs";
4
4
 
5
5
  //#region ../web/src/vite/hydration-entries.ts
6
6
  /** Stable Rollup/Vite entry name shared by development and production wiring. */
package/llms-full.txt CHANGED
@@ -116,7 +116,7 @@ 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, an explicit public-cache opt-in, a `route.validate` schema and `route.middleware` guards, 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`, `route.validate`, `route.middleware`, `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", "validate route params and query", "page-level middleware"; 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 `validation` schema and `middleware` guards, 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`, `validation`, `middleware`, `route.validate`, `route.middleware`, `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", "validate route params and query", "page-level middleware"; 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
@@ -218,9 +218,11 @@ Every segment of a page's URL is written down somewhere: `route.path` (or the de
218
218
 
219
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.
220
220
 
221
- ### Validate the route's own input — `route.validate`
221
+ ### Validate the page's input — the `validation` export
222
222
 
223
- The object form also accepts `validate`: a [Seal](https://www.npmjs.com/package/@warlock.js/seal) object schema run against `{ params, query }`, kept as two separate keys — never merged into one bag, so a `:id` path segment and a `?id=` query key can never collide or silently shadow one another:
223
+ Declare `validation` as its own top-level export: a [Seal](https://www.npmjs.com/package/@warlock.js/seal) schema per source, `params` and `query` kept as two separate keys — never merged into one bag, so a `:id` path segment and a `?id=` query key can never collide or silently shadow one another:
224
+
225
+ > **Withdrawn after 5.6.0: `route.validate`.** A page that still declares it does not silently lose its validation — the app **refuses to boot** and names the file. Move the schema to the `validation` export shown below; the shape and the 400 are unchanged.
224
226
 
225
227
  ```tsx title="src/web/products/product-details.page.tsx"
226
228
  import { v } from "@warlock.js/seal";
@@ -229,12 +231,13 @@ import type { PageLoader, PageProps } from "@warlock.js/web";
229
231
  export const route = {
230
232
  path: "/products/:id",
231
233
  name: "products.details",
232
- validate: v.object({
233
- params: v.object({ id: v.string().minLength(2) }),
234
- query: v.object({ tab: v.string().optional() }),
235
- }),
236
234
  } as const;
237
235
 
236
+ export const validation = {
237
+ params: v.object({ id: v.string().minLength(2) }),
238
+ query: v.object({ tab: v.string().optional() }),
239
+ };
240
+
238
241
  export const loader = (async ({ request }) => {
239
242
  const { params, query } = request.validated();
240
243
 
@@ -246,27 +249,30 @@ export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
246
249
  }
247
250
  ```
248
251
 
249
- `request.validated()` types `params` and `query` from the schema — never a flattened merge of the two. If the page also declares the top-level `validation` export ([load-page-data](../load-page-data/SKILL.md)), both surfaces' fields are merged by intersection into the one `validated()` call; neither overwrites the other.
252
+ `request.validated()` types `params` and `query` from the schema — never a flattened merge of the two. There is exactly ONE validation surface on a page; see [load-page-data](../load-page-data/SKILL.md) for how the validated data reaches the loader.
250
253
 
251
254
  Rejected input never reaches the loader. It renders the application's `error.page.tsx` boundary at status 400 carrying the failure — a page is a document, not an API endpoint, so invalid input never gets a raw JSON body. The same 400 travels the same way over the `_loader` client-navigation wire.
252
255
 
253
- ### `route.middleware` — a page's own guard, run last
256
+ ### `middleware` — a page's own guard, run last
254
257
 
255
- The object form also accepts `middleware`: an array of `(ctx) => unknown | Promise<unknown>` guards declared on the page itself, alongside any layout `middleware` above it ([use-layouts](../use-layouts/SKILL.md)). Ordering is fixed pipeline-wide: every layout on the chain runs outermost-first, and `route.middleware` runs LAST, closest to the loader — a layout's auth gate can never be bypassed by a page declaring its own middleware.
258
+ Declare `middleware` as its own top-level export: an array of `(ctx) => unknown | Promise<unknown>` guards on the page itself, alongside any layout `middleware` above it ([use-layouts](../use-layouts/SKILL.md)). Ordering is fixed pipeline-wide: every layout on the chain runs outermost-first, and the page's own `middleware` runs LAST, closest to the loader — **a layout's auth gate can never be bypassed by a page declaring its own middleware.**
259
+
260
+ > **Withdrawn after 5.6.0: `route.middleware`.** As with `route.validate`, a page still declaring it **refuses to boot** rather than quietly running without its guards — which for an auth guard is the difference between a broken deploy and an open door.
256
261
 
257
262
  ```tsx
258
263
  export const route = {
259
264
  path: "/account",
260
- middleware: [
261
- async ({ request, response }) => {
262
- if (!request.header("authorization")) {
263
- response.setStatusCode(401);
264
-
265
- return { error: "Unauthorized" };
266
- }
267
- },
268
- ],
269
265
  } as const;
266
+
267
+ export const middleware = [
268
+ async ({ request, response }) => {
269
+ if (!request.header("authorization")) {
270
+ response.setStatusCode(401);
271
+
272
+ return { error: "Unauthorized" };
273
+ }
274
+ },
275
+ ];
270
276
  ```
271
277
 
272
278
  Returning anything other than `undefined` from a middleware short-circuits the request with that value, exactly as an app or layout middleware does.
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  },
13
13
  "peerDependencies": {
14
14
  "@vitejs/plugin-react": "^5.2.0",
15
- "@warlock.js/core": "5.6.0",
16
- "@warlock.js/seal": "5.6.0",
15
+ "@warlock.js/core": "5.7.0",
16
+ "@warlock.js/seal": "5.7.0",
17
17
  "react": "*",
18
18
  "react-dom": "*",
19
19
  "vite": ">=7.3.5 <8"
@@ -38,7 +38,7 @@
38
38
  ],
39
39
  "author": "hassanzohdy",
40
40
  "license": "MIT",
41
- "version": "5.6.0",
41
+ "version": "5.7.0",
42
42
  "type": "module",
43
43
  "main": "./esm/index.mjs",
44
44
  "module": "./esm/index.mjs",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: create-a-page
3
- 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 `route.validate` schema and `route.middleware` guards, 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`, `route.validate`, `route.middleware`, `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", "validate route params and query", "page-level middleware"; 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.'
3
+ 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 `validation` schema and `middleware` guards, 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`, `validation`, `middleware`, `route.validate`, `route.middleware`, `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", "validate route params and query", "page-level middleware"; 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.'
4
4
  ---
5
5
 
6
6
  # Warlock — create a page
@@ -102,9 +102,11 @@ Every segment of a page's URL is written down somewhere: `route.path` (or the de
102
102
 
103
103
  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.
104
104
 
105
- ### Validate the route's own input — `route.validate`
105
+ ### Validate the page's input — the `validation` export
106
106
 
107
- The object form also accepts `validate`: a [Seal](https://www.npmjs.com/package/@warlock.js/seal) object schema run against `{ params, query }`, kept as two separate keys — never merged into one bag, so a `:id` path segment and a `?id=` query key can never collide or silently shadow one another:
107
+ Declare `validation` as its own top-level export: a [Seal](https://www.npmjs.com/package/@warlock.js/seal) schema per source, `params` and `query` kept as two separate keys — never merged into one bag, so a `:id` path segment and a `?id=` query key can never collide or silently shadow one another:
108
+
109
+ > **Withdrawn after 5.6.0: `route.validate`.** A page that still declares it does not silently lose its validation — the app **refuses to boot** and names the file. Move the schema to the `validation` export shown below; the shape and the 400 are unchanged.
108
110
 
109
111
  ```tsx title="src/web/products/product-details.page.tsx"
110
112
  import { v } from "@warlock.js/seal";
@@ -113,12 +115,13 @@ import type { PageLoader, PageProps } from "@warlock.js/web";
113
115
  export const route = {
114
116
  path: "/products/:id",
115
117
  name: "products.details",
116
- validate: v.object({
117
- params: v.object({ id: v.string().minLength(2) }),
118
- query: v.object({ tab: v.string().optional() }),
119
- }),
120
118
  } as const;
121
119
 
120
+ export const validation = {
121
+ params: v.object({ id: v.string().minLength(2) }),
122
+ query: v.object({ tab: v.string().optional() }),
123
+ };
124
+
122
125
  export const loader = (async ({ request }) => {
123
126
  const { params, query } = request.validated();
124
127
 
@@ -130,27 +133,30 @@ export default function ProductDetailsPage({ data }: PageProps<typeof loader>) {
130
133
  }
131
134
  ```
132
135
 
133
- `request.validated()` types `params` and `query` from the schema — never a flattened merge of the two. If the page also declares the top-level `validation` export ([load-page-data](../load-page-data/SKILL.md)), both surfaces' fields are merged by intersection into the one `validated()` call; neither overwrites the other.
136
+ `request.validated()` types `params` and `query` from the schema — never a flattened merge of the two. There is exactly ONE validation surface on a page; see [load-page-data](../load-page-data/SKILL.md) for how the validated data reaches the loader.
134
137
 
135
138
  Rejected input never reaches the loader. It renders the application's `error.page.tsx` boundary at status 400 carrying the failure — a page is a document, not an API endpoint, so invalid input never gets a raw JSON body. The same 400 travels the same way over the `_loader` client-navigation wire.
136
139
 
137
- ### `route.middleware` — a page's own guard, run last
140
+ ### `middleware` — a page's own guard, run last
138
141
 
139
- The object form also accepts `middleware`: an array of `(ctx) => unknown | Promise<unknown>` guards declared on the page itself, alongside any layout `middleware` above it ([use-layouts](../use-layouts/SKILL.md)). Ordering is fixed pipeline-wide: every layout on the chain runs outermost-first, and `route.middleware` runs LAST, closest to the loader — a layout's auth gate can never be bypassed by a page declaring its own middleware.
142
+ Declare `middleware` as its own top-level export: an array of `(ctx) => unknown | Promise<unknown>` guards on the page itself, alongside any layout `middleware` above it ([use-layouts](../use-layouts/SKILL.md)). Ordering is fixed pipeline-wide: every layout on the chain runs outermost-first, and the page's own `middleware` runs LAST, closest to the loader — **a layout's auth gate can never be bypassed by a page declaring its own middleware.**
143
+
144
+ > **Withdrawn after 5.6.0: `route.middleware`.** As with `route.validate`, a page still declaring it **refuses to boot** rather than quietly running without its guards — which for an auth guard is the difference between a broken deploy and an open door.
140
145
 
141
146
  ```tsx
142
147
  export const route = {
143
148
  path: "/account",
144
- middleware: [
145
- async ({ request, response }) => {
146
- if (!request.header("authorization")) {
147
- response.setStatusCode(401);
148
-
149
- return { error: "Unauthorized" };
150
- }
151
- },
152
- ],
153
149
  } as const;
150
+
151
+ export const middleware = [
152
+ async ({ request, response }) => {
153
+ if (!request.header("authorization")) {
154
+ response.setStatusCode(401);
155
+
156
+ return { error: "Unauthorized" };
157
+ }
158
+ },
159
+ ];
154
160
  ```
155
161
 
156
162
  Returning anything other than `undefined` from a middleware short-circuits the request with that value, exactly as an app or layout middleware does.
@@ -1,32 +0,0 @@
1
- //#region ../web/src/server/route-validation-error.ts
2
- /**
3
- * Raised when a page's `route.validate` schema rejects `{ params, query }`.
4
- *
5
- * A page is a document, not an API endpoint (canon `b79c4f55`, point 2): this
6
- * is thrown into the ordinary error-boundary pipeline rather than answered as
7
- * a raw JSON body, so the request renders the application's error page (or the
8
- * nearest authored `ErrorBoundary`) with status 400 instead of a blob of JSON
9
- * where a page was expected.
10
- *
11
- * `errors` stays a real, structured property — never flattened away — so an
12
- * `error.page.tsx` reading `(error as RouteValidationError).errors` during SSR
13
- * can say exactly which field was wrong. `message` ALSO summarizes every
14
- * failing field: SSR (`ServerErrorPageProps.error`) receives this instance
15
- * directly, but the wire (`serializePageError`, `server/error-page.ts`) keeps
16
- * only `name`/`message`/`stack` — the same 400 must still say what was wrong
17
- * after that trip, not just that something was.
18
- */
19
- var RouteValidationError = class extends Error {
20
- errors;
21
- /** Always 400 — a validation failure is the visitor's malformed input, never the server's fault. */
22
- statusCode = 400;
23
- constructor(errors) {
24
- super(`Route validation failed: ${errors.map((issue) => `${issue.input}: ${issue.error}`).join("; ")}`);
25
- this.errors = errors;
26
- this.name = "RouteValidationError";
27
- }
28
- };
29
-
30
- //#endregion
31
- export { RouteValidationError };
32
- //# sourceMappingURL=route-validation-error.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"route-validation-error.mjs","names":[],"sources":["../../../../../../../web/src/server/route-validation-error.ts"],"sourcesContent":["import type { ValidationResult } from \"@warlock.js/seal\";\n\n/**\n * Raised when a page's `route.validate` schema rejects `{ params, query }`.\n *\n * A page is a document, not an API endpoint (canon `b79c4f55`, point 2): this\n * is thrown into the ordinary error-boundary pipeline rather than answered as\n * a raw JSON body, so the request renders the application's error page (or the\n * nearest authored `ErrorBoundary`) with status 400 instead of a blob of JSON\n * where a page was expected.\n *\n * `errors` stays a real, structured property — never flattened away — so an\n * `error.page.tsx` reading `(error as RouteValidationError).errors` during SSR\n * can say exactly which field was wrong. `message` ALSO summarizes every\n * failing field: SSR (`ServerErrorPageProps.error`) receives this instance\n * directly, but the wire (`serializePageError`, `server/error-page.ts`) keeps\n * only `name`/`message`/`stack` — the same 400 must still say what was wrong\n * after that trip, not just that something was.\n */\nexport class RouteValidationError extends Error {\n /** Always 400 — a validation failure is the visitor's malformed input, never the server's fault. */\n public readonly statusCode = 400;\n\n public constructor(public readonly errors: ValidationResult[\"errors\"]) {\n super(\n `Route validation failed: ${errors.map((issue) => `${issue.input}: ${issue.error}`).join(\"; \")}`,\n );\n this.name = \"RouteValidationError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,IAAa,uBAAb,cAA0C,MAAM;CAIX;;CAFnC,AAAgB,aAAa;CAE7B,AAAO,YAAY,AAAgB,QAAoC;EACrE,MACE,4BAA4B,OAAO,KAAK,UAAU,GAAG,MAAM,MAAM,IAAI,MAAM,OAAO,CAAC,CAAC,KAAK,IAAI,GAC/F;EAHiC;EAIjC,KAAK,OAAO;CACd;AACF"}