@warlock.js/web 5.0.2 → 5.2.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 +250 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -4
- package/esm/build/contribution.mjs +46 -22
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +318 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +44 -7
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +15 -1
- package/esm/metadata.mjs +45 -0
- package/esm/metadata.mjs.map +1 -0
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +120 -0
- package/esm/routing/url-fragment.mjs.map +1 -0
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +58 -6
- package/esm/server/create-page-route-handler.mjs +127 -40
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +6 -5
- package/esm/server/index.mjs +4 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +91 -8
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -13
- package/esm/server/install-page-routes.mjs +112 -32
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -31
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +126 -0
- package/esm/server/not-found-page.mjs +157 -0
- package/esm/server/not-found-page.mjs.map +1 -0
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +6 -4
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +233 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +10 -6
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +247 -2
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +241 -49
- package/llms.txt +6 -2
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +130 -7
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +3 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +20 -8
- package/skills/write-the-root/SKILL.md +5 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { matchPath } from "./match-page-route.mjs";
|
|
2
|
+
import { isNotFoundPageFile } from "./not-found-page.mjs";
|
|
3
|
+
import { discoverPages, isDiscoveredRoutablePage, toPosix } from "../build/discover-pages.mjs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region web/src/server/unregistered-pages.ts
|
|
7
|
+
function fileKey(file) {
|
|
8
|
+
const posix = toPosix(file);
|
|
9
|
+
return process.platform === "win32" ? posix.toLowerCase() : posix;
|
|
10
|
+
}
|
|
11
|
+
function findUnregisteredPages(options) {
|
|
12
|
+
const { appRoot, appSrcRoot, registeredPageFiles, discover = discoverPages } = options;
|
|
13
|
+
const registered = new Set(registeredPageFiles().map(fileKey));
|
|
14
|
+
const webRoot = path.resolve(appSrcRoot, "web");
|
|
15
|
+
return discover({
|
|
16
|
+
appRoot,
|
|
17
|
+
srcDir: path.relative(appRoot, appSrcRoot)
|
|
18
|
+
}).filter(isDiscoveredRoutablePage).filter((page) => path.resolve(page.webRoot) === webRoot && !isNotFoundPageFile(page.pageFile) && !registered.has(fileKey(page.pageFile)));
|
|
19
|
+
}
|
|
20
|
+
function describeUnregisteredPages(pageFiles, appRoot, request) {
|
|
21
|
+
const named = pageFiles.map((pageFile) => ` - ${toPosix(path.relative(appRoot, pageFile))}`).join("\n");
|
|
22
|
+
return `[warlock:web] ${request.method} ${request.url} answered 404. These src/web page files exist on disk but are absent from the active route table:
|
|
23
|
+
` + named;
|
|
24
|
+
}
|
|
25
|
+
function createUnregisteredPageReporter(options) {
|
|
26
|
+
const { appRoot, warn = console.warn } = options;
|
|
27
|
+
const reported = /* @__PURE__ */ new Set();
|
|
28
|
+
return (request) => {
|
|
29
|
+
try {
|
|
30
|
+
const unregistered = findUnregisteredPages(options).find((page) => !reported.has(fileKey(page.pageFile)) && matchPath(page.routePath, request.pathname) !== void 0);
|
|
31
|
+
if (unregistered === void 0) return;
|
|
32
|
+
reported.add(fileKey(unregistered.pageFile));
|
|
33
|
+
warn(describeUnregisteredPages([unregistered.pageFile], appRoot, request));
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createUnregisteredPageReporter };
|
|
42
|
+
//# sourceMappingURL=unregistered-pages.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unregistered-pages.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/unregistered-pages.ts"],"sourcesContent":["import path from \"node:path\";\nimport {\n discoverPages,\n isDiscoveredRoutablePage,\n toPosix,\n type DiscoverPagesOptions,\n type DiscoveredPage,\n} from \"../build/discover-pages\";\nimport { matchPath } from \"./match-page-route\";\nimport { isNotFoundPageFile } from \"./not-found-page\";\n\ntype DiscoveredGlobalPage = Extract<DiscoveredPage, { type: \"page\" }>;\ntype DiscoverPages = (options: DiscoverPagesOptions) => readonly DiscoveredPage[];\n\nexport type UnregisteredPagesOptions = {\n appRoot: string;\n appSrcRoot: string;\n registeredPageFiles: () => readonly string[];\n discover?: DiscoverPages;\n warn?: (message: string) => void;\n};\n\nfunction fileKey(file: string): string {\n const posix = toPosix(file);\n\n return process.platform === \"win32\" ? posix.toLowerCase() : posix;\n}\n\nfunction findUnregisteredPages(options: UnregisteredPagesOptions): DiscoveredGlobalPage[] {\n const { appRoot, appSrcRoot, registeredPageFiles, discover = discoverPages } = options;\n const registered = new Set(registeredPageFiles().map(fileKey));\n const webRoot = path.resolve(appSrcRoot, \"web\");\n\n return discover({ appRoot, srcDir: path.relative(appRoot, appSrcRoot) })\n .filter(isDiscoveredRoutablePage)\n .filter(\n (page) =>\n path.resolve(page.webRoot) === webRoot &&\n !isNotFoundPageFile(page.pageFile) &&\n !registered.has(fileKey(page.pageFile)),\n );\n}\n\nexport function findUnregisteredPageFiles(options: UnregisteredPagesOptions): string[] {\n return findUnregisteredPages(options).map((page) => page.pageFile);\n}\n\nexport function describeUnregisteredPages(\n pageFiles: readonly string[],\n appRoot: string,\n request: { method: string; url: string },\n): string {\n const named = pageFiles\n .map((pageFile) => ` - ${toPosix(path.relative(appRoot, pageFile))}`)\n .join(\"\\n\");\n\n return (\n `[warlock:web] ${request.method} ${request.url} answered 404. ` +\n \"These src/web page files exist on disk but are absent from the active route table:\\n\" +\n named\n );\n}\n\nexport function createUnregisteredPageReporter(\n options: UnregisteredPagesOptions,\n): (request: { method: string; url: string; pathname: string }) => void {\n const { appRoot, warn = console.warn } = options;\n const reported = new Set<string>();\n\n return (request) => {\n // `discoverPages()` deliberately refuses malformed pages. Its install/build\n // diagnostic is authoritative, so a response hook must leave this 404 alone.\n try {\n const unregistered = findUnregisteredPages(options).find(\n (page) =>\n !reported.has(fileKey(page.pageFile)) &&\n matchPath(page.routePath, request.pathname) !== undefined,\n );\n\n if (unregistered === undefined) return;\n\n reported.add(fileKey(unregistered.pageFile));\n warn(describeUnregisteredPages([unregistered.pageFile], appRoot, request));\n } catch {\n return;\n }\n };\n}\n"],"mappings":";;;;;;AAsBA,SAAS,QAAQ,MAAsB;CACrC,MAAM,QAAQ,QAAQ,IAAI;CAE1B,OAAO,QAAQ,aAAa,UAAU,MAAM,YAAY,IAAI;AAC9D;AAEA,SAAS,sBAAsB,SAA2D;CACxF,MAAM,EAAE,SAAS,YAAY,qBAAqB,WAAW,kBAAkB;CAC/E,MAAM,aAAa,IAAI,IAAI,oBAAoB,EAAE,IAAI,OAAO,CAAC;CAC7D,MAAM,UAAU,KAAK,QAAQ,YAAY,KAAK;CAE9C,OAAO,SAAS;EAAE;EAAS,QAAQ,KAAK,SAAS,SAAS,UAAU;CAAE,CAAC,EACpE,OAAO,wBAAwB,EAC/B,QACE,SACD,KAAK,QAAQ,KAAK,OAAO,MAAM,WAC/B,CAAC,mBAAmB,KAAK,QAAQ,KACjC,CAAC,WAAW,IAAI,QAAQ,KAAK,QAAQ,CAAC,CACxC;AACJ;AAMA,SAAgB,0BACd,WACA,SACA,SACQ;CACR,MAAM,QAAQ,UACX,KAAK,aAAa,OAAO,QAAQ,KAAK,SAAS,SAAS,QAAQ,CAAC,GAAG,EACpE,KAAK,IAAI;CAEZ,OACE,iBAAiB,QAAQ,OAAO,GAAG,QAAQ,IAAI;IAE/C;AAEJ;AAEA,SAAgB,+BACd,SACsE;CACtE,MAAM,EAAE,SAAS,OAAO,QAAQ,SAAS;CACzC,MAAM,2BAAW,IAAI,IAAY;CAEjC,QAAQ,YAAY;EAGlB,IAAI;GACF,MAAM,eAAe,sBAAsB,OAAO,EAAE,MACjD,SACC,CAAC,SAAS,IAAI,QAAQ,KAAK,QAAQ,CAAC,KACpC,UAAU,KAAK,WAAW,QAAQ,QAAQ,MAAM,MACpD;GAEA,IAAI,iBAAiB,QAAW;GAEhC,SAAS,IAAI,QAAQ,aAAa,QAAQ,CAAC;GAC3C,KAAK,0BAA0B,CAAC,aAAa,QAAQ,GAAG,SAAS,OAAO,CAAC;EAC3E,QAAQ;GACN;EACF;CACF;AACF"}
|
|
@@ -2,12 +2,11 @@ import { WebBuildOptions } from "../build/contribution.mjs";
|
|
|
2
2
|
import { WebConnectorOptions } from "./web-connector.mjs";
|
|
3
3
|
import { Connector } from "@warlock.js/core";
|
|
4
4
|
|
|
5
|
-
//#region
|
|
5
|
+
//#region web/src/server/web-connector-factory.d.ts
|
|
6
6
|
type WebConnectorFactoryOptions = WebConnectorOptions & {
|
|
7
7
|
/**
|
|
8
8
|
* What web contributes to `warlock build`. Passed straight to
|
|
9
|
-
* {@link createWebBuildContribution}; JSON-serializable values only
|
|
10
|
-
* plugin or pipeline instances, so a config load never pulls Vite in.
|
|
9
|
+
* {@link createWebBuildContribution}; JSON-serializable values only.
|
|
11
10
|
*/
|
|
12
11
|
build?: WebBuildOptions;
|
|
13
12
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createWebBuildContribution } from "../build/contribution.mjs";
|
|
2
|
+
import { ConnectorLifecyclePhase } from "@warlock.js/core";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { ConnectorLifecyclePhase } from "@warlock.js/core";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region web/src/server/web-connector-factory.ts
|
|
7
7
|
/**
|
|
8
8
|
* `webConnector()` — the ONE thing `warlock.config.ts` imports from
|
|
9
9
|
* `@warlock.js/web/connector`, and the only value the `./connector` barrel
|
|
@@ -74,10 +74,11 @@ function deriveWebRoot() {
|
|
|
74
74
|
function webConnector(options = {}) {
|
|
75
75
|
const { build: buildOptions, ...connectorOptions } = options;
|
|
76
76
|
const webRoot = connectorOptions.webRoot ?? deriveWebRoot();
|
|
77
|
+
const connectorPlugins = [...connectorOptions.plugins ?? []];
|
|
77
78
|
const build = createWebBuildContribution({
|
|
78
79
|
...buildOptions,
|
|
79
80
|
webRoot: buildOptions?.webRoot ?? webRoot
|
|
80
|
-
});
|
|
81
|
+
}, connectorPlugins);
|
|
81
82
|
let instance;
|
|
82
83
|
/**
|
|
83
84
|
* Load the heavy half on first lifecycle call. `boot()` is always the first
|
|
@@ -89,6 +90,7 @@ function webConnector(options = {}) {
|
|
|
89
90
|
const { WebConnector: WebConnectorClass } = await import("./web-connector.mjs");
|
|
90
91
|
instance = new WebConnectorClass({
|
|
91
92
|
...connectorOptions,
|
|
93
|
+
plugins: connectorPlugins,
|
|
92
94
|
webRoot
|
|
93
95
|
});
|
|
94
96
|
}
|
|
@@ -112,7 +114,7 @@ function webConnector(options = {}) {
|
|
|
112
114
|
shutdown: async () => {
|
|
113
115
|
await instance?.shutdown();
|
|
114
116
|
},
|
|
115
|
-
shouldRestart: () => false
|
|
117
|
+
shouldRestart: (changedFiles) => instance?.shouldRestart(changedFiles) ?? false
|
|
116
118
|
};
|
|
117
119
|
}
|
|
118
120
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["C:/Work/Warlock.js/web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport {\n createWebBuildContribution,\n type WebBuildOptions,\n} from \"../build/contribution\";\nimport type { WebConnector, WebConnectorOptions } from \"./web-connector\";\n\n/**\n * Boot/shutdown position relative to core's own connectors.\n *\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\n * is \"immediately after http, before everything else\".\n *\n * Defined HERE and re-exported by `./web-connector` rather than the other way\n * round: the delegate must publish `priority` synchronously, and reading it\n * from the heavy module would defeat the whole point of the delegate.\n */\nexport const WEB_CONNECTOR_PRIORITY = 5.5;\n\nexport type WebConnectorFactoryOptions = WebConnectorOptions & {\n /**\n * What web contributes to `warlock build`. Passed straight to\n * {@link createWebBuildContribution}; JSON-serializable values only.\n */\n build?: WebBuildOptions;\n};\n\n/**\n * The `@warlock.js/web` package root, derived once from THIS module's location\n * and then passed EXPLICITLY to both halves.\n *\n * `web/src/server/web-connector-factory.ts` → `web/src/server` → `web/src` →\n * `web`; published as `web/esm/server/web-connector-factory.js`, two levels up\n * is the package root under both layouts — the same arithmetic\n * `contribution.ts:136-140` documents.\n *\n * Deriving it here and handing it down means the build contribution never falls\n * back to its own `import.meta.url` guess (`contribution.ts:129-141`): one\n * derivation, one place to be wrong, and `assertWebPackageRoot` verifies it\n * against `<root>/package.json`'s `name` either way.\n */\nfunction deriveWebRoot(): string {\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n}\n\n/**\n * Construct web's connector for `warlock.config.ts > connectors`.\n *\n * @example\n * ```ts\n * export default defineConfig({ connectors: [webConnector()] });\n * ```\n */\nexport function webConnector(\n options: WebConnectorFactoryOptions = {},\n): Connector {\n const { build: buildOptions, ...connectorOptions } = options;\n const webRoot = connectorOptions.webRoot ?? deriveWebRoot();\n // One snapshot feeds both halves. Mutating the caller's array after config\n // construction cannot make dev and `warlock build` observe different lists.\n const connectorPlugins = [...(connectorOptions.plugins ?? [])];\n\n const build: ConnectorBuildContribution = createWebBuildContribution(\n {\n ...buildOptions,\n webRoot: buildOptions?.webRoot ?? webRoot,\n },\n connectorPlugins,\n );\n\n let instance: WebConnector | undefined;\n\n /**\n * Load the heavy half on first lifecycle call. `boot()` is always the first\n * of these to run (`core/src/connectors/connectors-manager.ts:87-93`), so the\n * import lands in a process that has already committed to serving pages.\n */\n const load = async (): Promise<WebConnector> => {\n if (!instance) {\n const { WebConnector: WebConnectorClass } =\n await import(\"./web-connector\");\n\n instance = new WebConnectorClass({\n ...connectorOptions,\n plugins: connectorPlugins,\n webRoot,\n });\n }\n\n return instance;\n };\n\n return {\n name: \"web\" satisfies ConnectorName,\n priority: WEB_CONNECTOR_PRIORITY,\n lifecyclePhase: ConnectorLifecyclePhase.Late,\n build,\n\n // Never loads the heavy half: a connector that was never booted is not\n // active, and answering that must not cost a Vite import.\n isActive: () => instance?.isActive() ?? false,\n\n boot: async () => {\n await (await load()).boot();\n },\n start: async () => {\n await (await load()).start();\n },\n restart: async () => {\n await (await load()).restart();\n },\n\n // Both of these are asked of every registered connector, including ones\n // that never booted — so neither may force the import.\n shutdown: async () => {\n await instance?.shutdown();\n },\n\n // Asked on EVERY watcher batch, so the `instance?.` is load-bearing twice:\n // it keeps the answer free for a connector that never booted, and it is the\n // only reason this delegate can forward the question at all — by the time a\n // watcher batch exists, `boot()` has already loaded the heavy half, so\n // reading the real answer off it costs nothing beyond the call.\n //\n // It used to be a hard `false`, which meant a `*.page.tsx` created while\n // `warlock dev` was running was never noticed: page routes are installed\n // once, in `WebConnector.boot()`, so the new page's URL 404'd in silence.\n // The connector itself now decides: add/remove and route-identity edits are\n // live route-table work; component-body-only edits remain Vite HMR work.\n shouldRestart: (changedFiles: string[]) =>\n instance?.shouldRestart(changedFiles) ?? false,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,yBAAyB;;;;;;;;;;;;;;;AAwBtC,SAAS,gBAAwB;CAC/B,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI;AAC9E;;;;;;;;;AAUA,SAAgB,aACd,UAAsC,CAAC,GAC5B;CACX,MAAM,EAAE,OAAO,cAAc,GAAG,qBAAqB;CACrD,MAAM,UAAU,iBAAiB,WAAW,cAAc;CAG1D,MAAM,mBAAmB,CAAC,GAAI,iBAAiB,WAAW,CAAC,CAAE;CAE7D,MAAM,QAAoC,2BACxC;EACE,GAAG;EACH,SAAS,cAAc,WAAW;CACpC,GACA,gBACF;CAEA,IAAI;;;;;;CAOJ,MAAM,OAAO,YAAmC;EAC9C,IAAI,CAAC,UAAU;GACb,MAAM,EAAE,cAAc,sBACpB,MAAM,OAAO;GAEf,WAAW,IAAI,kBAAkB;IAC/B,GAAG;IACH,SAAS;IACT;GACF,CAAC;EACH;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,UAAU;EACV,gBAAgB,wBAAwB;EACxC;EAIA,gBAAgB,UAAU,SAAS,KAAK;EAExC,MAAM,YAAY;GAChB,OAAO,MAAM,KAAK,GAAG,KAAK;EAC5B;EACA,OAAO,YAAY;GACjB,OAAO,MAAM,KAAK,GAAG,MAAM;EAC7B;EACA,SAAS,YAAY;GACnB,OAAO,MAAM,KAAK,GAAG,QAAQ;EAC/B;EAIA,UAAU,YAAY;GACpB,MAAM,UAAU,SAAS;EAC3B;EAaA,gBAAgB,iBACd,UAAU,cAAc,YAAY,KAAK;CAC7C;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BaseConnector, ConnectorLifecyclePhase } from "@warlock.js/core";
|
|
2
2
|
import { Alias, PluginOption, ViteDevServer } from "vite";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region web/src/server/web-connector.d.ts
|
|
5
5
|
type WebConnectorOptions = {
|
|
6
6
|
/**
|
|
7
7
|
* Vite's `root` — the application directory that owns `src/`, `package.json`
|
|
@@ -4,16 +4,21 @@ import { consumePageManifest } from "./page-manifest.mjs";
|
|
|
4
4
|
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
|
+
import { invalidateClientPageRegistry } from "../vite/page-registry-plugin.mjs";
|
|
7
8
|
import { warlockClientBoundary } from "../vite/index.mjs";
|
|
8
9
|
import { appConventionAliases } from "../vite/app-convention-aliases.mjs";
|
|
9
|
-
import {
|
|
10
|
+
import { devErrorTransportPlugin, sendCapturedDevError } from "./dev-server.mjs";
|
|
10
11
|
import { installProductionPageRoutes } from "./install-production-page-routes.mjs";
|
|
11
|
-
import
|
|
12
|
+
import { classifyPageFileChanges, hasPageFileChanges } from "./page-file-change.mjs";
|
|
13
|
+
import { pageRouteSourceFiles, pageRoutesNeedReplacement, registeredPageFiles } from "./page-route-reload.mjs";
|
|
14
|
+
import { registerProductionPublicFiles } from "./register-production-public-files.mjs";
|
|
15
|
+
import { createUnregisteredPageReporter } from "./unregistered-pages.mjs";
|
|
16
|
+
import { Application, BaseConnector, ConnectorLifecyclePhase, container, requestContext, router } from "@warlock.js/core";
|
|
12
17
|
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
13
19
|
import { fileURLToPath } from "node:url";
|
|
14
|
-
import { Application, BaseConnector, ConnectorLifecyclePhase, container, requestContext, router } from "@warlock.js/core";
|
|
15
20
|
|
|
16
|
-
//#region
|
|
21
|
+
//#region web/src/server/web-connector.ts
|
|
17
22
|
/**
|
|
18
23
|
* `WebConnector` — the SSR page surface as a first-class Warlock connector.
|
|
19
24
|
*
|
|
@@ -89,6 +94,23 @@ import { Application, BaseConnector, ConnectorLifecyclePhase, container, request
|
|
|
89
94
|
function isProductionRuntime() {
|
|
90
95
|
return Application.runtimeStrategy === "production";
|
|
91
96
|
}
|
|
97
|
+
function pageFileVersion(file) {
|
|
98
|
+
try {
|
|
99
|
+
return `present:${fs.readFileSync(file, "utf8")}`;
|
|
100
|
+
} catch {
|
|
101
|
+
return "absent";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function pageChangeVersions(changes) {
|
|
105
|
+
return new Map([
|
|
106
|
+
...changes.added,
|
|
107
|
+
...changes.removed,
|
|
108
|
+
...changes.inspectionNeeded
|
|
109
|
+
].map((file) => {
|
|
110
|
+
const absoluteFile = path.resolve(file);
|
|
111
|
+
return [absoluteFile, pageFileVersion(absoluteFile)];
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
92
114
|
var WebPageManifestMissingError = class extends Error {
|
|
93
115
|
constructor() {
|
|
94
116
|
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.");
|
|
@@ -96,7 +118,7 @@ var WebPageManifestMissingError = class extends Error {
|
|
|
96
118
|
}
|
|
97
119
|
};
|
|
98
120
|
/**
|
|
99
|
-
* The manifest carried
|
|
121
|
+
* The manifest carried browser artifacts but no `clientDir`.
|
|
100
122
|
*
|
|
101
123
|
* The build bakes that field in beside the page table, so the only way to
|
|
102
124
|
* observe this is a VERSION SPLIT: an artifact produced by a `@warlock.js/web`
|
|
@@ -106,7 +128,7 @@ var WebPageManifestMissingError = class extends Error {
|
|
|
106
128
|
*/
|
|
107
129
|
var WebClientDirMissingError = class extends Error {
|
|
108
130
|
constructor() {
|
|
109
|
-
super("The page manifest carries pages but no `clientDir`, so the connector cannot locate the
|
|
131
|
+
super("The page manifest carries pages or public files but no `clientDir`, so the connector cannot locate the browser artifacts. 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
132
|
this.name = "WebClientDirMissingError";
|
|
111
133
|
}
|
|
112
134
|
};
|
|
@@ -194,8 +216,9 @@ var WebConnector = class extends BaseConnector {
|
|
|
194
216
|
priority = WEB_CONNECTOR_PRIORITY;
|
|
195
217
|
lifecyclePhase = ConnectorLifecyclePhase.Late;
|
|
196
218
|
/**
|
|
197
|
-
* Nothing.
|
|
198
|
-
*
|
|
219
|
+
* Nothing. Core already supplies each watcher batch to `shouldRestart`; page
|
|
220
|
+
* membership and route identity are classified there, while component and
|
|
221
|
+
* layout body edits remain Vite's HMR domain.
|
|
199
222
|
*/
|
|
200
223
|
watchedFiles = [];
|
|
201
224
|
options;
|
|
@@ -209,6 +232,21 @@ var WebConnector = class extends BaseConnector {
|
|
|
209
232
|
* production reading is a hard error and why the branch is on MODE.
|
|
210
233
|
*/
|
|
211
234
|
pageManifest;
|
|
235
|
+
/**
|
|
236
|
+
* The paths the DEVELOPMENT boot resolved, kept so `shouldRestart` can decide
|
|
237
|
+
* whether a changed file is a page without re-deriving (and re-proving) the
|
|
238
|
+
* web package root on every watcher batch. `undefined` in production and
|
|
239
|
+
* before boot, which is exactly when `shouldRestart` must answer `false`.
|
|
240
|
+
*/
|
|
241
|
+
resolvedPaths;
|
|
242
|
+
/** Synchronous watcher classification handed to the async reload phase. */
|
|
243
|
+
pendingPageChanges;
|
|
244
|
+
/** Reuses Vite's pipeline-barrel module instance for every dev reinstall. */
|
|
245
|
+
installDevPageRoutes;
|
|
246
|
+
/** Serializes Vite and core watcher callbacks that can observe the same edit. */
|
|
247
|
+
pageRouteReloadQueue = Promise.resolve();
|
|
248
|
+
/** Committed file versions awaiting the overlapping watcher callback. */
|
|
249
|
+
pendingHotUpdateSuppressions = /* @__PURE__ */ new Map();
|
|
212
250
|
constructor(options = {}) {
|
|
213
251
|
super();
|
|
214
252
|
this.options = options;
|
|
@@ -231,12 +269,12 @@ var WebConnector = class extends BaseConnector {
|
|
|
231
269
|
this.pageManifest = consumePageManifest();
|
|
232
270
|
if (isProductionRuntime() && !this.pageManifest) throw new WebPageManifestMissingError();
|
|
233
271
|
if (isProductionRuntime() && this.pageManifest) {
|
|
272
|
+
if ((this.pageManifest.publicFiles?.length ?? 0) > 0) registerProductionPublicFiles(router, this.resolveClientDir(), this.pageManifest.publicFiles ?? []);
|
|
234
273
|
this.installedPages = await installProductionPageRoutes({
|
|
235
274
|
router,
|
|
236
275
|
manifest: this.pageManifest,
|
|
237
276
|
pageContext: requestContext,
|
|
238
277
|
sharedStore: () => requestContext.getStore(),
|
|
239
|
-
applyBufferedCookie,
|
|
240
278
|
resolveHydrationClientModuleUrl: () => this.resolveHydrationClientModuleUrl(),
|
|
241
279
|
clientDir: this.pageManifest.clientDir
|
|
242
280
|
});
|
|
@@ -248,6 +286,7 @@ var WebConnector = class extends BaseConnector {
|
|
|
248
286
|
}
|
|
249
287
|
const fastify = this.resolveFastify();
|
|
250
288
|
const paths = await this.resolvePaths();
|
|
289
|
+
this.resolvedPaths = paths;
|
|
251
290
|
this.vite = await this.createViteServer(fastify, paths);
|
|
252
291
|
const webServerSsr = await this.vite.ssrLoadModule(paths.webServerBarrel);
|
|
253
292
|
webServerSsr.connectSharedStore(() => requestContext.getStore());
|
|
@@ -258,15 +297,28 @@ var WebConnector = class extends BaseConnector {
|
|
|
258
297
|
done(error);
|
|
259
298
|
});
|
|
260
299
|
});
|
|
261
|
-
|
|
300
|
+
const reportUnregisteredPages = createUnregisteredPageReporter({
|
|
301
|
+
appRoot: paths.appRoot,
|
|
302
|
+
appSrcRoot: paths.appSrcRoot,
|
|
303
|
+
registeredPageFiles: () => registeredPageFiles(router.list(), paths.appSrcRoot)
|
|
304
|
+
});
|
|
305
|
+
fastify.addHook("onResponse", (request, reply, done) => {
|
|
306
|
+
if (reply.statusCode === 404) reportUnregisteredPages({
|
|
307
|
+
method: request.method,
|
|
308
|
+
url: request.url,
|
|
309
|
+
pathname: new URL(request.url, "http://warlock.local").pathname
|
|
310
|
+
});
|
|
311
|
+
done();
|
|
312
|
+
});
|
|
313
|
+
this.installDevPageRoutes = () => webServerSsr.installPageRoutes({
|
|
262
314
|
router,
|
|
263
315
|
vite: this.vite,
|
|
264
316
|
appSrcRoot: paths.appSrcRoot,
|
|
265
317
|
appFile: paths.appFile,
|
|
266
318
|
hydrationClientModuleUrl: this.resolveHydrationClientModuleUrl(paths.webRoot),
|
|
267
|
-
stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile)
|
|
268
|
-
applyBufferedCookie
|
|
319
|
+
stylesheetUrls: webServerSsr.devStylesheetUrls(paths.appRoot, paths.appFile)
|
|
269
320
|
});
|
|
321
|
+
this.installedPages = await this.installDevPageRoutes();
|
|
270
322
|
}
|
|
271
323
|
/**
|
|
272
324
|
* Where the browser fetches the hydration entry from — the one line that
|
|
@@ -345,16 +397,81 @@ var WebConnector = class extends BaseConnector {
|
|
|
345
397
|
await this.vite?.close();
|
|
346
398
|
this.vite = void 0;
|
|
347
399
|
this.installedPages = [];
|
|
400
|
+
this.installDevPageRoutes = void 0;
|
|
401
|
+
this.pendingPageChanges = void 0;
|
|
402
|
+
this.pendingHotUpdateSuppressions.clear();
|
|
348
403
|
this.pageManifest = void 0;
|
|
349
404
|
this.active = false;
|
|
350
405
|
}
|
|
351
406
|
/**
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
407
|
+
* Queue page add/remove/edit candidates for asynchronous live routing work.
|
|
408
|
+
* Classification stays synchronous because core's connector interface is;
|
|
409
|
+
* edited route exports are evaluated later through Vite's fresh SSR graph.
|
|
410
|
+
*/
|
|
411
|
+
shouldRestart(changedFiles = []) {
|
|
412
|
+
if (this.vite === void 0 || this.resolvedPaths === void 0) return false;
|
|
413
|
+
this.pendingPageChanges = this.classifyPageChanges(changedFiles);
|
|
414
|
+
return this.pendingPageChanges !== void 0;
|
|
415
|
+
}
|
|
416
|
+
classifyPageChanges(changedFiles) {
|
|
417
|
+
if (this.resolvedPaths === void 0) return void 0;
|
|
418
|
+
const changes = classifyPageFileChanges(changedFiles, {
|
|
419
|
+
appRoot: this.resolvedPaths.appRoot,
|
|
420
|
+
appSrcRoot: this.resolvedPaths.appSrcRoot,
|
|
421
|
+
installedPageFiles: registeredPageFiles(router.list(), this.resolvedPaths.appSrcRoot)
|
|
422
|
+
});
|
|
423
|
+
return hasPageFileChanges(changes) ? changes : void 0;
|
|
424
|
+
}
|
|
425
|
+
enqueuePageRouteReload(changes) {
|
|
426
|
+
const eventVersions = pageChangeVersions(changes);
|
|
427
|
+
const run = this.pageRouteReloadQueue.catch(() => void 0).then(async () => {
|
|
428
|
+
const vite = this.vite;
|
|
429
|
+
const install = this.installDevPageRoutes;
|
|
430
|
+
const paths = this.resolvedPaths;
|
|
431
|
+
if (vite === void 0 || install === void 0 || paths === void 0) return false;
|
|
432
|
+
const matchingCommittedFiles = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const [file, eventVersion] of eventVersions) {
|
|
434
|
+
const committedVersion = this.pendingHotUpdateSuppressions.get(file);
|
|
435
|
+
if (committedVersion === void 0) continue;
|
|
436
|
+
this.pendingHotUpdateSuppressions.delete(file);
|
|
437
|
+
if (committedVersion === eventVersion) matchingCommittedFiles.add(file);
|
|
438
|
+
}
|
|
439
|
+
if (eventVersions.size > 0 && matchingCommittedFiles.size === eventVersions.size) return true;
|
|
440
|
+
if (!await pageRoutesNeedReplacement(changes, {
|
|
441
|
+
vite,
|
|
442
|
+
appSrcRoot: paths.appSrcRoot,
|
|
443
|
+
installedPages: this.installedPages
|
|
444
|
+
})) return false;
|
|
445
|
+
const nextInstalledPages = await router.replaceRoutesBySourceFiles(pageRouteSourceFiles(router.list()), install);
|
|
446
|
+
this.installedPages = nextInstalledPages;
|
|
447
|
+
invalidateClientPageRegistry(vite);
|
|
448
|
+
for (const [file, eventVersion] of eventVersions) if (!matchingCommittedFiles.has(file)) this.pendingHotUpdateSuppressions.set(file, eventVersion);
|
|
449
|
+
return true;
|
|
450
|
+
});
|
|
451
|
+
this.pageRouteReloadQueue = run;
|
|
452
|
+
return run;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Vite-side ordering barrier. It publishes a changed route graph before the
|
|
456
|
+
* page-registry plugin can reload the document; a matching change already
|
|
457
|
+
* handled by core is consumed once, preventing a duplicate full reload.
|
|
458
|
+
*/
|
|
459
|
+
async handlePageHotUpdate(file) {
|
|
460
|
+
const absoluteFile = path.resolve(file);
|
|
461
|
+
const changes = this.classifyPageChanges([absoluteFile]);
|
|
462
|
+
if (changes === void 0) return false;
|
|
463
|
+
return this.enqueuePageRouteReload(changes);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Live page routing update. Despite the connector API name, this never closes
|
|
467
|
+
* Vite: it atomically replaces page-owned routes only when membership or the
|
|
468
|
+
* canonical route identity changed, then refreshes the client registry.
|
|
355
469
|
*/
|
|
356
|
-
|
|
357
|
-
|
|
470
|
+
async restart() {
|
|
471
|
+
const changes = this.pendingPageChanges;
|
|
472
|
+
this.pendingPageChanges = void 0;
|
|
473
|
+
if (changes === void 0 || this.resolvedPaths === void 0 || this.vite === void 0 || this.installDevPageRoutes === void 0) return;
|
|
474
|
+
await this.enqueuePageRouteReload(changes);
|
|
358
475
|
}
|
|
359
476
|
/** The pages this connector registered on the router, in registration order. */
|
|
360
477
|
getInstalledPages() {
|
|
@@ -481,7 +598,10 @@ var WebConnector = class extends BaseConnector {
|
|
|
481
598
|
isProductionRuntime,
|
|
482
599
|
buildErrorMessage
|
|
483
600
|
}),
|
|
484
|
-
...warlockClientBoundary({
|
|
601
|
+
...warlockClientBoundary({
|
|
602
|
+
appRoot: paths.appRoot,
|
|
603
|
+
beforePageHotUpdate: ({ file }) => this.handlePageHotUpdate(file)
|
|
604
|
+
}),
|
|
485
605
|
...await this.resolveReactFastRefreshPlugins(paths),
|
|
486
606
|
...this.options.plugins ?? []
|
|
487
607
|
],
|
|
@@ -498,6 +618,101 @@ var WebConnector = class extends BaseConnector {
|
|
|
498
618
|
target: "es2022",
|
|
499
619
|
jsx: "automatic"
|
|
500
620
|
},
|
|
621
|
+
/**
|
|
622
|
+
* REACT MUST BE PRE-BUNDLED, and naming it here is the only thing that
|
|
623
|
+
* makes that happen for the framework's own client graph.
|
|
624
|
+
*
|
|
625
|
+
* `react-dom/client` is CommonJS. A browser cannot import a named export
|
|
626
|
+
* from it, so Vite's dep optimizer normally rewrites it into an ESM shim
|
|
627
|
+
* under `/node_modules/.vite/deps/`. Whether that rewrite happens is
|
|
628
|
+
* decided in `tryNodeResolve`, and one of the conditions that SKIPS it is
|
|
629
|
+
* `importer && isInNodeModules(importer)`
|
|
630
|
+
* (`vite/dist/node/chunks/config.js:32822`). Vite's reasoning is sound in
|
|
631
|
+
* general — a dependency's own internal imports are the optimizer's job,
|
|
632
|
+
* not the resolver's — but it is exactly wrong here:
|
|
633
|
+
*
|
|
634
|
+
* `<Scripts />` points the browser at the hydration entry, and in an
|
|
635
|
+
* INSTALLED app that entry is
|
|
636
|
+
* `<app>/node_modules/@warlock.js/web/esm/hydration/index.mjs`.
|
|
637
|
+
* Every module it reaches is therefore inside `node_modules`, so every
|
|
638
|
+
* bare import it makes takes the skip branch and is served as the raw
|
|
639
|
+
* file with a `?v=<browserHash>` cache key bolted on.
|
|
640
|
+
*
|
|
641
|
+
* For `esm/**.mjs` that is harmless — they are already ESM. For
|
|
642
|
+
* `react-dom/client` it is fatal, and it is the whole defect: the browser
|
|
643
|
+
* receives `"use strict"; function checkDCE()…` and refuses the module
|
|
644
|
+
* with
|
|
645
|
+
*
|
|
646
|
+
* SyntaxError: The requested module '/node_modules/react-dom/client.js?v=…'
|
|
647
|
+
* does not provide an export named 'hydrateRoot'
|
|
648
|
+
*
|
|
649
|
+
* Nothing in the client runtime then runs at all: no hydration, so no
|
|
650
|
+
* `useState`, no Fast Refresh, and `<Link>` degrades to a full document
|
|
651
|
+
* load because the navigation listener was never attached. Measured on a
|
|
652
|
+
* published 5.0.2 install (fresh app, no symlinks): all four symptoms
|
|
653
|
+
* before this block, none after.
|
|
654
|
+
*
|
|
655
|
+
* `include` is the fix rather than `entries` because it does not depend
|
|
656
|
+
* on the scanner reaching the entry: an included id is pre-bundled at
|
|
657
|
+
* server start and `tryOptimizedResolve` matches it by NAME
|
|
658
|
+
* (`config.js:32633`) before `tryNodeResolve` — and that lookup has no
|
|
659
|
+
* importer condition, so a node_modules importer resolves to the shim
|
|
660
|
+
* like anyone else. Pointing `optimizeDeps.entries` at the hydration
|
|
661
|
+
* entry would not work: the scanner classifies a resolution inside
|
|
662
|
+
* `node_modules` as a dependency to externalize rather than a source to
|
|
663
|
+
* walk.
|
|
664
|
+
*
|
|
665
|
+
* All four names are listed even though `react` and `react/jsx-runtime`
|
|
666
|
+
* usually get discovered anyway — they are discovered only because the
|
|
667
|
+
* APP's own pages import them, which is a fact about the app and not
|
|
668
|
+
* something the framework may rely on. `react-dom/client` is imported by
|
|
669
|
+
* `client/hydrate-page.tsx` and by nothing a normal app writes, which is
|
|
670
|
+
* why it was the one that broke.
|
|
671
|
+
*
|
|
672
|
+
* This is INVISIBLE from the monorepo checkout: there the hydration entry
|
|
673
|
+
* resolves to `web/src/hydration/index.ts`, a path with no `node_modules`
|
|
674
|
+
* segment, so the skip branch never fires and React optimizes normally.
|
|
675
|
+
* Canon: nothing measured inside the checkout is evidence about a
|
|
676
|
+
* published install.
|
|
677
|
+
*/
|
|
678
|
+
optimizeDeps: {
|
|
679
|
+
include: [
|
|
680
|
+
"react",
|
|
681
|
+
"react-dom",
|
|
682
|
+
"react-dom/client",
|
|
683
|
+
"react/jsx-runtime"
|
|
684
|
+
],
|
|
685
|
+
/**
|
|
686
|
+
* The SECOND instance problem, and the reason this is `exclude` and not
|
|
687
|
+
* another `include`.
|
|
688
|
+
*
|
|
689
|
+
* An app page's `import { Link } from "@warlock.js/web"` has an importer
|
|
690
|
+
* OUTSIDE `node_modules`, so it takes the opposite branch above and the
|
|
691
|
+
* whole package is pre-bundled into
|
|
692
|
+
* `/node_modules/.vite/deps/@warlock__js_web.js`. The hydration entry
|
|
693
|
+
* cannot reach that bundle — it is loaded by absolute path through
|
|
694
|
+
* `/@fs/` and its imports are relative, so they resolve to the raw
|
|
695
|
+
* `esm/*.mjs` files. The browser then holds TWO copies of every client
|
|
696
|
+
* module in this package: the app renders `<Link>` from the bundled one
|
|
697
|
+
* while the hydration entry publishes routes and mounts the document
|
|
698
|
+
* context on the raw one. Module-level state does not cross that line.
|
|
699
|
+
*
|
|
700
|
+
* `routing/route-table` survives it by accident — it keeps its table in
|
|
701
|
+
* a `Symbol.for` slot on `globalThis`, which is realm-wide — but
|
|
702
|
+
* `components/document-context` is a React context OBJECT, and two
|
|
703
|
+
* `createContext()` calls are two different contexts no provider can
|
|
704
|
+
* bridge.
|
|
705
|
+
*
|
|
706
|
+
* Excluding the package makes the app's import resolve to
|
|
707
|
+
* `esm/index.mjs` with the same `?v=` key the hydration entry's imports
|
|
708
|
+
* carry, so there is one instance again. It costs the pre-bundle (a
|
|
709
|
+
* handful of extra dev requests for files that are already valid ESM)
|
|
710
|
+
* and nothing else — the grep across every client-reachable module in
|
|
711
|
+
* this package finds exactly three bare imports, all of them React, all
|
|
712
|
+
* of them covered by `include` above.
|
|
713
|
+
*/
|
|
714
|
+
exclude: ["@warlock.js/web"]
|
|
715
|
+
},
|
|
501
716
|
ssr: {
|
|
502
717
|
external: [
|
|
503
718
|
...CORE_OPTIONAL_PEERS,
|