@warlock.js/web 5.1.0 → 5.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +182 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +10 -13
- package/esm/build/contribution.mjs +45 -57
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +104 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +46 -12
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +54 -12
- 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.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +39 -8
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/prefetch.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.mjs +2 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +12 -0
- package/esm/client/runtime/matcher.mjs +12 -0
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +14 -1
- package/esm/components/document-context.d.mts +39 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.mjs +3 -1
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +5 -101
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +36 -7
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/loaders.d.mts +6 -35
- package/esm/props.d.mts +15 -7
- package/esm/route.d.mts +1 -16
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- 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 +1 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/create-page-module-loader.mjs +1 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +30 -5
- package/esm/server/create-page-route-handler.mjs +127 -42
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +1 -12
- 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 +6 -3
- package/esm/server/execute-page-request.mjs +78 -79
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +32 -91
- package/esm/server/hydration-client-url.mjs +1 -1
- package/esm/server/index.d.mts +5 -5
- package/esm/server/index.mjs +3 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +16 -6
- package/esm/server/install-page-routes-from-manifest.mjs +70 -9
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +31 -14
- package/esm/server/install-page-routes.mjs +90 -38
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +2 -35
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +1 -1
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.mjs +1 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-context.d.mts +2 -8
- package/esm/server/page-context.mjs +2 -15
- 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 +24 -9
- 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 +10 -13
- package/esm/server/render-page.mjs +153 -27
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +110 -45
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +40 -29
- package/esm/server/stylesheet-urls.mjs +136 -51
- 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 +1 -2
- package/esm/server/web-connector-factory.mjs +6 -5
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.mjs +137 -17
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +17 -2
- package/esm/shared.mjs +2 -6
- package/esm/shared.mjs.map +1 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.mjs +66 -12
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.mjs +34 -16
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.mjs +5 -5
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs +1 -1
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +8 -5
- package/esm/vite/index.mjs +123 -10
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +21 -2
- package/esm/vite/page-registry-plugin.mjs +47 -13
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +1 -1
- package/esm/vite/projection.mjs +37 -14
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +220 -56
- package/llms.txt +5 -3
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +117 -10
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +1 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +14 -8
- package/skills/write-the-root/SKILL.md +3 -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":["../../../../../../../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"}
|
|
@@ -6,8 +6,7 @@ import { Connector } from "@warlock.js/core";
|
|
|
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,7 +1,7 @@
|
|
|
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
6
|
//#region ../web/src/server/web-connector-factory.ts
|
|
7
7
|
/**
|
|
@@ -74,11 +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
|
-
webRoot: buildOptions?.webRoot ?? webRoot
|
|
80
|
-
|
|
81
|
-
});
|
|
80
|
+
webRoot: buildOptions?.webRoot ?? webRoot
|
|
81
|
+
}, connectorPlugins);
|
|
82
82
|
let instance;
|
|
83
83
|
/**
|
|
84
84
|
* Load the heavy half on first lifecycle call. `boot()` is always the first
|
|
@@ -90,6 +90,7 @@ function webConnector(options = {}) {
|
|
|
90
90
|
const { WebConnector: WebConnectorClass } = await import("./web-connector.mjs");
|
|
91
91
|
instance = new WebConnectorClass({
|
|
92
92
|
...connectorOptions,
|
|
93
|
+
plugins: connectorPlugins,
|
|
93
94
|
webRoot
|
|
94
95
|
});
|
|
95
96
|
}
|
|
@@ -113,7 +114,7 @@ function webConnector(options = {}) {
|
|
|
113
114
|
shutdown: async () => {
|
|
114
115
|
await instance?.shutdown();
|
|
115
116
|
},
|
|
116
|
-
shouldRestart: () => false
|
|
117
|
+
shouldRestart: (changedFiles) => instance?.shouldRestart(changedFiles) ?? false
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport {
|
|
1
|
+
{"version":3,"file":"web-connector-factory.mjs","names":[],"sources":["../../../../../../../web/src/server/web-connector-factory.ts"],"sourcesContent":["/**\n * `webConnector()` — the ONE thing `warlock.config.ts` imports from\n * `@warlock.js/web/connector`, and the only value the `./connector` barrel\n * adds beyond the build/runtime seams.\n *\n * WHY THIS MODULE EXISTS AT ALL, rather than the barrel exporting\n * `WebConnector` directly: `./web-connector.ts` imports `../vite`\n * (`web-connector.ts:52` → `@babel/parser` + `magic-string`),\n * `../../../core/src/router/router` (`:51`) and `./dev-server` (`:53`, which\n * itself pulls core's http stack and `../vite`) at VALUE level. Re-exporting\n * that class from `web/src/connector/index.ts` would drag every one of those\n * into the static graph of every consuming app's config file — the exact\n * config-load weight the `./connector` subpath was created to prevent.\n *\n * So the factory returns a LAZY DELEGATE: a `Connector` whose identity fields\n * (`name`, `priority`, `lifecyclePhase`, `build`) are plain data available\n * synchronously, and whose lifecycle methods `await import(\"./web-connector\")`\n * on first use. `warlock build` reads `build` off this object and never boots\n * anything, so a build never loads Vite or React through\n * here either. This is the third instance of a pattern the codebase already\n * uses twice — `core/src/connectors/access-connector.ts:39` and\n * `web/src/server/dev-cli.ts:61` — not a new one.\n *\n * KEEP THIS MODULE LIGHT. Its whole value-level static graph is `node:path`,\n * `node:url`, `../../../core/src/connectors/types` (whose own two imports are\n * both `import type` and therefore erased — `core/src/connectors/types.ts:1-2`)\n * and `../build/contribution` (`node:fs` + `node:path` + type-only core).\n * Everything else here is `import type`, which `verbatimModuleSyntax` erases.\n */\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport {\n type Connector,\n type ConnectorBuildContribution,\n ConnectorLifecyclePhase,\n type ConnectorName,\n} from \"@warlock.js/core\";\nimport {\n createWebBuildContribution,\n type WebBuildOptions,\n} from \"../build/contribution\";\nimport type { WebConnector, WebConnectorOptions } from \"./web-connector\";\n\n/**\n * Boot/shutdown position relative to core's own connectors.\n *\n * `ConnectorPriority.HTTP` is `5` and `ConnectorPriority.STORAGE` is `6`\n * (`core/src/connectors/types.ts:187-188`), and the manager sorts on a plain\n * numeric compare (`core/src/connectors/connectors-manager.ts:46`) — so `5.5`\n * is \"immediately after http, before everything else\".\n *\n * Defined HERE and re-exported by `./web-connector` rather than the other way\n * round: the delegate must publish `priority` synchronously, and reading it\n * from the heavy module would defeat the whole point of the delegate.\n */\nexport const WEB_CONNECTOR_PRIORITY = 5.5;\n\nexport type WebConnectorFactoryOptions = WebConnectorOptions & {\n /**\n * What web contributes to `warlock build`. Passed straight to\n * {@link createWebBuildContribution}; JSON-serializable values only.\n */\n build?: WebBuildOptions;\n};\n\n/**\n * The `@warlock.js/web` package root, derived once from THIS module's location\n * and then passed EXPLICITLY to both halves.\n *\n * `web/src/server/web-connector-factory.ts` → `web/src/server` → `web/src` →\n * `web`; published as `web/esm/server/web-connector-factory.js`, two levels up\n * is the package root under both layouts — the same arithmetic\n * `contribution.ts:136-140` documents.\n *\n * Deriving it here and handing it down means the build contribution never falls\n * back to its own `import.meta.url` guess (`contribution.ts:129-141`): one\n * derivation, one place to be wrong, and `assertWebPackageRoot` verifies it\n * against `<root>/package.json`'s `name` either way.\n */\nfunction deriveWebRoot(): string {\n return path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n}\n\n/**\n * Construct web's connector for `warlock.config.ts > connectors`.\n *\n * @example\n * ```ts\n * export default defineConfig({ connectors: [webConnector()] });\n * ```\n */\nexport function webConnector(\n options: WebConnectorFactoryOptions = {},\n): Connector {\n const { build: buildOptions, ...connectorOptions } = options;\n const webRoot = connectorOptions.webRoot ?? deriveWebRoot();\n // One snapshot feeds both halves. Mutating the caller's array after config\n // construction cannot make dev and `warlock build` observe different lists.\n const connectorPlugins = [...(connectorOptions.plugins ?? [])];\n\n const build: ConnectorBuildContribution = createWebBuildContribution(\n {\n ...buildOptions,\n webRoot: buildOptions?.webRoot ?? webRoot,\n },\n connectorPlugins,\n );\n\n let instance: WebConnector | undefined;\n\n /**\n * Load the heavy half on first lifecycle call. `boot()` is always the first\n * of these to run (`core/src/connectors/connectors-manager.ts:87-93`), so the\n * import lands in a process that has already committed to serving pages.\n */\n const load = async (): Promise<WebConnector> => {\n if (!instance) {\n const { WebConnector: WebConnectorClass } =\n await import(\"./web-connector\");\n\n instance = new WebConnectorClass({\n ...connectorOptions,\n plugins: connectorPlugins,\n webRoot,\n });\n }\n\n return instance;\n };\n\n return {\n name: \"web\" satisfies ConnectorName,\n priority: WEB_CONNECTOR_PRIORITY,\n lifecyclePhase: ConnectorLifecyclePhase.Late,\n build,\n\n // Never loads the heavy half: a connector that was never booted is not\n // active, and answering that must not cost a Vite import.\n isActive: () => instance?.isActive() ?? false,\n\n boot: async () => {\n await (await load()).boot();\n },\n start: async () => {\n await (await load()).start();\n },\n restart: async () => {\n await (await load()).restart();\n },\n\n // Both of these are asked of every registered connector, including ones\n // that never booted — so neither may force the import.\n shutdown: async () => {\n await instance?.shutdown();\n },\n\n // Asked on EVERY watcher batch, so the `instance?.` is load-bearing twice:\n // it keeps the answer free for a connector that never booted, and it is the\n // only reason this delegate can forward the question at all — by the time a\n // watcher batch exists, `boot()` has already loaded the heavy half, so\n // reading the real answer off it costs nothing beyond the call.\n //\n // It used to be a hard `false`, which meant a `*.page.tsx` created while\n // `warlock dev` was running was never noticed: page routes are installed\n // once, in `WebConnector.boot()`, so the new page's URL 404'd in silence.\n // The connector itself now decides: add/remove and route-identity edits are\n // live route-table work; component-body-only edits remain Vite HMR work.\n shouldRestart: (changedFiles: string[]) =>\n instance?.shouldRestart(changedFiles) ?? false,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,yBAAyB;;;;;;;;;;;;;;;AAwBtC,SAAS,gBAAwB;CAC/B,OAAO,KAAK,QAAQ,KAAK,QAAQ,cAAc,OAAO,KAAK,GAAG,CAAC,GAAG,MAAM,IAAI;AAC9E;;;;;;;;;AAUA,SAAgB,aACd,UAAsC,CAAC,GAC5B;CACX,MAAM,EAAE,OAAO,cAAc,GAAG,qBAAqB;CACrD,MAAM,UAAU,iBAAiB,WAAW,cAAc;CAG1D,MAAM,mBAAmB,CAAC,GAAI,iBAAiB,WAAW,CAAC,CAAE;CAE7D,MAAM,QAAoC,2BACxC;EACE,GAAG;EACH,SAAS,cAAc,WAAW;CACpC,GACA,gBACF;CAEA,IAAI;;;;;;CAOJ,MAAM,OAAO,YAAmC;EAC9C,IAAI,CAAC,UAAU;GACb,MAAM,EAAE,cAAc,sBACpB,MAAM,OAAO;GAEf,WAAW,IAAI,kBAAkB;IAC/B,GAAG;IACH,SAAS;IACT;GACF,CAAC;EACH;EAEA,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,UAAU;EACV,gBAAgB,wBAAwB;EACxC;EAIA,gBAAgB,UAAU,SAAS,KAAK;EAExC,MAAM,YAAY;GAChB,OAAO,MAAM,KAAK,GAAG,KAAK;EAC5B;EACA,OAAO,YAAY;GACjB,OAAO,MAAM,KAAK,GAAG,MAAM;EAC7B;EACA,SAAS,YAAY;GACnB,OAAO,MAAM,KAAK,GAAG,QAAQ;EAC/B;EAIA,UAAU,YAAY;GACpB,MAAM,UAAU,SAAS;EAC3B;EAaA,gBAAgB,iBACd,UAAU,cAAc,YAAY,KAAK;CAC7C;AACF"}
|
|
@@ -4,14 +4,19 @@ 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
21
|
//#region ../web/src/server/web-connector.ts
|
|
17
22
|
/**
|
|
@@ -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.
|
|
355
410
|
*/
|
|
356
|
-
shouldRestart() {
|
|
357
|
-
return false;
|
|
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.
|
|
469
|
+
*/
|
|
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
|
],
|