@warlock.js/web 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/esm/build/contribution.d.mts +55 -0
- package/esm/build/contribution.mjs +132 -0
- package/esm/build/contribution.mjs.map +1 -0
- package/esm/build/discover-pages.mjs +294 -0
- package/esm/build/discover-pages.mjs.map +1 -0
- package/esm/build/generate-client-registry.mjs +123 -0
- package/esm/build/generate-client-registry.mjs.map +1 -0
- package/esm/build/generate-pages-barrel.mjs +330 -0
- package/esm/build/generate-pages-barrel.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +212 -0
- package/esm/build/read-route-exports.mjs.map +1 -0
- package/esm/client/build-hydrated-tree.mjs +113 -0
- package/esm/client/build-hydrated-tree.mjs.map +1 -0
- package/esm/client/hydrate-page.mjs +73 -0
- package/esm/client/hydrate-page.mjs.map +1 -0
- package/esm/client/navigation/current-route.d.mts +91 -0
- package/esm/client/navigation/current-route.mjs +66 -0
- package/esm/client/navigation/current-route.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +97 -0
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
- package/esm/client/navigation/navigation-root.mjs +196 -0
- package/esm/client/navigation/navigation-root.mjs.map +1 -0
- package/esm/client/navigation/prefetch.mjs +110 -0
- package/esm/client/navigation/prefetch.mjs.map +1 -0
- package/esm/client/navigation/refresh.d.mts +40 -0
- package/esm/client/navigation/refresh.mjs +146 -0
- package/esm/client/navigation/refresh.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +55 -0
- package/esm/client/navigation/verbs.mjs +66 -0
- package/esm/client/navigation/verbs.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +4 -0
- package/esm/client/runtime/index.mjs +4 -0
- package/esm/client/runtime/manifest.d.mts +8 -0
- package/esm/client/runtime/manifest.mjs +77 -0
- package/esm/client/runtime/manifest.mjs.map +1 -0
- package/esm/client/runtime/matcher.d.mts +7 -0
- package/esm/client/runtime/matcher.mjs +169 -0
- package/esm/client/runtime/matcher.mjs.map +1 -0
- package/esm/client/runtime/types.d.mts +22 -0
- package/esm/components/default-app.mjs +32 -0
- package/esm/components/default-app.mjs.map +1 -0
- package/esm/components/document-context.d.mts +1 -0
- package/esm/components/document-context.mjs +24 -0
- package/esm/components/document-context.mjs.map +1 -0
- package/esm/components/head.d.mts +14 -0
- package/esm/components/head.mjs +63 -0
- package/esm/components/head.mjs.map +1 -0
- package/esm/components/link.d.mts +157 -0
- package/esm/components/link.mjs +194 -0
- package/esm/components/link.mjs.map +1 -0
- package/esm/components/scripts.d.mts +14 -0
- package/esm/components/scripts.mjs +21 -0
- package/esm/components/scripts.mjs.map +1 -0
- package/esm/connector/index.d.mts +6 -0
- package/esm/connector/index.mjs +7 -0
- package/esm/context.d.mts +107 -0
- package/esm/hydration/index.d.mts +1 -0
- package/esm/hydration/index.mjs +33 -0
- package/esm/hydration/index.mjs.map +1 -0
- package/esm/hydration-payload.d.mts +1 -0
- package/esm/hydration-payload.mjs +72 -0
- package/esm/hydration-payload.mjs.map +1 -0
- package/esm/index.d.mts +35 -0
- package/esm/index.mjs +12 -0
- package/esm/loaders.d.mts +47 -0
- package/esm/metadata.d.mts +54 -0
- package/esm/props.d.mts +42 -0
- package/esm/route.d.mts +30 -0
- package/esm/routing/compose-route-path.mjs +33 -0
- package/esm/routing/compose-route-path.mjs.map +1 -0
- package/esm/routing/data-request.mjs +52 -0
- package/esm/routing/data-request.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +78 -0
- package/esm/routing/layout-policy.mjs.map +1 -0
- package/esm/routing/navigator.mjs +21 -0
- package/esm/routing/navigator.mjs.map +1 -0
- package/esm/routing/query-string.d.mts +250 -0
- package/esm/routing/query-string.mjs +340 -0
- package/esm/routing/query-string.mjs.map +1 -0
- package/esm/routing/route-identity.mjs +68 -0
- package/esm/routing/route-identity.mjs.map +1 -0
- package/esm/routing/route-table.d.mts +34 -0
- package/esm/routing/route-table.mjs +182 -0
- package/esm/routing/route-table.mjs.map +1 -0
- package/esm/routing/router-events.d.mts +159 -0
- package/esm/routing/router-events.mjs +83 -0
- package/esm/routing/router-events.mjs.map +1 -0
- package/esm/server/buffered-response.mjs +115 -0
- package/esm/server/buffered-response.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +27 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -0
- package/esm/server/client-asset-url-prefix.d.mts +22 -0
- package/esm/server/client-asset-url-prefix.mjs +23 -0
- package/esm/server/client-asset-url-prefix.mjs.map +1 -0
- package/esm/server/create-page-module-loader.mjs +49 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -0
- package/esm/server/create-page-route-handler.mjs +107 -0
- package/esm/server/create-page-route-handler.mjs.map +1 -0
- package/esm/server/dev-server.mjs +167 -0
- package/esm/server/dev-server.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +1 -0
- package/esm/server/execute-page-request.mjs +166 -0
- package/esm/server/execute-page-request.mjs.map +1 -0
- package/esm/server/execute-page-request.types.d.mts +1 -0
- package/esm/server/hydration-client-url.d.mts +37 -0
- package/esm/server/hydration-client-url.mjs +101 -0
- package/esm/server/hydration-client-url.mjs.map +1 -0
- package/esm/server/index.mjs +13 -0
- package/esm/server/install-page-routes-from-manifest.mjs +133 -0
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
- package/esm/server/install-page-routes.d.mts +1 -0
- package/esm/server/install-page-routes.mjs +6 -0
- package/esm/server/install-production-page-routes.mjs +55 -0
- package/esm/server/install-production-page-routes.mjs.map +1 -0
- package/esm/server/match-page-route.mjs +48 -0
- package/esm/server/match-page-route.mjs.map +1 -0
- package/esm/server/page-context.d.mts +1 -0
- package/esm/server/page-context.mjs +33 -0
- package/esm/server/page-context.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +102 -0
- package/esm/server/page-manifest.mjs +31 -0
- package/esm/server/page-manifest.mjs.map +1 -0
- package/esm/server/render-page.mjs +214 -0
- package/esm/server/render-page.mjs.map +1 -0
- package/esm/server/resolve-page-metadata.mjs +33 -0
- package/esm/server/resolve-page-metadata.mjs.map +1 -0
- package/esm/server/resolve-validation-data.mjs +27 -0
- package/esm/server/resolve-validation-data.mjs.map +1 -0
- package/esm/server/settle-page-response.d.mts +1 -0
- package/esm/server/settle-page-response.mjs +90 -0
- package/esm/server/settle-page-response.mjs.map +1 -0
- package/esm/server/stylesheet-urls.mjs +65 -0
- package/esm/server/stylesheet-urls.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +25 -0
- package/esm/server/web-connector-factory.mjs +121 -0
- package/esm/server/web-connector-factory.mjs.map +1 -0
- package/esm/server/web-connector.d.mts +34 -0
- package/esm/server/web-connector.mjs +516 -0
- package/esm/server/web-connector.mjs.map +1 -0
- package/esm/shared.d.mts +22 -0
- package/esm/shared.mjs +223 -0
- package/esm/shared.mjs.map +1 -0
- package/esm/validation.d.mts +31 -0
- package/esm/vite/app-convention-aliases.mjs +43 -0
- package/esm/vite/app-convention-aliases.mjs.map +1 -0
- package/esm/vite/build-client.d.mts +38 -0
- package/esm/vite/build-client.mjs +72 -0
- package/esm/vite/build-client.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +57 -0
- package/esm/vite/gate-a-resolve.mjs +834 -0
- package/esm/vite/gate-a-resolve.mjs.map +1 -0
- package/esm/vite/gate-b-secrets.d.mts +57 -0
- package/esm/vite/gate-b-secrets.mjs +299 -0
- package/esm/vite/gate-b-secrets.mjs.map +1 -0
- package/esm/vite/gate-c-verify.d.mts +118 -0
- package/esm/vite/gate-c-verify.mjs +263 -0
- package/esm/vite/gate-c-verify.mjs.map +1 -0
- package/esm/vite/hydration-entries.d.mts +17 -0
- package/esm/vite/hydration-entries.mjs +45 -0
- package/esm/vite/hydration-entries.mjs.map +1 -0
- package/esm/vite/index.d.mts +126 -0
- package/esm/vite/index.mjs +146 -0
- package/esm/vite/index.mjs.map +1 -0
- package/esm/vite/page-registry-plugin.d.mts +43 -0
- package/esm/vite/page-registry-plugin.mjs +138 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -0
- package/esm/vite/projection.d.mts +31 -0
- package/esm/vite/projection.mjs +384 -0
- package/esm/vite/projection.mjs.map +1 -0
- package/llms-full.txt +1041 -0
- package/llms.txt +21 -0
- package/package.json +77 -0
- package/skills/add-web-to-an-app/SKILL.md +103 -0
- package/skills/create-a-page/SKILL.md +141 -0
- package/skills/load-page-data/SKILL.md +183 -0
- package/skills/navigate-on-the-client/SKILL.md +182 -0
- package/skills/serve-styles/SKILL.md +145 -0
- package/skills/use-layouts/SKILL.md +121 -0
- package/skills/write-the-root/SKILL.md +131 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import { resolveWebPackageRoot } from "../build/contribution.mjs";
|
|
2
|
+
import { consumePageManifest } from "./page-manifest.mjs";
|
|
3
|
+
import { CLIENT_ASSET_URL_PREFIX } from "./client-asset-url-prefix.mjs";
|
|
4
|
+
import { createHydrationClientEntry } from "../vite/hydration-entries.mjs";
|
|
5
|
+
import { resolveHydrationClientUrl } from "./hydration-client-url.mjs";
|
|
6
|
+
import { WEB_CONNECTOR_PRIORITY } from "./web-connector-factory.mjs";
|
|
7
|
+
import { warlockClientBoundary } from "../vite/index.mjs";
|
|
8
|
+
import { appConventionAliases } from "../vite/app-convention-aliases.mjs";
|
|
9
|
+
import { applyBufferedCookie, devErrorTransportPlugin, sendCapturedDevError } from "./dev-server.mjs";
|
|
10
|
+
import { installProductionPageRoutes } from "./install-production-page-routes.mjs";
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { Application, BaseConnector, ConnectorLifecyclePhase, container, requestContext, router } from "@warlock.js/core";
|
|
15
|
+
|
|
16
|
+
//#region ../web/src/server/web-connector.ts
|
|
17
|
+
/**
|
|
18
|
+
* `WebConnector` — the SSR page surface as a first-class Warlock connector.
|
|
19
|
+
*
|
|
20
|
+
* It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React
|
|
21
|
+
* pages on one port, and `web` no longer owns a private copy of the HTTP
|
|
22
|
+
* lifecycle. Everything this file does used to live in `startDevServer()`
|
|
23
|
+
* (`web/src/server/dev-server.ts`), which created its own Fastify instance,
|
|
24
|
+
* scanned the router and called `listen()` itself — three responsibilities core
|
|
25
|
+
* already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.
|
|
26
|
+
*
|
|
27
|
+
* WHY A `Late` CONNECTOR IS THE RIGHT SEAM, in ordering terms:
|
|
28
|
+
* `ConnectorsManager.startPhase` runs **every** `boot()` in a phase before
|
|
29
|
+
* **any** `start()` (`core/src/connectors/connectors-manager.ts:87-93`).
|
|
30
|
+
* `HttpConnector` is itself `Late` (`core/src/connectors/http-connector.ts:41`)
|
|
31
|
+
* and publishes its Fastify instance during its own `boot()`
|
|
32
|
+
* (`container.set("http.server", …)`, `core/src/connectors/http-connector.ts:74`).
|
|
33
|
+
* So by the time this connector's `boot()` runs, Fastify and its plugins exist,
|
|
34
|
+
* the raw node server exists, and NOTHING has been scanned or bound yet — page
|
|
35
|
+
* routes registered here are picked up by `HttpConnector.start()`'s
|
|
36
|
+
* `router.scanDevServer(…)` (`core/src/connectors/http-connector.ts:133`) before
|
|
37
|
+
* `listen()` (`:147`). `SocketConnector.boot()` reads the same container key the
|
|
38
|
+
* same way (`core/src/connectors/socket-connector.ts:78-80`) — this file is
|
|
39
|
+
* deliberately shaped after it.
|
|
40
|
+
*
|
|
41
|
+
* What it can NOT do, and why that is fine: route COLLECTION happens earlier
|
|
42
|
+
* (`core/src/dev-server/development-server.ts:57` precedes `:66`), so pages are
|
|
43
|
+
* not discovered by the framework's file scanner. They are discovered here, by
|
|
44
|
+
* `installPageRoutes` (`./install-page-routes.ts:189`), and registered through
|
|
45
|
+
* the ordinary `router.get(…)` API — there is no second server matcher.
|
|
46
|
+
*
|
|
47
|
+
* DELIBERATE EXCEPTION to A.3 §2 ("web has no core dependency"), the same one
|
|
48
|
+
* `./dev-server.ts` and `./install-page-routes.ts` record in their own headers:
|
|
49
|
+
* this module is not re-exported from any package barrel (`web/src/index.ts`,
|
|
50
|
+
* `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of
|
|
51
|
+
* `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only
|
|
52
|
+
* ever imported by tooling that already depends on core.
|
|
53
|
+
*
|
|
54
|
+
* `@warlock.js/web/connector` reaches this class ONLY through
|
|
55
|
+
* `./web-connector-factory.ts`'s `await import("./web-connector")` — a
|
|
56
|
+
* deliberate seam, because a static edge from that barrel to this file would
|
|
57
|
+
* put `../vite`, core's router and `./dev-server` into the import graph of
|
|
58
|
+
* every consuming app's `warlock.config.ts`.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Production boot ran with no page manifest in the registry.
|
|
62
|
+
*
|
|
63
|
+
* `undefined` from `consumePageManifest()` is a FACT, not an error — the registry never throws
|
|
64
|
+
* on absence. The connector supplies the meaning, and it does so from MODE, not
|
|
65
|
+
* from the value: in dev the absence is normal because Vite supplies the
|
|
66
|
+
* modules and no build has run; in production it means the app was not built
|
|
67
|
+
* with web, and a prod server that boots anyway serves 404s while looking
|
|
68
|
+
* healthy — the exact silent failure this error exists to prevent.
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Which half of the handoff is live: Vite serving from source, or a bundle
|
|
72
|
+
* produced by `warlock build`.
|
|
73
|
+
*
|
|
74
|
+
* RUNTIME STRATEGY, NOT `Application.environment`. The two are explicitly a
|
|
75
|
+
* "separate axis" (`core/src/utils/environment.ts:4-7`), and the question this
|
|
76
|
+
* connector asks — "is there a page manifest, or does Vite supply the modules?"
|
|
77
|
+
* — is a HOSTING question. `warlock dev` with `NODE_ENV=production` (a staging
|
|
78
|
+
* checkout, or just an inherited shell variable) is still Vite-hosted and still
|
|
79
|
+
* has no manifest; keying off the environment would make that app refuse to
|
|
80
|
+
* boot. Core sets the strategy on both sides deliberately:
|
|
81
|
+
* `core/src/cli/commands/dev-server.command.ts:12` declares `"development"` for
|
|
82
|
+
* `warlock dev`, and the generated production entry declares `"production"`
|
|
83
|
+
* (`core/src/production/production-builder.ts:245`).
|
|
84
|
+
*
|
|
85
|
+
* This is core's own connector-level idiom, not a new one:
|
|
86
|
+
* `core/src/connectors/http-connector.ts:132` picks `scanDevServer` over `scan`
|
|
87
|
+
* the same way.
|
|
88
|
+
*/
|
|
89
|
+
function isProductionRuntime() {
|
|
90
|
+
return Application.runtimeStrategy === "production";
|
|
91
|
+
}
|
|
92
|
+
var WebPageManifestMissingError = class extends Error {
|
|
93
|
+
constructor() {
|
|
94
|
+
super("WebConnector booted in production without a page manifest. The generated `pages.ts` barrel never ran, so no page modules were handed to the connector and there is nothing to serve. Run `warlock build` with the web connector registered in `warlock.config.ts > connectors`, and start the artifact that build produced.");
|
|
95
|
+
this.name = "WebPageManifestMissingError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The manifest carried pages but no `clientDir`.
|
|
100
|
+
*
|
|
101
|
+
* The build bakes that field in beside the page table, so the only way to
|
|
102
|
+
* observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`
|
|
103
|
+
* older than the field, started against a newer runtime. Named rather than
|
|
104
|
+
* left to `path.resolve(cwd, undefined)`, which throws a `TypeError` naming
|
|
105
|
+
* neither the manifest nor the rebuild that fixes it.
|
|
106
|
+
*/
|
|
107
|
+
var WebClientDirMissingError = class extends Error {
|
|
108
|
+
constructor() {
|
|
109
|
+
super("The page manifest carries pages but no `clientDir`, so the connector cannot locate the client bundle. This artifact was built by an older @warlock.js/web than the one now running it. Re-run `warlock build` to regenerate the `pages.ts` barrel against the current version.");
|
|
110
|
+
this.name = "WebClientDirMissingError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Third-party packages core reaches through `await import(...)` and that must
|
|
115
|
+
* therefore never enter Vite's SSR transform graph.
|
|
116
|
+
*
|
|
117
|
+
* Left un-externalized, Vite's SSR module runner tries to resolve them anyway
|
|
118
|
+
* and jams: the failure is NOT a missing-module error but a `transport invoke
|
|
119
|
+
* timed out` on whatever unrelated module happened to be in flight. Derived in
|
|
120
|
+
* one pass from `peerDependenciesMeta.optional` across every workspace package
|
|
121
|
+
* reachable from `core/src/index.ts`, and carried over verbatim from
|
|
122
|
+
* `dev-server.ts`'s own list. Only THIRD-PARTY peers belong here — every
|
|
123
|
+
* `@warlock.js/*` sibling must stay in Vite's graph.
|
|
124
|
+
*
|
|
125
|
+
* This list is core's peer list, not web's; publishing it from core instead
|
|
126
|
+
* of duplicating it here is still outstanding.
|
|
127
|
+
*/
|
|
128
|
+
const CORE_OPTIONAL_PEERS = [
|
|
129
|
+
"nodemailer",
|
|
130
|
+
"@aws-sdk/client-sesv2",
|
|
131
|
+
"@react-email/render",
|
|
132
|
+
"redis",
|
|
133
|
+
"pg",
|
|
134
|
+
"mongodb",
|
|
135
|
+
"@sentry/node",
|
|
136
|
+
"sharp",
|
|
137
|
+
"socket.io",
|
|
138
|
+
"@aws-sdk/client-s3",
|
|
139
|
+
"@aws-sdk/lib-storage",
|
|
140
|
+
"@aws-sdk/s3-request-presigner",
|
|
141
|
+
"amqplib",
|
|
142
|
+
"langfuse",
|
|
143
|
+
"openai",
|
|
144
|
+
"pdf-parse"
|
|
145
|
+
];
|
|
146
|
+
/**
|
|
147
|
+
* `web`'s OWN optional peers, kept separate from {@link CORE_OPTIONAL_PEERS}
|
|
148
|
+
* because that list is core's and is documented as such.
|
|
149
|
+
*
|
|
150
|
+
* Same rule, same reason: anything this package reaches through
|
|
151
|
+
* `await import(...)` must be external to every bundler and SSR pipeline, or
|
|
152
|
+
* some pipeline will try to resolve it in an install that legitimately does not
|
|
153
|
+
* have it. `vite` itself is the precedent — `createViteServer` has always
|
|
154
|
+
* loaded it this way — and `@vitejs/plugin-react` is loaded from the same
|
|
155
|
+
* function, for the same dev-only reason, so it belongs in the same set.
|
|
156
|
+
*/
|
|
157
|
+
const WEB_OPTIONAL_PEERS = ["vite", "@vitejs/plugin-react"];
|
|
158
|
+
/**
|
|
159
|
+
* Public specifier for the React Fast Refresh preamble module this connector
|
|
160
|
+
* injects. See {@link WebConnector.resolveReactFastRefreshPlugins}.
|
|
161
|
+
*/
|
|
162
|
+
const REACT_REFRESH_PREAMBLE_ID = "virtual:warlock/react-refresh-preamble";
|
|
163
|
+
/** The `\0`-prefixed form Rollup uses to mark a module as not-a-file. */
|
|
164
|
+
const RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\0${REACT_REFRESH_PREAMBLE_ID}`;
|
|
165
|
+
/**
|
|
166
|
+
* A directory, plus the path the filesystem really stores it at when the two
|
|
167
|
+
* differ. Both forms belong in `server.fs.allow` — see the `fs` block in
|
|
168
|
+
* {@link WebConnector.createViteServer} for why one of them is never enough.
|
|
169
|
+
*
|
|
170
|
+
* A missing directory is not this function's problem to report: the roots it is
|
|
171
|
+
* handed are already proven (`resolveWebPackageRoot`) or are the app's own cwd,
|
|
172
|
+
* and an allow-list entry that points nowhere simply matches nothing.
|
|
173
|
+
*/
|
|
174
|
+
function withRealPath(directory) {
|
|
175
|
+
try {
|
|
176
|
+
const realPath = fs.realpathSync(directory);
|
|
177
|
+
return realPath === directory ? [directory] : [directory, realPath];
|
|
178
|
+
} catch {
|
|
179
|
+
return [directory];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */
|
|
183
|
+
function normalizeModuleId(id) {
|
|
184
|
+
const [filepath] = id.split("?");
|
|
185
|
+
return filepath.replace(/\\/g, "/").toLowerCase();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Web Connector
|
|
189
|
+
* Manages the Vite dev server and the SSR page routes, mounted on the HTTP
|
|
190
|
+
* connector's Fastify instance.
|
|
191
|
+
*/
|
|
192
|
+
var WebConnector = class extends BaseConnector {
|
|
193
|
+
name = "web";
|
|
194
|
+
priority = WEB_CONNECTOR_PRIORITY;
|
|
195
|
+
lifecyclePhase = ConnectorLifecyclePhase.Late;
|
|
196
|
+
/**
|
|
197
|
+
* Nothing. Page, layout and component edits are Vite's HMR to own — a
|
|
198
|
+
* connector restart would tear down the module graph Vite is keeping warm.
|
|
199
|
+
*/
|
|
200
|
+
watchedFiles = [];
|
|
201
|
+
options;
|
|
202
|
+
vite;
|
|
203
|
+
installedPages = [];
|
|
204
|
+
/**
|
|
205
|
+
* The build→runtime handoff table, read once at boot.
|
|
206
|
+
*
|
|
207
|
+
* `undefined` in dev is the normal case and carries no meaning beyond "no
|
|
208
|
+
* build has run" — see {@link WebPageManifestMissingError} for why the
|
|
209
|
+
* production reading is a hard error and why the branch is on MODE.
|
|
210
|
+
*/
|
|
211
|
+
pageManifest;
|
|
212
|
+
constructor(options = {}) {
|
|
213
|
+
super();
|
|
214
|
+
this.options = options;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Boot the connector — wire the page pipeline's request context and register
|
|
218
|
+
* every page on the router.
|
|
219
|
+
*
|
|
220
|
+
* Two ways of doing that, one per hosting mode, and they share the shape
|
|
221
|
+
* rather than the mechanism. Development creates Vite in middleware mode,
|
|
222
|
+
* mounts it on the HTTP connector's Fastify instance and discovers pages by
|
|
223
|
+
* walking `app/`; production takes both answers from the manifest the build
|
|
224
|
+
* handed over (`./install-production-page-routes`) and touches no Vite at all.
|
|
225
|
+
*
|
|
226
|
+
* Everything here happens BEFORE `HttpConnector.start()` scans and listens,
|
|
227
|
+
* which is the entire reason this is a `Late` connector's `boot()` and not its
|
|
228
|
+
* `start()`.
|
|
229
|
+
*/
|
|
230
|
+
async boot() {
|
|
231
|
+
this.pageManifest = consumePageManifest();
|
|
232
|
+
if (isProductionRuntime() && !this.pageManifest) throw new WebPageManifestMissingError();
|
|
233
|
+
if (isProductionRuntime() && this.pageManifest) {
|
|
234
|
+
this.installedPages = await installProductionPageRoutes({
|
|
235
|
+
router,
|
|
236
|
+
manifest: this.pageManifest,
|
|
237
|
+
pageContext: requestContext,
|
|
238
|
+
sharedStore: () => requestContext.getStore(),
|
|
239
|
+
applyBufferedCookie,
|
|
240
|
+
resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),
|
|
241
|
+
clientDir: this.pageManifest.clientDir
|
|
242
|
+
});
|
|
243
|
+
if (this.pageManifest.pages.length > 0) router.directory({
|
|
244
|
+
root: path.join(this.resolveClientDir(), "assets"),
|
|
245
|
+
prefix: `${CLIENT_ASSET_URL_PREFIX}/`
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const fastify = this.resolveFastify();
|
|
250
|
+
const paths = await this.resolvePaths();
|
|
251
|
+
this.vite = await this.createViteServer(fastify, paths);
|
|
252
|
+
const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);
|
|
253
|
+
webServerSsr.connectSharedStore(() => requestContext.getStore());
|
|
254
|
+
webServerSsr.connectPageContext(requestContext);
|
|
255
|
+
fastify.addHook("onRequest", (request, reply, done) => {
|
|
256
|
+
this.vite?.middlewares(request.raw, reply.raw, (error) => {
|
|
257
|
+
if (sendCapturedDevError(request.raw, reply.raw)) return;
|
|
258
|
+
done(error);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
this.installedPages = await webServerSsr.installPageRoutes({
|
|
262
|
+
router,
|
|
263
|
+
vite: this.vite,
|
|
264
|
+
appSrcRoot: paths.appSrcRoot,
|
|
265
|
+
appFile: paths.appFile,
|
|
266
|
+
hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),
|
|
267
|
+
stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile),
|
|
268
|
+
applyBufferedCookie
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Where the browser fetches the hydration entry from — the one line that
|
|
273
|
+
* differs between the two modes, so it is the only thing that branches.
|
|
274
|
+
*
|
|
275
|
+
* Dev keeps Vite's `/@fs/` URL, which Vite's own middleware transforms on
|
|
276
|
+
* demand. Production reads the hashed filename out of the client build's
|
|
277
|
+
* `.vite/manifest.json` exactly once, at boot, and NEVER falls back: each way
|
|
278
|
+
* that read can fail is its own named error (`./hydration-client-url.ts:24`,
|
|
279
|
+
* `:36`, `:53`), because "serve without hydration" would be a page that
|
|
280
|
+
* renders and then does nothing.
|
|
281
|
+
*
|
|
282
|
+
* `webRoot` is OPTIONAL because only the dev branch has any use for it, and
|
|
283
|
+
* asking production for it would be worse than useless: it is proved by
|
|
284
|
+
* reading `<root>/package.json` off disk, which is exactly the kind of
|
|
285
|
+
* source-tree lookup a built artifact must never depend on. A dev boot that
|
|
286
|
+
* somehow reaches here without one is refused by name by the entry factory.
|
|
287
|
+
*/
|
|
288
|
+
resolveHydrationClientModuleUrl(webRoot) {
|
|
289
|
+
if (isProductionRuntime()) return resolveHydrationClientUrl({ clientDir: this.resolveClientDir() });
|
|
290
|
+
return createHydrationClientEntry(webRoot ?? "").devUrl;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* `<outdir>/client` — the layout the build half writes and this half reads
|
|
294
|
+
* at boot, taken from the value the build BAKED into the page manifest.
|
|
295
|
+
*
|
|
296
|
+
* It used to call `resolveBuildConfig()`, which reads `warlock.config.ts`
|
|
297
|
+
* through `warlockConfigManager`. That works in `warlock build` and in the
|
|
298
|
+
* `warlock start` SUPERVISOR, and it cannot work here: the supervisor spawns
|
|
299
|
+
* a plain `node dist/app.js` CHILD, and that process never loads — and could
|
|
300
|
+
* not load — a TypeScript build-time config. The call threw
|
|
301
|
+
* `WarlockConfig not loaded` inside connector boot, so the production server
|
|
302
|
+
* died before it ever listened.
|
|
303
|
+
*
|
|
304
|
+
* Baking it also settles the drift the old comment was worried about, and
|
|
305
|
+
* settles it harder: `build` and `start` cannot disagree about where the
|
|
306
|
+
* bundle lives, because `start` is no longer re-deriving the path at all —
|
|
307
|
+
* it reads back the one string `build` wrote.
|
|
308
|
+
*/
|
|
309
|
+
resolveClientDir() {
|
|
310
|
+
const clientDir = this.pageManifest?.clientDir;
|
|
311
|
+
if (clientDir === void 0) throw new WebClientDirMissingError();
|
|
312
|
+
return path.resolve(process.cwd(), clientDir);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* The page manifest this connector consumed at boot, or `undefined` in dev
|
|
316
|
+
* where Vite supplies the modules instead.
|
|
317
|
+
*/
|
|
318
|
+
getPageManifest() {
|
|
319
|
+
return this.pageManifest;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Activate. There is nothing to listen on — `HttpConnector.start()` owns the
|
|
323
|
+
* single `listen()` for the whole process — so this only marks the connector
|
|
324
|
+
* live once `boot()` has wired everything.
|
|
325
|
+
*/
|
|
326
|
+
async start() {
|
|
327
|
+
if (!this.vite) return;
|
|
328
|
+
this.active = true;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Shutdown — close Vite, and drop the sockets Vite's middleware left behind.
|
|
332
|
+
*
|
|
333
|
+
* Reverse-priority teardown (`core/src/connectors/connectors-manager.ts:118`)
|
|
334
|
+
* puts this BEFORE `HttpConnector.shutdown()`, which is exactly what the
|
|
335
|
+
* second call needs: requests answered by Vite's connect stack are written
|
|
336
|
+
* straight to `reply.raw`, so Fastify never observes them completing and their
|
|
337
|
+
* keep-alive sockets are never counted idle. Core's default
|
|
338
|
+
* `forceCloseConnections: "idle"` (`core/src/http/server.ts:34`) then waits on
|
|
339
|
+
* them forever. A dev server has no draining obligation, and the connector
|
|
340
|
+
* that caused the raw writes is the right one to clean up after them.
|
|
341
|
+
*/
|
|
342
|
+
async shutdown() {
|
|
343
|
+
if (!this.active) return;
|
|
344
|
+
if (container.has("http.server")) container.get("http.server").server.closeAllConnections();
|
|
345
|
+
await this.vite?.close();
|
|
346
|
+
this.vite = void 0;
|
|
347
|
+
this.installedPages = [];
|
|
348
|
+
this.pageManifest = void 0;
|
|
349
|
+
this.active = false;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Never restart on a file change. Pages, layouts and components are Vite's
|
|
353
|
+
* HMR domain; rebooting this connector would drop Vite's module graph and
|
|
354
|
+
* re-register every page route on a Fastify instance that is already serving.
|
|
355
|
+
*/
|
|
356
|
+
shouldRestart() {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
/** The pages this connector registered on the router, in registration order. */
|
|
360
|
+
getInstalledPages() {
|
|
361
|
+
return this.installedPages;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* The Fastify instance the HTTP connector published during its own `boot()`
|
|
365
|
+
* (`core/src/connectors/http-connector.ts:74`).
|
|
366
|
+
*
|
|
367
|
+
* Absence is fatal rather than a silent no-op: unlike sockets, there is no
|
|
368
|
+
* standalone fallback a page surface could serve from, and the failure this
|
|
369
|
+
* guards against — an app with no `src/config/http.ts` — otherwise shows up
|
|
370
|
+
* as every page 404ing with no explanation.
|
|
371
|
+
*/
|
|
372
|
+
resolveFastify() {
|
|
373
|
+
if (!container.has("http.server")) throw new Error("WebConnector requires the HTTP connector's Fastify instance, but `http.server` is not in the container. The `http` config is missing — add `src/config/http.ts` so `HttpConnector.boot()` runs (core/src/connectors/http-connector.ts:61-74).");
|
|
374
|
+
return container.get("http.server");
|
|
375
|
+
}
|
|
376
|
+
/** Resolve every path this connector needs from the (optional) options. */
|
|
377
|
+
async resolvePaths() {
|
|
378
|
+
const appRoot = this.options.appRoot ?? process.cwd();
|
|
379
|
+
const appSrcRoot = this.options.appSrcRoot ?? path.join(appRoot, "src");
|
|
380
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
381
|
+
const webRoot = await resolveWebPackageRoot(this.options.webRoot);
|
|
382
|
+
return {
|
|
383
|
+
appRoot,
|
|
384
|
+
appSrcRoot,
|
|
385
|
+
appFile: this.options.appFile ?? path.join(appSrcRoot, "web/root.tsx"),
|
|
386
|
+
webRoot,
|
|
387
|
+
webServerBarrel: path.join(path.dirname(selfPath), `index${path.extname(selfPath)}`)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Vite in middleware mode, `appType: "custom"` — Warlock owns the response
|
|
392
|
+
* shape and Vite never fronts the server.
|
|
393
|
+
*
|
|
394
|
+
* `server.hmr.server` is handed the RAW node server (`fastify.server`), so the
|
|
395
|
+
* HMR websocket shares the one port the app already listens on. No `hmr.port`
|
|
396
|
+
* and no `clientPort`: Vite's HMR path stays the default `"/"`, socket.io
|
|
397
|
+
* stays on `"/socket.io"` (`core/src/connectors/socket-connector.ts:90`), and
|
|
398
|
+
* the two `upgrade` listeners coexist because each is a selective filter that
|
|
399
|
+
* leaves a non-matching socket alone — verified empirically in both attachment
|
|
400
|
+
* orders.
|
|
401
|
+
*/
|
|
402
|
+
/**
|
|
403
|
+
* React Fast Refresh, or nothing at all — never half of it.
|
|
404
|
+
*
|
|
405
|
+
* TWO plugins, and the second one is not optional. `@vitejs/plugin-react`
|
|
406
|
+
* emits, into every client component module, a wrapper that reads
|
|
407
|
+
* `window.$RefreshReg$` and THROWS "can't detect preamble" if it is missing.
|
|
408
|
+
* The preamble that defines it normally arrives through Vite's
|
|
409
|
+
* `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:
|
|
410
|
+
* the document is React's SSR output and the only script appended to it is
|
|
411
|
+
* the hydration entry (`./create-page-route-handler.ts:95`). So the preamble
|
|
412
|
+
* is delivered as a virtual module that the hydration entry imports FIRST.
|
|
413
|
+
*
|
|
414
|
+
* "First" is load-bearing, twice over. ESM evaluates a module's imports in
|
|
415
|
+
* source order before the importer's own body, so an import placed at
|
|
416
|
+
* position 0 runs before `virtual:warlock/pages` pulls in a single component
|
|
417
|
+
* — which is what `$RefreshReg$` has to exist for — and before `react-dom`
|
|
418
|
+
* initialises, which is what `injectIntoGlobalHook` has to precede.
|
|
419
|
+
*
|
|
420
|
+
* The preamble TEXT is read from the plugin's own `preambleCode` export
|
|
421
|
+
* rather than copied here, so a version bump cannot leave this file holding
|
|
422
|
+
* a stale runtime contract. If that export ever stops being a string, Fast
|
|
423
|
+
* Refresh is DECLINED entirely (empty array) instead of registered without
|
|
424
|
+
* its preamble — a loud "no HMR" beats a component graph that throws on
|
|
425
|
+
* first paint.
|
|
426
|
+
*
|
|
427
|
+
* Dev-only by construction, like everything else this method is called from:
|
|
428
|
+
* `apply: "serve"` on our own plugin, `apply: "serve"` on the plugin's
|
|
429
|
+
* refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The
|
|
430
|
+
* PRODUCTION client bundle cannot reach any of this — it is built by
|
|
431
|
+
* `buildWarlockHydrationClient` (`../vite/index.ts:185`), which composes
|
|
432
|
+
* `warlockClientBoundary()` and nothing else, and never calls this method.
|
|
433
|
+
*
|
|
434
|
+
* @param paths resolved by {@link WebConnector.resolvePaths}
|
|
435
|
+
*/
|
|
436
|
+
async resolveReactFastRefreshPlugins(paths) {
|
|
437
|
+
let viteReact;
|
|
438
|
+
try {
|
|
439
|
+
({default: viteReact} = await import("@vitejs/plugin-react"));
|
|
440
|
+
} catch {
|
|
441
|
+
console.warn("[warlock:web] React Fast Refresh is OFF: `@vitejs/plugin-react` is not installed. Edits to a component will reload the page instead of hot-swapping it. Install it with `npm i -D @vitejs/plugin-react` to enable it.");
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
const preambleCode = viteReact.preambleCode;
|
|
445
|
+
if (typeof preambleCode !== "string") {
|
|
446
|
+
console.warn("[warlock:web] React Fast Refresh is OFF: this `@vitejs/plugin-react` no longer exports `preambleCode`, so the refresh preamble cannot be injected into the hydration entry. Registering the plugin without it would make every component module throw \"can't detect preamble\" in the browser.");
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
const preambleSource = preambleCode.replace("__BASE__", "/");
|
|
450
|
+
const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);
|
|
451
|
+
return [{
|
|
452
|
+
name: "warlock:react-refresh-preamble",
|
|
453
|
+
enforce: "pre",
|
|
454
|
+
apply: "serve",
|
|
455
|
+
applyToEnvironment: (environment) => environment.config.consumer === "client",
|
|
456
|
+
resolveId(source) {
|
|
457
|
+
if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;
|
|
458
|
+
return null;
|
|
459
|
+
},
|
|
460
|
+
load(id) {
|
|
461
|
+
if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;
|
|
462
|
+
return null;
|
|
463
|
+
},
|
|
464
|
+
transform(code, id) {
|
|
465
|
+
if (normalizeModuleId(id) !== hydrationEntryId) return null;
|
|
466
|
+
return {
|
|
467
|
+
code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\n${code}`,
|
|
468
|
+
map: null
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}, ...viteReact()];
|
|
472
|
+
}
|
|
473
|
+
async createViteServer(fastify, paths) {
|
|
474
|
+
const { createServer, buildErrorMessage, searchForWorkspaceRoot } = await import("vite");
|
|
475
|
+
const workspaceRoot = searchForWorkspaceRoot(paths.appRoot);
|
|
476
|
+
return createServer({
|
|
477
|
+
root: paths.appRoot,
|
|
478
|
+
appType: "custom",
|
|
479
|
+
plugins: [
|
|
480
|
+
devErrorTransportPlugin({
|
|
481
|
+
isProductionRuntime,
|
|
482
|
+
buildErrorMessage
|
|
483
|
+
}),
|
|
484
|
+
...warlockClientBoundary({ appRoot: paths.appRoot }),
|
|
485
|
+
...await this.resolveReactFastRefreshPlugins(paths),
|
|
486
|
+
...this.options.plugins ?? []
|
|
487
|
+
],
|
|
488
|
+
server: {
|
|
489
|
+
middlewareMode: true,
|
|
490
|
+
hmr: { server: fastify.server },
|
|
491
|
+
fs: { allow: [...new Set([
|
|
492
|
+
...withRealPath(workspaceRoot),
|
|
493
|
+
...withRealPath(paths.appRoot),
|
|
494
|
+
...withRealPath(paths.webRoot)
|
|
495
|
+
])] }
|
|
496
|
+
},
|
|
497
|
+
esbuild: {
|
|
498
|
+
target: "es2022",
|
|
499
|
+
jsx: "automatic"
|
|
500
|
+
},
|
|
501
|
+
ssr: { external: [
|
|
502
|
+
...CORE_OPTIONAL_PEERS,
|
|
503
|
+
...WEB_OPTIONAL_PEERS,
|
|
504
|
+
...this.options.ssrExternal ?? []
|
|
505
|
+
] },
|
|
506
|
+
resolve: {
|
|
507
|
+
dedupe: ["react", "react-dom"],
|
|
508
|
+
alias: [...this.options.resolveAlias ?? [], ...appConventionAliases(paths.appSrcRoot)]
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
//#endregion
|
|
515
|
+
export { WebConnector };
|
|
516
|
+
//# sourceMappingURL=web-connector.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-connector.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector.ts"],"sourcesContent":["/**\n * `WebConnector` — the SSR page surface as a first-class Warlock connector.\n *\n * It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React\n * pages on one port, and `web` no longer owns a private copy of the HTTP\n * lifecycle. Everything this file does used to live in `startDevServer()`\n * (`web/src/server/dev-server.ts`), which created its own Fastify instance,\n * scanned the router and called `listen()` itself — three responsibilities core\n * already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.\n *\n * WHY A `Late` CONNECTOR IS THE RIGHT SEAM, in ordering terms:\n * `ConnectorsManager.startPhase` runs **every** `boot()` in a phase before\n * **any** `start()` (`core/src/connectors/connectors-manager.ts:87-93`).\n * `HttpConnector` is itself `Late` (`core/src/connectors/http-connector.ts:41`)\n * and publishes its Fastify instance during its own `boot()`\n * (`container.set(\"http.server\", …)`, `core/src/connectors/http-connector.ts:74`).\n * So by the time this connector's `boot()` runs, Fastify and its plugins exist,\n * the raw node server exists, and NOTHING has been scanned or bound yet — page\n * routes registered here are picked up by `HttpConnector.start()`'s\n * `router.scanDevServer(…)` (`core/src/connectors/http-connector.ts:133`) before\n * `listen()` (`:147`). `SocketConnector.boot()` reads the same container key the\n * same way (`core/src/connectors/socket-connector.ts:78-80`) — this file is\n * deliberately shaped after it.\n *\n * What it can NOT do, and why that is fine: route COLLECTION happens earlier\n * (`core/src/dev-server/development-server.ts:57` precedes `:66`), so pages are\n * not discovered by the framework's file scanner. They are discovered here, by\n * `installPageRoutes` (`./install-page-routes.ts:189`), and registered through\n * the ordinary `router.get(…)` API — there is no second server matcher.\n *\n * DELIBERATE EXCEPTION to A.3 §2 (\"web has no core dependency\"), the same one\n * `./dev-server.ts` and `./install-page-routes.ts` record in their own headers:\n * this module is not re-exported from any package barrel (`web/src/index.ts`,\n * `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of\n * `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only\n * ever imported by tooling that already depends on core.\n *\n * `@warlock.js/web/connector` reaches this class ONLY through\n * `./web-connector-factory.ts`'s `await import(\"./web-connector\")` — a\n * deliberate seam, because a static edge from that barrel to this file would\n * put `../vite`, core's router and `./dev-server` into the import graph of\n * every consuming app's `warlock.config.ts`.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { FastifyReply, FastifyRequest, HookHandlerDoneFunction } from \"fastify\";\nimport type { Alias, Plugin, PluginOption, ViteDevServer } from \"vite\";\nimport { Application, BaseConnector, ConnectorLifecyclePhase, type ConnectorName, container, type FastifyInstance, requestContext, router } from \"@warlock.js/core\";\nimport { resolveWebPackageRoot } from \"../build/contribution\";\nimport { appConventionAliases } from \"../vite/app-convention-aliases\";\nimport { createHydrationClientEntry, warlockClientBoundary } from \"../vite\";\nimport { CLIENT_ASSET_URL_PREFIX } from \"./client-asset-url-prefix\";\nimport { applyBufferedCookie, 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 { consumePageManifest, type PageManifest } from \"./page-manifest\";\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\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 pages 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 but no `clientDir`, so the connector cannot \" +\n \"locate the client bundle. 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. Page, layout and component edits are Vite's HMR to own — a\n * connector restart would tear down the module graph Vite is keeping warm.\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 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 this.installedPages = await installProductionPageRoutes({\n router,\n manifest: this.pageManifest,\n pageContext: requestContext,\n sharedStore: () => requestContext.getStore(),\n applyBufferedCookie,\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 `emit` hook is:\n // a zero-page build produces NO client bundle, so there is no directory\n // to mount and no `clientDir` baked into the manifest to name one. The\n // two halves skip on the same condition, so neither can expect an\n // artifact the other did not make.\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 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 this.installedPages = await 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 applyBufferedCookie,\n });\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 pages but no `clientDir` — i.e. a\n // 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.pageManifest = undefined;\n\n this.active = false;\n }\n\n /**\n * Never restart on a file change. Pages, layouts and components are Vite's\n * HMR domain; rebooting this connector would drop Vite's module graph and\n * re-register every page route on a Fastify instance that is already serving.\n */\n public shouldRestart(): boolean {\n return false;\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({ appRoot: paths.appRoot }),\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 ssr: {\n external: [\n ...CORE_OPTIONAL_PEERS,\n ...WEB_OPTIONAL_PEERS,\n ...(this.options.ssrExternal ?? []),\n ],\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
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SharedContext } from "./index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/shared.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The WRITABLE per-request payload — middleware's half of the contract.
|
|
6
|
+
*
|
|
7
|
+
* Every `shared.x = …` at a call site reads like a global write; it is not —
|
|
8
|
+
* each trap below resolves the CURRENT request's target through the connected
|
|
9
|
+
* store resolver, on every single access — two concurrent requests writing
|
|
10
|
+
* `shared.locale` write to two different objects.
|
|
11
|
+
*/
|
|
12
|
+
declare const shared: SharedContext;
|
|
13
|
+
/**
|
|
14
|
+
* The READ half for components at depth. On the server it preserves the live
|
|
15
|
+
* ALS proxy behavior. In the browser it returns the exact recursively frozen
|
|
16
|
+
* object installed from the validated hydration payload, never an empty or
|
|
17
|
+
* process-wide fallback.
|
|
18
|
+
*/
|
|
19
|
+
declare function useShared(): Readonly<SharedContext>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { shared, useShared };
|
|
22
|
+
//# sourceMappingURL=shared.d.mts.map
|