@warlock.js/web 5.5.0 → 5.7.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 +53 -5
- package/esm/build/contribution.d.mts +2 -2
- package/esm/build/contribution.mjs +1 -1
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +130 -208
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +7 -5
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-routes-manifest.mjs +1 -1
- package/esm/build/public-files.mjs +30 -2
- package/esm/build/public-files.mjs.map +1 -1
- package/esm/build/read-metadata-keys.mjs +225 -0
- package/esm/build/read-metadata-keys.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +1 -1
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/navigation/document-metadata.mjs +108 -0
- package/esm/client/navigation/document-metadata.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +3 -10
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +24 -104
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -1
- package/esm/client/navigation/use-query-string.d.mts +35 -0
- package/esm/client/navigation/use-query-string.mjs +68 -0
- package/esm/client/navigation/use-query-string.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +1 -1
- package/esm/client/runtime/index.mjs +1 -1
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/core/src/router/normalize-route-path.mjs +33 -0
- package/esm/core/src/router/normalize-route-path.mjs.map +1 -0
- package/esm/{hydration → entry}/index.mjs +1 -1
- package/esm/entry/index.mjs.map +1 -0
- package/esm/hydration-payload.mjs +19 -10
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +3 -2
- package/esm/index.mjs +3 -2
- package/esm/loaders.d.mts +13 -1
- package/esm/localization.mjs.map +1 -1
- package/esm/metadata.d.mts +4 -4
- package/esm/metadata.mjs.map +1 -1
- package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs +33 -0
- package/esm/node_modules/.pnpm/@mongez_concat-route@1.2.0/node_modules/@mongez/concat-route/esm/index.mjs.map +1 -0
- package/esm/{runtime/register-modules.d.mts → register-modules.d.mts} +1 -1
- package/esm/{runtime/register-modules.mjs → register-modules.mjs} +1 -1
- package/esm/register-modules.mjs.map +1 -0
- package/esm/route.d.mts +9 -0
- package/esm/routing/duplicate-route-path.mjs +22 -0
- package/esm/routing/duplicate-route-path.mjs.map +1 -0
- package/esm/routing/layout-level.mjs +50 -0
- package/esm/routing/layout-level.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +23 -1
- package/esm/routing/query-string.mjs +23 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +27 -2
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +5 -4
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +2 -2
- package/esm/server/create-page-route-handler.mjs +18 -12
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/{dev-server.mjs → dev-error-transport.mjs} +2 -2
- package/esm/server/dev-error-transport.mjs.map +1 -0
- package/esm/server/execute-page-request.mjs +91 -35
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +21 -1
- package/esm/server/fold-layout-loaders.mjs +36 -0
- package/esm/server/fold-layout-loaders.mjs.map +1 -0
- package/esm/server/hydration-client-url.mjs +3 -3
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +4 -3
- package/esm/server/index.mjs +6 -6
- package/esm/server/install-page-routes-from-manifest.d.mts +10 -1
- package/esm/server/install-page-routes-from-manifest.mjs +61 -58
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +24 -26
- package/esm/server/install-page-routes.mjs +149 -60
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +6 -10
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +1 -0
- package/esm/server/not-found-page.mjs +8 -1
- package/esm/server/not-found-page.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +1 -1
- package/esm/server/page-file-change.mjs.map +1 -1
- package/esm/server/page-module-shapes.d.mts +24 -0
- package/esm/server/page-route-reload.mjs +4 -3
- package/esm/server/page-route-reload.mjs.map +1 -1
- package/esm/server/register-production-public-files.mjs +26 -2
- package/esm/server/register-production-public-files.mjs.map +1 -1
- package/esm/server/render-page.d.mts +13 -49
- package/esm/server/render-page.mjs +14 -55
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-route-validation-input.mjs +11 -0
- package/esm/server/resolve-route-validation-input.mjs.map +1 -0
- 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.mjs +5 -3
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.mjs +78 -19
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +2 -1
- package/esm/server/unregistered-pages.mjs.map +1 -1
- package/esm/server/web-connector-factory.mjs +1 -1
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.mjs +25 -322
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared/module-key.mjs +15 -0
- package/esm/shared/module-key.mjs.map +1 -0
- package/esm/shared/to-posix.mjs +13 -0
- package/esm/shared/to-posix.mjs.map +1 -0
- package/esm/shared.mjs +24 -0
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +12 -1
- package/esm/vite/build-client.mjs +1 -1
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/dev-server-config.mjs +240 -0
- package/esm/vite/dev-server-config.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +5 -4
- package/esm/vite/gate-a-resolve.mjs +32 -71
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +1 -1
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.mjs +6 -8
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +1 -1
- package/esm/vite/index.mjs +4 -110
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.mjs +9 -26
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.mjs.map +1 -1
- package/esm/vite/react-refresh-preamble.mjs +90 -0
- package/esm/vite/react-refresh-preamble.mjs.map +1 -0
- package/esm/vite/ssr-client-view.mjs +135 -0
- package/esm/vite/ssr-client-view.mjs.map +1 -0
- package/llms-full.txt +564 -482
- package/llms.txt +2 -2
- package/package.json +6 -6
- package/skills/create-a-page/SKILL.md +354 -295
- package/skills/navigate-on-the-client/SKILL.md +212 -189
- package/esm/hydration/index.mjs.map +0 -1
- package/esm/runtime/register-modules.mjs.map +0 -1
- package/esm/server/dev-server.mjs.map +0 -1
- /package/esm/{hydration → entry}/index.d.mts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@warlock.js/web` are documented here.
|
|
4
4
|
|
|
5
|
+
## 5.7.0 - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### Removed
|
|
8
|
+
|
|
9
|
+
- **`route.validate` and `route.middleware` are withdrawn, one release after 5.6.0 added them.** They were a second way to say what the top-level `validation` and `middleware` exports already said, on the same file — and the two validation surfaces disagreed about the status code. **Migration is a move, not a rewrite:** the schema shape is unchanged (`params` and `query` stay separate, never merged) and the failure is still 400.
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- export const route = { path: "/products/:id", validate: v.object({ … }), middleware: [guard] } as const;
|
|
13
|
+
+ export const route = { path: "/products/:id" } as const;
|
|
14
|
+
+ export const validation = { params: v.object({ … }), query: v.object({ … }) };
|
|
15
|
+
+ export const middleware = [guard];
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**A page still declaring either one refuses to boot and names the file.** It is never silently ignored — which for `route.middleware` is the difference between a deploy that fails and a route that serves without its auth guard.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **Dev and production agree about stylesheets.** A stylesheet reached only through a component import was collected by production's bundler-graph walk and was structurally invisible to dev's scan of the page file — so a page rendered unstyled in development and correct in production. Both sides now end in one traversal, gated by a fixture built through **both** pipelines with the outputs diffed.
|
|
23
|
+
- **A client navigation whose data payload is incomplete now loads the page normally instead of rendering it blank.** Navigation carried its own copy of the payload rule and checked two of the six required keys, so a payload that could not render a page was accepted and handed to React anyway; the failure surfaced later, somewhere else, pointing at nothing. It now falls back to a full page load — slower for that one click, and the page arrives.
|
|
24
|
+
- **The dev server no longer says it is watching for changes while it is not yet serving.** On a slow boot that line arrived up to three minutes before the port was bound; every word of it was true and the impression it left was false.
|
|
25
|
+
- **The `create-a-page` skill and `llms-full.txt` taught `route.validate` and `route.middleware`** — with a complete worked example — after both were withdrawn. Following our own documentation produced an app that would not start.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- The dev and production page installers now agree on the layout **chain**, the layout **level**, and the hydration entry URL by construction rather than by inspection, each gated with a red control. The three places they still differ — live `public/` serving, its cache header, and hashed-asset caching — are deliberate and are now declared in the code that implements them.
|
|
30
|
+
|
|
31
|
+
## 5.6.0 - 2026-09-08
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **A page can declare its input contract on its `route` export.** The export now accepts an object as well as a string: `{ path, name?, cache?, validate?, middleware? }`. `validate` is a Seal schema over `{ params, query }` — kept separate, never merged — and the validated value reaches the loader typed from the schema. A failure renders the **error page at 400** carrying the failure, and travels the same way over the client-navigation wire. Layout middleware runs outermost-first with the page's own last, so a layout's auth gate cannot be bypassed by a page that declares its own.
|
|
35
|
+
- `useQueryString(key)` — a subscription to one query-string parameter that re-renders on client navigation. Wiring it up exposed that `routerEvents` was only ever fired by `refresh()`: `<Link>` and browser back/forward emitted nothing, so anything subscribed to navigation silently never updated. Navigation now emits its events on every path.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- **Every rendered page returned 500 in `warlock dev`.** The dev server decided the client/server boundary by FILE LOCATION — anything under `src/web/**` was treated as inherently client-safe — which contradicts the rule the production build applies and made a server-only import reachable from the client graph. Dev now decides the boundary by the import graph, exactly as production does.
|
|
40
|
+
- **A page route could not be served at all in `warlock dev`.** The handler read the Fastify instance from the container while running inside Vite's SSR module graph, where that lookup can never hit. The instance is now resolved on the Node side and handed in.
|
|
41
|
+
- **Page files were ignored in silence.** A `*.page.tsx` or a layout under `src/app/**/web/**` was discovered by nothing and reported by nothing — an app with seven pages served zero. Discovery now NAMES every file it ignores, at boot, and for a layout it says what was lost: its `prefix`, `middleware` and `loader` apply to no page, so a guard a page relied on is silently absent.
|
|
42
|
+
- **A file added to `public/` after the last build 404'd in production with no diagnostic.** The build-time snapshot is deliberate and stays — but production now names the files its snapshot missed instead of failing them wordlessly.
|
|
43
|
+
- One page that fails to import no longer takes the whole dev boot down with it.
|
|
44
|
+
- Writing to `shared` from the browser failed with a message that read as a fixable wiring bug — "the server bootstrap must call `connectSharedStore(...)`". It now names the value, explains that what the client holds is a dead server-render snapshot that can never be written to, and says what to use instead.
|
|
45
|
+
- The `public/` staleness check runs on every client build rather than only some.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **The dev and production page installers now share their composition rules** — layout-level selection, loader folding, route identity, and the duplicate-route message — instead of implementing them twice. A route collision reported in dev used to quote a dev-only file path in a message production also raises.
|
|
50
|
+
- The published `./vite` subpath is a barrel again: the connector no longer authors Vite plugins, so importing the runtime never drags the build tooling in behind it.
|
|
51
|
+
- Importing the metadata linter no longer pulls 4544 modules and 18 MB into a build-tool module for the sake of one function; it now costs 17 modules.
|
|
52
|
+
- One name for one thing: "runtime" everywhere, `hydration/` renamed to `entry/`, and four files renamed to match what they contain.
|
|
53
|
+
|
|
5
54
|
## 5.5.0 - 2026-09-07
|
|
6
55
|
|
|
7
56
|
### Fixed
|
|
@@ -105,8 +154,7 @@ All notable changes to `@warlock.js/web` are documented here.
|
|
|
105
154
|
- **Page requests now tolerate one trailing slash identically in development
|
|
106
155
|
and production.** `/about` and `/about/` serve the same page; `/` remains the
|
|
107
156
|
root path and case handling is unchanged. Previously the development
|
|
108
|
-
dispatcher accepted the slash while the production Fastify route returned
|
|
109
|
-
404.
|
|
157
|
+
dispatcher accepted the slash while the production Fastify route returned 404.
|
|
110
158
|
|
|
111
159
|
- ⚠ **BREAKING — `process.env` is refused entirely in the client/universal
|
|
112
160
|
graph, and there is no `PUBLIC_` exception.** Neither a static key
|
|
@@ -182,7 +230,7 @@ All notable changes to `@warlock.js/web` are documented here.
|
|
|
182
230
|
layout loader from outermost to innermost, then the page loader. The runtime
|
|
183
231
|
has three top-level slots (`app`, `layout`, `page`), but the layout slot
|
|
184
232
|
composes the full matched layout chain. A page still has at most one
|
|
185
|
-
|
|
233
|
+
_rendering_ layout; loader-only and middleware-only layouts may appear at
|
|
186
234
|
multiple ancestry levels.
|
|
187
235
|
|
|
188
236
|
⚠ **This package's own documentation previously described the three levels as
|
|
@@ -234,7 +282,7 @@ All notable changes to `@warlock.js/web` are documented here.
|
|
|
234
282
|
## 5.1.0
|
|
235
283
|
|
|
236
284
|
> **Upgrade if you installed 5.0.0, 5.0.1 or 5.0.2.** React did not execute at all in
|
|
237
|
-
> a published install of any of them — see the first entry under
|
|
285
|
+
> a published install of any of them — see the first entry under _Fixed_. Every
|
|
238
286
|
> interactive page shipped on those versions was inert in the browser.
|
|
239
287
|
|
|
240
288
|
### Added
|
|
@@ -280,7 +328,7 @@ All notable changes to `@warlock.js/web` are documented here.
|
|
|
280
328
|
never ran, metadata never refreshed, and `<Link>` fell back to a full page reload.
|
|
281
329
|
Fixed by declaring the React entries in the dev server's `optimizeDeps` so they are
|
|
282
330
|
pre-bundled to ESM before the browser asks for them. This is not a hydration
|
|
283
|
-
|
|
331
|
+
_improvement_ — hydration did not happen.
|
|
284
332
|
- **The browser was loading two copies of every `@warlock.js/web` client module.**
|
|
285
333
|
Module-level state (context, the navigation runtime) existed twice, so a value written
|
|
286
334
|
through one copy was invisible to the component reading the other.
|
|
@@ -6,8 +6,8 @@ type WebBuildOptions = {
|
|
|
6
6
|
/** Source directory name under the app root. Default: `"src"`. */srcDir?: string;
|
|
7
7
|
/**
|
|
8
8
|
* Absolute path to the `@warlock.js/web` package root, which holds the
|
|
9
|
-
* hydration entry (packaged as `esm/
|
|
10
|
-
* fallback at `src/
|
|
9
|
+
* hydration entry (packaged as `esm/entry/index.mjs`, with a source
|
|
10
|
+
* fallback at `src/entry/index.ts` for this checkout).
|
|
11
11
|
*
|
|
12
12
|
* Defaults to this module's own package root, derived from `import.meta.url`
|
|
13
13
|
* at emit time. Set it explicitly when the build process loads this module
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contribution.mjs","names":[],"sources":["../../../../../../../web/src/build/contribution.ts"],"sourcesContent":["/**\r\n * Web's build-time half — the object a `WebConnector` hands to\r\n * `warlock build` as its `build` contribution.\r\n *\r\n * KEPT DELIBERATELY LIGHT: everything this\r\n * module imports statically is a type or `node:path`. The heavy halves — the\r\n * filesystem discovery/barrel generator, and the Vite client build — are\r\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\r\n * constructs the connector never drags Vite, React or the page graph into its\r\n * static import graph.\r\n *\r\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\r\n * B): no plugin instances, no functions, no class instances. Anything heavy is\r\n * constructed inside a hook after that hook's dynamic import.\r\n */\r\nimport { readFileSync } from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type {\r\n ConnectorBuildContext,\r\n ConnectorBuildContribution,\r\n ConnectorBuildGenerateResult,\r\n} from \"@warlock.js/core\";\r\nimport type { PluginOption } from \"vite\";\r\n\r\nexport type WebBuildOptions = {\r\n /** Source directory name under the app root. Default: `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * Absolute path to the `@warlock.js/web` package root, which holds the\r\n * hydration entry (packaged as `esm/hydration/index.mjs`, with a source\r\n * fallback at `src/hydration/index.ts` for this checkout).\r\n *\r\n * Defaults to this module's own package root, derived from `import.meta.url`\r\n * at emit time. Set it explicitly when the build process loads this module\r\n * through a transform that rewrites `import.meta` (e.g. a CJS config\r\n * pipeline) — the derivation is the only thing here that depends on it.\r\n *\r\n * Derived or configured, the root is VERIFIED at emit time against\r\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\r\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\r\n */\r\n webRoot?: string;\r\n /**\r\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\r\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\r\n * runtime does not yet consult a relocated path. Setting this option throws\r\n * {@link ClientOutDirNotSupportedError}.\r\n */\r\n clientOutDir?: string;\r\n /** Client-build resolve aliases: specifier -> absolute path. */\r\n aliases?: Record<string, string>;\r\n /** Extra package names to keep external to the client bundle. */\r\n external?: string[];\r\n};\r\n\r\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\r\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\r\n}\r\n\r\n/**\r\n * `build.clientOutDir` was set.\r\n *\r\n * The production runtime hardcodes where it reads the client bundle from\r\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\r\n * consult the build config for a relocated path. A build that honored a\r\n * custom `clientOutDir` would therefore produce an artifact the runtime\r\n * cannot find, silently. Rejected at build start, before any work happens,\r\n * rather than left to surface later as a missing-bundle failure in\r\n * production.\r\n */\r\nexport class ClientOutDirNotSupportedError extends Error {\r\n public constructor() {\r\n super(\r\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\r\n \"from its default location and does not consult this option, so a build honoring it \" +\r\n \"would produce an artifact production cannot serve correctly. Remove \" +\r\n '\"build.clientOutDir\" from the build config.',\r\n );\r\n this.name = \"ClientOutDirNotSupportedError\";\r\n }\r\n}\r\n\r\n/** The package name every candidate web root must declare to be one. */\r\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\r\n\r\n/**\r\n * The resolved `@warlock.js/web` root is not that package.\r\n *\r\n * Same posture as the manifest resolver's three named errors: a root that\r\n * cannot be proven is REJECTED, never quietly\r\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\r\n * would otherwise surface much later as an unintelligible Vite entry failure.\r\n */\r\nexport class WebPackageRootResolutionError extends Error {\r\n public constructor(webRoot: string, reason: string, cause?: unknown) {\r\n super(\r\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\r\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\r\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\r\n { cause },\r\n );\r\n this.name = \"WebPackageRootResolutionError\";\r\n }\r\n}\r\n\r\n/**\r\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\r\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\r\n */\r\nexport function assertWebPackageRoot(webRoot: string): string {\r\n const manifestPath = path.join(webRoot, \"package.json\");\r\n\r\n let raw: string;\r\n\r\n try {\r\n raw = readFileSync(manifestPath, \"utf-8\");\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\r\n }\r\n\r\n let parsed: unknown;\r\n\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json that is not valid JSON\",\r\n error,\r\n );\r\n }\r\n\r\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json whose root is not a JSON object\",\r\n );\r\n }\r\n\r\n const name = (parsed as { name?: unknown }).name;\r\n\r\n if (name !== WEB_PACKAGE_NAME) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\r\n );\r\n }\r\n\r\n return webRoot;\r\n}\r\n\r\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\r\n if (configured !== undefined) {\r\n return assertWebPackageRoot(path.resolve(configured));\r\n }\r\n\r\n const { fileURLToPath } = await import(\"node:url\");\r\n\r\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\r\n // — two levels up is the package root under both layouts.\r\n return assertWebPackageRoot(\r\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\r\n );\r\n}\r\n\r\n/**\r\n * Builds web's `ConnectorBuildContribution`.\r\n *\r\n * `generate` writes the page barrel into `ctx.productionDir` and patches\r\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\r\n * closure facts — how many pages exist, the discovered route table, and the\n * exact app-public file list. A build with zero pages skips the hydration\n * bundle but still copies public files when present. `generate` itself does\n * NOT skip: the barrel and its entry import are emitted either way, so the\n * runtime can tell a page-free web build from a build without web.\n *\r\n * Connector plugins are a separate argument rather than a `WebBuildOptions`\r\n * field: the app constructs them once, the factory snapshots that array for\r\n * both dev and build, and this module keeps Vite as a type-only import until\r\n * `emit` dynamically loads the real client builder.\r\n */\r\nexport function createWebBuildContribution(\r\n options: WebBuildOptions = {},\r\n connectorPlugins: readonly PluginOption[] = [],\r\n): ConnectorBuildContribution {\r\n if (options.clientOutDir !== undefined) {\r\n throw new ClientOutDirNotSupportedError();\r\n }\r\n\r\n let pageCount = 0;\r\n let pageRoutes: import(\"./generate-pages-barrel\").PageRoutesManifest = {\n version: 1,\n routes: [],\n };\n let publicFiles: string[] = [];\n\r\n return {\r\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\r\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } =\n await import(\"./generate-pages-barrel\");\n const { collectPublicFiles } = await import(\"./public-files\");\n\n publicFiles = await collectPublicFiles(path.join(context.appRoot, \"public\"));\n\n const result = await generatePagesBarrel({\n appRoot: context.appRoot,\r\n productionDir: context.productionDir,\r\n srcDir: options.srcDir,\r\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\r\n // to the Vite build below — so the path baked into the manifest and\r\n // the path the bundle is written to are one expression, not two that\r\n // happen to agree today.\r\n // POSIX-normalised inline rather than via the generator's `toPosix`:\r\n // this module's static graph is `node:path` and types only, and the\r\n // generator is reached by `await import` precisely to keep it that way.\r\n clientDir: path\r\n .relative(context.appRoot, resolveClientOutDir(context))\r\n .split(path.sep)\n .join(\"/\"),\n publicFiles,\n });\n\r\n pageCount = result.pageCount;\r\n pageRoutes = result.pageRoutes;\r\n\r\n // Contributed unconditionally, zero pages included: the barrel is always\r\n // written, and the entry has to IMPORT it for the empty table to reach\r\n // the runtime. Withholding this line on zero pages would leave the\r\n // manifest absent, which is the runtime's signal for \"never built with\r\n // web\" — the exact confusion the always-written barrel removes.\r\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\r\n },\r\n\r\n async emit(context: ConnectorBuildContext): Promise<void> {\r\n if (pageCount > 0) {\n const { buildWarlockHydrationClient } = await import(\"../vite\");\r\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\r\n\r\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\r\n // also reach the production build, from the SAME definition — see\r\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\r\n // caller can win a conflict, matching the dev server's ordering.\r\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\r\n find,\r\n replacement,\r\n }));\r\n\r\n await buildWarlockHydrationClient({\r\n appRoot: context.appRoot,\r\n webRoot: await resolveWebPackageRoot(options.webRoot),\r\n outDir: resolveClientOutDir(context),\r\n resolveAliases: [\r\n ...callerAliases,\r\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\r\n ],\r\n external: options.external,\r\n plugins: connectorPlugins,\r\n });\n }\n\n if (publicFiles.length > 0) {\n const { copyPublicFiles } = await import(\"./public-files\");\n await copyPublicFiles(\n path.join(context.appRoot, \"public\"),\n path.join(resolveClientOutDir(context), \"public\"),\n publicFiles,\n );\n }\n\n const { writePageRoutesManifest } = await import(\"./page-routes-manifest\");\n await writePageRoutesManifest(\r\n path.resolve(context.appRoot, context.options.outdir),\r\n pageRoutes,\r\n );\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contribution.mjs","names":[],"sources":["../../../../../../../web/src/build/contribution.ts"],"sourcesContent":["/**\r\n * Web's build-time half — the object a `WebConnector` hands to\r\n * `warlock build` as its `build` contribution.\r\n *\r\n * KEPT DELIBERATELY LIGHT: everything this\r\n * module imports statically is a type or `node:path`. The heavy halves — the\r\n * filesystem discovery/barrel generator, and the Vite client build — are\r\n * `await import(...)`ed INSIDE the hooks, so a config file that merely\r\n * constructs the connector never drags Vite, React or the page graph into its\r\n * static import graph.\r\n *\r\n * {@link WebBuildOptions} carries JSON-SERIALIZABLE VALUES ONLY (constraint\r\n * B): no plugin instances, no functions, no class instances. Anything heavy is\r\n * constructed inside a hook after that hook's dynamic import.\r\n */\r\nimport { readFileSync } from \"node:fs\";\r\nimport path from \"node:path\";\r\nimport type {\r\n ConnectorBuildContext,\r\n ConnectorBuildContribution,\r\n ConnectorBuildGenerateResult,\r\n} from \"@warlock.js/core\";\r\nimport type { PluginOption } from \"vite\";\r\n\r\nexport type WebBuildOptions = {\r\n /** Source directory name under the app root. Default: `\"src\"`. */\r\n srcDir?: string;\r\n /**\r\n * Absolute path to the `@warlock.js/web` package root, which holds the\r\n * hydration entry (packaged as `esm/entry/index.mjs`, with a source\r\n * fallback at `src/entry/index.ts` for this checkout).\r\n *\r\n * Defaults to this module's own package root, derived from `import.meta.url`\r\n * at emit time. Set it explicitly when the build process loads this module\r\n * through a transform that rewrites `import.meta` (e.g. a CJS config\r\n * pipeline) — the derivation is the only thing here that depends on it.\r\n *\r\n * Derived or configured, the root is VERIFIED at emit time against\r\n * `<root>/package.json`'s `name`; a root that is not `@warlock.js/web`\r\n * throws {@link WebPackageRootResolutionError} rather than being guessed at.\r\n */\r\n webRoot?: string;\r\n /**\r\n * NOT SUPPORTED. The client bundle always lands at `<build.outdir>/client`,\r\n * the layout the runtime half reads (`resolveHydrationClientUrl`) — the\r\n * runtime does not yet consult a relocated path. Setting this option throws\r\n * {@link ClientOutDirNotSupportedError}.\r\n */\r\n clientOutDir?: string;\r\n /** Client-build resolve aliases: specifier -> absolute path. */\r\n aliases?: Record<string, string>;\r\n /** Extra package names to keep external to the client bundle. */\r\n external?: string[];\r\n};\r\n\r\nfunction resolveClientOutDir(context: ConnectorBuildContext): string {\r\n return path.resolve(context.appRoot, context.options.outdir, \"client\");\r\n}\r\n\r\n/**\r\n * `build.clientOutDir` was set.\r\n *\r\n * The production runtime hardcodes where it reads the client bundle from\r\n * (`<outdir>/client`, via `resolveHydrationClientUrl`) — it does not yet\r\n * consult the build config for a relocated path. A build that honored a\r\n * custom `clientOutDir` would therefore produce an artifact the runtime\r\n * cannot find, silently. Rejected at build start, before any work happens,\r\n * rather than left to surface later as a missing-bundle failure in\r\n * production.\r\n */\r\nexport class ClientOutDirNotSupportedError extends Error {\r\n public constructor() {\r\n super(\r\n '\"build.clientOutDir\" is not supported: the production server reads the client bundle ' +\r\n \"from its default location and does not consult this option, so a build honoring it \" +\r\n \"would produce an artifact production cannot serve correctly. Remove \" +\r\n '\"build.clientOutDir\" from the build config.',\r\n );\r\n this.name = \"ClientOutDirNotSupportedError\";\r\n }\r\n}\r\n\r\n/** The package name every candidate web root must declare to be one. */\r\nconst WEB_PACKAGE_NAME = \"@warlock.js/web\";\r\n\r\n/**\r\n * The resolved `@warlock.js/web` root is not that package.\r\n *\r\n * Same posture as the manifest resolver's three named errors: a root that\r\n * cannot be proven is REJECTED, never quietly\r\n * repaired by walking upwards or falling back to `process.cwd()`. A wrong root\r\n * would otherwise surface much later as an unintelligible Vite entry failure.\r\n */\r\nexport class WebPackageRootResolutionError extends Error {\r\n public constructor(webRoot: string, reason: string, cause?: unknown) {\r\n super(\r\n `Cannot resolve the \"${WEB_PACKAGE_NAME}\" package root: \"${webRoot}\" ${reason}. ` +\r\n \"Pass `webRoot` explicitly in the web build options to point at the package root \" +\r\n `whose package.json declares \"name\": \"${WEB_PACKAGE_NAME}\".`,\r\n { cause },\r\n );\r\n this.name = \"WebPackageRootResolutionError\";\r\n }\r\n}\r\n\r\n/**\r\n * Proves that `webRoot` really is the `@warlock.js/web` package root by reading\r\n * `<webRoot>/package.json` and matching its `name`. Returns the path unchanged.\r\n */\r\nexport function assertWebPackageRoot(webRoot: string): string {\r\n const manifestPath = path.join(webRoot, \"package.json\");\r\n\r\n let raw: string;\r\n\r\n try {\r\n raw = readFileSync(manifestPath, \"utf-8\");\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(webRoot, \"has no readable package.json\", error);\r\n }\r\n\r\n let parsed: unknown;\r\n\r\n try {\r\n parsed = JSON.parse(raw);\r\n } catch (error) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json that is not valid JSON\",\r\n error,\r\n );\r\n }\r\n\r\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n \"has a package.json whose root is not a JSON object\",\r\n );\r\n }\r\n\r\n const name = (parsed as { name?: unknown }).name;\r\n\r\n if (name !== WEB_PACKAGE_NAME) {\r\n throw new WebPackageRootResolutionError(\r\n webRoot,\r\n `declares package name ${JSON.stringify(name)}, not \"${WEB_PACKAGE_NAME}\"`,\r\n );\r\n }\r\n\r\n return webRoot;\r\n}\r\n\r\nexport async function resolveWebPackageRoot(configured: string | undefined): Promise<string> {\r\n if (configured !== undefined) {\r\n return assertWebPackageRoot(path.resolve(configured));\r\n }\r\n\r\n const { fileURLToPath } = await import(\"node:url\");\r\n\r\n // `web/src/build/contribution.ts` published as `web/esm/build/contribution.js`\r\n // — two levels up is the package root under both layouts.\r\n return assertWebPackageRoot(\r\n path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\"),\r\n );\r\n}\r\n\r\n/**\r\n * Builds web's `ConnectorBuildContribution`.\r\n *\r\n * `generate` writes the page barrel into `ctx.productionDir` and patches\r\n * esbuild; `emit` produces the client bundle esbuild cannot. The two share one\r\n * closure facts — how many pages exist, the discovered route table, and the\r\n * exact app-public file list. A build with zero pages skips the hydration\r\n * bundle but still copies public files when present. `generate` itself does\r\n * NOT skip: the barrel and its entry import are emitted either way, so the\r\n * runtime can tell a page-free web build from a build without web.\r\n *\r\n * Connector plugins are a separate argument rather than a `WebBuildOptions`\r\n * field: the app constructs them once, the factory snapshots that array for\r\n * both dev and build, and this module keeps Vite as a type-only import until\r\n * `emit` dynamically loads the real client builder.\r\n */\r\nexport function createWebBuildContribution(\r\n options: WebBuildOptions = {},\r\n connectorPlugins: readonly PluginOption[] = [],\r\n): ConnectorBuildContribution {\r\n if (options.clientOutDir !== undefined) {\r\n throw new ClientOutDirNotSupportedError();\r\n }\r\n\r\n let pageCount = 0;\r\n let pageRoutes: import(\"./generate-pages-barrel\").PageRoutesManifest = {\r\n version: 1,\r\n routes: [],\r\n };\r\n let publicFiles: string[] = [];\r\n\r\n return {\r\n async generate(context: ConnectorBuildContext): Promise<ConnectorBuildGenerateResult | void> {\r\n const { generatePagesBarrel, WEB_ENTRY_IMPORT, WEB_ESBUILD_PATCH } =\r\n await import(\"./generate-pages-barrel\");\r\n const { collectPublicFiles } = await import(\"./public-files\");\r\n\r\n publicFiles = await collectPublicFiles(path.join(context.appRoot, \"public\"));\r\n\r\n const result = await generatePagesBarrel({\r\n appRoot: context.appRoot,\r\n productionDir: context.productionDir,\r\n srcDir: options.srcDir,\r\n // Derived from `resolveClientOutDir` — the SAME function `emit` passes\r\n // to the Vite build below — so the path baked into the manifest and\r\n // the path the bundle is written to are one expression, not two that\r\n // happen to agree today.\r\n // POSIX-normalised inline rather than via the generator's `toPosix`:\r\n // this module's static graph is `node:path` and types only, and the\r\n // generator is reached by `await import` precisely to keep it that way.\r\n clientDir: path\r\n .relative(context.appRoot, resolveClientOutDir(context))\r\n .split(path.sep)\r\n .join(\"/\"),\r\n publicFiles,\r\n });\r\n\r\n pageCount = result.pageCount;\r\n pageRoutes = result.pageRoutes;\r\n\r\n // Contributed unconditionally, zero pages included: the barrel is always\r\n // written, and the entry has to IMPORT it for the empty table to reach\r\n // the runtime. Withholding this line on zero pages would leave the\r\n // manifest absent, which is the runtime's signal for \"never built with\r\n // web\" — the exact confusion the always-written barrel removes.\r\n return { entryImports: [WEB_ENTRY_IMPORT], esbuild: WEB_ESBUILD_PATCH };\r\n },\r\n\r\n async emit(context: ConnectorBuildContext): Promise<void> {\r\n if (pageCount > 0) {\r\n const { buildWarlockHydrationClient } = await import(\"../vite\");\r\n const { appConventionAliases } = await import(\"../vite/app-convention-aliases\");\r\n\r\n // The app-tree convention (`web/*`, `app/*`) the dev server installs must\r\n // also reach the production build, from the SAME definition — see\r\n // `app-convention-aliases.ts`. Caller-supplied aliases come first so a\r\n // caller can win a conflict, matching the dev server's ordering.\r\n const callerAliases = Object.entries(options.aliases ?? {}).map(([find, replacement]) => ({\r\n find,\r\n replacement,\r\n }));\r\n\r\n await buildWarlockHydrationClient({\r\n appRoot: context.appRoot,\r\n webRoot: await resolveWebPackageRoot(options.webRoot),\r\n outDir: resolveClientOutDir(context),\r\n resolveAliases: [\r\n ...callerAliases,\r\n ...appConventionAliases(path.join(context.appRoot, options.srcDir ?? \"src\")),\r\n ],\r\n external: options.external,\r\n plugins: connectorPlugins,\r\n });\r\n }\r\n\r\n if (publicFiles.length > 0) {\r\n const { copyPublicFiles } = await import(\"./public-files\");\r\n await copyPublicFiles(\r\n path.join(context.appRoot, \"public\"),\r\n path.join(resolveClientOutDir(context), \"public\"),\r\n publicFiles,\r\n );\r\n }\r\n\r\n const { writePageRoutesManifest } = await import(\"./page-routes-manifest\");\r\n await writePageRoutesManifest(\r\n path.resolve(context.appRoot, context.options.outdir),\r\n pageRoutes,\r\n );\r\n },\r\n };\r\n}\r\n"],"mappings":"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"}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { composeRoutePath } from "../routing/compose-route-path.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, isNotFoundPageFile } from "../server/not-found-page.mjs";
|
|
3
3
|
import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
|
|
4
|
+
import { deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
|
|
4
5
|
import { canonicalizeRouteExport, resolvePageRouteName } from "../routing/route-identity.mjs";
|
|
5
|
-
import { NOT_FOUND_ROUTE_NAME, NOT_FOUND_ROUTE_PATH, NotFoundPageDeclaresRouteError, isNotFoundPageFile } from "../server/not-found-page.mjs";
|
|
6
|
-
import { METADATA_KEYS, OPEN_GRAPH_KEYS, TWITTER_KEYS } from "../metadata.mjs";
|
|
7
6
|
import { assertPageHasDefaultExport } from "./page-default-export.mjs";
|
|
7
|
+
import { UnknownMetadataKeyError, readMetadataKeys } from "./read-metadata-keys.mjs";
|
|
8
8
|
import { NonLiteralRouteExportError, readRouteExports } from "./read-route-exports.mjs";
|
|
9
|
-
import
|
|
9
|
+
import { toPosix } from "../shared/to-posix.mjs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import fs from "node:fs";
|
|
11
12
|
import { parse } from "@babel/parser";
|
|
12
13
|
|
|
13
14
|
//#region ../web/src/build/discover-pages.ts
|
|
@@ -44,10 +45,6 @@ import { parse } from "@babel/parser";
|
|
|
44
45
|
* ({@link "../routing/layout-policy.ts"}) owns the rule but may not touch a
|
|
45
46
|
* filesystem to learn the facts the rule needs. That classification is another
|
|
46
47
|
* parse, never an import: a layout is read exactly the way a page's `route` is.
|
|
47
|
-
*
|
|
48
|
-
* And it CHECKS the page's `metadata` keys ({@link UnknownMetadataKeyError}) —
|
|
49
|
-
* see that error and {@link readMetadataKeys} for why a type alone does not
|
|
50
|
-
* close that hole.
|
|
51
48
|
*/
|
|
52
49
|
function isDiscoveredRoutablePage(page) {
|
|
53
50
|
return page.type === "page";
|
|
@@ -93,9 +90,6 @@ var DuplicatePageRoutePathError = class extends Error {
|
|
|
93
90
|
this.name = "DuplicatePageRoutePathError";
|
|
94
91
|
}
|
|
95
92
|
};
|
|
96
|
-
function toPosix(value) {
|
|
97
|
-
return value.replace(/\\/g, "/");
|
|
98
|
-
}
|
|
99
93
|
function isDirectory(candidate) {
|
|
100
94
|
try {
|
|
101
95
|
return fs.statSync(candidate).isDirectory();
|
|
@@ -122,6 +116,113 @@ function byName(left, right) {
|
|
|
122
116
|
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
123
117
|
}
|
|
124
118
|
/**
|
|
119
|
+
* Every directory named exactly `web` beneath `src/app`, at any depth — the
|
|
120
|
+
* shape a page or a named layout takes BEFORE it moves to the one supported
|
|
121
|
+
* page root, {@link discoverWebRoots}. Descent stops at a `web` directory
|
|
122
|
+
* itself: a `web` folder nested inside another is not a shape this codebase
|
|
123
|
+
* uses, so there is nothing further down worth walking.
|
|
124
|
+
*/
|
|
125
|
+
function discoverIgnoredAppWebDirs(srcRoot) {
|
|
126
|
+
const appDir = path.join(srcRoot, "app");
|
|
127
|
+
if (!isDirectory(appDir)) return [];
|
|
128
|
+
const found = [];
|
|
129
|
+
const walk = (dir) => {
|
|
130
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
131
|
+
if (!entry.isDirectory()) continue;
|
|
132
|
+
const full = path.join(dir, entry.name);
|
|
133
|
+
if (entry.name === "web") found.push(full);
|
|
134
|
+
else walk(full);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
walk(appDir);
|
|
138
|
+
return found;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Every `*.page.tsx` and every `layout.tsx` (named or positional) beneath
|
|
142
|
+
* `src/app/**\/web/**`, sorted lexicographically (POSIX) for a stable
|
|
143
|
+
* diagnostic.
|
|
144
|
+
*
|
|
145
|
+
* `src/web` is the only page root (canon `4a6524f3`) — nothing found here is
|
|
146
|
+
* registered, ever. This function exists only to NAME what is being ignored
|
|
147
|
+
* instead of ignoring it in silence; see {@link warnIgnoredAppWebFiles}.
|
|
148
|
+
*
|
|
149
|
+
* A POSITIONAL `layout.tsx` is scanned here deliberately (closes board card
|
|
150
|
+
* `eb82c3c7`): it may declare `middleware`, and a page that loses its layout
|
|
151
|
+
* chain by moving to `src/app/**\/web/**` loses that guard with it, silently —
|
|
152
|
+
* the page still serves, just unprotected. This function does not read
|
|
153
|
+
* whether a given layout actually declares `middleware`; it reports every
|
|
154
|
+
* `layout.tsx` under the ignored tree, because a layout's whole job — prefix,
|
|
155
|
+
* middleware, loader — is lost the moment it is wired to nothing.
|
|
156
|
+
*/
|
|
157
|
+
function discoverIgnoredAppWebFiles(srcRoot) {
|
|
158
|
+
const found = [];
|
|
159
|
+
for (const webDir of discoverIgnoredAppWebDirs(srcRoot)) {
|
|
160
|
+
for (const file of walkFiles(webDir, (fileName) => fileName.endsWith(".page.tsx"))) found.push({
|
|
161
|
+
file,
|
|
162
|
+
kind: "page"
|
|
163
|
+
});
|
|
164
|
+
for (const file of walkFiles(webDir, (fileName) => fileName.endsWith(".layout.tsx") || fileName === "layout.tsx")) found.push({
|
|
165
|
+
file,
|
|
166
|
+
kind: "layout"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return found.sort((left, right) => compareStrings(toPosix(left.file), toPosix(right.file)));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Files {@link discoverIgnoredAppWebFiles} has already reported once this
|
|
173
|
+
* process — the "once per boot" guard. Keyed by absolute path rather than
|
|
174
|
+
* cleared between calls: {@link discoverPages} runs once per dev reload as
|
|
175
|
+
* well as once per production build, and the point of this set is that the
|
|
176
|
+
* same stray file does not get a fresh warning on every one of those calls
|
|
177
|
+
* within a single boot.
|
|
178
|
+
*/
|
|
179
|
+
const warnedIgnoredAppWebFiles = /* @__PURE__ */ new Set();
|
|
180
|
+
/** Renders one ignored file's path, relative to `appRoot`, POSIX. */
|
|
181
|
+
function namedFile(file, appRoot) {
|
|
182
|
+
return ` - ${toPosix(path.relative(appRoot, file))}`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The page half of the diagnostic: a missing page is visible the moment
|
|
186
|
+
* someone loads its URL, so this says only that it is absent from the route
|
|
187
|
+
* graph.
|
|
188
|
+
*/
|
|
189
|
+
function pageIgnoredMessage(files, appRoot) {
|
|
190
|
+
const plural = files.length === 1 ? "" : "s";
|
|
191
|
+
const named = files.map((file) => namedFile(file, appRoot)).join("\n");
|
|
192
|
+
return `${files.length} page file${plural} under src/app/**/web/** ${files.length === 1 ? "is" : "are"} ignored — the only supported page root is src/web/**, so ${files.length === 1 ? "it is" : "they are"} absent from the route graph:\n${named}\nMove ${files.length === 1 ? "it" : "them"} under src/web/** to register ${files.length === 1 ? "it" : "them"}.`;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The layout half of the diagnostic: worded differently from
|
|
196
|
+
* {@link pageIgnoredMessage} on purpose. A missing route guard is not visible
|
|
197
|
+
* at all — the page it would have gated still serves, just unprotected — so
|
|
198
|
+
* this names what is lost (`prefix`, `middleware`, `loader`) rather than
|
|
199
|
+
* merely saying a file was ignored.
|
|
200
|
+
*/
|
|
201
|
+
function layoutIgnoredMessage(files, appRoot) {
|
|
202
|
+
const plural = files.length === 1 ? "" : "s";
|
|
203
|
+
const named = files.map((file) => namedFile(file, appRoot)).join("\n");
|
|
204
|
+
return `${files.length} layout file${plural} under src/app/**/web/** ${files.length === 1 ? "is" : "are"} ignored — the only supported page root is src/web/**, so ${files.length === 1 ? "its" : "their"} \`prefix\`, \`middleware\` and \`loader\` apply to no page at all:\n${named}\nA page that relied on ${files.length === 1 ? "it" : "them"} for a guard — auth, a redirect, anything in \`middleware\` — is served with that guard silently absent. Move ${files.length === 1 ? "it" : "them"} under src/web/** to apply ${files.length === 1 ? "it" : "them"} again.`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The one place this module writes to the console. Reports every file
|
|
208
|
+
* {@link discoverIgnoredAppWebFiles} found and has not already warned about
|
|
209
|
+
* this boot — never crashes the build, because an application mid-migration
|
|
210
|
+
* off `src/app/**\/web/**` is legitimate, not broken.
|
|
211
|
+
*
|
|
212
|
+
* Pages and layouts are worded separately (see {@link pageIgnoredMessage},
|
|
213
|
+
* {@link layoutIgnoredMessage}) but printed in ONE `console.warn` call, so a
|
|
214
|
+
* boot that has both still prints once.
|
|
215
|
+
*/
|
|
216
|
+
function warnIgnoredAppWebFiles(files, appRoot) {
|
|
217
|
+
const unwarned = files.filter((entry) => !warnedIgnoredAppWebFiles.has(entry.file));
|
|
218
|
+
if (unwarned.length === 0) return;
|
|
219
|
+
for (const entry of unwarned) warnedIgnoredAppWebFiles.add(entry.file);
|
|
220
|
+
const pages = unwarned.filter((entry) => entry.kind === "page").map((entry) => entry.file);
|
|
221
|
+
const layouts = unwarned.filter((entry) => entry.kind === "layout").map((entry) => entry.file);
|
|
222
|
+
const sections = [...pages.length > 0 ? [pageIgnoredMessage(pages, appRoot)] : [], ...layouts.length > 0 ? [layoutIgnoredMessage(layouts, appRoot)] : []];
|
|
223
|
+
console.warn(sections.map((section) => `[warlock:web] ${section}`).join("\n\n"));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
125
226
|
* The subject list: every `*.page.tsx` under the page root, one call for the
|
|
126
227
|
* whole graph.
|
|
127
228
|
*
|
|
@@ -131,9 +232,19 @@ function byName(left, right) {
|
|
|
131
232
|
* `ssrLoadModule`-driven installer, chiefly) can share the walk without
|
|
132
233
|
* inheriting the static-parsing refusals that answering "what route is this"
|
|
133
234
|
* requires.
|
|
235
|
+
*
|
|
236
|
+
* ALSO the ignored-`src/app/**\/web/**` diagnostic's boot-path call site: dev's
|
|
237
|
+
* route installer (`../server/install-page-routes.ts`) calls this function,
|
|
238
|
+
* not {@link discoverPages}, so the warning has to fire from here too or a
|
|
239
|
+
* plain `warlock dev` boot never prints it — only a request for a page's
|
|
240
|
+
* hydration bundle would. `appRoot` is derived as this srcRoot's parent, which
|
|
241
|
+
* holds for every real caller: both this function and {@link discoverPages}
|
|
242
|
+
* are only ever handed `<appRoot>/src`.
|
|
134
243
|
*/
|
|
135
244
|
function discoverPageFiles(srcRoot) {
|
|
136
245
|
const found = [];
|
|
246
|
+
const ignoredAppWebFiles = discoverIgnoredAppWebFiles(srcRoot);
|
|
247
|
+
if (ignoredAppWebFiles.length > 0) warnIgnoredAppWebFiles(ignoredAppWebFiles, path.dirname(srcRoot));
|
|
137
248
|
for (const webRoot of discoverWebRoots(srcRoot)) for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(".page.tsx"))) found.push({
|
|
138
249
|
pageFile,
|
|
139
250
|
webRoot
|
|
@@ -229,202 +340,6 @@ function assertUniqueRoutePaths(pages, appRoot, explicitFiles) {
|
|
|
229
340
|
}
|
|
230
341
|
}
|
|
231
342
|
/**
|
|
232
|
-
* Raised when a page's `metadata` export declares a key nothing reads.
|
|
233
|
-
*
|
|
234
|
-
* THE POINT OF THIS ERROR IS THE UNANNOTATED CASE. A page that writes
|
|
235
|
-
* `export const metadata: PageMetadata = { tittle: "x" }` is already refused by
|
|
236
|
-
* TypeScript, and if that were the whole story this class would not need to
|
|
237
|
-
* exist. But the annotation is optional, nobody writes it, and
|
|
238
|
-
* `export const metadata = { tittle: "x" }` is a perfectly well-typed program:
|
|
239
|
-
* the compiler infers `{ tittle: string }`, has nothing to check it against, and
|
|
240
|
-
* says nothing. The page is then served with no `<title>` — not a wrong title, a
|
|
241
|
-
* missing one — and no error is raised anywhere, at build or at runtime, ever.
|
|
242
|
-
*
|
|
243
|
-
* So the check lives HERE instead, at the gate every page already passes
|
|
244
|
-
* through: a page that silently omits requested metadata is worse than a build
|
|
245
|
-
* that stops and says which line to fix.
|
|
246
|
-
*
|
|
247
|
-
* The alternative considered and rejected was a `defineMetadata({...})` wrapper,
|
|
248
|
-
* which would infer the type for free. It also puts framework ceremony in every
|
|
249
|
-
* page, and a page is meant to be two lines of framework surface (canon
|
|
250
|
-
* `6ea0662f`). The gate gets the same safety without spending that.
|
|
251
|
-
*/
|
|
252
|
-
var UnknownMetadataKeyError = class extends Error {
|
|
253
|
-
pageFile;
|
|
254
|
-
unknownKeys;
|
|
255
|
-
constructor(pageFile, unknownKeys) {
|
|
256
|
-
const findings = unknownKeys.map(({ container, key, line, suggestion }) => {
|
|
257
|
-
const where = `line ${line}: \`${container}.${key}\` — no such key.`;
|
|
258
|
-
return suggestion === void 0 ? where : `${where} Did you mean \`${suggestion}\`?`;
|
|
259
|
-
}).join("\n ");
|
|
260
|
-
super(`The \`metadata\` export of "${pageFile}" declares a key nothing reads:\n ${findings}\nNothing writes an unknown key to \`<head>\`, so the tag it was meant to produce would simply be absent from every response, with no error at build time or at runtime. The build refuses it here instead.
|
|
261
|
-
Known keys: ${METADATA_KEYS.join(", ")}.\n Inside \`openGraph\`: ${OPEN_GRAPH_KEYS.join(", ")}.\n Inside \`twitter\`: ${TWITTER_KEYS.join(", ")}.\nAnnotating the export — \`export const metadata: PageMetadata = { … }\` — gets you the same list as autocomplete in the editor, before the build runs.`);
|
|
262
|
-
this.pageFile = pageFile;
|
|
263
|
-
this.unknownKeys = unknownKeys;
|
|
264
|
-
this.name = "UnknownMetadataKeyError";
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
/** `as const`, `satisfies`, `!` and parentheses wrap a value without changing it. */
|
|
268
|
-
function unwrapValue(node) {
|
|
269
|
-
switch (node.type) {
|
|
270
|
-
case "TSAsExpression":
|
|
271
|
-
case "TSSatisfiesExpression":
|
|
272
|
-
case "TSNonNullExpression":
|
|
273
|
-
case "TypeCastExpression":
|
|
274
|
-
case "ParenthesizedExpression": return unwrapValue(node.expression);
|
|
275
|
-
default: return node;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
/** Levenshtein distance — small strings, so the plain two-row table is the whole cost. */
|
|
279
|
-
function editDistance(left, right) {
|
|
280
|
-
let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
281
|
-
for (let row = 1; row <= left.length; row++) {
|
|
282
|
-
const current = [row];
|
|
283
|
-
for (let column = 1; column <= right.length; column++) {
|
|
284
|
-
const substitution = previous[column - 1] + (left[row - 1] === right[column - 1] ? 0 : 1);
|
|
285
|
-
current[column] = Math.min(substitution, previous[column] + 1, current[column - 1] + 1);
|
|
286
|
-
}
|
|
287
|
-
previous = current;
|
|
288
|
-
}
|
|
289
|
-
return previous[right.length];
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* The known key the written one was probably meant to be.
|
|
293
|
-
*
|
|
294
|
-
* Two edits, because that covers the typos this exists for — `tittle`,
|
|
295
|
-
* `descriptoin`, `keywrods` — without reaching so far that `image` gets
|
|
296
|
-
* suggested for `alt`. Case is ignored first, so `Title` resolves exactly.
|
|
297
|
-
*/
|
|
298
|
-
function suggestKey(written, known) {
|
|
299
|
-
const lowered = written.toLowerCase();
|
|
300
|
-
const sameLetters = known.find((candidate) => candidate.toLowerCase() === lowered);
|
|
301
|
-
if (sameLetters !== void 0) return sameLetters;
|
|
302
|
-
let best;
|
|
303
|
-
let bestDistance = Number.POSITIVE_INFINITY;
|
|
304
|
-
for (const candidate of known) {
|
|
305
|
-
const distance = editDistance(lowered, candidate.toLowerCase());
|
|
306
|
-
if (distance < bestDistance) {
|
|
307
|
-
best = candidate;
|
|
308
|
-
bestDistance = distance;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return bestDistance <= 2 ? best : void 0;
|
|
312
|
-
}
|
|
313
|
-
/** The name an object key denotes, or `undefined` when knowing it needs evaluation. */
|
|
314
|
-
function propertyKeyName(property) {
|
|
315
|
-
if (property.computed) return void 0;
|
|
316
|
-
const { key } = property;
|
|
317
|
-
if (key.type === "Identifier") return key.name;
|
|
318
|
-
if (key.type === "StringLiteral") return key.value;
|
|
319
|
-
}
|
|
320
|
-
/** The nested objects that carry a key set of their own. */
|
|
321
|
-
const NESTED_METADATA_KEYS = {
|
|
322
|
-
openGraph: OPEN_GRAPH_KEYS,
|
|
323
|
-
twitter: TWITTER_KEYS
|
|
324
|
-
};
|
|
325
|
-
/**
|
|
326
|
-
* Every unknown key in one metadata object literal, and in the `openGraph` /
|
|
327
|
-
* `twitter` literals inside it.
|
|
328
|
-
*
|
|
329
|
-
* A SPREAD does not suppress the check, unlike the route reader's rule: a
|
|
330
|
-
* spread can only ADD keys, and no value it contributes can make a key written
|
|
331
|
-
* out beside it correct. A COMPUTED key is skipped — its name is not knowable
|
|
332
|
-
* without running the page, and refusing what cannot be read would fail builds
|
|
333
|
-
* that are fine. Both are silence in the narrow places where the parse genuinely
|
|
334
|
-
* does not know, and the annotation is the second net there.
|
|
335
|
-
*/
|
|
336
|
-
function collectUnknownKeys(object, allowed, container, into) {
|
|
337
|
-
for (const property of object.properties) {
|
|
338
|
-
if (property.type === "SpreadElement") continue;
|
|
339
|
-
const key = propertyKeyName(property);
|
|
340
|
-
if (key === void 0) continue;
|
|
341
|
-
if (!allowed.includes(key)) {
|
|
342
|
-
const suggestion = suggestKey(key, allowed);
|
|
343
|
-
into.push({
|
|
344
|
-
container,
|
|
345
|
-
key,
|
|
346
|
-
line: property.loc?.start.line ?? 0,
|
|
347
|
-
...suggestion === void 0 ? {} : { suggestion }
|
|
348
|
-
});
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
const nested = container === "metadata" ? NESTED_METADATA_KEYS[key] : void 0;
|
|
352
|
-
if (nested === void 0 || property.type !== "ObjectProperty") continue;
|
|
353
|
-
const value = unwrapValue(property.value);
|
|
354
|
-
if (value.type === "ObjectExpression") collectUnknownKeys(value, nested, `${container}.${key}`, into);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Every object literal a function form RETURNS, without descending into
|
|
359
|
-
* functions nested inside it — a callback's return value is not the metadata.
|
|
360
|
-
*
|
|
361
|
-
* A generic walk rather than a statement-by-statement one because a `return` is
|
|
362
|
-
* legal anywhere a statement is: inside an `if`, a `switch`, a `try`. Enumerating
|
|
363
|
-
* the statement types that may contain one is a list that is wrong the moment
|
|
364
|
-
* the language grows.
|
|
365
|
-
*/
|
|
366
|
-
function collectReturnedObjects(node, into) {
|
|
367
|
-
if (node === null || typeof node !== "object") return;
|
|
368
|
-
if (Array.isArray(node)) {
|
|
369
|
-
for (const item of node) collectReturnedObjects(item, into);
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
const candidate = node;
|
|
373
|
-
if (candidate.type === "FunctionDeclaration" || candidate.type === "FunctionExpression" || candidate.type === "ArrowFunctionExpression" || candidate.type === "ObjectMethod" || candidate.type === "ClassMethod") return;
|
|
374
|
-
if (candidate.type === "ReturnStatement") {
|
|
375
|
-
if (candidate.argument === null || candidate.argument === void 0) return;
|
|
376
|
-
const returned = unwrapValue(candidate.argument);
|
|
377
|
-
if (returned.type === "ObjectExpression") into.push(returned);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
for (const value of Object.values(node)) collectReturnedObjects(value, into);
|
|
381
|
-
}
|
|
382
|
-
/** The metadata object literals one `metadata` export declares, if any can be seen at all. */
|
|
383
|
-
function metadataObjectsOf(init) {
|
|
384
|
-
const value = unwrapValue(init);
|
|
385
|
-
if (value.type === "ObjectExpression") return [value];
|
|
386
|
-
if (value.type === "ArrowFunctionExpression" || value.type === "FunctionExpression") {
|
|
387
|
-
const body = unwrapValue(value.body);
|
|
388
|
-
if (body.type === "ObjectExpression") return [body];
|
|
389
|
-
const returned = [];
|
|
390
|
-
collectReturnedObjects(value.body, returned);
|
|
391
|
-
return returned;
|
|
392
|
-
}
|
|
393
|
-
return [];
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* The unknown keys a page's `metadata` export declares, read by PARSING — the
|
|
397
|
-
* same rule the rest of this module lives by, and the reason this check can run
|
|
398
|
-
* before anything is built.
|
|
399
|
-
*
|
|
400
|
-
* Empty for a page with no `metadata` export, for one whose metadata is a value
|
|
401
|
-
* this file cannot see into, and for a correct one.
|
|
402
|
-
*/
|
|
403
|
-
function readMetadataKeys(pageFile, source) {
|
|
404
|
-
let program;
|
|
405
|
-
try {
|
|
406
|
-
program = parse(source, {
|
|
407
|
-
sourceType: "module",
|
|
408
|
-
plugins: ["typescript", "jsx"],
|
|
409
|
-
errorRecovery: false
|
|
410
|
-
}).program;
|
|
411
|
-
} catch (error) {
|
|
412
|
-
throw new Error(`Cannot read the \`metadata\` export of "${pageFile}": the file could not be parsed (${error.message}). Fix the syntax error and the build will continue.`);
|
|
413
|
-
}
|
|
414
|
-
const unknownKeys = [];
|
|
415
|
-
for (const statement of program.body) {
|
|
416
|
-
if (statement.type !== "ExportNamedDeclaration" || statement.exportKind === "type") continue;
|
|
417
|
-
const { declaration } = statement;
|
|
418
|
-
if (declaration?.type !== "VariableDeclaration") continue;
|
|
419
|
-
for (const declarator of declaration.declarations) {
|
|
420
|
-
if (declarator.id.type !== "Identifier" || declarator.id.name !== "metadata") continue;
|
|
421
|
-
if (declarator.init === null || declarator.init === void 0) continue;
|
|
422
|
-
for (const object of metadataObjectsOf(declarator.init)) collectUnknownKeys(object, METADATA_KEYS, "metadata", unknownKeys);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return unknownKeys;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
343
|
* The declared exports of one file, or a thrown
|
|
429
344
|
* {@link NonLiteralRouteExportError} when they cannot be read without running
|
|
430
345
|
* the application. Layouts are read once per run and remembered: a layout is
|
|
@@ -508,6 +423,11 @@ function readLayoutShape(layoutFile, cache) {
|
|
|
508
423
|
* with a silently missing tag; and a page whose layout chain holds more than one RENDERING layout, which
|
|
509
424
|
* the production installer would refuse anyway — discovery refuses it first so
|
|
510
425
|
* that artefact is never produced.
|
|
426
|
+
*
|
|
427
|
+
* It also WARNS — never refuses — when it finds a `*.page.tsx` or named
|
|
428
|
+
* `*.layout.tsx` under `src/app/**\/web/**`, the shape the page root took
|
|
429
|
+
* before it moved to `src/web`. Those files are not registered either way;
|
|
430
|
+
* see {@link discoverIgnoredAppWebFiles}.
|
|
511
431
|
*/
|
|
512
432
|
function discoverPages(options) {
|
|
513
433
|
const { appRoot } = options;
|
|
@@ -521,6 +441,8 @@ function discoverPages(options) {
|
|
|
521
441
|
const pages = [];
|
|
522
442
|
const explicitRouteFiles = /* @__PURE__ */ new Set();
|
|
523
443
|
let errorPage;
|
|
444
|
+
const ignoredAppWebFiles = discoverIgnoredAppWebFiles(srcRoot);
|
|
445
|
+
if (ignoredAppWebFiles.length > 0) warnIgnoredAppWebFiles(ignoredAppWebFiles, appRoot);
|
|
524
446
|
for (const webRoot of webRoots) for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(".page.tsx"))) {
|
|
525
447
|
const pageSource = fs.readFileSync(pageFile, "utf-8");
|
|
526
448
|
assertPageHasDefaultExport(relativeToApp(pageFile), pageSource);
|
|
@@ -581,5 +503,5 @@ function discoverPages(options) {
|
|
|
581
503
|
}
|
|
582
504
|
|
|
583
505
|
//#endregion
|
|
584
|
-
export {
|
|
506
|
+
export { ErrorPageDeclaresRouteError, discoverPageFiles, discoverPages, discoverWebRoots, isDiscoveredRoutablePage, isErrorPageFile, isFile, layoutChainFor, walkFiles };
|
|
585
507
|
//# sourceMappingURL=discover-pages.mjs.map
|