@warlock.js/web 5.4.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/esm/build/contribution.d.mts +2 -2
  3. package/esm/build/contribution.mjs.map +1 -1
  4. package/esm/build/discover-pages.mjs +127 -205
  5. package/esm/build/discover-pages.mjs.map +1 -1
  6. package/esm/build/generate-pages-barrel.mjs +5 -3
  7. package/esm/build/generate-pages-barrel.mjs.map +1 -1
  8. package/esm/build/public-files.mjs +29 -1
  9. package/esm/build/public-files.mjs.map +1 -1
  10. package/esm/build/read-metadata-keys.mjs +225 -0
  11. package/esm/build/read-metadata-keys.mjs.map +1 -0
  12. package/esm/build/read-route-exports.mjs.map +1 -1
  13. package/esm/client/build-hydrated-tree.mjs +1 -1
  14. package/esm/client/build-hydrated-tree.mjs.map +1 -1
  15. package/esm/client/navigation/document-metadata.mjs +108 -0
  16. package/esm/client/navigation/document-metadata.mjs.map +1 -0
  17. package/esm/client/navigation/navigation-root.mjs +23 -103
  18. package/esm/client/navigation/navigation-root.mjs.map +1 -1
  19. package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
  20. package/esm/client/navigation/use-query-string.d.mts +35 -0
  21. package/esm/client/navigation/use-query-string.mjs +68 -0
  22. package/esm/client/navigation/use-query-string.mjs.map +1 -0
  23. package/esm/client/runtime/index.d.mts +1 -1
  24. package/esm/client/runtime/index.mjs +1 -1
  25. package/esm/client/runtime/manifest.mjs.map +1 -1
  26. package/esm/client/runtime/matcher.mjs.map +1 -1
  27. package/esm/client/runtime/types.d.mts +1 -1
  28. package/esm/components/link.mjs.map +1 -1
  29. package/esm/core/src/router/normalize-route-path.mjs +33 -0
  30. package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
  31. package/esm/{hydration → entry}/index.mjs +1 -1
  32. package/esm/entry/index.mjs.map +1 -0
  33. package/esm/hydration-payload.mjs.map +1 -1
  34. package/esm/index.d.mts +3 -2
  35. package/esm/index.mjs +3 -2
  36. package/esm/loaders.d.mts +10 -3
  37. package/esm/localization.mjs.map +1 -1
  38. package/esm/metadata.d.mts +4 -4
  39. package/esm/metadata.mjs.map +1 -1
  40. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
  41. package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
  42. package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
  43. package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
  44. package/esm/register-modules.mjs.map +1 -0
  45. package/esm/route.d.mts +35 -2
  46. package/esm/routing/duplicate-route-path.mjs +22 -0
  47. package/esm/routing/duplicate-route-path.mjs.map +1 -0
  48. package/esm/routing/layout-level.mjs +50 -0
  49. package/esm/routing/layout-level.mjs.map +1 -0
  50. package/esm/routing/layout-policy.mjs.map +1 -1
  51. package/esm/routing/query-string.d.mts +23 -1
  52. package/esm/routing/query-string.mjs +23 -1
  53. package/esm/routing/query-string.mjs.map +1 -1
  54. package/esm/routing/route-identity.mjs +27 -2
  55. package/esm/routing/route-identity.mjs.map +1 -1
  56. package/esm/routing/route-table.mjs.map +1 -1
  57. package/esm/routing/router-events.d.mts +5 -4
  58. package/esm/routing/router-events.mjs.map +1 -1
  59. package/esm/server/create-page-route-handler.d.mts +2 -2
  60. package/esm/server/create-page-route-handler.mjs +6 -6
  61. package/esm/server/create-page-route-handler.mjs.map +1 -1
  62. package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
  63. package/esm/server/dev-error-transport.mjs.map +1 -0
  64. package/esm/server/execute-page-request.mjs +87 -23
  65. package/esm/server/execute-page-request.mjs.map +1 -1
  66. package/esm/server/execute-page-request.types.d.mts +10 -1
  67. package/esm/server/fold-layout-loaders.mjs +36 -0
  68. package/esm/server/fold-layout-loaders.mjs.map +1 -0
  69. package/esm/server/hydration-client-url.mjs +2 -2
  70. package/esm/server/hydration-client-url.mjs.map +1 -1
  71. package/esm/server/index.d.mts +2 -2
  72. package/esm/server/index.mjs +2 -2
  73. package/esm/server/install-page-routes-from-manifest.d.mts +1 -1
  74. package/esm/server/install-page-routes-from-manifest.mjs +43 -40
  75. package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
  76. package/esm/server/install-page-routes.d.mts +22 -2
  77. package/esm/server/install-page-routes.mjs +108 -42
  78. package/esm/server/install-page-routes.mjs.map +1 -1
  79. package/esm/server/install-production-page-routes.mjs.map +1 -1
  80. package/esm/server/match-page-route.mjs.map +1 -1
  81. package/esm/server/page-file-change.mjs.map +1 -1
  82. package/esm/server/page-route-reload.mjs +4 -3
  83. package/esm/server/page-route-reload.mjs.map +1 -1
  84. package/esm/server/register-production-public-files.mjs +25 -1
  85. package/esm/server/register-production-public-files.mjs.map +1 -1
  86. package/esm/server/render-page.d.mts +11 -48
  87. package/esm/server/render-page.mjs +13 -55
  88. package/esm/server/render-page.mjs.map +1 -1
  89. package/esm/server/resolve-page-metadata.mjs.map +1 -1
  90. package/esm/server/resolve-route-validation-input.mjs +11 -0
  91. package/esm/server/resolve-route-validation-input.mjs.map +1 -0
  92. package/esm/server/route-validation-error.mjs +32 -0
  93. package/esm/server/route-validation-error.mjs.map +1 -0
  94. package/esm/server/settle-page-response.mjs +5 -3
  95. package/esm/server/settle-page-response.mjs.map +1 -1
  96. package/esm/server/unregistered-pages.mjs +2 -1
  97. package/esm/server/unregistered-pages.mjs.map +1 -1
  98. package/esm/server/web-connector-factory.mjs +1 -1
  99. package/esm/server/web-connector-factory.mjs.map +1 -1
  100. package/esm/server/web-connector.mjs +24 -321
  101. package/esm/server/web-connector.mjs.map +1 -1
  102. package/esm/shared/module-key.mjs +15 -0
  103. package/esm/shared/module-key.mjs.map +1 -0
  104. package/esm/shared/to-posix.mjs +13 -0
  105. package/esm/shared/to-posix.mjs.map +1 -0
  106. package/esm/shared.mjs +24 -0
  107. package/esm/shared.mjs.map +1 -1
  108. package/esm/vite/build-client.mjs.map +1 -1
  109. package/esm/vite/dev-server-config.mjs +240 -0
  110. package/esm/vite/dev-server-config.mjs.map +1 -0
  111. package/esm/vite/gate-a-resolve.d.mts +5 -4
  112. package/esm/vite/gate-a-resolve.mjs +31 -70
  113. package/esm/vite/gate-a-resolve.mjs.map +1 -1
  114. package/esm/vite/gate-b-secrets.mjs.map +1 -1
  115. package/esm/vite/gate-c-verify.d.mts +1 -1
  116. package/esm/vite/gate-c-verify.mjs +1 -1
  117. package/esm/vite/gate-c-verify.mjs.map +1 -1
  118. package/esm/vite/hydration-entries.mjs +5 -7
  119. package/esm/vite/hydration-entries.mjs.map +1 -1
  120. package/esm/vite/index.d.mts +1 -1
  121. package/esm/vite/index.mjs +4 -110
  122. package/esm/vite/index.mjs.map +1 -1
  123. package/esm/vite/page-registry-plugin.mjs +9 -26
  124. package/esm/vite/page-registry-plugin.mjs.map +1 -1
  125. package/esm/vite/projection.mjs.map +1 -1
  126. package/esm/vite/react-refresh-preamble.mjs +90 -0
  127. package/esm/vite/react-refresh-preamble.mjs.map +1 -0
  128. package/esm/vite/ssr-client-view.mjs +135 -0
  129. package/esm/vite/ssr-client-view.mjs.map +1 -0
  130. package/llms-full.txt +564 -485
  131. package/llms.txt +2 -2
  132. package/package.json +6 -6
  133. package/skills/add-web-to-an-app/SKILL.md +3 -3
  134. package/skills/create-a-page/SKILL.md +348 -295
  135. package/skills/navigate-on-the-client/SKILL.md +212 -189
  136. package/esm/hydration/index.mjs.map +0 -1
  137. package/esm/runtime/register-modules.mjs.map +0 -1
  138. package/esm/server/dev-server.mjs.map +0 -1
  139. /package/esm/{hydration → entry}/index.d.mts +0 -0
