@warlock.js/web 5.2.4 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/esm/build/discover-pages.mjs +5 -7
  2. package/esm/build/discover-pages.mjs.map +1 -1
  3. package/esm/client/hydrate-page.mjs +4 -3
  4. package/esm/client/hydrate-page.mjs.map +1 -1
  5. package/esm/client/navigation/fetch-page-data.mjs +2 -2
  6. package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
  7. package/esm/client/navigation/navigation-root.mjs +5 -1
  8. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  9. package/esm/components/document-context.mjs.map +1 -1
  10. package/esm/hydration-payload.mjs +6 -2
  11. package/esm/hydration-payload.mjs.map +1 -1
  12. package/esm/index.d.mts +4 -2
  13. package/esm/index.mjs +2 -1
  14. package/esm/localization.d.mts +21 -0
  15. package/esm/localization.mjs +28 -0
  16. package/esm/localization.mjs.map +1 -0
  17. package/esm/routing/data-request.mjs +5 -3
  18. package/esm/routing/data-request.mjs.map +1 -1
  19. package/esm/routing/filesystem-route.mjs +36 -7
  20. package/esm/routing/filesystem-route.mjs.map +1 -1
  21. package/esm/routing/page-file-segment.mjs +66 -0
  22. package/esm/routing/page-file-segment.mjs.map +1 -0
  23. package/esm/routing/page-route-grammar.mjs +79 -0
  24. package/esm/routing/page-route-grammar.mjs.map +1 -0
  25. package/esm/routing/route-identity.d.mts +69 -0
  26. package/esm/routing/route-identity.mjs +100 -44
  27. package/esm/routing/route-identity.mjs.map +1 -1
  28. package/esm/server/build-hydration-payload.mjs +3 -2
  29. package/esm/server/build-hydration-payload.mjs.map +1 -1
  30. package/esm/server/create-page-route-handler.d.mts +34 -1
  31. package/esm/server/create-page-route-handler.mjs +35 -4
  32. package/esm/server/create-page-route-handler.mjs.map +1 -1
  33. package/esm/server/framework-default-not-found-stylesheet.mjs +102 -0
  34. package/esm/server/framework-default-not-found-stylesheet.mjs.map +1 -0
  35. package/esm/server/install-page-routes-from-manifest.mjs +13 -16
  36. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  37. package/esm/server/install-page-routes.d.mts +3 -1
  38. package/esm/server/install-page-routes.mjs +11 -11
  39. package/esm/server/install-page-routes.mjs.map +1 -1
  40. package/esm/server/not-found-page.d.mts +1 -13
  41. package/esm/server/not-found-page.mjs +50 -5
  42. package/esm/server/not-found-page.mjs.map +1 -1
  43. package/esm/server/register-production-public-files.mjs +16 -1
  44. package/esm/server/register-production-public-files.mjs.map +1 -1
  45. package/esm/server/render-page.d.mts +1 -8
  46. package/esm/server/render-page.mjs +24 -21
  47. package/esm/server/render-page.mjs.map +1 -1
  48. package/esm/server/response-cache-floor.mjs +79 -0
  49. package/esm/server/response-cache-floor.mjs.map +1 -0
  50. package/esm/server/set-cookie-cache-floor-hook.mjs +41 -0
  51. package/esm/server/set-cookie-cache-floor-hook.mjs.map +1 -0
  52. package/esm/server/web-connector.d.mts +1 -1
  53. package/esm/server/web-connector.mjs +23 -4
  54. package/esm/server/web-connector.mjs.map +1 -1
  55. package/llms-full.txt +62 -33
  56. package/llms.txt +1 -1
  57. package/package.json +4 -3
  58. package/skills/create-a-page/SKILL.md +59 -28
  59. package/skills/navigate-on-the-client/SKILL.md +16 -11
  60. package/skills/serve-styles/SKILL.md +2 -1
  61. package/skills/use-layouts/SKILL.md +3 -6
  62. package/skills/write-the-root/SKILL.md +0 -2
