@warlock.js/web 5.2.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/esm/build/contribution.d.mts +1 -1
- package/esm/build/contribution.mjs +1 -1
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +1 -1
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +1 -1
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +1 -1
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +1 -1
- package/esm/build/page-default-export.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs +1 -1
- package/esm/build/page-routes-manifest.mjs.map +1 -1
- package/esm/build/public-files.mjs +1 -1
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +1 -1
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +1 -1
- package/esm/client/runtime/matcher.mjs +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +1 -1
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +1 -1
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +1 -1
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +1 -1
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.mjs +1 -1
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +1 -1
- package/esm/loaders.d.mts +1 -1
- package/esm/metadata.d.mts +1 -1
- package/esm/metadata.mjs +1 -1
- package/esm/metadata.mjs.map +1 -1
- package/esm/props.d.mts +1 -1
- package/esm/route.d.mts +1 -1
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +1 -1
- package/esm/routing/filesystem-route.mjs.map +1 -1
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +1 -1
- package/esm/routing/url-fragment.mjs.map +1 -1
- package/esm/runtime/register-modules.d.mts +1 -1
- package/esm/runtime/register-modules.mjs +1 -1
- package/esm/runtime/register-modules.mjs.map +1 -1
- package/esm/server/build-hydration-payload.mjs +1 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +1 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +1 -1
- package/esm/server/create-page-route-handler.mjs +1 -1
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +1 -1
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +1 -1
- package/esm/server/error-page.mjs +1 -1
- package/esm/server/error-page.mjs.map +1 -1
- package/esm/server/execute-page-request.d.mts +1 -1
- package/esm/server/execute-page-request.mjs +1 -1
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +1 -1
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +1 -1
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs +1 -1
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +1 -1
- package/esm/server/install-page-routes.mjs +1 -1
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +1 -1
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -1
- package/esm/server/not-found-page.mjs +1 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +1 -1
- package/esm/server/page-context.mjs +1 -1
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-manifest.d.mts +1 -1
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +1 -1
- package/esm/server/page-render-bundle.mjs.map +1 -1
- package/esm/server/page-route-reload.mjs +1 -1
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +1 -1
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +1 -1
- package/esm/server/render-page.mjs +1 -1
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +1 -1
- package/esm/server/settle-page-response.mjs +1 -1
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +1 -1
- package/esm/server/stylesheet-urls.mjs +1 -1
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +1 -1
- package/esm/server/unregistered-pages.mjs.map +1 -1
- package/esm/server/web-connector-factory.d.mts +1 -1
- package/esm/server/web-connector-factory.mjs +1 -1
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +1 -1
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +1 -1
- package/esm/shared.mjs +1 -1
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +1 -1
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +1 -1
- package/esm/vite/index.mjs +1 -1
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +1 -1
- package/esm/vite/page-registry-plugin.mjs +1 -1
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +1 -1
- package/esm/vite/projection.mjs +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settle-page-response.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Response } from \"@warlock.js/core\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageRouteEntry,\n} from \"./execute-page-request.types\";\n\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { originalError: thrown, error: surrogate, boundary, digest, scrubbed: true };\n }\n\n // `error` already IS the real thrown value here — `originalError` only ever\n // needs to diverge from it on the scrubbed (production) path above. Leaving\n // it `undefined` rather than a redundant second reference to the same object\n // keeps the record's `toEqual` shape honest (undefined properties compare as\n // absent) and readers still get the real error via\n // `record.originalError ?? record.error`.\n return { originalError: undefined, error: thrown, boundary, digest, scrubbed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Stage 6/7 — buffered per-level responses, and the root→leaf commit\n// ---------------------------------------------------------------------------\n\n/** A single committed response header, in application order. */\nexport type BufferedHeader = { key: string; value: string };\n\n/** A single committed response cookie — the shape `applyBufferedCookie` replays. */\nexport type BufferedCookie = {\n name: string;\n value: unknown;\n options?: Record<string, unknown>;\n};\n\n/** The two loader short-circuit kinds a buffered response can signal. */\nexport type LoaderShortCircuitKind = \"redirect\" | \"notFound\";\n\nconst LOADER_SHORT_CIRCUIT = Symbol(\"warlock.page.loaderShortCircuit\");\n\n/**\n * What `response.redirect()` / `response.permanentRedirect()` / `response.notFound()`\n * return from inside a loader — a branded value the stage 7 settle scan\n * recognises by symbol, never by shape (so an app returning an\n * accidentally-similar plain object can't be mistaken for one).\n */\nexport type LoaderShortCircuitSignal = {\n readonly [LOADER_SHORT_CIRCUIT]: true;\n kind: LoaderShortCircuitKind;\n statusCode: number;\n url?: string;\n body?: unknown;\n};\n\nexport function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal {\n return Boolean(value) && typeof value === \"object\" && LOADER_SHORT_CIRCUIT in (value as object);\n}\n\n/** One level's scratch buffer — what `response.header()`/`.cookie()` write into. */\nexport type LevelBuffer = {\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\nexport function createLevelBuffer(): LevelBuffer {\n return { headers: [], cookies: [] };\n}\n\n/**\n * The response surface a LOADER sees — never the live core `Response`.\n * `header()`/`cookie()` queue into the level's own buffer; nothing here\n * touches the real reply. `redirect()`/`permanentRedirect()`/`notFound()`\n * queue the buffer's own status (+ `Location`, for the two redirects) AND\n * return the branded signal stage 7 detects — the loader is expected to\n * `return response.redirect(...)`.\n */\nexport type BufferedResponse = {\n header(key: string, value: unknown): BufferedResponse;\n headers(bag: Record<string, unknown>): BufferedResponse;\n cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedResponse;\n setStatusCode(statusCode: number): BufferedResponse;\n redirect(url: string, statusCode?: number): LoaderShortCircuitSignal;\n permanentRedirect(url: string): LoaderShortCircuitSignal;\n notFound(body?: unknown): LoaderShortCircuitSignal;\n};\n\nexport function createBufferedResponse(buffer: LevelBuffer): BufferedResponse {\n const bufferedResponse: BufferedResponse = {\n header(key, value) {\n buffer.headers.push({ key, value: String(value) });\n return bufferedResponse;\n },\n headers(bag) {\n for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);\n return bufferedResponse;\n },\n cookie(name, value, options) {\n buffer.cookies.push({ name, value, options });\n return bufferedResponse;\n },\n setStatusCode(statusCode) {\n buffer.statusCode = statusCode;\n return bufferedResponse;\n },\n redirect(url, statusCode = 302) {\n buffer.statusCode = statusCode;\n buffer.headers.push({ key: \"Location\", value: url });\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"redirect\", statusCode, url, body: undefined };\n },\n permanentRedirect(url) {\n return bufferedResponse.redirect(url, 301);\n },\n notFound(body) {\n buffer.statusCode = 404;\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"notFound\", statusCode: 404, url: undefined, body };\n },\n };\n\n return bufferedResponse;\n}\n\n/** Stage 7's folded, applied result — what `bundle.commit` carries. */\nexport type PageResponseCommit = {\n committedLevels: PageLevelName[];\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\n/**\n * Fold every surviving buffer root→leaf into ONE map per key (header key\n * case-insensitively, cookie by name) — leafward wins, insertion position\n * stays where the key FIRST appeared. Applies the folded headers and status\n * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed\n * sets, so this is safe even though `commitBuffers` can run before render\n * changes its mind about the status later). Cookies are NOT applied to the\n * real response here — `cookie()` APPENDS, so mirroring it here and again at\n * the wire emit would duplicate every `Set-Cookie`. The single application\n * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`\n * over `bundle.commit.cookies`).\n */\nexport function commitBuffers(\n response: Response,\n buffers: Record<PageLevelName, LevelBuffer>,\n committedLevels: PageLevelName[],\n): PageResponseCommit {\n const headerOrder: string[] = [];\n const headerMap = new Map<string, BufferedHeader>();\n const cookieOrder: string[] = [];\n const cookieMap = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const level of committedLevels) {\n const buffer = buffers[level];\n\n for (const header of buffer.headers) {\n const key = header.key.toLowerCase();\n if (!headerMap.has(key)) headerOrder.push(key);\n headerMap.set(key, header);\n }\n\n for (const cookie of buffer.cookies) {\n if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);\n cookieMap.set(cookie.name, cookie);\n }\n\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n const headers = headerOrder.map(key => headerMap.get(key)!);\n const cookies = cookieOrder.map(name => cookieMap.get(name)!);\n\n for (const header of headers) response.header(header.key, header.value);\n if (statusCode !== undefined) response.setStatusCode(statusCode);\n\n return { committedLevels, headers, cookies, statusCode };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"settle-page-response.mjs","names":[],"sources":["../../../../../../../web/src/server/settle-page-response.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Response } from \"@warlock.js/core\";\nimport type {\n PageBoundaryDesignation,\n PageErrorRecord,\n PageLevelName,\n PageRouteEntry,\n} from \"./execute-page-request.types\";\n\nexport const LEVEL_ORDER: readonly PageLevelName[] = [\"app\", \"layout\", \"page\"];\n\nexport function designateBoundary(\n throwingLevel: PageLevelName,\n triple: PageRouteEntry[\"triple\"],\n): PageBoundaryDesignation {\n const throwingIndex = LEVEL_ORDER.indexOf(throwingLevel);\n\n for (let index = throwingIndex; index >= 0; index--) {\n const level = LEVEL_ORDER[index];\n\n if (triple[level].ErrorBoundary) {\n return { throwingLevel, boundaryLevel: level };\n }\n }\n\n return { throwingLevel, boundaryLevel: \"app\" };\n}\n\nexport function buildErrorRecord(\n thrown: unknown,\n boundary: PageBoundaryDesignation,\n requestPath?: string,\n): PageErrorRecord {\n const digest = randomUUID();\n\n console.error(\"[warlock] page error\", digest, ...(requestPath ? [requestPath] : []), thrown);\n\n if (process.env.NODE_ENV === \"production\") {\n const surrogate = new Error(\"An unexpected error occurred.\");\n\n (surrogate as Error & { digest: string }).digest = digest;\n\n return { originalError: thrown, error: surrogate, boundary, digest, scrubbed: true };\n }\n\n // `error` already IS the real thrown value here — `originalError` only ever\n // needs to diverge from it on the scrubbed (production) path above. Leaving\n // it `undefined` rather than a redundant second reference to the same object\n // keeps the record's `toEqual` shape honest (undefined properties compare as\n // absent) and readers still get the real error via\n // `record.originalError ?? record.error`.\n return { originalError: undefined, error: thrown, boundary, digest, scrubbed: false };\n}\n\n// ---------------------------------------------------------------------------\n// Stage 6/7 — buffered per-level responses, and the root→leaf commit\n// ---------------------------------------------------------------------------\n\n/** A single committed response header, in application order. */\nexport type BufferedHeader = { key: string; value: string };\n\n/** A single committed response cookie — the shape `applyBufferedCookie` replays. */\nexport type BufferedCookie = {\n name: string;\n value: unknown;\n options?: Record<string, unknown>;\n};\n\n/** The two loader short-circuit kinds a buffered response can signal. */\nexport type LoaderShortCircuitKind = \"redirect\" | \"notFound\";\n\nconst LOADER_SHORT_CIRCUIT = Symbol(\"warlock.page.loaderShortCircuit\");\n\n/**\n * What `response.redirect()` / `response.permanentRedirect()` / `response.notFound()`\n * return from inside a loader — a branded value the stage 7 settle scan\n * recognises by symbol, never by shape (so an app returning an\n * accidentally-similar plain object can't be mistaken for one).\n */\nexport type LoaderShortCircuitSignal = {\n readonly [LOADER_SHORT_CIRCUIT]: true;\n kind: LoaderShortCircuitKind;\n statusCode: number;\n url?: string;\n body?: unknown;\n};\n\nexport function isLoaderShortCircuit(value: unknown): value is LoaderShortCircuitSignal {\n return Boolean(value) && typeof value === \"object\" && LOADER_SHORT_CIRCUIT in (value as object);\n}\n\n/** One level's scratch buffer — what `response.header()`/`.cookie()` write into. */\nexport type LevelBuffer = {\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\nexport function createLevelBuffer(): LevelBuffer {\n return { headers: [], cookies: [] };\n}\n\n/**\n * The response surface a LOADER sees — never the live core `Response`.\n * `header()`/`cookie()` queue into the level's own buffer; nothing here\n * touches the real reply. `redirect()`/`permanentRedirect()`/`notFound()`\n * queue the buffer's own status (+ `Location`, for the two redirects) AND\n * return the branded signal stage 7 detects — the loader is expected to\n * `return response.redirect(...)`.\n */\nexport type BufferedResponse = {\n header(key: string, value: unknown): BufferedResponse;\n headers(bag: Record<string, unknown>): BufferedResponse;\n cookie(name: string, value: unknown, options?: Record<string, unknown>): BufferedResponse;\n setStatusCode(statusCode: number): BufferedResponse;\n redirect(url: string, statusCode?: number): LoaderShortCircuitSignal;\n permanentRedirect(url: string): LoaderShortCircuitSignal;\n notFound(body?: unknown): LoaderShortCircuitSignal;\n};\n\nexport function createBufferedResponse(buffer: LevelBuffer): BufferedResponse {\n const bufferedResponse: BufferedResponse = {\n header(key, value) {\n buffer.headers.push({ key, value: String(value) });\n return bufferedResponse;\n },\n headers(bag) {\n for (const [key, value] of Object.entries(bag)) bufferedResponse.header(key, value);\n return bufferedResponse;\n },\n cookie(name, value, options) {\n buffer.cookies.push({ name, value, options });\n return bufferedResponse;\n },\n setStatusCode(statusCode) {\n buffer.statusCode = statusCode;\n return bufferedResponse;\n },\n redirect(url, statusCode = 302) {\n buffer.statusCode = statusCode;\n buffer.headers.push({ key: \"Location\", value: url });\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"redirect\", statusCode, url, body: undefined };\n },\n permanentRedirect(url) {\n return bufferedResponse.redirect(url, 301);\n },\n notFound(body) {\n buffer.statusCode = 404;\n return { [LOADER_SHORT_CIRCUIT]: true, kind: \"notFound\", statusCode: 404, url: undefined, body };\n },\n };\n\n return bufferedResponse;\n}\n\n/** Stage 7's folded, applied result — what `bundle.commit` carries. */\nexport type PageResponseCommit = {\n committedLevels: PageLevelName[];\n headers: BufferedHeader[];\n cookies: BufferedCookie[];\n statusCode?: number;\n};\n\n/**\n * Fold every surviving buffer root→leaf into ONE map per key (header key\n * case-insensitively, cookie by name) — leafward wins, insertion position\n * stays where the key FIRST appeared. Applies the folded headers and status\n * to the REAL response (`header()`/`setStatusCode()` are idempotent keyed\n * sets, so this is safe even though `commitBuffers` can run before render\n * changes its mind about the status later). Cookies are NOT applied to the\n * real response here — `cookie()` APPENDS, so mirroring it here and again at\n * the wire emit would duplicate every `Set-Cookie`. The single application\n * site is the emit (`create-page-route-handler.ts`, via `applyBufferedCookie`\n * over `bundle.commit.cookies`).\n */\nexport function commitBuffers(\n response: Response,\n buffers: Record<PageLevelName, LevelBuffer>,\n committedLevels: PageLevelName[],\n): PageResponseCommit {\n const headerOrder: string[] = [];\n const headerMap = new Map<string, BufferedHeader>();\n const cookieOrder: string[] = [];\n const cookieMap = new Map<string, BufferedCookie>();\n let statusCode: number | undefined;\n\n for (const level of committedLevels) {\n const buffer = buffers[level];\n\n for (const header of buffer.headers) {\n const key = header.key.toLowerCase();\n if (!headerMap.has(key)) headerOrder.push(key);\n headerMap.set(key, header);\n }\n\n for (const cookie of buffer.cookies) {\n if (!cookieMap.has(cookie.name)) cookieOrder.push(cookie.name);\n cookieMap.set(cookie.name, cookie);\n }\n\n if (buffer.statusCode !== undefined) statusCode = buffer.statusCode;\n }\n\n const headers = headerOrder.map(key => headerMap.get(key)!);\n const cookies = cookieOrder.map(name => cookieMap.get(name)!);\n\n for (const header of headers) response.header(header.key, header.value);\n if (statusCode !== undefined) response.setStatusCode(statusCode);\n\n return { committedLevels, headers, cookies, statusCode };\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
//#region web/src/server/stylesheet-urls.ts
|
|
5
|
+
//#region ../web/src/server/stylesheet-urls.ts
|
|
6
6
|
/**
|
|
7
7
|
* Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.
|
|
8
8
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside\n * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple\n * (widened to a chain) `create-page-route-handler.ts` loads per request — so a\n * handler's CSS is the ordered, deduped union of what those specific source\n * files pull in, and nothing else. Collecting across the WHOLE application\n * (every page's manifest entry, every root import) is the bug this shape\n * exists to avoid: it ships page B's stylesheet on page A's response, and it\n * only gets worse as an application grows.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, matching each source file's own\n * id and walking its recorded `css` and imported chunks.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from each source file's own import statements.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\n/**\n * ONE source file's own directly imported stylesheets, as dev URLs.\n *\n * Dev has no manifest, so the source is the file itself: whatever it imports\n * with a stylesheet extension is what it needs. This is deliberately narrow —\n * it answers \"what CSS does THIS file set up\", not \"what CSS does the whole\n * module graph this file eventually reaches end up pulling in\" — because the\n * latter would require reproducing Vite's module graph, which is precisely\n * what dev has no manifest to shortcut.\n *\n * The narrowness is the honest part: production splits CSS per chunk and can\n * follow imported chunks (`productionStylesheetUrls` below); dev can only read\n * the ONE file handed to it. A page whose own module imports its own\n * stylesheet still gets it in dev — Vite's client graph injects it as before —\n * it simply is not render-blocking the way a chain member's own import is.\n *\n * Called once per chain member — root, then every matched layout outer to\n * inner, then the page — by the installers below, which is what turns \"one\n * file's own imports\" into a handler's whole CSS chain.\n *\n * Specifiers are resolved against `sourceFile` and expressed relative to\n * `appRoot`, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, sourceFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(sourceFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*(?:\\(\\s*)?[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(sourceFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct\n * stylesheet imports (`devStylesheetUrls`), in the order given — the caller\n * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and\n * deduped across the WHOLE chain, not just within one file.\n *\n * Cross-file dedup matters as much as within-file dedup: an application-wide\n * `app.css` imported by both the root and a page must still produce one\n * `<link>`, not two.\n */\nexport function devHandlerStylesheetUrls(\n appRoot: string,\n sourceFiles: readonly string[],\n): string[] {\n const urls: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n for (const url of devStylesheetUrls(appRoot, sourceFile)) {\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n\ntype ManifestEntry = {\n css?: unknown;\n imports?: unknown;\n};\n\n/**\n * Find the manifest key for an app-root-relative POSIX source id.\n *\n * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's\n * `appRoot` — the client build's `root` is the framework's own package\n * (`build-client.ts`), so a key for an app source file carries a `../`-laden\n * prefix (`\"../my-app/src/web/root.tsx\"`) rather than matching `sourceFile`\n * (`\"src/web/root.tsx\"`) byte for byte. The two forms always share the same\n * TAIL, though — both are anchored at the same file — so an exact match is\n * tried first (the case where the client build's root IS the app root, which\n * every fixture and every test below uses) and a `/`-boundary suffix match\n * second, rather than trying to reconstruct the build's own root here, which\n * this runtime read has no way to independently confirm.\n */\nfunction findManifestKey(\n manifest: Record<string, ManifestEntry | undefined>,\n sourceFile: string,\n): string | undefined {\n if (manifest[sourceFile] !== undefined) return sourceFile;\n\n const suffix = `/${sourceFile}`;\n\n for (const key of Object.keys(manifest)) {\n if (key.endsWith(suffix)) return key;\n }\n\n return undefined;\n}\n\n/**\n * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,\n * plus the same walk repeated over every chunk it STATICALLY `imports`.\n *\n * `imports` only, never `dynamicImports`. Vite's manifest records\n * `dynamicImports` on shared entry points (the hydration entry names every\n * page as one) precisely because the browser must NOT download them eagerly —\n * walking that array here would pull every other page's CSS onto this one,\n * which is the exact \"unrelated pages\" leak this module exists to end.\n * `imports`, by contrast, are chunks THIS module synchronously depends on:\n * code Vite split out of it but that loads whenever it does, so their CSS is\n * this handler's CSS too.\n *\n * `visited` guards against a chunk graph cycle; sharing one set across the\n * whole walk from a single entry is enough; a shared chunk revisited from a\n * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately\n * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,\n * and a fresh `visited` set per entry is simpler to reason about than one\n * threaded across unrelated chains.\n */\nfunction collectManifestCss(\n manifest: Record<string, ManifestEntry | undefined>,\n key: string,\n visited: Set<string>,\n): string[] {\n if (visited.has(key)) return [];\n visited.add(key);\n\n const entry = manifest[key];\n if (entry === undefined) return [];\n\n const ownCss = Array.isArray(entry.css)\n ? entry.css.filter((file): file is string => typeof file === \"string\" && file !== \"\")\n : [];\n\n const imports = Array.isArray(entry.imports)\n ? entry.imports.filter((id): id is string => typeof id === \"string\")\n : [];\n\n return [\n ...ownCss,\n ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited)),\n ];\n}\n\n/**\n * ONE handler's whole production CSS chain.\n *\n * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an\n * app-root-relative POSIX source id — the SAME id\n * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on\n * every manifest entry, because that identity is what lets this function match\n * EXPLICITLY rather than guess: every id is looked up on its own\n * (`findManifestKey`), its own chunk's CSS is collected\n * (`collectManifestCss`), and an id with no matching entry contributes\n * nothing — it is never treated as license to fall back to scanning the whole\n * manifest, which is what let an unrelated page's CSS leak onto this handler\n * before.\n *\n * Duplicates are collapsed and order is preserved across the WHOLE chain, in\n * the order `sourceFiles` was given — root's own CSS first, then each\n * layout's outer to inner, then the page's — so cascade order matches the\n * chain's own outer-to-inner rendering order.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(\n clientDir: string,\n sourceFiles: readonly string[],\n): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const files: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n const key = findManifestKey(manifest, sourceFile);\n if (key === undefined) continue;\n\n files.push(...collectManifestCss(manifest, key, new Set()));\n }\n\n const urls: string[] = [];\n\n for (const file of files) {\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n\n return urls;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,mBAAmB;CAAC;CAAQ;CAAS;CAAS;CAAS;AAAO;;;;;;;;;;AAWpE,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;AAyBrC,SAAgB,kBAAkB,SAAiB,YAA8B;CAC/E,IAAI;CAEJ,IAAI;EACF,SAAS,aAAa,YAAY,OAAO;CAC3C,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,MAAM,OAAiB,CAAC;CACxB,MAAM,UAAU;CAEhB,IAAI,QAAQ,QAAQ,KAAK,MAAM;CAE/B,OAAO,UAAU,MAAM;EACrB,MAAM,YAAY,MAAM;EACxB,MAAM,UAAU,UAAU,YAAY;EAEtC,IAAI,iBAAiB,MAAM,cAAc,QAAQ,SAAS,SAAS,CAAC,GAAG;GACrE,MAAM,WAAW,KAAK,QAAQ,KAAK,QAAQ,UAAU,GAAG,SAAS;GACjE,MAAM,WAAW,KAAK,SAAS,SAAS,QAAQ,EAAE,MAAM,KAAK,GAAG,EAAE,KAAK,GAAG;GAK1E,IAAI,CAAC,SAAS,WAAW,IAAI,GAAG;IAC9B,MAAM,MAAM,IAAI,WAAW;IAE3B,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;GACxC;EACF;EAEA,QAAQ,QAAQ,KAAK,MAAM;CAC7B;CAEA,OAAO;AACT;;;;;;;;;;;AAYA,SAAgB,yBACd,SACA,aACU;CACV,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,cAAc,aACvB,KAAK,MAAM,OAAO,kBAAkB,SAAS,UAAU,GACrD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CAI1C,OAAO;AACT;;;;;;;;;;;;;;;AAqBA,SAAS,gBACP,UACA,YACoB;CACpB,IAAI,SAAS,gBAAgB,QAAW,OAAO;CAE/C,MAAM,SAAS,IAAI;CAEnB,KAAK,MAAM,OAAO,OAAO,KAAK,QAAQ,GACpC,IAAI,IAAI,SAAS,MAAM,GAAG,OAAO;AAIrC;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAS,mBACP,UACA,KACA,SACU;CACV,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,CAAC;CAC9B,QAAQ,IAAI,GAAG;CAEf,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,QAAW,OAAO,CAAC;CAEjC,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,IAClC,MAAM,IAAI,QAAQ,SAAyB,OAAO,SAAS,YAAY,SAAS,EAAE,IAClF,CAAC;CAEL,MAAM,UAAU,MAAM,QAAQ,MAAM,OAAO,IACvC,MAAM,QAAQ,QAAQ,OAAqB,OAAO,OAAO,QAAQ,IACjE,CAAC;CAEL,OAAO,CACL,GAAG,QACH,GAAG,QAAQ,SAAS,gBAAgB,mBAAmB,UAAU,aAAa,OAAO,CAAC,CACxF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,yBACd,WACA,aACU;CACV,MAAM,eAAe,KAAK,KAAK,WAAW,SAAS,eAAe;CAElE,IAAI;CAEJ,IAAI;EACF,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;CAI3D,QAAQ;EACN,OAAO,CAAC;CACV;CAEA,IAAI,OAAO,aAAa,YAAY,aAAa,MAAM,OAAO,CAAC;CAE/D,MAAM,QAAkB,CAAC;CAEzB,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,MAAM,gBAAgB,UAAU,UAAU;EAChD,IAAI,QAAQ,QAAW;EAEvB,MAAM,KAAK,GAAG,mBAAmB,UAAU,qBAAK,IAAI,IAAI,CAAC,CAAC;CAC5D;CAEA,MAAM,OAAiB,CAAC;CAExB,KAAK,MAAM,QAAQ,OAAO;EAMxB,MAAM,MAAM,IAAI;EAKhB,IAAI,CAAC,IAAI,WAAW,aAA2B,EAAE,GAAG;EAEpD,IAAI,CAAC,KAAK,SAAS,GAAG,GAAG,KAAK,KAAK,GAAG;CACxC;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"stylesheet-urls.mjs","names":[],"sources":["../../../../../../../web/src/server/stylesheet-urls.ts"],"sourcesContent":["/**\n * Which stylesheets a REGISTERED HANDLER must link, in each of the two modes.\n *\n * WHY THIS EXISTS AT ALL. Nothing used to put CSS into the server-rendered\n * document. A stylesheet reached the browser only because the CLIENT bundle\n * imported it, which means JavaScript applied it after the module graph\n * loaded — so every full page load painted unstyled first and restyled a\n * moment later. The markup was correct the whole time, which is precisely why\n * it was easy to miss.\n *\n * THE SCOPE IS PER HANDLER, NOT PER APPLICATION. Every page renders inside\n * `[root, ...outer-to-inner matched layouts, page]` — that is the exact triple\n * (widened to a chain) `create-page-route-handler.ts` loads per request — so a\n * handler's CSS is the ordered, deduped union of what those specific source\n * files pull in, and nothing else. Collecting across the WHOLE application\n * (every page's manifest entry, every root import) is the bug this shape\n * exists to avoid: it ships page B's stylesheet on page A's response, and it\n * only gets worse as an application grows.\n *\n * The two modes learn the answer from different places, and neither can use\n * the other's:\n *\n * - PRODUCTION reads Vite's client manifest, matching each source file's own\n * id and walking its recorded `css` and imported chunks.\n * - DEV has no manifest — Vite serves modules on demand — so the URLs are\n * derived from each source file's own import statements.\n */\nimport { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\n\n/** Stylesheet extensions Vite can serve directly. Mirrors the build's list. */\nconst STYLE_EXTENSIONS = [\".css\", \".scss\", \".sass\", \".less\", \".styl\"];\n\n/**\n * A stylesheet Vite serves in DEV must be requested with `?direct`.\n *\n * Without it Vite answers the same URL with `text/javascript` — its CSS-as-JS\n * module transform, meant for `import \"./app.css\"` — and a\n * `<link rel=\"stylesheet\">` pointing at a JavaScript response applies\n * NOTHING, silently. No console error, no network failure, just an unstyled\n * page. `?direct` is what makes Vite reply with real `text/css`.\n */\nexport const VITE_DIRECT_CSS_QUERY = \"?direct\";\n\n/**\n * ONE source file's own directly imported stylesheets, as dev URLs.\n *\n * Dev has no manifest, so the source is the file itself: whatever it imports\n * with a stylesheet extension is what it needs. This is deliberately narrow —\n * it answers \"what CSS does THIS file set up\", not \"what CSS does the whole\n * module graph this file eventually reaches end up pulling in\" — because the\n * latter would require reproducing Vite's module graph, which is precisely\n * what dev has no manifest to shortcut.\n *\n * The narrowness is the honest part: production splits CSS per chunk and can\n * follow imported chunks (`productionStylesheetUrls` below); dev can only read\n * the ONE file handed to it. A page whose own module imports its own\n * stylesheet still gets it in dev — Vite's client graph injects it as before —\n * it simply is not render-blocking the way a chain member's own import is.\n *\n * Called once per chain member — root, then every matched layout outer to\n * inner, then the page — by the installers below, which is what turns \"one\n * file's own imports\" into a handler's whole CSS chain.\n *\n * Specifiers are resolved against `sourceFile` and expressed relative to\n * `appRoot`, because that is the shape Vite's dev server serves from.\n */\nexport function devStylesheetUrls(appRoot: string, sourceFile: string): string[] {\n let source: string;\n\n try {\n source = readFileSync(sourceFile, \"utf-8\");\n } catch {\n return [];\n }\n\n const urls: string[] = [];\n const pattern = /\\bimport\\s*(?:\\(\\s*)?[\"']([^\"']+)[\"']/g;\n\n let match = pattern.exec(source);\n\n while (match !== null) {\n const specifier = match[1];\n const lowered = specifier.toLowerCase();\n\n if (STYLE_EXTENSIONS.some((extension) => lowered.endsWith(extension))) {\n const absolute = path.resolve(path.dirname(sourceFile), specifier);\n const relative = path.relative(appRoot, absolute).split(path.sep).join(\"/\");\n\n // Outside the app root Vite would need an `/@fs/` URL and a widened\n // `fs.allow`; a stylesheet living there is unusual enough that guessing\n // is worse than leaving it to the client import.\n if (!relative.startsWith(\"..\")) {\n const url = `/${relative}${VITE_DIRECT_CSS_QUERY}`;\n\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n match = pattern.exec(source);\n }\n\n return urls;\n}\n\n/**\n * ONE handler's whole dev CSS chain: every `sourceFiles` member's own direct\n * stylesheet imports (`devStylesheetUrls`), in the order given — the caller\n * passes `[root, ...outer-to-inner matched layouts, page]` — concatenated and\n * deduped across the WHOLE chain, not just within one file.\n *\n * Cross-file dedup matters as much as within-file dedup: an application-wide\n * `app.css` imported by both the root and a page must still produce one\n * `<link>`, not two.\n */\nexport function devHandlerStylesheetUrls(\n appRoot: string,\n sourceFiles: readonly string[],\n): string[] {\n const urls: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n for (const url of devStylesheetUrls(appRoot, sourceFile)) {\n if (!urls.includes(url)) urls.push(url);\n }\n }\n\n return urls;\n}\n\ntype ManifestEntry = {\n css?: unknown;\n imports?: unknown;\n};\n\n/**\n * Find the manifest key for an app-root-relative POSIX source id.\n *\n * VITE KEYS BY SOURCE PATH RELATIVE TO ITS OWN `root`, not to the app's\n * `appRoot` — the client build's `root` is the framework's own package\n * (`build-client.ts`), so a key for an app source file carries a `../`-laden\n * prefix (`\"../my-app/src/web/root.tsx\"`) rather than matching `sourceFile`\n * (`\"src/web/root.tsx\"`) byte for byte. The two forms always share the same\n * TAIL, though — both are anchored at the same file — so an exact match is\n * tried first (the case where the client build's root IS the app root, which\n * every fixture and every test below uses) and a `/`-boundary suffix match\n * second, rather than trying to reconstruct the build's own root here, which\n * this runtime read has no way to independently confirm.\n */\nfunction findManifestKey(\n manifest: Record<string, ManifestEntry | undefined>,\n sourceFile: string,\n): string | undefined {\n if (manifest[sourceFile] !== undefined) return sourceFile;\n\n const suffix = `/${sourceFile}`;\n\n for (const key of Object.keys(manifest)) {\n if (key.endsWith(suffix)) return key;\n }\n\n return undefined;\n}\n\n/**\n * Every stylesheet reachable from ONE manifest entry: its own recorded `css`,\n * plus the same walk repeated over every chunk it STATICALLY `imports`.\n *\n * `imports` only, never `dynamicImports`. Vite's manifest records\n * `dynamicImports` on shared entry points (the hydration entry names every\n * page as one) precisely because the browser must NOT download them eagerly —\n * walking that array here would pull every other page's CSS onto this one,\n * which is the exact \"unrelated pages\" leak this module exists to end.\n * `imports`, by contrast, are chunks THIS module synchronously depends on:\n * code Vite split out of it but that loads whenever it does, so their CSS is\n * this handler's CSS too.\n *\n * `visited` guards against a chunk graph cycle; sharing one set across the\n * whole walk from a single entry is enough; a shared chunk revisited from a\n * SEPARATE top-level entry (root vs. a layout vs. the page) is deliberately\n * walked again — the final merge in `productionStylesheetUrls` dedupes by URL,\n * and a fresh `visited` set per entry is simpler to reason about than one\n * threaded across unrelated chains.\n */\nfunction collectManifestCss(\n manifest: Record<string, ManifestEntry | undefined>,\n key: string,\n visited: Set<string>,\n): string[] {\n if (visited.has(key)) return [];\n visited.add(key);\n\n const entry = manifest[key];\n if (entry === undefined) return [];\n\n const ownCss = Array.isArray(entry.css)\n ? entry.css.filter((file): file is string => typeof file === \"string\" && file !== \"\")\n : [];\n\n const imports = Array.isArray(entry.imports)\n ? entry.imports.filter((id): id is string => typeof id === \"string\")\n : [];\n\n return [\n ...ownCss,\n ...imports.flatMap((importedKey) => collectManifestCss(manifest, importedKey, visited)),\n ];\n}\n\n/**\n * ONE handler's whole production CSS chain.\n *\n * `sourceFiles` is `[root, ...outer-to-inner matched layouts, page]`, each an\n * app-root-relative POSIX source id — the SAME id\n * `install-page-routes-from-manifest.ts` already carries as `sourceFile` on\n * every manifest entry, because that identity is what lets this function match\n * EXPLICITLY rather than guess: every id is looked up on its own\n * (`findManifestKey`), its own chunk's CSS is collected\n * (`collectManifestCss`), and an id with no matching entry contributes\n * nothing — it is never treated as license to fall back to scanning the whole\n * manifest, which is what let an unrelated page's CSS leak onto this handler\n * before.\n *\n * Duplicates are collapsed and order is preserved across the WHOLE chain, in\n * the order `sourceFiles` was given — root's own CSS first, then each\n * layout's outer to inner, then the page's — so cascade order matches the\n * chain's own outer-to-inner rendering order.\n *\n * A missing or malformed manifest returns NOTHING rather than throwing. The\n * hydration resolver already fails loudly on exactly those conditions, from\n * exactly the same file, and it runs first — a second, worse error for the\n * same cause helps nobody.\n */\nexport function productionStylesheetUrls(\n clientDir: string,\n sourceFiles: readonly string[],\n): string[] {\n const manifestPath = path.join(clientDir, \".vite\", \"manifest.json\");\n\n let manifest: Record<string, ManifestEntry | undefined>;\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf-8\")) as Record<\n string,\n ManifestEntry | undefined\n >;\n } catch {\n return [];\n }\n\n if (typeof manifest !== \"object\" || manifest === null) return [];\n\n const files: string[] = [];\n\n for (const sourceFile of sourceFiles) {\n const key = findManifestKey(manifest, sourceFile);\n if (key === undefined) continue;\n\n files.push(...collectManifestCss(manifest, key, new Set()));\n }\n\n const urls: string[] = [];\n\n for (const file of files) {\n // Built EXACTLY as the hydration entry's URL is built — `/${file}`, then\n // checked against the prefix — rather than reassembled from a basename.\n // The manifest already records `assets/root-<hash>.css`, and rebuilding\n // that path here would be a second expression of a convention\n // `client-asset-url-prefix.ts` owns.\n const url = `/${file}`;\n\n // A stylesheet outside the directory the asset route mounts would 404.\n // Dropped rather than emitted, because a dead <link> in <head> is a\n // silent styling failure — the exact thing this module exists to end.\n if (!url.startsWith(`${CLIENT_ASSET_URL_PREFIX}/`)) continue;\n\n if (!urls.includes(url)) urls.push(url);\n }\n\n return urls;\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { isNotFoundPageFile } from "./not-found-page.mjs";
|
|
|
3
3
|
import { discoverPages, isDiscoveredRoutablePage, toPosix } from "../build/discover-pages.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
//#region web/src/server/unregistered-pages.ts
|
|
6
|
+
//#region ../web/src/server/unregistered-pages.ts
|
|
7
7
|
function fileKey(file) {
|
|
8
8
|
const posix = toPosix(file);
|
|
9
9
|
return process.platform === "win32" ? posix.toLowerCase() : posix;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unregistered-pages.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"unregistered-pages.mjs","names":[],"sources":["../../../../../../../web/src/server/unregistered-pages.ts"],"sourcesContent":["import path from \"node:path\";\nimport {\n discoverPages,\n isDiscoveredRoutablePage,\n toPosix,\n type DiscoverPagesOptions,\n type DiscoveredPage,\n} from \"../build/discover-pages\";\nimport { matchPath } from \"./match-page-route\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\n\ntype DiscoveredGlobalPage = Extract<DiscoveredPage, { type: \"page\" }>;\ntype DiscoverPages = (options: DiscoverPagesOptions) => readonly DiscoveredPage[];\n\nexport type UnregisteredPagesOptions = {\n appRoot: string;\n appSrcRoot: string;\n registeredPageFiles: () => readonly string[];\n discover?: DiscoverPages;\n warn?: (message: string) => void;\n};\n\nfunction fileKey(file: string): string {\n const posix = toPosix(file);\n\n return process.platform === \"win32\" ? posix.toLowerCase() : posix;\n}\n\nfunction findUnregisteredPages(options: UnregisteredPagesOptions): DiscoveredGlobalPage[] {\n const { appRoot, appSrcRoot, registeredPageFiles, discover = discoverPages } = options;\n const registered = new Set(registeredPageFiles().map(fileKey));\n const webRoot = path.resolve(appSrcRoot, \"web\");\n\n return discover({ appRoot, srcDir: path.relative(appRoot, appSrcRoot) })\n .filter(isDiscoveredRoutablePage)\n .filter(\n (page) =>\n path.resolve(page.webRoot) === webRoot &&\n !isNotFoundPageFile(page.pageFile) &&\n !registered.has(fileKey(page.pageFile)),\n );\n}\n\nexport function findUnregisteredPageFiles(options: UnregisteredPagesOptions): string[] {\n return findUnregisteredPages(options).map((page) => page.pageFile);\n}\n\nexport function describeUnregisteredPages(\n pageFiles: readonly string[],\n appRoot: string,\n request: { method: string; url: string },\n): string {\n const named = pageFiles\n .map((pageFile) => ` - ${toPosix(path.relative(appRoot, pageFile))}`)\n .join(\"\\n\");\n\n return (\n `[warlock:web] ${request.method} ${request.url} answered 404. ` +\n \"These src/web page files exist on disk but are absent from the active route table:\\n\" +\n named\n );\n}\n\nexport function createUnregisteredPageReporter(\n options: UnregisteredPagesOptions,\n): (request: { method: string; url: string; pathname: string }) => void {\n const { appRoot, warn = console.warn } = options;\n const reported = new Set<string>();\n\n return (request) => {\n // `discoverPages()` deliberately refuses malformed pages. Its install/build\n // diagnostic is authoritative, so a response hook must leave this 404 alone.\n try {\n const unregistered = findUnregisteredPages(options).find(\n (page) =>\n !reported.has(fileKey(page.pageFile)) &&\n matchPath(page.routePath, request.pathname) !== undefined,\n );\n\n if (unregistered === undefined) return;\n\n reported.add(fileKey(unregistered.pageFile));\n warn(describeUnregisteredPages([unregistered.pageFile], appRoot, request));\n } catch {\n return;\n }\n };\n}\n"],"mappings":";;;;;;AAsBA,SAAS,QAAQ,MAAsB;CACrC,MAAM,QAAQ,QAAQ,IAAI;CAE1B,OAAO,QAAQ,aAAa,UAAU,MAAM,YAAY,IAAI;AAC9D;AAEA,SAAS,sBAAsB,SAA2D;CACxF,MAAM,EAAE,SAAS,YAAY,qBAAqB,WAAW,kBAAkB;CAC/E,MAAM,aAAa,IAAI,IAAI,oBAAoB,EAAE,IAAI,OAAO,CAAC;CAC7D,MAAM,UAAU,KAAK,QAAQ,YAAY,KAAK;CAE9C,OAAO,SAAS;EAAE;EAAS,QAAQ,KAAK,SAAS,SAAS,UAAU;CAAE,CAAC,EACpE,OAAO,wBAAwB,EAC/B,QACE,SACD,KAAK,QAAQ,KAAK,OAAO,MAAM,WAC/B,CAAC,mBAAmB,KAAK,QAAQ,KACjC,CAAC,WAAW,IAAI,QAAQ,KAAK,QAAQ,CAAC,CACxC;AACJ;AAMA,SAAgB,0BACd,WACA,SACA,SACQ;CACR,MAAM,QAAQ,UACX,KAAK,aAAa,OAAO,QAAQ,KAAK,SAAS,SAAS,QAAQ,CAAC,GAAG,EACpE,KAAK,IAAI;CAEZ,OACE,iBAAiB,QAAQ,OAAO,GAAG,QAAQ,IAAI;IAE/C;AAEJ;AAEA,SAAgB,+BACd,SACsE;CACtE,MAAM,EAAE,SAAS,OAAO,QAAQ,SAAS;CACzC,MAAM,2BAAW,IAAI,IAAY;CAEjC,QAAQ,YAAY;EAGlB,IAAI;GACF,MAAM,eAAe,sBAAsB,OAAO,EAAE,MACjD,SACC,CAAC,SAAS,IAAI,QAAQ,KAAK,QAAQ,CAAC,KACpC,UAAU,KAAK,WAAW,QAAQ,QAAQ,MAAM,MACpD;GAEA,IAAI,iBAAiB,QAAW;GAEhC,SAAS,IAAI,QAAQ,aAAa,QAAQ,CAAC;GAC3C,KAAK,0BAA0B,CAAC,aAAa,QAAQ,GAAG,SAAS,OAAO,CAAC;EAC3E,QAAQ;GACN;EACF;CACF;AACF"}
|
|
@@ -2,7 +2,7 @@ import { WebBuildOptions } from "../build/contribution.mjs";
|
|
|
2
2
|
import { WebConnectorOptions } from "./web-connector.mjs";
|
|
3
3
|
import { Connector } from "@warlock.js/core";
|
|
4
4
|
|
|
5
|
-
//#region web/src/server/web-connector-factory.d.ts
|
|
5
|
+
//#region ../web/src/server/web-connector-factory.d.ts
|
|
6
6
|
type WebConnectorFactoryOptions = WebConnectorOptions & {
|
|
7
7
|
/**
|
|
8
8
|
* What web contributes to `warlock build`. Passed straight to
|
|
@@ -3,7 +3,7 @@ import { ConnectorLifecyclePhase } from "@warlock.js/core";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
-
//#region web/src/server/web-connector-factory.ts
|
|
6
|
+
//#region ../web/src/server/web-connector-factory.ts
|
|
7
7
|
/**
|
|
8
8
|
* `webConnector()` — the ONE thing `warlock.config.ts` imports from
|
|
9
9
|
* `@warlock.js/web/connector`, and the only value the `./connector` barrel
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport {\n createWebBuildContribution,\n type WebBuildOptions,\n} from \"../build/contribution\";\nimport type { WebConnector, WebConnectorOptions } from \"./web-connector\";\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\".\n *\n * Defined HERE and re-exported by `./web-connector` rather than the other way\n * round: the delegate must publish `priority` synchronously, and reading it\n * from the heavy module would defeat the whole point of the delegate.\n */\nexport const WEB_CONNECTOR_PRIORITY = 5.5;\n\nexport type WebConnectorFactoryOptions = WebConnectorOptions & {\n /**\n * What web contributes to `warlock build`. Passed straight to\n * {@link createWebBuildContribution}; JSON-serializable values only.\n */\n build?: WebBuildOptions;\n};\n\n/**\n * The `@warlock.js/web` package root, derived once from THIS module's location\n * and then passed EXPLICITLY to both halves.\n *\n * `web/src/server/web-connector-factory.ts` → `web/src/server` → `web/src` →\n * `web`; published as `web/esm/server/web-connector-factory.js`, two levels up\n * is the package root under both layouts — the same arithmetic\n * `contribution.ts:136-140` documents.\n *\n * Deriving it here and handing it down means the build contribution never falls\n * back to its own `import.meta.url` guess (`contribution.ts:129-141`): one\n * derivation, one place to be wrong, and `assertWebPackageRoot` verifies it\n * against `<root>/package.json`'s `name` either way.\n */\nfunction deriveWebRoot(): string {\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n}\n\n/**\n * Construct web's connector for `warlock.config.ts > connectors`.\n *\n * @example\n * ```ts\n * export default defineConfig({ connectors: [webConnector()] });\n * ```\n */\nexport function webConnector(\n options: WebConnectorFactoryOptions = {},\n): Connector {\n const { build: buildOptions, ...connectorOptions } = options;\n const webRoot = connectorOptions.webRoot ?? deriveWebRoot();\n // One snapshot feeds both halves. Mutating the caller's array after config\n // construction cannot make dev and `warlock build` observe different lists.\n const connectorPlugins = [...(connectorOptions.plugins ?? [])];\n\n const build: ConnectorBuildContribution = createWebBuildContribution(\n {\n ...buildOptions,\n webRoot: buildOptions?.webRoot ?? webRoot,\n },\n connectorPlugins,\n );\n\n let instance: WebConnector | undefined;\n\n /**\n * Load the heavy half on first lifecycle call. `boot()` is always the first\n * of these to run (`core/src/connectors/connectors-manager.ts:87-93`), so the\n * import lands in a process that has already committed to serving pages.\n */\n const load = async (): Promise<WebConnector> => {\n if (!instance) {\n const { WebConnector: WebConnectorClass } =\n await import(\"./web-connector\");\n\n instance = new WebConnectorClass({\n ...connectorOptions,\n plugins: connectorPlugins,\n webRoot,\n });\n }\n\n return instance;\n };\n\n return {\n name: \"web\" satisfies ConnectorName,\n priority: WEB_CONNECTOR_PRIORITY,\n lifecyclePhase: ConnectorLifecyclePhase.Late,\n build,\n\n // Never loads the heavy half: a connector that was never booted is not\n // active, and answering that must not cost a Vite import.\n isActive: () => instance?.isActive() ?? false,\n\n boot: async () => {\n await (await load()).boot();\n },\n start: async () => {\n await (await load()).start();\n },\n restart: async () => {\n await (await load()).restart();\n },\n\n // Both of these are asked of every registered connector, including ones\n // that never booted — so neither may force the import.\n shutdown: async () => {\n await instance?.shutdown();\n },\n\n // Asked on EVERY watcher batch, so the `instance?.` is load-bearing twice:\n // it keeps the answer free for a connector that never booted, and it is the\n // only reason this delegate can forward the question at all — by the time a\n // watcher batch exists, `boot()` has already loaded the heavy half, so\n // reading the real answer off it costs nothing beyond the call.\n //\n // It used to be a hard `false`, which meant a `*.page.tsx` created while\n // `warlock dev` was running was never noticed: page routes are installed\n // once, in `WebConnector.boot()`, so the new page's URL 404'd in silence.\n // The connector itself now decides: add/remove and route-identity edits are\n // live route-table work; component-body-only edits remain Vite HMR work.\n shouldRestart: (changedFiles: string[]) =>\n instance?.shouldRestart(changedFiles) ?? false,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,yBAAyB;;;;;;;;;;;;;;;AAwBtC,SAAS,gBAAwB;CAC/B,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI;AAC9E;;;;;;;;;AAUA,SAAgB,aACd,UAAsC,CAAC,GAC5B;CACX,MAAM,EAAE,OAAO,cAAc,GAAG,qBAAqB;CACrD,MAAM,UAAU,iBAAiB,WAAW,cAAc;CAG1D,MAAM,mBAAmB,CAAC,GAAI,iBAAiB,WAAW,CAAC,CAAE;CAE7D,MAAM,QAAoC,2BACxC;EACE,GAAG;EACH,SAAS,cAAc,WAAW;CACpC,GACA,gBACF;CAEA,IAAI;;;;;;CAOJ,MAAM,OAAO,YAAmC;EAC9C,IAAI,CAAC,UAAU;GACb,MAAM,EAAE,cAAc,sBACpB,MAAM,OAAO;GAEf,WAAW,IAAI,kBAAkB;IAC/B,GAAG;IACH,SAAS;IACT;GACF,CAAC;EACH;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,UAAU;EACV,gBAAgB,wBAAwB;EACxC;EAIA,gBAAgB,UAAU,SAAS,KAAK;EAExC,MAAM,YAAY;GAChB,OAAO,MAAM,KAAK,GAAG,KAAK;EAC5B;EACA,OAAO,YAAY;GACjB,OAAO,MAAM,KAAK,GAAG,MAAM;EAC7B;EACA,SAAS,YAAY;GACnB,OAAO,MAAM,KAAK,GAAG,QAAQ;EAC/B;EAIA,UAAU,YAAY;GACpB,MAAM,UAAU,SAAS;EAC3B;EAaA,gBAAgB,iBACd,UAAU,cAAc,YAAY,KAAK;CAC7C;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseConnector, ConnectorLifecyclePhase } from "@warlock.js/core";
|
|
2
2
|
import { Alias, PluginOption, ViteDevServer } from "vite";
|
|
3
3
|
|
|
4
|
-
//#region web/src/server/web-connector.d.ts
|
|
4
|
+
//#region ../web/src/server/web-connector.d.ts
|
|
5
5
|
type WebConnectorOptions = {
|
|
6
6
|
/**
|
|
7
7
|
* Vite's `root` — the application directory that owns `src/`, `package.json`
|
|
@@ -18,7 +18,7 @@ import fs from "node:fs";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
|
|
21
|
-
//#region web/src/server/web-connector.ts
|
|
21
|
+
//#region ../web/src/server/web-connector.ts
|
|
22
22
|
/**
|
|
23
23
|
* `WebConnector` — the SSR page surface as a first-class Warlock connector.
|
|
24
24
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-connector.mjs","names":[],"sources":["C:/Work/Warlock.js/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":"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"}
|
|
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":"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"}
|
package/esm/shared.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SharedContext } from "./index.mjs";
|
|
2
2
|
|
|
3
|
-
//#region web/src/shared.d.ts
|
|
3
|
+
//#region ../web/src/shared.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Vite's own `ImportMetaEnv` augmentation ships with the `vite/client` triple-slash
|
|
6
6
|
* types, which this package does not pull in (`shared.ts` compiles standalone, not
|