@@ -5,9 +5,9 @@ import { createHydrationClientEntry } from "../vite/hydration-entries.mjs";
5
5
  import { resolveHydrationClientUrl } from "./hydration-client-url.mjs";
6
6
  import { WEB_CONNECTOR_PRIORITY } from "./web-connector-factory.mjs";
7
7
  import { invalidateClientPageRegistry } from "../vite/page-registry-plugin.mjs";
8
- import { warlockClientBoundary } from "../vite/index.mjs";
9
- import { appConventionAliases } from "../vite/app-convention-aliases.mjs";
10
- import { devErrorTransportPlugin, sendCapturedDevError } from "./dev-server.mjs";
8
+ import "../vite/index.mjs";
9
+ import { createWebConnectorViteConfig } from "../vite/dev-server-config.mjs";
10
+ import { devErrorTransportPlugin, sendCapturedDevError } from "./dev-error-transport.mjs";
11
11
  import { installProductionPageRoutes } from "./install-production-page-routes.mjs";
12
12
  import { classifyPageFileChanges, hasPageFileChanges } from "./page-file-change.mjs";
13
13
  import { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles } from "./page-route-reload.mjs";
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
25
25
  * It runs BESIDE `HttpConnector`, never instead of it: `warlock dev` alone now boots the API *and* serves React