@@ -0,0 +1 @@
1
+ {"version":3,"file":"set-cookie-cache-floor-hook.mjs","names":[],"sources":["../../../../../../../web/src/server/set-cookie-cache-floor-hook.ts"],"sourcesContent":["/**\n * The `onSend` half of the `Set-Cookie` cache floor.\n *\n * `response-cache-floor.ts`'s `carriesSetCookie` check, run at the\n * `create-page-route-handler.ts` seam, only sees a `Set-Cookie` that was\n * already written to the response BEFORE that seam runs. `@fastify/cookie@10.0.1`\n * does not behave that way for `response.cookie()` / `response.clearCookie()`:\n * `setCookie`/`clearCookie` park the cookie, and it is flushed onto the real\n * `Set-Cookie` header only inside the COOKIE PLUGIN'S OWN `onSend` hook\n * (measured under NODE_ENV=production, test and development —\n * `getHeader(\"set-cookie\")` is `undefined` immediately after `setCookie()`,\n * and the header exists only once that hook has run). The only place this can\n * be observed is a SECOND `onSend` hook, registered to run AFTER the cookie\n * plugin's.\n *\n * REGISTRATION POINT AND WHY IT RUNS AFTER THE COOKIE PLUGIN'S HOOK: Fastify's\n * `addHook(\"onSend\", …)` does not push the hook onto the live list\n * immediately — it queues the push through `this.after(…)` (`fastify.js`'s\n * `addHook`), the same avvio queue `server.register(…)` uses. Avvio drains\n * that queue strictly in the order items were added, and does not move on to\n * the next queued item until the current one — including any nested\n * `addHook`/`register` calls made while it loads — has fully settled. Core's\n * `registerHttpPlugins` (`core/src/http/plugins.ts`) calls\n * `server.register(import(\"@fastify/cookie\"), …)` during `HttpConnector.boot()`.\n * `ensureSetCookieCacheFloorHook` is called from `createPageRouteHandler`,\n * which only ever runs while installing page routes during `WebConnector.boot()`\n * — a LATER `boot()` than `HttpConnector`'s (`web-connector.ts`'s own doc\n * comment on `WEB_CONNECTOR_PRIORITY`, `5.5` vs `5`). So by the time this\n * file's `addHook` call executes, the cookie plugin's registration is already\n * queued ahead of it; avvio resolves the cookie plugin (and the `onSend` hook\n * IT adds from inside its own plugin body) before it ever reaches our\n * later-queued hook. Net effect: cookie's `onSend` hook is pushed onto the\n * hooks array first, ours second, and Fastify runs `onSend` hooks in that\n * push order. `set-cookie-cache-floor.spec.ts`'s registration-order test\n * proves this rather than trusting the reasoning above.\n *\n * SCOPE: registered ONCE, instance-wide, directly on the root Fastify server\n * — not nested inside a `.register()` plugin scope. Fastify only guarantees\n * hook order relative to OTHER items at the SAME encapsulation level, and\n * `@fastify/cookie` is registered on the root instance; nesting ours would\n * put it in an unrelated child context with no ordering guarantee against the\n * root-level plugin at all.\n *\n * Despite being instance-wide, the EFFECT is scoped to page routes only: the\n * hook is a no-op unless the page pipeline explicitly marked the request via\n * `markPageResponse` (called from `create-page-route-handler.ts`, at the same\n * seam that sets `authDerived`). This is deliberate, not a heuristic on URL\n * shape or content-type — inferring \"is this a page route\" from the request\n * shape is exactly the \"one form inspected, a second form reaches the same\n * place unexamined\" defect this floor exists to close. An ordinary API\n * response is therefore untouched by this hook's EFFECT, even though the hook\n * FUNCTION itself runs for every response on the instance — widening that\n * effect to every API response is out of scope for this change.\n */\nimport type { FastifyInstance, FastifyReply, FastifyRequest } from \"fastify\";\nimport type { Request as CoreRequest, RequestLocals } from \"@warlock.js/core\";\nimport { carriesSetCookie } from \"./response-cache-floor\";\n\ndeclare module \"fastify\" {\n interface FastifyRequest {\n /**\n * Mirrors the core `Request`'s `locals` bag onto the raw Fastify request\n * — the only object an `onSend` hook actually receives. Set once, by\n * `markPageResponse`, from the exact same object `request.locals` points\n * at, so later mutations of either reference stay visible through both.\n */\n locals?: RequestLocals;\n }\n}\n\nconst instrumentedServers = new WeakSet<FastifyInstance>();\n\n/**\n * Mark the current request as a page-pipeline response.\n *\n * Called once per request from `create-page-route-handler.ts`, at the same\n * seam that decides `authDerived`. A no-op when `request.locals` is absent —\n * several existing unit tests hand the page route handler a plain\n * `{ path, header }` mock, never a real core `Request`; treated the same way\n * `applyResponseCacheFloor` treats a missing capability as \"not observable\".\n */\nexport function markPageResponse(request: CoreRequest): void {\n if (!request.locals) return;\n\n request.locals.isPageResponse = true;\n\n if (request.baseRequest) {\n request.baseRequest.locals = request.locals;\n }\n}\n\n/**\n * Register the `Set-Cookie` cache-floor `onSend` hook on `server`, once.\n *\n * Idempotent because `createPageRouteHandler` — the only caller — runs once\n * PER PAGE ROUTE, not once per server; without the guard, a second page route\n * would queue a second, redundant copy of the same hook.\n */\nexport function ensureSetCookieCacheFloorHook(server: FastifyInstance): void {\n if (instrumentedServers.has(server)) return;\n\n instrumentedServers.add(server);\n\n server.addHook(\n \"onSend\",\n (request: FastifyRequest, reply: FastifyReply, payload: unknown, done) => {\n if (request.locals?.isPageResponse !== true) {\n done(null, payload);\n return;\n }\n\n if (carriesSetCookie(reply)) {\n reply.header(\"Cache-Control\", \"private, no-store\");\n }\n\n done(null, payload);\n },\n );\n}\n"],"mappings":";;;AAsEA,MAAM,sCAAsB,IAAI,QAAyB;;;;;;;;;;AAWzD,SAAgB,iBAAiB,SAA4B;CAC3D,IAAI,CAAC,QAAQ,QAAQ;CAErB,QAAQ,OAAO,iBAAiB;CAEhC,IAAI,QAAQ,aACV,QAAQ,YAAY,SAAS,QAAQ;AAEzC;;;;;;;;AASA,SAAgB,8BAA8B,QAA+B;CAC3E,IAAI,oBAAoB,IAAI,MAAM,GAAG;CAErC,oBAAoB,IAAI,MAAM;CAE9B,OAAO,QACL,WACC,SAAyB,OAAqB,SAAkB,SAAS;EACxE,IAAI,QAAQ,QAAQ,mBAAmB,MAAM;GAC3C,KAAK,MAAM,OAAO;GAClB;EACF;EAEA,IAAI,iBAAiB,KAAK,GACxB,MAAM,OAAO,iBAAiB,mBAAmB;EAGnD,KAAK,MAAM,OAAO;CACpB,CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { BaseConnector, ConnectorLifecyclePhase } from "@warlock.js/core";
1
+ import { BaseConnector, ConnectorLifecyclePhase, FastifyInstance } from "@warlock.js/core";
2
2
  import { Alias, PluginOption, ViteDevServer } from "vite";
3
3
 
4
4
  //#region ../web/src/server/web-connector.d.ts
@@ -185,6 +185,28 @@ const REACT_REFRESH_PREAMBLE_ID = "virtual:warlock/react-refresh-preamble";
185
185
  /** The `\0`-prefixed form Rollup uses to mark a module as not-a-file. */
186
186
  const RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\0${REACT_REFRESH_PREAMBLE_ID}`;
187
187
  /**
188
+ * `@fastify/static`'s `maxAge` is milliseconds (the `send` package's option,
189
+ * not seconds like `Cache-Control`'s own `max-age`) — one year, matching the
190
+ * header this produces: `public, max-age=31536000, immutable`. Safe forever
191
+ * because every filename under {@link CLIENT_ASSET_URL_PREFIX} is content-hashed
192
+ * by the client build: a changed file is a changed URL, never a changed
193
+ * response at the same URL, which is the one condition `immutable` requires.
194
+ */
195
+ const HASHED_ASSET_CACHE_MAX_AGE_MS = 31536e3 * 1e3;
196
+ /**
197
+ * Exported so a test can assert the real options this connector hands
198
+ * `router.directory` — the same object `boot()` uses below, not a copy a spec
199
+ * could drift from unnoticed.
200
+ */
201
+ function productionAssetsDirectoryOptions(clientDir) {
202
+ return {
203
+ root: path.join(clientDir, "assets"),
204
+ prefix: `${CLIENT_ASSET_URL_PREFIX}/`,
205
+ maxAge: HASHED_ASSET_CACHE_MAX_AGE_MS,
206
+ immutable: true
207
+ };
208
+ }
209
+ /**
188
210
  * A directory, plus the path the filesystem really stores it at when the two
189
211
  * differ. Both forms belong in `server.fs.allow` — see the `fs` block in
190
212
  * {@link WebConnector.createViteServer} for why one of them is never enough.
@@ -278,10 +300,7 @@ var WebConnector = class extends BaseConnector {
278
300
  resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),
279
301
  clientDir: this.pageManifest.clientDir
280
302
  });
281
- if (this.pageManifest.pages.length > 0) router.directory({
282
- root: path.join(this.resolveClientDir(), "assets"),
283
- prefix: `${CLIENT_ASSET_URL_PREFIX}/`
284
- });
303
+ if (this.pageManifest.pages.length > 0) router.directory(productionAssetsDirectoryOptions(this.resolveClientDir()));
285
304
  return;
286
305
  }
287
306
  const fastify = this.resolveFastify();
@@ -1 +1 @@
1
- {"version":3,"file":"web-connector.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector.ts"],"sourcesContent":["/**\n * `WebConnector` — the SSR page surface as a first-class Warlock connector.\n *\n * It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React\n * pages on one port, and `web` no longer owns a private copy of the HTTP\n * lifecycle. Everything this file does used to live in `startDevServer()`\n * (`web/src/server/dev-server.ts`), which created its own Fastify instance,\n * scanned the router and called `listen()` itself — three responsibilities core\n * already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.\n *\n * WHY A `Late` CONNECTOR IS THE RIGHT SEAM, in ordering terms:\n * `ConnectorsManager.startPhase` runs **every** `boot()` in a phase before\n * **any** `start()` (`core/src/connectors/connectors-manager.ts:87-93`).\n * `HttpConnector` is itself `Late` (`core/src/connectors/http-connector.ts:41`)\n * and publishes its Fastify instance during its own `boot()`\n * (`container.set(\"http.server\", …)`, `core/src/connectors/http-connector.ts:74`).\n * So by the time this connector's `boot()` runs, Fastify and its plugins exist,\n * the raw node server exists, and NOTHING has been scanned or bound yet — page\n * routes registered here are picked up by `HttpConnector.start()`'s\n * `router.scanDevServer(…)` (`core/src/connectors/http-connector.ts:133`) before\n * `listen()` (`:147`). `SocketConnector.boot()` reads the same container key the\n * same way (`core/src/connectors/socket-connector.ts:78-80`) — this file is\n * deliberately shaped after it.\n *\n * What it can NOT do, and why that is fine: route COLLECTION happens earlier\n * (`core/src/dev-server/development-server.ts:57` precedes `:66`), so pages are\n * not discovered by the framework's file scanner. They are discovered here, by\n * `installPageRoutes` (`./install-page-routes.ts:189`), and registered through\n * the ordinary `router.get(…)` API — there is no second server matcher.\n *\n * DELIBERATE EXCEPTION to A.3 §2 (\"web has no core dependency\"), the same one\n * `./dev-server.ts` and `./install-page-routes.ts` record in their own headers:\n * this module is not re-exported from any package barrel (`web/src/index.ts`,\n * `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of\n * `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only\n * ever imported by tooling that already depends on core.\n *\n * `@warlock.js/web/connector` reaches this class ONLY through\n * `./web-connector-factory.ts`'s `await import(\"./web-connector\")` — a\n * deliberate seam, because a static edge from that barrel to this file would\n * put `../vite`, core's router and `./dev-server` into the import graph of\n * every consuming app's `warlock.config.ts`.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from \"fastify\";\nimport type { Alias, Plugin, PluginOption, ViteDevServer } from \"vite\";\nimport { Application, BaseConnector, ConnectorLifecyclePhase, type ConnectorName, container, type FastifyInstance, requestContext, router } from \"@warlock.js/core\";\nimport { resolveWebPackageRoot } from \"../build/contribution\";\nimport { appConventionAliases } from \"../vite/app-convention-aliases\";\nimport {\n createHydrationClientEntry,\n invalidateClientPageRegistry,\n warlockClientBoundary,\n} from \"../vite\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\nimport { devErrorTransportPlugin, sendCapturedDevError } from \"./dev-server\";\nimport { resolveHydrationClientUrl } from \"./hydration-client-url\";\nimport type { InstalledPageRoute } from \"./install-page-routes\";\nimport { installProductionPageRoutes } from \"./install-production-page-routes\";\nimport {\n classifyPageFileChanges,\n hasPageFileChanges,\n type PageFileChanges,\n} from \"./page-file-change\";\nimport {\n pageRouteSourceFiles,\n pageRoutesNeedReplacement,\n registeredPageFiles,\n} from \"./page-route-reload\";\nimport { consumePageManifest, type PageManifest } from \"./page-manifest\";\nimport { registerProductionPublicFiles } from \"./register-production-public-files\";\nimport { createUnregisteredPageReporter } from \"./unregistered-pages\";\nimport { WEB_CONNECTOR_PRIORITY } from \"./web-connector-factory\";\n\n/**\n * Boot/shutdown position relative to core's own connectors.\n *\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\n * is \"immediately after http, before everything else\". Two consequences, both\n * wanted:\n *\n * - `boot()` sees a Fastify instance that already has core's plugins and\n * health routes on it (`core/src/connectors/http-connector.ts:76`, `:85`).\n * - teardown is reverse-priority (`core/src/connectors/connectors-manager.ts:118`),\n * so Vite closes BEFORE the HTTP server does, not after.\n *\n * Note this is a magic number, not a declared dependency: core has no\n * `after`/`dependsOn` on the connector interface (`core/src/connectors/types.ts:8-71`).\n * Ordering only *needs* to be right for shutdown — `boot()` correctness is\n * guaranteed by the phase's boot-all-then-start-all pass regardless of priority.\n *\n * Declared in `./web-connector-factory` and re-exported here: the lazy delegate\n * that `webConnector()` returns must publish `priority` without loading this\n * (heavy) module. Importers keep the specifier they already use.\n */\nexport { WEB_CONNECTOR_PRIORITY };\n\n/**\n * Production boot ran with no page manifest in the registry.\n *\n * `undefined` from `consumePageManifest()` is a FACT, not an error — the registry never throws\n * on absence. The connector supplies the meaning, and it does so from MODE, not\n * from the value: in dev the absence is normal because Vite supplies the\n * modules and no build has run; in production it means the app was not built\n * with web, and a prod server that boots anyway serves 404s while looking\n * healthy — the exact silent failure this error exists to prevent.\n */\n/**\n * Which half of the handoff is live: Vite serving from source, or a bundle\n * produced by `warlock build`.\n *\n * RUNTIME STRATEGY, NOT `Application.environment`. The two are explicitly a\n * \"separate axis\" (`core/src/utils/environment.ts:4-7`), and the question this\n * connector asks — \"is there a page manifest, or does Vite supply the modules?\"\n * — is a HOSTING question. `warlock dev` with `NODE_ENV=production` (a staging\n * checkout, or just an inherited shell variable) is still Vite-hosted and still\n * has no manifest; keying off the environment would make that app refuse to\n * boot. Core sets the strategy on both sides deliberately:\n * `core/src/cli/commands/dev-server.command.ts:12` declares `\"development\"` for\n * `warlock dev`, and the generated production entry declares `\"production\"`\n * (`core/src/production/production-builder.ts:245`).\n *\n * This is core's own connector-level idiom, not a new one:\n * `core/src/connectors/http-connector.ts:132` picks `scanDevServer` over `scan`\n * the same way.\n */\nfunction isProductionRuntime(): boolean {\n return Application.runtimeStrategy === \"production\";\n}\n\nfunction pageFileVersion(file: string): string {\n try {\n return `present:${fs.readFileSync(file, \"utf8\")}`;\n } catch {\n return \"absent\";\n }\n}\n\nfunction pageChangeVersions(changes: PageFileChanges): Map<string, string> {\n return new Map(\n [...changes.added, ...changes.removed, ...changes.inspectionNeeded].map((file) => {\n const absoluteFile = path.resolve(file);\n return [absoluteFile, pageFileVersion(absoluteFile)] as const;\n }),\n );\n}\n\nexport class WebPageManifestMissingError extends Error {\n public constructor() {\n super(\n \"WebConnector booted in production without a page manifest. The generated \" +\n \"`pages.ts` barrel never ran, so no page modules were handed to the connector \" +\n \"and there is nothing to serve. Run `warlock build` with the web connector \" +\n \"registered in `warlock.config.ts > connectors`, and start the artifact that \" +\n \"build produced.\",\n );\n this.name = \"WebPageManifestMissingError\";\n }\n}\n\n/**\n * The manifest carried browser artifacts but no `clientDir`.\n *\n * The build bakes that field in beside the page table, so the only way to\n * observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`\n * older than the field, started against a newer runtime. Named rather than\n * left to `path.resolve(cwd, undefined)`, which throws a `TypeError` naming\n * neither the manifest nor the rebuild that fixes it.\n */\nexport class WebClientDirMissingError extends Error {\n public constructor() {\n super(\n \"The page manifest carries pages or public files but no `clientDir`, so the \" +\n \"connector cannot \" +\n \"locate the browser artifacts. This artifact was built by an older @warlock.js/web \" +\n \"than the one now running it. Re-run `warlock build` to regenerate the \" +\n \"`pages.ts` barrel against the current version.\",\n );\n this.name = \"WebClientDirMissingError\";\n }\n}\n\n/**\n * Third-party packages core reaches through `await import(...)` and that must\n * therefore never enter Vite's SSR transform graph.\n *\n * Left un-externalized, Vite's SSR module runner tries to resolve them anyway\n * and jams: the failure is NOT a missing-module error but a `transport invoke\n * timed out` on whatever unrelated module happened to be in flight. Derived in\n * one pass from `peerDependenciesMeta.optional` across every workspace package\n * reachable from `core/src/index.ts`, and carried over verbatim from\n * `dev-server.ts`'s own list. Only THIRD-PARTY peers belong here — every\n * `@warlock.js/*` sibling must stay in Vite's graph.\n *\n * This list is core's peer list, not web's; publishing it from core instead\n * of duplicating it here is still outstanding.\n */\nconst CORE_OPTIONAL_PEERS = [\n // mail\n \"nodemailer\",\n \"@aws-sdk/client-sesv2\",\n \"@react-email/render\",\n // cache\n \"redis\",\n \"pg\",\n // cascade\n \"mongodb\",\n // logger\n \"@sentry/node\",\n // core\n \"sharp\",\n \"socket.io\",\n \"@aws-sdk/client-s3\",\n \"@aws-sdk/lib-storage\",\n \"@aws-sdk/s3-request-presigner\",\n // herald\n \"amqplib\",\n // ai\n \"langfuse\",\n \"openai\",\n \"pdf-parse\",\n] as const;\n\n/**\n * `web`'s OWN optional peers, kept separate from {@link CORE_OPTIONAL_PEERS}\n * because that list is core's and is documented as such.\n *\n * Same rule, same reason: anything this package reaches through\n * `await import(...)` must be external to every bundler and SSR pipeline, or\n * some pipeline will try to resolve it in an install that legitimately does not\n * have it. `vite` itself is the precedent — `createViteServer` has always\n * loaded it this way — and `@vitejs/plugin-react` is loaded from the same\n * function, for the same dev-only reason, so it belongs in the same set.\n */\nconst WEB_OPTIONAL_PEERS = [\"vite\", \"@vitejs/plugin-react\"] as const;\n\n/**\n * Public specifier for the React Fast Refresh preamble module this connector\n * injects. See {@link WebConnector.resolveReactFastRefreshPlugins}.\n */\nconst REACT_REFRESH_PREAMBLE_ID = \"virtual:warlock/react-refresh-preamble\";\n\n/** The `\\0`-prefixed form Rollup uses to mark a module as not-a-file. */\nconst RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\\0${REACT_REFRESH_PREAMBLE_ID}`;\n\n/** `@vitejs/plugin-react`'s default export, plus the one static it publishes. */\ntype ReactPluginFactory = ((options?: Record<string, unknown>) => PluginOption[]) & {\n preambleCode?: string;\n};\n\n/**\n * A directory, plus the path the filesystem really stores it at when the two\n * differ. Both forms belong in `server.fs.allow` — see the `fs` block in\n * {@link WebConnector.createViteServer} for why one of them is never enough.\n *\n * A missing directory is not this function's problem to report: the roots it is\n * handed are already proven (`resolveWebPackageRoot`) or are the app's own cwd,\n * and an allow-list entry that points nowhere simply matches nothing.\n */\nfunction withRealPath(directory: string): string[] {\n try {\n const realPath = fs.realpathSync(directory);\n\n return realPath === directory ? [directory] : [directory, realPath];\n } catch {\n return [directory];\n }\n}\n\n/** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */\nfunction normalizeModuleId(id: string): string {\n const [filepath] = id.split(\"?\");\n\n return filepath.replace(/\\\\/g, \"/\").toLowerCase();\n}\n\nexport type WebConnectorOptions = {\n /**\n * Vite's `root` — the application directory that owns `src/`, `package.json`\n * and `tsconfig.json`. Rooting Vite at the APP (not at the `web` package) is\n * what makes the app's own bare specifiers and dependency-optimizer scan\n * resolve correctly. Defaults to `process.cwd()`, which is where `warlock dev`\n * already runs.\n */\n appRoot?: string;\n /** `<appRoot>/src` by default. Pages are `<appSrcRoot>/app/**\\/*.page.tsx`. */\n appSrcRoot?: string;\n /** The single global app-root file. `<appSrcRoot>/web/root.tsx` by default. */\n appFile?: string;\n /**\n * Root of the `@warlock.js/web` package, used to locate the hydration client\n * entry. Derived from this module's own location by default — a caller only\n * sets it when the package is not laid out normally.\n */\n webRoot?: string;\n /**\n * Extra `resolve.alias` entries, prepended to the app-convention aliases\n * (`web/*` → `src/web`, `app/*` → `src/app`) so a caller can win a conflict.\n * A normal application needs none of these; a monorepo checkout with unbuilt\n * workspace packages does.\n */\n resolveAlias?: Alias[];\n /** Extra `ssr.external` entries, appended to {@link CORE_OPTIONAL_PEERS}. */\n ssrExternal?: string[];\n /** Extra Vite plugins, appended after the client-boundary gates. */\n plugins?: PluginOption[];\n};\n\n/**\n * Web Connector\n * Manages the Vite dev server and the SSR page routes, mounted on the HTTP\n * connector's Fastify instance.\n */\nexport class WebConnector extends BaseConnector {\n public readonly name: ConnectorName = \"web\";\n public readonly priority = WEB_CONNECTOR_PRIORITY;\n public readonly lifecyclePhase = ConnectorLifecyclePhase.Late;\n\n /**\n * Nothing. Core already supplies each watcher batch to `shouldRestart`; page\n * membership and route identity are classified there, while component and\n * layout body edits remain Vite's HMR domain.\n */\n protected readonly watchedFiles: string[] = [];\n\n protected readonly options: WebConnectorOptions;\n\n protected vite?: ViteDevServer;\n\n protected installedPages: InstalledPageRoute[] = [];\n\n /**\n * The build→runtime handoff table, read once at boot.\n *\n * `undefined` in dev is the normal case and carries no meaning beyond \"no\n * build has run\" — see {@link WebPageManifestMissingError} for why the\n * production reading is a hard error and why the branch is on MODE.\n */\n protected pageManifest?: PageManifest;\n\n /**\n * The paths the DEVELOPMENT boot resolved, kept so `shouldRestart` can decide\n * whether a changed file is a page without re-deriving (and re-proving) the\n * web package root on every watcher batch. `undefined` in production and\n * before boot, which is exactly when `shouldRestart` must answer `false`.\n */\n protected resolvedPaths?: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>;\n\n /** Synchronous watcher classification handed to the async reload phase. */\n protected pendingPageChanges?: PageFileChanges;\n\n /** Reuses Vite's pipeline-barrel module instance for every dev reinstall. */\n protected installDevPageRoutes?: () => Promise<InstalledPageRoute[]>;\n\n /** Serializes Vite and core watcher callbacks that can observe the same edit. */\n protected pageRouteReloadQueue: Promise<unknown> = Promise.resolve();\n\n /** Committed file versions awaiting the overlapping watcher callback. */\n protected pendingHotUpdateSuppressions = new Map<string, string>();\n\n public constructor(options: WebConnectorOptions = {}) {\n super();\n this.options = options;\n }\n\n /**\n * Boot the connector — wire the page pipeline's request context and register\n * every page on the router.\n *\n * Two ways of doing that, one per hosting mode, and they share the shape\n * rather than the mechanism. Development creates Vite in middleware mode,\n * mounts it on the HTTP connector's Fastify instance and discovers pages by\n * walking `app/`; production takes both answers from the manifest the build\n * handed over (`./install-production-page-routes`) and touches no Vite at all.\n *\n * Everything here happens BEFORE `HttpConnector.start()` scans and listens,\n * which is the entire reason this is a `Late` connector's `boot()` and not its\n * `start()`.\n */\n public async boot() {\n // THE MODE BRANCH. One `if`, and it reads the mode — never the value.\n // `consumePageManifest()` returning `undefined` must not mean two different\n // things at one call site, so the only\n // question asked of the value here is \"is it there\", and the only thing\n // that decides whether that matters is {@link isProductionRuntime}.\n this.pageManifest = consumePageManifest();\n\n if (isProductionRuntime() && !this.pageManifest) {\n throw new WebPageManifestMissingError();\n }\n\n // The manifest is guaranteed present by the guard above; naming it again is\n // what narrows the type, not a second check of the same condition.\n if (isProductionRuntime() && this.pageManifest) {\n if ((this.pageManifest.publicFiles?.length ?? 0) > 0) {\n registerProductionPublicFiles(\n router,\n this.resolveClientDir(),\n this.pageManifest.publicFiles ?? [],\n );\n }\n\n this.installedPages = await installProductionPageRoutes({\n router,\n manifest: this.pageManifest,\n pageContext: requestContext,\n sharedStore: () => requestContext.getStore(),\n // The URL is resolved lazily, by the production path, only if there are\n // pages to hydrate — see the option's own note.\n resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),\n // The stylesheets are read from the manifest in this directory, by the\n // installer itself — it already imports the barrel that owns that\n // reader, and production has one module graph, so resolving there\n // rather than here avoids loading the barrel twice.\n clientDir: this.pageManifest.clientDir,\n });\n\n // SERVE THE CLIENT BUNDLE. Without this the whole production page path\n // completes and still ships a dead page: the SSR HTML carries\n // `<script type=\"module\" src=\"/assets/hydration-<hash>.js\">`, that request\n // 404s, and React never takes over. Nothing else in the process serves\n // that directory — `CLIENT_ASSET_URL_PREFIX` was, until now, only ever\n // read to VALIDATE the URL written into the HTML, never to mount the\n // files it points at.\n //\n // In `boot()` rather than `start()` because the router registers static\n // directories during its SCAN, and the scan is `HttpConnector.start()` —\n // which every `boot()` precedes. Registering in `start()` would be a\n // no-op that looked correct.\n //\n // Dev needs no equivalent: Vite's middleware serves the module graph\n // itself, which is why this sits inside the production branch and not\n // above it.\n //\n // Gated on the page count for the same reason the build's hydration\n // client step is: a zero-page build produces no `assets/` bundle to\n // mount. Such a build may still carry `clientDir` for copied public\n // files, which were registered individually above rather than exposing\n // this directory wholesale.\n if (this.pageManifest.pages.length > 0) {\n router.directory({\n root: path.join(this.resolveClientDir(), \"assets\"),\n prefix: `${CLIENT_ASSET_URL_PREFIX}/`,\n });\n }\n\n return;\n }\n\n // Everything below this line is the Vite-hosted development path. Nothing\n // above it touches Vite: it is an optional peer, so a production install\n // does not carry it, and this method is the only place the two halves meet.\n //\n // Fastify is required by the DEVELOPMENT path alone, and the guard sits\n // here rather than above the branch for that reason: production mounts no\n // middleware and needs no HMR socket, it registers page routes on the\n // router and `HttpConnector.start()` scans them like any other route.\n const fastify = this.resolveFastify();\n const paths = await this.resolvePaths();\n\n // Kept for `shouldRestart`, which is asked of this connector on every\n // watcher batch and must answer without re-resolving anything.\n this.resolvedPaths = paths;\n\n this.vite = await this.createViteServer(fastify, paths);\n\n // The pipeline barrel is loaded THROUGH VITE, not imported directly, and\n // that is load-bearing: page modules are evaluated inside Vite's SSR module\n // graph, so `connectSharedStore`/`connectPageContext` must be called on\n // VITE's instance of those modules. A plain Node `import` here would wire a\n // second, unrelated module instance and every page would render with an\n // empty shared store.\n const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);\n\n webServerSsr.connectSharedStore(() => requestContext.getStore());\n webServerSsr.connectPageContext(requestContext);\n\n // Vite's `middlewares` is a plain Connect `(req, res, next)` stack and\n // Fastify's `request.raw`/`reply.raw` ARE node's `req`/`res`, so an\n // `onRequest` hook mounts it with no plugin at all — `@fastify/middie` is\n // not needed and is not a core dependency (`core/package.json`). Vite never\n // fronts the server: it either answers its own asset request or calls\n // `done()` and Warlock's router owns the response.\n //\n // ONE EXCEPTION, and it is why `done` is wrapped rather than passed\n // straight through: vite reaches this callback for TWO different reasons in\n // middleware mode — \"not mine\" and \"mine, and it failed\". The second one\n // arrives indistinguishable from the first, because vite's own error\n // handler logs the failure and then calls `next()` with the error cleared\n // (`node_modules/vite/dist/node/chunks/config.js:9525-9527`). Handing that\n // to the framework produced an empty 404 on a module that exists — via the\n // app's catch-all page route (`./render-page.ts:604`,\n // `./create-page-route-handler.ts:147`) — and threw the only useful\n // explanation away. `devErrorTransportPlugin` captures it upstream; this\n // reads it back.\n // Dev-only on both sides — nothing below this line runs in production.\n fastify.addHook(\n \"onRequest\",\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\n this.vite?.middlewares(request.raw, reply.raw, (error?: Error) => {\n if (sendCapturedDevError(request.raw, reply.raw)) return;\n\n done(error);\n });\n },\n );\n\n const reportUnregisteredPages = createUnregisteredPageReporter({\n appRoot: paths.appRoot,\n appSrcRoot: paths.appSrcRoot,\n registeredPageFiles: () => registeredPageFiles(router.list(), paths.appSrcRoot),\n });\n\n fastify.addHook(\n \"onResponse\",\n (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => {\n if (reply.statusCode === 404) {\n reportUnregisteredPages({\n method: request.method,\n url: request.url,\n pathname: new URL(request.url, \"http://warlock.local\").pathname,\n });\n }\n\n done();\n },\n );\n\n this.installDevPageRoutes = () => webServerSsr.installPageRoutes({\n router,\n vite: this.vite,\n appSrcRoot: paths.appSrcRoot,\n appFile: paths.appFile,\n hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),\n // Without these the first paint of every full page load is unstyled: the\n // client bundle imports the CSS, so JavaScript applies it only after the\n // module graph loads. A render-blocking <link> in <head> is what makes\n // the page arrive styled instead of arriving and then correcting itself.\n stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile),\n });\n\n this.installedPages = await this.installDevPageRoutes();\n }\n\n /**\n * Where the browser fetches the hydration entry from — the one line that\n * differs between the two modes, so it is the only thing that branches.\n *\n * Dev keeps Vite's `/@fs/` URL, which Vite's own middleware transforms on\n * demand. Production reads the hashed filename out of the client build's\n * `.vite/manifest.json` exactly once, at boot, and NEVER falls back: each way\n * that read can fail is its own named error (`./hydration-client-url.ts:24`,\n * `:36`, `:53`), because \"serve without hydration\" would be a page that\n * renders and then does nothing.\n *\n * `webRoot` is OPTIONAL because only the dev branch has any use for it, and\n * asking production for it would be worse than useless: it is proved by\n * reading `<root>/package.json` off disk, which is exactly the kind of\n * source-tree lookup a built artifact must never depend on. A dev boot that\n * somehow reaches here without one is refused by name by the entry factory.\n */\n protected resolveHydrationClientModuleUrl(webRoot?: string): string {\n if (isProductionRuntime()) {\n return resolveHydrationClientUrl({ clientDir: this.resolveClientDir() });\n }\n\n return createHydrationClientEntry(webRoot ?? \"\").devUrl;\n }\n\n /**\n * `<outdir>/client` — the layout the build half writes and this half reads\n * at boot, taken from the value the build BAKED into the page manifest.\n *\n * It used to call `resolveBuildConfig()`, which reads `warlock.config.ts`\n * through `warlockConfigManager`. That works in `warlock build` and in the\n * `warlock start` SUPERVISOR, and it cannot work here: the supervisor spawns\n * a plain `node dist/app.js` CHILD, and that process never loads — and could\n * not load — a TypeScript build-time config. The call threw\n * `WarlockConfig not loaded` inside connector boot, so the production server\n * died before it ever listened.\n *\n * Baking it also settles the drift the old comment was worried about, and\n * settles it harder: `build` and `start` cannot disagree about where the\n * bundle lives, because `start` is no longer re-deriving the path at all —\n * it reads back the one string `build` wrote.\n */\n protected resolveClientDir(): string {\n // The manifest CACHED at boot (line ~269), not a second `consumePageManifest()`:\n // this runs from a callback the production branch invokes lazily, long after\n // that assignment, and reading the same field the mode branch already\n // decided on keeps one source of truth for the boot's view of the manifest.\n const clientDir = this.pageManifest?.clientDir;\n\n if (clientDir === undefined) {\n // Reached only via a manifest with browser artifacts but no `clientDir`\n // — i.e. a bundle built by a web version older than this field. Named here\n // rather than left to surface as an ENOENT on a `path.join(undefined)`\n // deep inside the manifest read.\n throw new WebClientDirMissingError();\n }\n\n return path.resolve(process.cwd(), clientDir);\n }\n\n /**\n * The page manifest this connector consumed at boot, or `undefined` in dev\n * where Vite supplies the modules instead.\n */\n public getPageManifest(): PageManifest | undefined {\n return this.pageManifest;\n }\n\n /**\n * Activate. There is nothing to listen on — `HttpConnector.start()` owns the\n * single `listen()` for the whole process — so this only marks the connector\n * live once `boot()` has wired everything.\n */\n public async start(): Promise<void> {\n if (!this.vite) return;\n\n this.active = true;\n }\n\n /**\n * Shutdown — close Vite, and drop the sockets Vite's middleware left behind.\n *\n * Reverse-priority teardown (`core/src/connectors/connectors-manager.ts:118`)\n * puts this BEFORE `HttpConnector.shutdown()`, which is exactly what the\n * second call needs: requests answered by Vite's connect stack are written\n * straight to `reply.raw`, so Fastify never observes them completing and their\n * keep-alive sockets are never counted idle. Core's default\n * `forceCloseConnections: \"idle\"` (`core/src/http/server.ts:34`) then waits on\n * them forever. A dev server has no draining obligation, and the connector\n * that caused the raw writes is the right one to clean up after them.\n */\n public async shutdown(): Promise<void> {\n if (!this.active) return;\n\n if (container.has(\"http.server\")) {\n container.get(\"http.server\").server.closeAllConnections();\n }\n\n await this.vite?.close();\n this.vite = undefined;\n this.installedPages = [];\n this.installDevPageRoutes = undefined;\n this.pendingPageChanges = undefined;\n this.pendingHotUpdateSuppressions.clear();\n this.pageManifest = undefined;\n\n this.active = false;\n }\n\n /**\n * Queue page add/remove/edit candidates for asynchronous live routing work.\n * Classification stays synchronous because core's connector interface is;\n * edited route exports are evaluated later through Vite's fresh SSR graph.\n */\n public shouldRestart(changedFiles: string[] = []): boolean {\n if (this.vite === undefined || this.resolvedPaths === undefined) {\n return false;\n }\n\n this.pendingPageChanges = this.classifyPageChanges(changedFiles);\n return this.pendingPageChanges !== undefined;\n }\n\n protected classifyPageChanges(changedFiles: readonly string[]): PageFileChanges | undefined {\n if (this.resolvedPaths === undefined) return undefined;\n\n const changes = classifyPageFileChanges(changedFiles, {\n appRoot: this.resolvedPaths.appRoot,\n appSrcRoot: this.resolvedPaths.appSrcRoot,\n installedPageFiles: registeredPageFiles(router.list(), this.resolvedPaths.appSrcRoot),\n });\n\n return hasPageFileChanges(changes) ? changes : undefined;\n }\n\n protected enqueuePageRouteReload(changes: PageFileChanges): Promise<boolean> {\n const eventVersions = pageChangeVersions(changes);\n const run = this.pageRouteReloadQueue.catch(() => undefined).then(async () => {\n const vite = this.vite;\n const install = this.installDevPageRoutes;\n const paths = this.resolvedPaths;\n\n if (vite === undefined || install === undefined || paths === undefined) return false;\n\n const matchingCommittedFiles = new Set<string>();\n\n // This check belongs inside the queue: a matching core transaction may\n // commit while a Vite callback is waiting behind it. File versions are\n // captured when the job is queued so a later edit cannot consume an\n // earlier event's marker.\n for (const [file, eventVersion] of eventVersions) {\n const committedVersion = this.pendingHotUpdateSuppressions.get(file);\n if (committedVersion === undefined) continue;\n\n // Matching markers are consumed exactly once. A mismatched marker is\n // obsolete and must not survive to suppress a future reverted edit.\n this.pendingHotUpdateSuppressions.delete(file);\n if (committedVersion === eventVersion) matchingCommittedFiles.add(file);\n }\n\n if (eventVersions.size > 0 && matchingCommittedFiles.size === eventVersions.size) {\n return true;\n }\n\n const replace = await pageRoutesNeedReplacement(changes, {\n vite,\n appSrcRoot: paths.appSrcRoot,\n installedPages: this.installedPages,\n });\n\n if (!replace) return false;\n\n const nextInstalledPages = await router.replaceRoutesBySourceFiles(\n pageRouteSourceFiles(router.list()),\n install,\n );\n\n // Advance observable state only after the router transaction commits.\n // A rejected install keeps both the old route table and browser live.\n this.installedPages = nextInstalledPages;\n invalidateClientPageRegistry(vite);\n\n for (const [file, eventVersion] of eventVersions) {\n if (!matchingCommittedFiles.has(file)) {\n this.pendingHotUpdateSuppressions.set(file, eventVersion);\n }\n }\n\n return true;\n });\n\n this.pageRouteReloadQueue = run;\n return run;\n }\n\n /**\n * Vite-side ordering barrier. It publishes a changed route graph before the\n * page-registry plugin can reload the document; a matching change already\n * handled by core is consumed once, preventing a duplicate full reload.\n */\n protected async handlePageHotUpdate(file: string): Promise<boolean> {\n const absoluteFile = path.resolve(file);\n const changes = this.classifyPageChanges([absoluteFile]);\n if (changes === undefined) return false;\n\n return this.enqueuePageRouteReload(changes);\n }\n\n /**\n * Live page routing update. Despite the connector API name, this never closes\n * Vite: it atomically replaces page-owned routes only when membership or the\n * canonical route identity changed, then refreshes the client registry.\n */\n public async restart(): Promise<void> {\n const changes = this.pendingPageChanges;\n this.pendingPageChanges = undefined;\n\n if (\n changes === undefined ||\n this.resolvedPaths === undefined ||\n this.vite === undefined ||\n this.installDevPageRoutes === undefined\n ) {\n return;\n }\n\n await this.enqueuePageRouteReload(changes);\n }\n\n /** The pages this connector registered on the router, in registration order. */\n public getInstalledPages(): readonly InstalledPageRoute[] {\n return this.installedPages;\n }\n\n /**\n * The Fastify instance the HTTP connector published during its own `boot()`\n * (`core/src/connectors/http-connector.ts:74`).\n *\n * Absence is fatal rather than a silent no-op: unlike sockets, there is no\n * standalone fallback a page surface could serve from, and the failure this\n * guards against — an app with no `src/config/http.ts` — otherwise shows up\n * as every page 404ing with no explanation.\n */\n protected resolveFastify(): FastifyInstance {\n if (!container.has(\"http.server\")) {\n throw new Error(\n \"WebConnector requires the HTTP connector's Fastify instance, but \" +\n '`http.server` is not in the container. The `http` config is missing — add `src/config/http.ts` ' +\n \"so `HttpConnector.boot()` runs (core/src/connectors/http-connector.ts:61-74).\",\n );\n }\n\n return container.get(\"http.server\");\n }\n\n /** Resolve every path this connector needs from the (optional) options. */\n protected async resolvePaths() {\n const appRoot = this.options.appRoot ?? process.cwd();\n const appSrcRoot = this.options.appSrcRoot ?? path.join(appRoot, \"src\");\n const selfPath = fileURLToPath(import.meta.url);\n // The web package root goes through `resolveWebPackageRoot`, which PROVES\n // the directory by reading `<root>/package.json` and matching its `name`,\n // rather than trusting a fixed number of `..` hops. A configured root is\n // asserted the same way. Either failure throws\n // `WebPackageRootResolutionError` naming the directory at boot — the\n // alternative was a wrong root surfacing much later as a 404 on the\n // hydration entry with nothing to point at.\n const webRoot = await resolveWebPackageRoot(this.options.webRoot);\n\n return {\n appRoot,\n appSrcRoot,\n appFile: this.options.appFile ?? path.join(appSrcRoot, \"web/root.tsx\"),\n webRoot,\n // Extension-agnostic on purpose: the sibling barrel is `index.ts` when\n // this package runs from source and `index.js` once it is built, the same\n // trick `registerLoader` uses when it resolves its own siblings.\n webServerBarrel: path.join(path.dirname(selfPath), `index${path.extname(selfPath)}`),\n };\n }\n\n /**\n * Vite in middleware mode, `appType: \"custom\"` — Warlock owns the response\n * shape and Vite never fronts the server.\n *\n * `server.hmr.server` is handed the RAW node server (`fastify.server`), so the\n * HMR websocket shares the one port the app already listens on. No `hmr.port`\n * and no `clientPort`: Vite's HMR path stays the default `\"/\"`, socket.io\n * stays on `\"/socket.io\"` (`core/src/connectors/socket-connector.ts:90`), and\n * the two `upgrade` listeners coexist because each is a selective filter that\n * leaves a non-matching socket alone — verified empirically in both attachment\n * orders.\n */\n /**\n * React Fast Refresh, or nothing at all — never half of it.\n *\n * TWO plugins, and the second one is not optional. `@vitejs/plugin-react`\n * emits, into every client component module, a wrapper that reads\n * `window.$RefreshReg$` and THROWS \"can't detect preamble\" if it is missing.\n * The preamble that defines it normally arrives through Vite's\n * `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:\n * the document is React's SSR output and the only script appended to it is\n * the hydration entry (`./create-page-route-handler.ts:95`). So the preamble\n * is delivered as a virtual module that the hydration entry imports FIRST.\n *\n * \"First\" is load-bearing, twice over. ESM evaluates a module's imports in\n * source order before the importer's own body, so an import placed at\n * position 0 runs before `virtual:warlock/pages` pulls in a single component\n * — which is what `$RefreshReg$` has to exist for — and before `react-dom`\n * initialises, which is what `injectIntoGlobalHook` has to precede.\n *\n * The preamble TEXT is read from the plugin's own `preambleCode` export\n * rather than copied here, so a version bump cannot leave this file holding\n * a stale runtime contract. If that export ever stops being a string, Fast\n * Refresh is DECLINED entirely (empty array) instead of registered without\n * its preamble — a loud \"no HMR\" beats a component graph that throws on\n * first paint.\n *\n * Dev-only by construction, like everything else this method is called from:\n * `apply: \"serve\"` on our own plugin, `apply: \"serve\"` on the plugin's\n * refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The\n * PRODUCTION client bundle cannot reach any of this — it is built by\n * `buildWarlockHydrationClient` (`../vite/index.ts:185`), which composes\n * `warlockClientBoundary()` and nothing else, and never calls this method.\n *\n * @param paths resolved by {@link WebConnector.resolvePaths}\n */\n protected async resolveReactFastRefreshPlugins(\n paths: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>,\n ): Promise<PluginOption[]> {\n let viteReact: ReactPluginFactory;\n\n try {\n // OPTIONAL peer (`web/package.json`'s `peerDependenciesMeta`), so this is\n // a lazy `await import` exactly like the `vite` one above it, and\n // `@vitejs/plugin-react` is listed in `WEB_OPTIONAL_PEERS` for the same\n // reason `vite` is.\n ({ default: viteReact } = (await import(\"@vitejs/plugin-react\")) as unknown as {\n default: ReactPluginFactory;\n });\n } catch {\n console.warn(\n \"[warlock:web] React Fast Refresh is OFF: `@vitejs/plugin-react` is not installed. \" +\n \"Edits to a component will reload the page instead of hot-swapping it. \" +\n \"Install it with `npm i -D @vitejs/plugin-react` to enable it.\",\n );\n\n return [];\n }\n\n const preambleCode = viteReact.preambleCode;\n\n if (typeof preambleCode !== \"string\") {\n console.warn(\n \"[warlock:web] React Fast Refresh is OFF: this `@vitejs/plugin-react` no longer exports \" +\n \"`preambleCode`, so the refresh preamble cannot be injected into the hydration entry. \" +\n \"Registering the plugin without it would make every component module throw \" +\n \"\\\"can't detect preamble\\\" in the browser.\",\n );\n\n return [];\n }\n\n // `base` is never set on the config below, so it is Vite's default `\"/\"`.\n const preambleSource = preambleCode.replace(\"__BASE__\", \"/\");\n const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);\n\n const preamblePlugin: Plugin = {\n name: \"warlock:react-refresh-preamble\",\n // BEFORE `warlock:projection` and the gates would be wrong and BEFORE\n // esbuild's TS transform is required: this prepends one import statement\n // to TypeScript source, so it has to see the file before anything lowers\n // it. It touches exactly one module, so it cannot reorder anything else.\n enforce: \"pre\",\n apply: \"serve\",\n // The preamble is browser state (`window.$RefreshReg$`). The SSR\n // environment must never evaluate it — there is no `window` there, and\n // the server render must stay byte-identical to what it produced before\n // this plugin existed.\n applyToEnvironment: environment => environment.config.consumer === \"client\",\n resolveId(source) {\n if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;\n\n return null;\n },\n load(id) {\n if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;\n\n return null;\n },\n transform(code, id) {\n if (normalizeModuleId(id) !== hydrationEntryId) return null;\n\n return { code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\\n${code}`, map: null };\n },\n };\n\n return [preamblePlugin, ...viteReact()];\n }\n\n protected async createViteServer(\n fastify: FastifyInstance,\n paths: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>,\n ): Promise<ViteDevServer> {\n const { createServer, buildErrorMessage, searchForWorkspaceRoot } = await import(\"vite\");\n\n // Vite's own default for `server.fs.allow`, reproduced rather than dropped:\n // naming the key at all REPLACES the default, and an application that\n // legitimately serves files from above its own root has to keep working.\n const workspaceRoot = searchForWorkspaceRoot(paths.appRoot);\n\n return createServer({\n root: paths.appRoot,\n appType: \"custom\",\n plugins: [\n // FIRST, and dev-only by construction: this method is reachable only\n // from `boot()`'s Vite branch, past the `isProductionRuntime()` guard.\n // The same predicate is handed in rather than re-derived, and the\n // factory throws if it is ever true — see the plugin's own header for\n // why the layer has to be registered from a plugin and not from\n // `vite.middlewares.use(...)` after this call returns.\n devErrorTransportPlugin({ isProductionRuntime, buildErrorMessage }),\n ...warlockClientBoundary({\n appRoot: paths.appRoot,\n beforePageHotUpdate: ({ file }) => this.handlePageHotUpdate(file),\n }),\n // AFTER the boundary, and the order matters among `enforce: \"pre\"`\n // plugins (Vite keeps array order within an enforce bucket).\n // `warlock:projection` strips a page's server exports — `loader` and\n // friends — before React's babel pass sees the module, so Fast Refresh\n // never registers a refresh boundary for an export that is not supposed\n // to reach the browser at all.\n //\n // BEFORE `options.plugins` so an application can still override.\n ...(await this.resolveReactFastRefreshPlugins(paths)),\n ...(this.options.plugins ?? []),\n ],\n server: {\n middlewareMode: true,\n hmr: { server: fastify.server },\n fs: {\n // `<Scripts />` points the browser at the hydration client entry under\n // `<webRoot>`: the published `esm/hydration/index.mjs` when installed,\n // or `src/hydration/index.ts` in this checkout. A dependency normally\n // lives under the app root's `node_modules`; when `@warlock.js/web` is\n // LINKED — a monorepo checkout, `npm link`, or a `file:` dependency —\n // its real path can sit outside every directory Vite allows by default\n // and the request comes back `403 Restricted`.\n //\n // That failure is silent in the worst way: SSR has already produced\n // the markup by the time the browser asks for the script, so the page\n // renders perfectly, nothing is logged, and the only symptom is that\n // no button ever works. Naming web's own root makes a linked install\n // behave like an installed one.\n //\n // Every root is listed twice, as given and as `realpathSync` reports\n // it, because Vite resolves a requested file to its REAL path before\n // testing it against this list. Allowing the symlink alone therefore\n // matches nothing — the 403 page prints the link that was allowed\n // directly above the real path it rejected.\n allow: [\n ...new Set([\n ...withRealPath(workspaceRoot),\n ...withRealPath(paths.appRoot),\n ...withRealPath(paths.webRoot),\n ]),\n ],\n },\n },\n // Without an explicit target, esbuild assumes native (TC39) decorator\n // support and leaves `@RegisterModel()`-style syntax untouched — but Vite's\n // SSR module runner evaluates transformed code via `new AsyncFunction(...)`,\n // which node has no native decorator support for. `es2022` downlevels them\n // into helper calls. `jsx` is named explicitly rather than left to tsconfig\n // discovery, because Vite matches a file against a tsconfig's `include` and\n // `web/tsconfig.json`'s is narrow enough that most of `web/src` matched no\n // config at all and fell back to the CLASSIC transform — emitting\n // `React.createElement` into modules that import no `React` binding.\n esbuild: { target: \"es2022\", jsx: \"automatic\" },\n /**\n * REACT MUST BE PRE-BUNDLED, and naming it here is the only thing that\n * makes that happen for the framework's own client graph.\n *\n * `react-dom/client` is CommonJS. A browser cannot import a named export\n * from it, so Vite's dep optimizer normally rewrites it into an ESM shim\n * under `/node_modules/.vite/deps/`. Whether that rewrite happens is\n * decided in `tryNodeResolve`, and one of the conditions that SKIPS it is\n * `importer && isInNodeModules(importer)`\n * (`vite/dist/node/chunks/config.js:32822`). Vite's reasoning is sound in\n * general — a dependency's own internal imports are the optimizer's job,\n * not the resolver's — but it is exactly wrong here:\n *\n * `<Scripts />` points the browser at the hydration entry, and in an\n * INSTALLED app that entry is\n * `<app>/node_modules/@warlock.js/web/esm/hydration/index.mjs`.\n * Every module it reaches is therefore inside `node_modules`, so every\n * bare import it makes takes the skip branch and is served as the raw\n * file with a `?v=<browserHash>` cache key bolted on.\n *\n * For `esm/**.mjs` that is harmless — they are already ESM. For\n * `react-dom/client` it is fatal, and it is the whole defect: the browser\n * receives `\"use strict\"; function checkDCE()…` and refuses the module\n * with\n *\n * SyntaxError: The requested module '/node_modules/react-dom/client.js?v=…'\n * does not provide an export named 'hydrateRoot'\n *\n * Nothing in the client runtime then runs at all: no hydration, so no\n * `useState`, no Fast Refresh, and `<Link>` degrades to a full document\n * load because the navigation listener was never attached. Measured on a\n * published 5.0.2 install (fresh app, no symlinks): all four symptoms\n * before this block, none after.\n *\n * `include` is the fix rather than `entries` because it does not depend\n * on the scanner reaching the entry: an included id is pre-bundled at\n * server start and `tryOptimizedResolve` matches it by NAME\n * (`config.js:32633`) before `tryNodeResolve` — and that lookup has no\n * importer condition, so a node_modules importer resolves to the shim\n * like anyone else. Pointing `optimizeDeps.entries` at the hydration\n * entry would not work: the scanner classifies a resolution inside\n * `node_modules` as a dependency to externalize rather than a source to\n * walk.\n *\n * All four names are listed even though `react` and `react/jsx-runtime`\n * usually get discovered anyway — they are discovered only because the\n * APP's own pages import them, which is a fact about the app and not\n * something the framework may rely on. `react-dom/client` is imported by\n * `client/hydrate-page.tsx` and by nothing a normal app writes, which is\n * why it was the one that broke.\n *\n * This is INVISIBLE from the monorepo checkout: there the hydration entry\n * resolves to `web/src/hydration/index.ts`, a path with no `node_modules`\n * segment, so the skip branch never fires and React optimizes normally.\n * Canon: nothing measured inside the checkout is evidence about a\n * published install.\n */\n optimizeDeps: {\n include: [\"react\", \"react-dom\", \"react-dom/client\", \"react/jsx-runtime\"],\n /**\n * The SECOND instance problem, and the reason this is `exclude` and not\n * another `include`.\n *\n * An app page's `import { Link } from \"@warlock.js/web\"` has an importer\n * OUTSIDE `node_modules`, so it takes the opposite branch above and the\n * whole package is pre-bundled into\n * `/node_modules/.vite/deps/@warlock__js_web.js`. The hydration entry\n * cannot reach that bundle — it is loaded by absolute path through\n * `/@fs/` and its imports are relative, so they resolve to the raw\n * `esm/*.mjs` files. The browser then holds TWO copies of every client\n * module in this package: the app renders `<Link>` from the bundled one\n * while the hydration entry publishes routes and mounts the document\n * context on the raw one. Module-level state does not cross that line.\n *\n * `routing/route-table` survives it by accident — it keeps its table in\n * a `Symbol.for` slot on `globalThis`, which is realm-wide — but\n * `components/document-context` is a React context OBJECT, and two\n * `createContext()` calls are two different contexts no provider can\n * bridge.\n *\n * Excluding the package makes the app's import resolve to\n * `esm/index.mjs` with the same `?v=` key the hydration entry's imports\n * carry, so there is one instance again. It costs the pre-bundle (a\n * handful of extra dev requests for files that are already valid ESM)\n * and nothing else — the grep across every client-reachable module in\n * this package finds exactly three bare imports, all of them React, all\n * of them covered by `include` above.\n */\n exclude: [\"@warlock.js/web\"],\n },\n ssr: {\n external: [\n ...CORE_OPTIONAL_PEERS,\n ...WEB_OPTIONAL_PEERS,\n ...(this.options.ssrExternal ?? []),\n ],\n /**\n * ONE `@warlock.js/web`, for the same reason `resolve.dedupe` below\n * insists on one React — and it is invisible from inside this repo.\n *\n * Vite externalises `node_modules` in SSR by default, so an installed\n * app gets TWO instances: the app's own `root.tsx` imports\n * `@warlock.js/web` and Vite hands that off to Node, while the pipeline\n * is loaded deliberately through `vite.ssrLoadModule(...)` and stays\n * inside Vite's graph. `renderPage` then sets the document context on\n * Vite's copy of `components/document-context`, and the app's `<Head/>`\n * reads Node's copy, which has nothing in it:\n *\n * <Head/> was rendered outside the page pipeline's document context\n *\n * Measured on a published 5.0.1 install: `GET /` 500 without this line,\n * 200 with it (404 control still 404). In THIS checkout the package\n * resolves to source under Vite's root, never through `node_modules`,\n * so both paths land on one instance and the bug cannot reproduce.\n * Canon `6b7ab838`.\n */\n noExternal: [\"@warlock.js/web\"],\n },\n resolve: {\n // ONE React, resolved from the application. A linked `@warlock.js/web`\n // resolves `react` out of its own tree while the app's pages resolve it\n // out of theirs; two React instances share no hook dispatcher, and SSR\n // dies on the first `useState` with \"Cannot read properties of null\".\n // `dedupe` forces these package names to resolve from Vite's `root` —\n // the app — whoever imported them, in the SSR environment as much as in\n // the client one.\n //\n // Two names cover every entry point. Vite matches a deep import against\n // the package it belongs to before consulting this list, so\n // `react-dom/client`, `react/jsx-runtime` and `react/jsx-dev-runtime`\n // are already deduped by `react-dom` and `react`; listing them\n // separately would only add entries that can never match.\n //\n // Do NOT express this as a `resolve.alias` entry instead. Pointing a\n // bare React specifier at a directory drags React's CommonJS entry into\n // Vite's SSR module graph, and the dev server then dies at startup with\n // \"module is not defined\" before it renders anything at all.\n dedupe: [\"react\", \"react-dom\"],\n alias: [\n ...(this.options.resolveAlias ?? []),\n // The app-tree convention `v5/app/tsconfig.json`'s own `paths` declare.\n // Vite does not read tsconfig paths on its own and no\n // `vite-tsconfig-paths` plugin is installed in this workspace.\n //\n // ONE definition, shared with the production build contribution\n // (`web/src/build/contribution.ts`). These were two separate literals\n // until 2026-08-24, and the production half simply did not have them —\n // dev resolved `web/*` while the production client build died on the\n // first page. Do not inline them back here.\n ...appConventionAliases(paths.appSrcRoot),\n ],\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkIA,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;;;;;;;;;;;;;;;;AAiBA,MAAM,sBAAsB;CAE1B;CACA;CACA;CAEA;CACA;CAEA;CAEA;CAEA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACA;CACA;AACF;;;;;;;;;;;;AAaA,MAAM,qBAAqB,CAAC,QAAQ,sBAAsB;;;;;AAM1D,MAAM,4BAA4B;;AAGlC,MAAM,qCAAqC,KAAK;;;;;;;;;;AAgBhD,SAAS,aAAa,WAA6B;CACjD,IAAI;EACF,MAAM,WAAW,GAAG,aAAa,SAAS;EAE1C,OAAO,aAAa,YAAY,CAAC,SAAS,IAAI,CAAC,WAAW,QAAQ;CACpE,QAAQ;EACN,OAAO,CAAC,SAAS;CACnB;AACF;;AAGA,SAAS,kBAAkB,IAAoB;CAC7C,MAAM,CAAC,YAAY,GAAG,MAAM,GAAG;CAE/B,OAAO,SAAS,QAAQ,OAAO,GAAG,CAAC,CAAC,YAAY;AAClD;;;;;;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;GAC9C,KAAK,KAAK,aAAa,aAAa,UAAU,KAAK,GACjD,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;IACf,MAAM,KAAK,KAAK,KAAK,iBAAiB,GAAG,QAAQ;IACjD,QAAQ,GAAG,wBAAwB;GACrC,CAAC;GAGH;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,6BAA6B,aAAa,kBAAkB;GAC/D;GACA,MAAM,KAAK;GACX,YAAY,MAAM;GAClB,SAAS,MAAM;GACf,0BAA0B,KAAK,gCAAgC,MAAM,OAAO;GAK5E,gBAAgB,aAAa,kBAAkB,MAAM,SAAS,MAAM,OAAO;EAC7E,CAAC;EAED,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,qBAAqB,YAAY,MAAS,CAAC,CAAC,KAAK,YAAY;GAC5E,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;EAED,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgDA,MAAgB,+BACd,OACyB;EACzB,IAAI;EAEJ,IAAI;GAKF,CAAC,CAAE,SAAS,aAAe,MAAM,OAAO;EAG1C,QAAQ;GACN,QAAQ,KACN,uNAGF;GAEA,OAAO,CAAC;EACV;EAEA,MAAM,eAAe,UAAU;EAE/B,IAAI,OAAO,iBAAiB,UAAU;GACpC,QAAQ,KACN,iSAIF;GAEA,OAAO,CAAC;EACV;EAGA,MAAM,iBAAiB,aAAa,QAAQ,YAAY,GAAG;EAC3D,MAAM,mBAAmB,kBAAkB,2BAA2B,MAAM,OAAO,CAAC,CAAC,UAAU;EAgC/F,OAAO,CAAC;GA7BN,MAAM;GAKN,SAAS;GACT,OAAO;GAKP,qBAAoB,gBAAe,YAAY,OAAO,aAAa;GACnE,UAAU,QAAQ;IAChB,IAAI,WAAW,2BAA2B,OAAO;IAEjD,OAAO;GACT;GACA,KAAK,IAAI;IACP,IAAI,OAAO,oCAAoC,OAAO;IAEtD,OAAO;GACT;GACA,UAAU,MAAM,IAAI;IAClB,IAAI,kBAAkB,EAAE,MAAM,kBAAkB,OAAO;IAEvD,OAAO;KAAE,MAAM,UAAU,KAAK,UAAU,yBAAyB,EAAE,KAAK;KAAQ,KAAK;IAAK;GAC5F;EAGmB,GAAG,GAAG,UAAU,CAAC;CACxC;CAEA,MAAgB,iBACd,SACA,OACwB;EACxB,MAAM,EAAE,cAAc,mBAAmB,2BAA2B,MAAM,OAAO;EAKjF,MAAM,gBAAgB,uBAAuB,MAAM,OAAO;EAE1D,OAAO,aAAa;GAClB,MAAM,MAAM;GACZ,SAAS;GACT,SAAS;IAOP,wBAAwB;KAAE;KAAqB;IAAkB,CAAC;IAClE,GAAG,sBAAsB;KACvB,SAAS,MAAM;KACf,sBAAsB,EAAE,WAAW,KAAK,oBAAoB,IAAI;IAClE,CAAC;IASD,GAAI,MAAM,KAAK,+BAA+B,KAAK;IACnD,GAAI,KAAK,QAAQ,WAAW,CAAC;GAC/B;GACA,QAAQ;IACN,gBAAgB;IAChB,KAAK,EAAE,QAAQ,QAAQ,OAAO;IAC9B,IAAI,EAoBF,OAAO,CACL,GAAG,IAAI,IAAI;KACT,GAAG,aAAa,aAAa;KAC7B,GAAG,aAAa,MAAM,OAAO;KAC7B,GAAG,aAAa,MAAM,OAAO;IAC/B,CAAC,CACH,EACF;GACF;GAUA,SAAS;IAAE,QAAQ;IAAU,KAAK;GAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0D9C,cAAc;IACZ,SAAS;KAAC;KAAS;KAAa;KAAoB;IAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8BvE,SAAS,CAAC,iBAAiB;GAC7B;GACA,KAAK;IACH,UAAU;KACR,GAAG;KACH,GAAG;KACH,GAAI,KAAK,QAAQ,eAAe,CAAC;IACnC;;;;;;;;;;;;;;;;;;;;;IAqBA,YAAY,CAAC,iBAAiB;GAChC;GACA,SAAS;IAmBP,QAAQ,CAAC,SAAS,WAAW;IAC7B,OAAO,CACL,GAAI,KAAK,QAAQ,gBAAgB,CAAC,GAUlC,GAAG,qBAAqB,MAAM,UAAU,CAC1C;GACF;EACF,CAAC;CACH;AACF"}
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-server.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-server.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-server` 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, Plugin, PluginOption, ViteDevServer } from \"vite\";\r\nimport { Application, BaseConnector, ConnectorLifecyclePhase, type ConnectorName, container, type FastifyInstance, requestContext, router } from \"@warlock.js/core\";\r\nimport { resolveWebPackageRoot } from \"../build/contribution\";\r\nimport { appConventionAliases } from \"../vite/app-convention-aliases\";\r\nimport {\r\n createHydrationClientEntry,\r\n invalidateClientPageRegistry,\r\n warlockClientBoundary,\r\n} from \"../vite\";\r\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\r\nimport { devErrorTransportPlugin, sendCapturedDevError } from \"./dev-server\";\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 * Third-party packages core reaches through `await import(...)` and that must\r\n * therefore never enter Vite's SSR transform graph.\r\n *\r\n * Left un-externalized, Vite's SSR module runner tries to resolve them anyway\r\n * and jams: the failure is NOT a missing-module error but a `transport invoke\r\n * timed out` on whatever unrelated module happened to be in flight. Derived in\r\n * one pass from `peerDependenciesMeta.optional` across every workspace package\r\n * reachable from `core/src/index.ts`, and carried over verbatim from\r\n * `dev-server.ts`'s own list. Only THIRD-PARTY peers belong here — every\r\n * `@warlock.js/*` sibling must stay in Vite's graph.\r\n *\r\n * This list is core's peer list, not web's; publishing it from core instead\r\n * of duplicating it here is still outstanding.\r\n */\r\nconst CORE_OPTIONAL_PEERS = [\r\n // mail\r\n \"nodemailer\",\r\n \"@aws-sdk/client-sesv2\",\r\n \"@react-email/render\",\r\n // cache\r\n \"redis\",\r\n \"pg\",\r\n // cascade\r\n \"mongodb\",\r\n // logger\r\n \"@sentry/node\",\r\n // core\r\n \"sharp\",\r\n \"socket.io\",\r\n \"@aws-sdk/client-s3\",\r\n \"@aws-sdk/lib-storage\",\r\n \"@aws-sdk/s3-request-presigner\",\r\n // herald\r\n \"amqplib\",\r\n // ai\r\n \"langfuse\",\r\n \"openai\",\r\n \"pdf-parse\",\r\n] as const;\r\n\r\n/**\r\n * `web`'s OWN optional peers, kept separate from {@link CORE_OPTIONAL_PEERS}\r\n * because that list is core's and is documented as such.\r\n *\r\n * Same rule, same reason: anything this package reaches through\r\n * `await import(...)` must be external to every bundler and SSR pipeline, or\r\n * some pipeline will try to resolve it in an install that legitimately does not\r\n * have it. `vite` itself is the precedent — `createViteServer` has always\r\n * loaded it this way — and `@vitejs/plugin-react` is loaded from the same\r\n * function, for the same dev-only reason, so it belongs in the same set.\r\n */\r\nconst WEB_OPTIONAL_PEERS = [\"vite\", \"@vitejs/plugin-react\"] as const;\r\n\r\n/**\r\n * Public specifier for the React Fast Refresh preamble module this connector\r\n * injects. See {@link WebConnector.resolveReactFastRefreshPlugins}.\r\n */\r\nconst REACT_REFRESH_PREAMBLE_ID = \"virtual:warlock/react-refresh-preamble\";\r\n\r\n/** The `\\0`-prefixed form Rollup uses to mark a module as not-a-file. */\r\nconst RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\\0${REACT_REFRESH_PREAMBLE_ID}`;\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\n/** `@vitejs/plugin-react`'s default export, plus the one static it publishes. */\r\ntype ReactPluginFactory = ((options?: Record<string, unknown>) => PluginOption[]) & {\r\n preambleCode?: string;\r\n};\r\n\r\n/**\r\n * A directory, plus the path the filesystem really stores it at when the two\r\n * differ. Both forms belong in `server.fs.allow` — see the `fs` block in\r\n * {@link WebConnector.createViteServer} for why one of them is never enough.\r\n *\r\n * A missing directory is not this function's problem to report: the roots it is\r\n * handed are already proven (`resolveWebPackageRoot`) or are the app's own cwd,\r\n * and an allow-list entry that points nowhere simply matches nothing.\r\n */\r\nfunction withRealPath(directory: string): string[] {\r\n try {\r\n const realPath = fs.realpathSync(directory);\r\n\r\n return realPath === directory ? [directory] : [directory, realPath];\r\n } catch {\r\n return [directory];\r\n }\r\n}\r\n\r\n/** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */\r\nfunction normalizeModuleId(id: string): string {\r\n const [filepath] = id.split(\"?\");\r\n\r\n return filepath.replace(/\\\\/g, \"/\").toLowerCase();\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 if ((this.pageManifest.publicFiles?.length ?? 0) > 0) {\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 = () => 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 });\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.catch(() => undefined).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 /**\r\n * Vite in middleware mode, `appType: \"custom\"` — Warlock owns the response\r\n * shape and Vite never fronts the server.\r\n *\r\n * `server.hmr.server` is handed the RAW node server (`fastify.server`), so the\r\n * HMR websocket shares the one port the app already listens on. No `hmr.port`\r\n * and no `clientPort`: Vite's HMR path stays the default `\"/\"`, socket.io\r\n * stays on `\"/socket.io\"` (`core/src/connectors/socket-connector.ts:90`), and\r\n * the two `upgrade` listeners coexist because each is a selective filter that\r\n * leaves a non-matching socket alone — verified empirically in both attachment\r\n * orders.\r\n */\r\n /**\r\n * React Fast Refresh, or nothing at all — never half of it.\r\n *\r\n * TWO plugins, and the second one is not optional. `@vitejs/plugin-react`\r\n * emits, into every client component module, a wrapper that reads\r\n * `window.$RefreshReg$` and THROWS \"can't detect preamble\" if it is missing.\r\n * The preamble that defines it normally arrives through Vite's\r\n * `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:\r\n * the document is React's SSR output and the only script appended to it is\r\n * the hydration entry (`./create-page-route-handler.ts:95`). So the preamble\r\n * is delivered as a virtual module that the hydration entry imports FIRST.\r\n *\r\n * \"First\" is load-bearing, twice over. ESM evaluates a module's imports in\r\n * source order before the importer's own body, so an import placed at\r\n * position 0 runs before `virtual:warlock/pages` pulls in a single component\r\n * — which is what `$RefreshReg$` has to exist for — and before `react-dom`\r\n * initialises, which is what `injectIntoGlobalHook` has to precede.\r\n *\r\n * The preamble TEXT is read from the plugin's own `preambleCode` export\r\n * rather than copied here, so a version bump cannot leave this file holding\r\n * a stale runtime contract. If that export ever stops being a string, Fast\r\n * Refresh is DECLINED entirely (empty array) instead of registered without\r\n * its preamble — a loud \"no HMR\" beats a component graph that throws on\r\n * first paint.\r\n *\r\n * Dev-only by construction, like everything else this method is called from:\r\n * `apply: \"serve\"` on our own plugin, `apply: \"serve\"` on the plugin's\r\n * refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The\r\n * PRODUCTION client bundle cannot reach any of this — it is built by\r\n * `buildWarlockHydrationClient` (`../vite/index.ts:185`), which composes\r\n * `warlockClientBoundary()` and nothing else, and never calls this method.\r\n *\r\n * @param paths resolved by {@link WebConnector.resolvePaths}\r\n */\r\n protected async resolveReactFastRefreshPlugins(\r\n paths: Awaited<ReturnType<WebConnector[\"resolvePaths\"]>>,\r\n ): Promise<PluginOption[]> {\r\n let viteReact: ReactPluginFactory;\r\n\r\n try {\r\n // OPTIONAL peer (`web/package.json`'s `peerDependenciesMeta`), so this is\r\n // a lazy `await import` exactly like the `vite` one above it, and\r\n // `@vitejs/plugin-react` is listed in `WEB_OPTIONAL_PEERS` for the same\r\n // reason `vite` is.\r\n ({ default: viteReact } = (await import(\"@vitejs/plugin-react\")) as unknown as {\r\n default: ReactPluginFactory;\r\n });\r\n } catch {\r\n console.warn(\r\n \"[warlock:web] React Fast Refresh is OFF: `@vitejs/plugin-react` is not installed. \" +\r\n \"Edits to a component will reload the page instead of hot-swapping it. \" +\r\n \"Install it with `npm i -D @vitejs/plugin-react` to enable it.\",\r\n );\r\n\r\n return [];\r\n }\r\n\r\n const preambleCode = viteReact.preambleCode;\r\n\r\n if (typeof preambleCode !== \"string\") {\r\n console.warn(\r\n \"[warlock:web] React Fast Refresh is OFF: this `@vitejs/plugin-react` no longer exports \" +\r\n \"`preambleCode`, so the refresh preamble cannot be injected into the hydration entry. \" +\r\n \"Registering the plugin without it would make every component module throw \" +\r\n \"\\\"can't detect preamble\\\" in the browser.\",\r\n );\r\n\r\n return [];\r\n }\r\n\r\n // `base` is never set on the config below, so it is Vite's default `\"/\"`.\r\n const preambleSource = preambleCode.replace(\"__BASE__\", \"/\");\r\n const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);\r\n\r\n const preamblePlugin: Plugin = {\r\n name: \"warlock:react-refresh-preamble\",\r\n // BEFORE `warlock:projection` and the gates would be wrong and BEFORE\r\n // esbuild's TS transform is required: this prepends one import statement\r\n // to TypeScript source, so it has to see the file before anything lowers\r\n // it. It touches exactly one module, so it cannot reorder anything else.\r\n enforce: \"pre\",\r\n apply: \"serve\",\r\n // The preamble is browser state (`window.$RefreshReg$`). The SSR\r\n // environment must never evaluate it — there is no `window` there, and\r\n // the server render must stay byte-identical to what it produced before\r\n // this plugin existed.\r\n applyToEnvironment: environment => environment.config.consumer === \"client\",\r\n resolveId(source) {\r\n if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;\r\n\r\n return null;\r\n },\r\n load(id) {\r\n if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;\r\n\r\n return null;\r\n },\r\n transform(code, id) {\r\n if (normalizeModuleId(id) !== hydrationEntryId) return null;\r\n\r\n return { code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\\n${code}`, map: null };\r\n },\r\n };\r\n\r\n return [preamblePlugin, ...viteReact()];\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 root: paths.appRoot,\r\n appType: \"custom\",\r\n plugins: [\r\n // FIRST, and dev-only by construction: this method is reachable only\r\n // from `boot()`'s Vite branch, past the `isProductionRuntime()` guard.\r\n // The same predicate is handed in rather than re-derived, and the\r\n // factory throws if it is ever true — see the plugin's own header for\r\n // why the layer has to be registered from a plugin and not from\r\n // `vite.middlewares.use(...)` after this call returns.\r\n devErrorTransportPlugin({ isProductionRuntime, buildErrorMessage }),\r\n ...warlockClientBoundary({\r\n appRoot: paths.appRoot,\r\n beforePageHotUpdate: ({ file }) => this.handlePageHotUpdate(file),\r\n }),\r\n // AFTER the boundary, and the order matters among `enforce: \"pre\"`\r\n // plugins (Vite keeps array order within an enforce bucket).\r\n // `warlock:projection` strips a page's server exports — `loader` and\r\n // friends — before React's babel pass sees the module, so Fast Refresh\r\n // never registers a refresh boundary for an export that is not supposed\r\n // to reach the browser at all.\r\n //\r\n // BEFORE `options.plugins` so an application can still override.\r\n ...(await this.resolveReactFastRefreshPlugins(paths)),\r\n ...(this.options.plugins ?? []),\r\n ],\r\n server: {\r\n middlewareMode: true,\r\n hmr: { server: fastify.server },\r\n fs: {\r\n // `<Scripts />` points the browser at the hydration client entry under\r\n // `<webRoot>`: the published `esm/hydration/index.mjs` when installed,\r\n // or `src/hydration/index.ts` in this checkout. A dependency normally\r\n // lives under the app root's `node_modules`; when `@warlock.js/web` is\r\n // LINKED — a monorepo checkout, `npm link`, or a `file:` dependency —\r\n // its real path can sit outside every directory Vite allows by default\r\n // and the request comes back `403 Restricted`.\r\n //\r\n // That failure is silent in the worst way: SSR has already produced\r\n // the markup by the time the browser asks for the script, so the page\r\n // renders perfectly, nothing is logged, and the only symptom is that\r\n // no button ever works. Naming web's own root makes a linked install\r\n // behave like an installed one.\r\n //\r\n // Every root is listed twice, as given and as `realpathSync` reports\r\n // it, because Vite resolves a requested file to its REAL path before\r\n // testing it against this list. Allowing the symlink alone therefore\r\n // matches nothing — the 403 page prints the link that was allowed\r\n // directly above the real path it rejected.\r\n allow: [\r\n ...new Set([\r\n ...withRealPath(workspaceRoot),\r\n ...withRealPath(paths.appRoot),\r\n ...withRealPath(paths.webRoot),\r\n ]),\r\n ],\r\n },\r\n },\r\n // Without an explicit target, esbuild assumes native (TC39) decorator\r\n // support and leaves `@RegisterModel()`-style syntax untouched — but Vite's\r\n // SSR module runner evaluates transformed code via `new AsyncFunction(...)`,\r\n // which node has no native decorator support for. `es2022` downlevels them\r\n // into helper calls. `jsx` is named explicitly rather than left to tsconfig\r\n // discovery, because Vite matches a file against a tsconfig's `include` and\r\n // `web/tsconfig.json`'s is narrow enough that most of `web/src` matched no\r\n // config at all and fell back to the CLASSIC transform — emitting\r\n // `React.createElement` into modules that import no `React` binding.\r\n esbuild: { target: \"es2022\", jsx: \"automatic\" },\r\n /**\r\n * REACT MUST BE PRE-BUNDLED, and naming it here is the only thing that\r\n * makes that happen for the framework's own client graph.\r\n *\r\n * `react-dom/client` is CommonJS. A browser cannot import a named export\r\n * from it, so Vite's dep optimizer normally rewrites it into an ESM shim\r\n * under `/node_modules/.vite/deps/`. Whether that rewrite happens is\r\n * decided in `tryNodeResolve`, and one of the conditions that SKIPS it is\r\n * `importer && isInNodeModules(importer)`\r\n * (`vite/dist/node/chunks/config.js:32822`). Vite's reasoning is sound in\r\n * general — a dependency's own internal imports are the optimizer's job,\r\n * not the resolver's — but it is exactly wrong here:\r\n *\r\n * `<Scripts />` points the browser at the hydration entry, and in an\r\n * INSTALLED app that entry is\r\n * `<app>/node_modules/@warlock.js/web/esm/hydration/index.mjs`.\r\n * Every module it reaches is therefore inside `node_modules`, so every\r\n * bare import it makes takes the skip branch and is served as the raw\r\n * file with a `?v=<browserHash>` cache key bolted on.\r\n *\r\n * For `esm/**.mjs` that is harmless — they are already ESM. For\r\n * `react-dom/client` it is fatal, and it is the whole defect: the browser\r\n * receives `\"use strict\"; function checkDCE()…` and refuses the module\r\n * with\r\n *\r\n * SyntaxError: The requested module '/node_modules/react-dom/client.js?v=…'\r\n * does not provide an export named 'hydrateRoot'\r\n *\r\n * Nothing in the client runtime then runs at all: no hydration, so no\r\n * `useState`, no Fast Refresh, and `<Link>` degrades to a full document\r\n * load because the navigation listener was never attached. Measured on a\r\n * published 5.0.2 install (fresh app, no symlinks): all four symptoms\r\n * before this block, none after.\r\n *\r\n * `include` is the fix rather than `entries` because it does not depend\r\n * on the scanner reaching the entry: an included id is pre-bundled at\r\n * server start and `tryOptimizedResolve` matches it by NAME\r\n * (`config.js:32633`) before `tryNodeResolve` — and that lookup has no\r\n * importer condition, so a node_modules importer resolves to the shim\r\n * like anyone else. Pointing `optimizeDeps.entries` at the hydration\r\n * entry would not work: the scanner classifies a resolution inside\r\n * `node_modules` as a dependency to externalize rather than a source to\r\n * walk.\r\n *\r\n * All four names are listed even though `react` and `react/jsx-runtime`\r\n * usually get discovered anyway — they are discovered only because the\r\n * APP's own pages import them, which is a fact about the app and not\r\n * something the framework may rely on. `react-dom/client` is imported by\r\n * `client/hydrate-page.tsx` and by nothing a normal app writes, which is\r\n * why it was the one that broke.\r\n *\r\n * This is INVISIBLE from the monorepo checkout: there the hydration entry\r\n * resolves to `web/src/hydration/index.ts`, a path with no `node_modules`\r\n * segment, so the skip branch never fires and React optimizes normally.\r\n * Canon: nothing measured inside the checkout is evidence about a\r\n * published install.\r\n */\r\n optimizeDeps: {\r\n include: [\"react\", \"react-dom\", \"react-dom/client\", \"react/jsx-runtime\"],\r\n /**\r\n * The SECOND instance problem, and the reason this is `exclude` and not\r\n * another `include`.\r\n *\r\n * An app page's `import { Link } from \"@warlock.js/web\"` has an importer\r\n * OUTSIDE `node_modules`, so it takes the opposite branch above and the\r\n * whole package is pre-bundled into\r\n * `/node_modules/.vite/deps/@warlock__js_web.js`. The hydration entry\r\n * cannot reach that bundle — it is loaded by absolute path through\r\n * `/@fs/` and its imports are relative, so they resolve to the raw\r\n * `esm/*.mjs` files. The browser then holds TWO copies of every client\r\n * module in this package: the app renders `<Link>` from the bundled one\r\n * while the hydration entry publishes routes and mounts the document\r\n * context on the raw one. Module-level state does not cross that line.\r\n *\r\n * `routing/route-table` survives it by accident — it keeps its table in\r\n * a `Symbol.for` slot on `globalThis`, which is realm-wide — but\r\n * `components/document-context` is a React context OBJECT, and two\r\n * `createContext()` calls are two different contexts no provider can\r\n * bridge.\r\n *\r\n * Excluding the package makes the app's import resolve to\r\n * `esm/index.mjs` with the same `?v=` key the hydration entry's imports\r\n * carry, so there is one instance again. It costs the pre-bundle (a\r\n * handful of extra dev requests for files that are already valid ESM)\r\n * and nothing else — the grep across every client-reachable module in\r\n * this package finds exactly three bare imports, all of them React, all\r\n * of them covered by `include` above.\r\n */\r\n exclude: [\"@warlock.js/web\"],\r\n },\r\n ssr: {\r\n external: [\r\n ...CORE_OPTIONAL_PEERS,\r\n ...WEB_OPTIONAL_PEERS,\r\n ...(this.options.ssrExternal ?? []),\r\n ],\r\n /**\r\n * ONE `@warlock.js/web`, for the same reason `resolve.dedupe` below\r\n * insists on one React — and it is invisible from inside this repo.\r\n *\r\n * Vite externalises `node_modules` in SSR by default, so an installed\r\n * app gets TWO instances: the app's own `root.tsx` imports\r\n * `@warlock.js/web` and Vite hands that off to Node, while the pipeline\r\n * is loaded deliberately through `vite.ssrLoadModule(...)` and stays\r\n * inside Vite's graph. `renderPage` then sets the document context on\r\n * Vite's copy of `components/document-context`, and the app's `<Head/>`\r\n * reads Node's copy, which has nothing in it:\r\n *\r\n * <Head/> was rendered outside the page pipeline's document context\r\n *\r\n * Measured on a published 5.0.1 install: `GET /` 500 without this line,\r\n * 200 with it (404 control still 404). In THIS checkout the package\r\n * resolves to source under Vite's root, never through `node_modules`,\r\n * so both paths land on one instance and the bug cannot reproduce.\r\n * Canon `6b7ab838`.\r\n */\r\n noExternal: [\"@warlock.js/web\"],\r\n },\r\n resolve: {\r\n // ONE React, resolved from the application. A linked `@warlock.js/web`\r\n // resolves `react` out of its own tree while the app's pages resolve it\r\n // out of theirs; two React instances share no hook dispatcher, and SSR\r\n // dies on the first `useState` with \"Cannot read properties of null\".\r\n // `dedupe` forces these package names to resolve from Vite's `root` —\r\n // the app — whoever imported them, in the SSR environment as much as in\r\n // the client one.\r\n //\r\n // Two names cover every entry point. Vite matches a deep import against\r\n // the package it belongs to before consulting this list, so\r\n // `react-dom/client`, `react/jsx-runtime` and `react/jsx-dev-runtime`\r\n // are already deduped by `react-dom` and `react`; listing them\r\n // separately would only add entries that can never match.\r\n //\r\n // Do NOT express this as a `resolve.alias` entry instead. Pointing a\r\n // bare React specifier at a directory drags React's CommonJS entry into\r\n // Vite's SSR module graph, and the dev server then dies at startup with\r\n // \"module is not defined\" before it renders anything at all.\r\n dedupe: [\"react\", \"react-dom\"],\r\n alias: [\r\n ...(this.options.resolveAlias ?? []),\r\n // The app-tree convention `v5/app/tsconfig.json`'s own `paths` declare.\r\n // Vite does not read tsconfig paths on its own and no\r\n // `vite-tsconfig-paths` plugin is installed in this workspace.\r\n //\r\n // ONE definition, shared with the production build contribution\r\n // (`web/src/build/contribution.ts`). These were two separate literals\r\n // until 2026-08-24, and the production half simply did not have them —\r\n // dev resolved `web/*` while the production client build died on the\r\n // first page. Do not inline them back here.\r\n ...appConventionAliases(paths.appSrcRoot),\r\n ],\r\n },\r\n });\r\n }\r\n}\r\n"],"mappings":"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"}