26
26
  * pages on one port, and `web` no longer owns a private copy of the HTTP
27
27
  * lifecycle. Everything this file does used to live in `startDevServer()`
28
- * (`web/src/server/dev-server.ts`), which created its own Fastify instance,
28
+ * (`web/src/server/dev-error-transport.ts`), which created its own Fastify instance,
29
29
  * scanned the router and called `listen()` itself — three responsibilities core
30
30
  * already owns at `core/src/connectors/http-connector.ts:72`, `:133` and `:147`.
31
31
  *
@@ -50,7 +50,7 @@ import { fileURLToPath } from "node:url";
50
50
  * the ordinary `router.get(…)` API — there is no second server matcher.
51
51
  *
52
52
  * DELIBERATE EXCEPTION to A.3 §2 ("web has no core dependency"), the same one
53
- * `./dev-server.ts` and `./install-page-routes.ts` record in their own headers:
53
+ * `./dev-error-transport.ts` and `./install-page-routes.ts` record in their own headers:
54
54
  * this module is not re-exported from any package barrel (`web/src/index.ts`,
55
55
  * `web/src/server/index.ts`, `web/src/connector/index.ts`) and is not part of
56
56
  * `web/package.json`'s dependency graph. It is dev/CLI bootstrap code, only
@@ -59,7 +59,7 @@ import { fileURLToPath } from "node:url";
59
59
  * `@warlock.js/web/connector` reaches this class ONLY through
60
60
  * `./web-connector-factory.ts`'s `await import("./web-connector")` — a
61
61
  * deliberate seam, because a static edge from that barrel to this file would
62
- * put `../vite`, core's router and `./dev-server` into the import graph of
62
+ * put `../vite`, core's router and `./dev-error-transport` into the import graph of
63
63
  * every consuming app's `warlock.config.ts`.
64
64
  */
65
65
  /**
@@ -133,58 +133,6 @@ var WebClientDirMissingError = class extends Error {
133
133
  }
134
134
  };
135
135
  /**
136
- * Third-party packages core reaches through `await import(...)` and that must
137
- * therefore never enter Vite's SSR transform graph.
138
- *
139
- * Left un-externalized, Vite's SSR module runner tries to resolve them anyway
140
- * and jams: the failure is NOT a missing-module error but a `transport invoke
141
- * timed out` on whatever unrelated module happened to be in flight. Derived in
142
- * one pass from `peerDependenciesMeta.optional` across every workspace package
143
- * reachable from `core/src/index.ts`, and carried over verbatim from
144
- * `dev-server.ts`'s own list. Only THIRD-PARTY peers belong here — every
145
- * `@warlock.js/*` sibling must stay in Vite's graph.
146
- *
147
- * This list is core's peer list, not web's; publishing it from core instead
148
- * of duplicating it here is still outstanding.
149
- */
150
- const CORE_OPTIONAL_PEERS = [
151
- "nodemailer",
152
- "@aws-sdk/client-sesv2",
153
- "@react-email/render",
154
- "redis",
155
- "pg",
156
- "mongodb",
157
- "@sentry/node",
158
- "sharp",
159
- "socket.io",
160
- "@aws-sdk/client-s3",
161
- "@aws-sdk/lib-storage",
162
- "@aws-sdk/s3-request-presigner",
163
- "amqplib",
164
- "langfuse",
165
- "openai",
166
- "pdf-parse"
167
- ];
168
- /**
169
- * `web`'s OWN optional peers, kept separate from {@link CORE_OPTIONAL_PEERS}
170
- * because that list is core's and is documented as such.
171
- *
172
- * Same rule, same reason: anything this package reaches through
173
- * `await import(...)` must be external to every bundler and SSR pipeline, or
174
- * some pipeline will try to resolve it in an install that legitimately does not
175
- * have it. `vite` itself is the precedent — `createViteServer` has always
176
- * loaded it this way — and `@vitejs/plugin-react` is loaded from the same
177
- * function, for the same dev-only reason, so it belongs in the same set.
178
- */
179
- const WEB_OPTIONAL_PEERS = ["vite", "@vitejs/plugin-react"];
180
- /**
181
- * Public specifier for the React Fast Refresh preamble module this connector
182
- * injects. See {@link WebConnector.resolveReactFastRefreshPlugins}.
183
- */
184
- const REACT_REFRESH_PREAMBLE_ID = "virtual:warlock/react-refresh-preamble";
185
- /** The `\0`-prefixed form Rollup uses to mark a module as not-a-file. */
186
- const RESOLVED_REACT_REFRESH_PREAMBLE_ID = `\0${REACT_REFRESH_PREAMBLE_ID}`;
187
- /**
188
136
  * `@fastify/static`'s `maxAge` is milliseconds (the `send` package's option,
189
137
  * not seconds like `Cache-Control`'s own `max-age`) — one year, matching the
190
138
  * header this produces: `public, max-age=31536000, immutable`. Safe forever
@@ -207,28 +155,6 @@ function productionAssetsDirectoryOptions(clientDir) {
207
155
  };
208
156
  }
209
157
  /**
210
- * A directory, plus the path the filesystem really stores it at when the two
211
- * differ. Both forms belong in `server.fs.allow` — see the `fs` block in
212
- * {@link WebConnector.createViteServer} for why one of them is never enough.
213
- *
214
- * A missing directory is not this function's problem to report: the roots it is
215
- * handed are already proven (`resolveWebPackageRoot`) or are the app's own cwd,
216
- * and an allow-list entry that points nowhere simply matches nothing.
217
- */
218
- function withRealPath(directory) {
219
- try {
220
- const realPath = fs.realpathSync(directory);
221
- return realPath === directory ? [directory] : [directory, realPath];
222
- } catch {
223
- return [directory];
224
- }
225
- }
226
- /** POSIX-normalised, case-folded — Vite ids are `/`-separated on Windows too. */
227
- function normalizeModuleId(id) {
228
- const [filepath] = id.split("?");
229
- return filepath.replace(/\\/g, "/").toLowerCase();
230
- }
231
- /**
232
158
  * Web Connector
233
159
  * Manages the Vite dev server and the SSR page routes, mounted on the HTTP
234
160
  * connector's Fastify instance.
@@ -291,7 +217,7 @@ var WebConnector = class extends BaseConnector {
291
217
  this.pageManifest = consumePageManifest();
292
218
  if (isProductionRuntime() && !this.pageManifest) throw new WebPageManifestMissingError();
293
219
  if (isProductionRuntime() && this.pageManifest) {
294
- if ((this.pageManifest.publicFiles?.length ?? 0) > 0) registerProductionPublicFiles(router, this.resolveClientDir(), this.pageManifest.publicFiles ?? []);
220
+ if (this.pageManifest.clientDir !== void 0) registerProductionPublicFiles(router, this.resolveClientDir(), this.pageManifest.publicFiles ?? []);
295
221
  this.installedPages = await installProductionPageRoutes({
296
222
  router,
297
223
  manifest: this.pageManifest,
@@ -335,7 +261,8 @@ var WebConnector = class extends BaseConnector {
335
261
  appSrcRoot: paths.appSrcRoot,
336
262
  appFile: paths.appFile,
337
263
  hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),
338
- stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile)
264
+ stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile),
265
+ httpServer: fastify
339
266
  });
340
267
  this.installedPages = await this.installDevPageRoutes();
341
268
  }
@@ -523,248 +450,24 @@ var WebConnector = class extends BaseConnector {
523
450
  webServerBarrel: path.join(path.dirname(selfPath), `index${path.extname(selfPath)}`)
524
451
  };
525
452
  }
526
- /**
527
- * Vite in middleware mode, `appType: "custom"` — Warlock owns the response
528
- * shape and Vite never fronts the server.
529
- *
530
- * `server.hmr.server` is handed the RAW node server (`fastify.server`), so the
531
- * HMR websocket shares the one port the app already listens on. No `hmr.port`
532
- * and no `clientPort`: Vite's HMR path stays the default `"/"`, socket.io
533
- * stays on `"/socket.io"` (`core/src/connectors/socket-connector.ts:90`), and
534
- * the two `upgrade` listeners coexist because each is a selective filter that
535
- * leaves a non-matching socket alone — verified empirically in both attachment
536
- * orders.
537
- */
538
- /**
539
- * React Fast Refresh, or nothing at all — never half of it.
540
- *
541
- * TWO plugins, and the second one is not optional. `@vitejs/plugin-react`
542
- * emits, into every client component module, a wrapper that reads
543
- * `window.$RefreshReg$` and THROWS "can't detect preamble" if it is missing.
544
- * The preamble that defines it normally arrives through Vite's
545
- * `transformIndexHtml`, and this pipeline has no HTML for Vite to transform:
546
- * the document is React's SSR output and the only script appended to it is
547
- * the hydration entry (`./create-page-route-handler.ts:95`). So the preamble
548
- * is delivered as a virtual module that the hydration entry imports FIRST.
549
- *
550
- * "First" is load-bearing, twice over. ESM evaluates a module's imports in
551
- * source order before the importer's own body, so an import placed at
552
- * position 0 runs before `virtual:warlock/pages` pulls in a single component
553
- * — which is what `$RefreshReg$` has to exist for — and before `react-dom`
554
- * initialises, which is what `injectIntoGlobalHook` has to precede.
555
- *
556
- * The preamble TEXT is read from the plugin's own `preambleCode` export
557
- * rather than copied here, so a version bump cannot leave this file holding
558
- * a stale runtime contract. If that export ever stops being a string, Fast
559
- * Refresh is DECLINED entirely (empty array) instead of registered without
560
- * its preamble — a loud "no HMR" beats a component graph that throws on
561
- * first paint.
562
- *
563
- * Dev-only by construction, like everything else this method is called from:
564
- * `apply: "serve"` on our own plugin, `apply: "serve"` on the plugin's
565
- * refresh half, and `skipFastRefresh` on `config.isProduction` inside it. The
566
- * PRODUCTION client bundle cannot reach any of this — it is built by
567
- * `buildWarlockHydrationClient` (`../vite/index.ts:185`), which composes
568
- * `warlockClientBoundary()` and nothing else, and never calls this method.
569
- *
570
- * @param paths resolved by {@link WebConnector.resolvePaths}
571
- */
572
- async resolveReactFastRefreshPlugins(paths) {
573
- let viteReact;
574
- try {
575
- ({default: viteReact} = await import("@vitejs/plugin-react"));
576
- } catch {
577
- 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.");
578
- return [];
579
- }
580
- const preambleCode = viteReact.preambleCode;
581
- if (typeof preambleCode !== "string") {
582
- 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.");
583
- return [];
584
- }
585
- const preambleSource = preambleCode.replace("__BASE__", "/");
586
- const hydrationEntryId = normalizeModuleId(createHydrationClientEntry(paths.webRoot).sourcePath);
587
- return [{
588
- name: "warlock:react-refresh-preamble",
589
- enforce: "pre",
590
- apply: "serve",
591
- applyToEnvironment: (environment) => environment.config.consumer === "client",
592
- resolveId(source) {
593
- if (source === REACT_REFRESH_PREAMBLE_ID) return RESOLVED_REACT_REFRESH_PREAMBLE_ID;
594
- return null;
595
- },
596
- load(id) {
597
- if (id === RESOLVED_REACT_REFRESH_PREAMBLE_ID) return preambleSource;
598
- return null;
599
- },
600
- transform(code, id) {
601
- if (normalizeModuleId(id) !== hydrationEntryId) return null;
602
- return {
603
- code: `import ${JSON.stringify(REACT_REFRESH_PREAMBLE_ID)};\n${code}`,
604
- map: null
605
- };
606
- }
607
- }, ...viteReact()];
608
- }
609
453
  async createViteServer(fastify, paths) {
610
454
  const { createServer, buildErrorMessage, searchForWorkspaceRoot } = await import("vite");
611
455
  const workspaceRoot = searchForWorkspaceRoot(paths.appRoot);
612
- return createServer({
613
- root: paths.appRoot,
614
- appType: "custom",
615
- plugins: [
616
- devErrorTransportPlugin({
617
- isProductionRuntime,
618
- buildErrorMessage
619
- }),
620
- ...warlockClientBoundary({
621
- appRoot: paths.appRoot,
622
- beforePageHotUpdate: ({ file }) => this.handlePageHotUpdate(file)
623
- }),
624
- ...await this.resolveReactFastRefreshPlugins(paths),
625
- ...this.options.plugins ?? []
626
- ],
627
- server: {
628
- middlewareMode: true,
629
- hmr: { server: fastify.server },
630
- fs: { allow: [...new Set([
631
- ...withRealPath(workspaceRoot),
632
- ...withRealPath(paths.appRoot),
633
- ...withRealPath(paths.webRoot)
634
- ])] }
635
- },
636
- esbuild: {
637
- target: "es2022",
638
- jsx: "automatic"
639
- },
640
- /**
641
- * REACT MUST BE PRE-BUNDLED, and naming it here is the only thing that
642
- * makes that happen for the framework's own client graph.
643
- *
644
- * `react-dom/client` is CommonJS. A browser cannot import a named export
645
- * from it, so Vite's dep optimizer normally rewrites it into an ESM shim
646
- * under `/node_modules/.vite/deps/`. Whether that rewrite happens is
647
- * decided in `tryNodeResolve`, and one of the conditions that SKIPS it is
648
- * `importer && isInNodeModules(importer)`
649
- * (`vite/dist/node/chunks/config.js:32822`). Vite's reasoning is sound in
650
- * general — a dependency's own internal imports are the optimizer's job,
651
- * not the resolver's — but it is exactly wrong here:
652
- *
653
- * `<Scripts />` points the browser at the hydration entry, and in an
654
- * INSTALLED app that entry is
655
- * `<app>/node_modules/@warlock.js/web/esm/hydration/index.mjs`.
656
- * Every module it reaches is therefore inside `node_modules`, so every
657
- * bare import it makes takes the skip branch and is served as the raw
658
- * file with a `?v=<browserHash>` cache key bolted on.
659
- *
660
- * For `esm/**.mjs` that is harmless — they are already ESM. For
661
- * `react-dom/client` it is fatal, and it is the whole defect: the browser
662
- * receives `"use strict"; function checkDCE()…` and refuses the module
663
- * with
664
- *
665
- * SyntaxError: The requested module '/node_modules/react-dom/client.js?v=…'
666
- * does not provide an export named 'hydrateRoot'
667
- *
668
- * Nothing in the client runtime then runs at all: no hydration, so no
669
- * `useState`, no Fast Refresh, and `<Link>` degrades to a full document
670
- * load because the navigation listener was never attached. Measured on a
671
- * published 5.0.2 install (fresh app, no symlinks): all four symptoms
672
- * before this block, none after.
673
- *
674
- * `include` is the fix rather than `entries` because it does not depend
675
- * on the scanner reaching the entry: an included id is pre-bundled at
676
- * server start and `tryOptimizedResolve` matches it by NAME
677
- * (`config.js:32633`) before `tryNodeResolve` — and that lookup has no
678
- * importer condition, so a node_modules importer resolves to the shim
679
- * like anyone else. Pointing `optimizeDeps.entries` at the hydration
680
- * entry would not work: the scanner classifies a resolution inside
681
- * `node_modules` as a dependency to externalize rather than a source to
682
- * walk.
683
- *
684
- * All four names are listed even though `react` and `react/jsx-runtime`
685
- * usually get discovered anyway — they are discovered only because the
686
- * APP's own pages import them, which is a fact about the app and not
687
- * something the framework may rely on. `react-dom/client` is imported by
688
- * `client/hydrate-page.tsx` and by nothing a normal app writes, which is
689
- * why it was the one that broke.
690
- *
691
- * This is INVISIBLE from the monorepo checkout: there the hydration entry
692
- * resolves to `web/src/hydration/index.ts`, a path with no `node_modules`
693
- * segment, so the skip branch never fires and React optimizes normally.
694
- * Canon: nothing measured inside the checkout is evidence about a
695
- * published install.
696
- */
697
- optimizeDeps: {
698
- include: [
699
- "react",
700
- "react-dom",
701
- "react-dom/client",
702
- "react/jsx-runtime"
703
- ],
704
- /**
705
- * The SECOND instance problem, and the reason this is `exclude` and not
706
- * another `include`.
707
- *
708
- * An app page's `import { Link } from "@warlock.js/web"` has an importer
709
- * OUTSIDE `node_modules`, so it takes the opposite branch above and the
710
- * whole package is pre-bundled into
711
- * `/node_modules/.vite/deps/@warlock__js_web.js`. The hydration entry
712
- * cannot reach that bundle — it is loaded by absolute path through
713
- * `/@fs/` and its imports are relative, so they resolve to the raw
714
- * `esm/*.mjs` files. The browser then holds TWO copies of every client
715
- * module in this package: the app renders `<Link>` from the bundled one
716
- * while the hydration entry publishes routes and mounts the document
717
- * context on the raw one. Module-level state does not cross that line.
718
- *
719
- * `routing/route-table` survives it by accident — it keeps its table in
720
- * a `Symbol.for` slot on `globalThis`, which is realm-wide — but
721
- * `components/document-context` is a React context OBJECT, and two
722
- * `createContext()` calls are two different contexts no provider can
723
- * bridge.
724
- *
725
- * Excluding the package makes the app's import resolve to
726
- * `esm/index.mjs` with the same `?v=` key the hydration entry's imports
727
- * carry, so there is one instance again. It costs the pre-bundle (a
728
- * handful of extra dev requests for files that are already valid ESM)
729
- * and nothing else — the grep across every client-reachable module in
730
- * this package finds exactly three bare imports, all of them React, all
731
- * of them covered by `include` above.
732
- */
733
- exclude: ["@warlock.js/web"]
734
- },
735
- ssr: {
736
- external: [
737
- ...CORE_OPTIONAL_PEERS,
738
- ...WEB_OPTIONAL_PEERS,
739
- ...this.options.ssrExternal ?? []
740
- ],
741
- /**
742
- * ONE `@warlock.js/web`, for the same reason `resolve.dedupe` below
743
- * insists on one React — and it is invisible from inside this repo.
744
- *
745
- * Vite externalises `node_modules` in SSR by default, so an installed
746
- * app gets TWO instances: the app's own `root.tsx` imports
747
- * `@warlock.js/web` and Vite hands that off to Node, while the pipeline
748
- * is loaded deliberately through `vite.ssrLoadModule(...)` and stays
749
- * inside Vite's graph. `renderPage` then sets the document context on
750
- * Vite's copy of `components/document-context`, and the app's `<Head/>`
751
- * reads Node's copy, which has nothing in it:
752
- *
753
- * <Head/> was rendered outside the page pipeline's document context
754
- *
755
- * Measured on a published 5.0.1 install: `GET /` 500 without this line,
756
- * 200 with it (404 control still 404). In THIS checkout the package
757
- * resolves to source under Vite's root, never through `node_modules`,
758
- * so both paths land on one instance and the bug cannot reproduce.
759
- * Canon `6b7ab838`.
760
- */
761
- noExternal: ["@warlock.js/web"]
762
- },
763
- resolve: {
764
- dedupe: ["react", "react-dom"],
765
- alias: [...this.options.resolveAlias ?? [], ...appConventionAliases(paths.appSrcRoot)]
766
- }
767
- });
456
+ return createServer(await createWebConnectorViteConfig({
457
+ appRoot: paths.appRoot,
458
+ appSrcRoot: paths.appSrcRoot,
459
+ webRoot: paths.webRoot,
460
+ workspaceRoot,
461
+ hmrServer: fastify.server,
462
+ handlePageHotUpdate: (file) => this.handlePageHotUpdate(file),
463
+ leadingPlugins: [devErrorTransportPlugin({
464
+ isProductionRuntime,
465
+ buildErrorMessage
466
+ })],
467
+ resolveAlias: this.options.resolveAlias,
468
+ ssrExternal: this.options.ssrExternal,
469
+ plugins: this.options.plugins
470
+ }));
768
471
  }
769
472
  };
770
473