@warlock.js/web 5.0.2 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +250 -0
- package/README.md +126 -12
- package/esm/build/contribution.d.mts +11 -4
- package/esm/build/contribution.mjs +46 -22
- package/esm/build/contribution.mjs.map +1 -1
- package/esm/build/discover-pages.mjs +318 -44
- package/esm/build/discover-pages.mjs.map +1 -1
- package/esm/build/generate-client-registry.mjs +47 -13
- package/esm/build/generate-client-registry.mjs.map +1 -1
- package/esm/build/generate-pages-barrel.mjs +55 -13
- package/esm/build/generate-pages-barrel.mjs.map +1 -1
- package/esm/build/page-default-export.mjs +48 -0
- package/esm/build/page-default-export.mjs.map +1 -0
- package/esm/build/page-routes-manifest.mjs +21 -0
- package/esm/build/page-routes-manifest.mjs.map +1 -0
- package/esm/build/public-files.mjs +45 -0
- package/esm/build/public-files.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +1 -1
- package/esm/build/read-route-exports.mjs.map +1 -1
- package/esm/client/build-hydrated-tree.mjs +40 -9
- package/esm/client/build-hydrated-tree.mjs.map +1 -1
- package/esm/client/hydrate-page.mjs +1 -1
- package/esm/client/hydrate-page.mjs.map +1 -1
- package/esm/client/navigation/current-route.d.mts +1 -1
- package/esm/client/navigation/current-route.mjs +1 -1
- package/esm/client/navigation/current-route.mjs.map +1 -1
- package/esm/client/navigation/fetch-page-data.mjs +1 -1
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -1
- package/esm/client/navigation/navigation-root.mjs +44 -7
- package/esm/client/navigation/navigation-root.mjs.map +1 -1
- package/esm/client/navigation/prefetch.mjs +1 -1
- package/esm/client/navigation/prefetch.mjs.map +1 -1
- package/esm/client/navigation/refresh.d.mts +1 -1
- package/esm/client/navigation/refresh.mjs +1 -1
- package/esm/client/navigation/refresh.mjs.map +1 -1
- package/esm/client/navigation/scroll-to-fragment.mjs +26 -0
- package/esm/client/navigation/scroll-to-fragment.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +1 -1
- package/esm/client/navigation/verbs.mjs +1 -1
- package/esm/client/navigation/verbs.mjs.map +1 -1
- package/esm/client/runtime/index.d.mts +2 -1
- package/esm/client/runtime/index.mjs +2 -1
- package/esm/client/runtime/manifest.d.mts +1 -1
- package/esm/client/runtime/manifest.mjs +3 -2
- package/esm/client/runtime/manifest.mjs.map +1 -1
- package/esm/client/runtime/matcher.d.mts +13 -1
- package/esm/client/runtime/matcher.mjs +13 -1
- package/esm/client/runtime/matcher.mjs.map +1 -1
- package/esm/client/runtime/types.d.mts +15 -2
- package/esm/components/default-app.mjs +1 -1
- package/esm/components/default-app.mjs.map +1 -1
- package/esm/components/document-context.d.mts +40 -2
- package/esm/components/document-context.mjs +1 -1
- package/esm/components/document-context.mjs.map +1 -1
- package/esm/components/head.d.mts +1 -1
- package/esm/components/head.mjs +1 -1
- package/esm/components/head.mjs.map +1 -1
- package/esm/components/link.d.mts +1 -1
- package/esm/components/link.mjs +1 -1
- package/esm/components/link.mjs.map +1 -1
- package/esm/components/scripts.d.mts +1 -1
- package/esm/components/scripts.mjs +4 -2
- package/esm/components/scripts.mjs.map +1 -1
- package/esm/context.d.mts +6 -102
- package/esm/hydration/index.mjs +1 -1
- package/esm/hydration/index.mjs.map +1 -1
- package/esm/hydration-payload.d.mts +1 -1
- package/esm/hydration-payload.mjs +37 -8
- package/esm/hydration-payload.mjs.map +1 -1
- package/esm/index.d.mts +4 -3
- package/esm/loaders.d.mts +7 -36
- package/esm/metadata.d.mts +15 -1
- package/esm/metadata.mjs +45 -0
- package/esm/metadata.mjs.map +1 -0
- package/esm/props.d.mts +16 -8
- package/esm/route.d.mts +2 -17
- package/esm/routing/compose-route-path.d.mts +1 -1
- package/esm/routing/compose-route-path.mjs +1 -1
- package/esm/routing/compose-route-path.mjs.map +1 -1
- package/esm/routing/data-request.mjs +1 -1
- package/esm/routing/data-request.mjs.map +1 -1
- package/esm/routing/filesystem-route.mjs +45 -0
- package/esm/routing/filesystem-route.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +1 -1
- package/esm/routing/layout-policy.mjs.map +1 -1
- package/esm/routing/navigator.mjs +1 -1
- package/esm/routing/navigator.mjs.map +1 -1
- package/esm/routing/query-string.d.mts +1 -1
- package/esm/routing/query-string.mjs +1 -1
- package/esm/routing/query-string.mjs.map +1 -1
- package/esm/routing/route-identity.mjs +1 -1
- package/esm/routing/route-identity.mjs.map +1 -1
- package/esm/routing/route-table.d.mts +1 -1
- package/esm/routing/route-table.mjs +1 -1
- package/esm/routing/route-table.mjs.map +1 -1
- package/esm/routing/router-events.d.mts +1 -1
- package/esm/routing/router-events.mjs +1 -1
- package/esm/routing/router-events.mjs.map +1 -1
- package/esm/routing/url-fragment.mjs +120 -0
- package/esm/routing/url-fragment.mjs.map +1 -0
- package/esm/runtime/register-modules.d.mts +19 -0
- package/esm/runtime/register-modules.mjs +22 -0
- package/esm/runtime/register-modules.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +2 -1
- package/esm/server/build-hydration-payload.mjs.map +1 -1
- package/esm/server/client-asset-url-prefix.d.mts +1 -1
- package/esm/server/client-asset-url-prefix.mjs +1 -1
- package/esm/server/client-asset-url-prefix.mjs.map +1 -1
- package/esm/server/create-page-module-loader.d.mts +1 -1
- package/esm/server/create-page-module-loader.mjs +2 -1
- package/esm/server/create-page-module-loader.mjs.map +1 -1
- package/esm/server/create-page-route-handler.d.mts +58 -6
- package/esm/server/create-page-route-handler.mjs +127 -40
- package/esm/server/create-page-route-handler.mjs.map +1 -1
- package/esm/server/dev-server.mjs +2 -13
- package/esm/server/dev-server.mjs.map +1 -1
- package/esm/server/error-page.d.mts +14 -0
- package/esm/server/error-page.mjs +39 -0
- package/esm/server/error-page.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +7 -4
- package/esm/server/execute-page-request.mjs +79 -80
- package/esm/server/execute-page-request.mjs.map +1 -1
- package/esm/server/execute-page-request.types.d.mts +33 -92
- package/esm/server/hydration-client-url.d.mts +1 -1
- package/esm/server/hydration-client-url.mjs +2 -2
- package/esm/server/hydration-client-url.mjs.map +1 -1
- package/esm/server/index.d.mts +6 -5
- package/esm/server/index.mjs +4 -4
- package/esm/server/install-page-routes-from-manifest.d.mts +17 -7
- package/esm/server/install-page-routes-from-manifest.mjs +91 -8
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -1
- package/esm/server/install-page-routes.d.mts +32 -13
- package/esm/server/install-page-routes.mjs +112 -32
- package/esm/server/install-page-routes.mjs.map +1 -1
- package/esm/server/install-production-page-routes.mjs +3 -31
- package/esm/server/install-production-page-routes.mjs.map +1 -1
- package/esm/server/match-page-route.mjs +2 -2
- package/esm/server/match-page-route.mjs.map +1 -1
- package/esm/server/not-found-page.d.mts +126 -0
- package/esm/server/not-found-page.mjs +157 -0
- package/esm/server/not-found-page.mjs.map +1 -0
- package/esm/server/page-context.d.mts +3 -9
- package/esm/server/page-context.mjs +3 -16
- package/esm/server/page-context.mjs.map +1 -1
- package/esm/server/page-file-change.mjs +77 -0
- package/esm/server/page-file-change.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +25 -10
- package/esm/server/page-manifest.mjs +1 -1
- package/esm/server/page-manifest.mjs.map +1 -1
- package/esm/server/page-render-bundle.mjs +38 -0
- package/esm/server/page-render-bundle.mjs.map +1 -0
- package/esm/server/page-route-reload.mjs +54 -0
- package/esm/server/page-route-reload.mjs.map +1 -0
- package/esm/server/register-production-public-files.mjs +40 -0
- package/esm/server/register-production-public-files.mjs.map +1 -0
- package/esm/server/render-page.d.mts +11 -14
- package/esm/server/render-page.mjs +154 -28
- package/esm/server/render-page.mjs.map +1 -1
- package/esm/server/resolve-page-metadata.mjs +2 -2
- package/esm/server/resolve-page-metadata.mjs.map +1 -1
- package/esm/server/resolve-validation-data.mjs +1 -1
- package/esm/server/resolve-validation-data.mjs.map +1 -1
- package/esm/server/settle-page-response.d.mts +58 -1
- package/esm/server/settle-page-response.mjs +111 -46
- package/esm/server/settle-page-response.mjs.map +1 -1
- package/esm/server/stylesheet-urls.d.mts +41 -30
- package/esm/server/stylesheet-urls.mjs +137 -52
- package/esm/server/stylesheet-urls.mjs.map +1 -1
- package/esm/server/unregistered-pages.mjs +42 -0
- package/esm/server/unregistered-pages.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +2 -3
- package/esm/server/web-connector-factory.mjs +6 -4
- package/esm/server/web-connector-factory.mjs.map +1 -1
- package/esm/server/web-connector.d.mts +1 -1
- package/esm/server/web-connector.mjs +233 -18
- package/esm/server/web-connector.mjs.map +1 -1
- package/esm/shared.d.mts +18 -3
- package/esm/shared.mjs +3 -7
- package/esm/shared.mjs.map +1 -1
- package/esm/validation.d.mts +1 -1
- package/esm/vite/app-convention-aliases.mjs +1 -1
- package/esm/vite/app-convention-aliases.mjs.map +1 -1
- package/esm/vite/build-client.d.mts +1 -1
- package/esm/vite/build-client.mjs +2 -2
- package/esm/vite/build-client.mjs.map +1 -1
- package/esm/vite/gate-a-resolve.d.mts +1 -1
- package/esm/vite/gate-a-resolve.mjs +67 -13
- package/esm/vite/gate-a-resolve.mjs.map +1 -1
- package/esm/vite/gate-b-secrets.d.mts +1 -1
- package/esm/vite/gate-b-secrets.mjs +35 -17
- package/esm/vite/gate-b-secrets.mjs.map +1 -1
- package/esm/vite/gate-c-verify.d.mts +1 -1
- package/esm/vite/gate-c-verify.mjs +6 -6
- package/esm/vite/gate-c-verify.mjs.map +1 -1
- package/esm/vite/hydration-entries.d.mts +1 -1
- package/esm/vite/hydration-entries.mjs +10 -6
- package/esm/vite/hydration-entries.mjs.map +1 -1
- package/esm/vite/index.d.mts +9 -6
- package/esm/vite/index.mjs +124 -11
- package/esm/vite/index.mjs.map +1 -1
- package/esm/vite/page-registry-plugin.d.mts +22 -3
- package/esm/vite/page-registry-plugin.mjs +247 -2
- package/esm/vite/page-registry-plugin.mjs.map +1 -1
- package/esm/vite/projection.d.mts +2 -2
- package/esm/vite/projection.mjs +38 -15
- package/esm/vite/projection.mjs.map +1 -1
- package/llms-full.txt +241 -49
- package/llms.txt +6 -2
- package/package.json +3 -3
- package/skills/add-web-to-an-app/SKILL.md +3 -3
- package/skills/create-a-page/SKILL.md +130 -7
- package/skills/load-page-data/SKILL.md +92 -12
- package/skills/navigate-on-the-client/SKILL.md +3 -1
- package/skills/serve-styles/SKILL.md +13 -12
- package/skills/use-layouts/SKILL.md +20 -8
- package/skills/write-the-root/SKILL.md +5 -1
- package/esm/server/buffered-response.d.mts +0 -58
- package/esm/server/buffered-response.mjs +0 -115
- package/esm/server/buffered-response.mjs.map +0 -1
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { composeRoutePath } from "../routing/compose-route-path.mjs";
|
|
2
|
+
import { deriveFilesystemRouteName, deriveFilesystemRoutePath } from "../routing/filesystem-route.mjs";
|
|
2
3
|
import { NestedLayoutsNotSupportedError, selectPageLayout } from "../routing/layout-policy.mjs";
|
|
3
4
|
import { deriveFallbackRouteName } 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
|
+
import { assertPageHasDefaultExport } from "./page-default-export.mjs";
|
|
4
8
|
import { NonLiteralRouteExportError, readRouteExports } from "./read-route-exports.mjs";
|
|
5
|
-
import path from "node:path";
|
|
6
9
|
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
7
11
|
import { parse } from "@babel/parser";
|
|
8
12
|
|
|
9
|
-
//#region
|
|
13
|
+
//#region web/src/build/discover-pages.ts
|
|
10
14
|
/**
|
|
11
15
|
* Static discovery of the application's page graph — the ONE scan every
|
|
12
16
|
* provider shares.
|
|
@@ -29,20 +33,40 @@ import { parse } from "@babel/parser";
|
|
|
29
33
|
* `route` and each layout's declared `prefix` by PARSING the source
|
|
30
34
|
* ({@link readRouteExports}). It still imports no application module.
|
|
31
35
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* deliberately and in one direction: build and boot agree because one of them
|
|
38
|
-
* copies the other, not because two conventions were written to match.
|
|
36
|
+
* An explicit `route` export wins. Otherwise the URL is derived from the page's
|
|
37
|
+
* path beneath `src/web`: directories contribute segments, `(groups)` do not,
|
|
38
|
+
* `index.page.tsx` claims its directory, and `[id]` becomes `:id`. A layout
|
|
39
|
+
* `prefix` replaces its own directory segment. The same recipe is emitted to
|
|
40
|
+
* every provider so build and boot cannot drift.
|
|
39
41
|
*
|
|
40
42
|
* Discovery also CLASSIFIES each layout — does its module have a default
|
|
41
43
|
* export, does it export `middleware` — because the layout policy
|
|
42
44
|
* ({@link "../routing/layout-policy.ts"}) owns the rule but may not touch a
|
|
43
45
|
* filesystem to learn the facts the rule needs. That classification is another
|
|
44
46
|
* 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.
|
|
45
51
|
*/
|
|
52
|
+
function isDiscoveredRoutablePage(page) {
|
|
53
|
+
return page.type === "page";
|
|
54
|
+
}
|
|
55
|
+
function isErrorPageFile(file) {
|
|
56
|
+
return path.basename(file) === "error.page.tsx";
|
|
57
|
+
}
|
|
58
|
+
var DuplicateErrorPageError = class extends Error {
|
|
59
|
+
constructor(firstFile, secondFile) {
|
|
60
|
+
super(`Two error pages were found: "${firstFile}" and "${secondFile}". An application may own exactly one \`error.page.tsx\` anywhere beneath src/web.`);
|
|
61
|
+
this.name = "DuplicateErrorPageError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var ErrorPageDeclaresRouteError = class extends Error {
|
|
65
|
+
constructor(pageFile) {
|
|
66
|
+
super(`The error page "${pageFile}" exports \`route\`. error.page.tsx is an error boundary, not a browsable page; remove the route export.`);
|
|
67
|
+
this.name = "ErrorPageDeclaresRouteError";
|
|
68
|
+
}
|
|
69
|
+
};
|
|
46
70
|
/** Raised when two pages claim one route name. */
|
|
47
71
|
var DuplicatePageRouteNameError = class extends Error {
|
|
48
72
|
routeName;
|
|
@@ -56,6 +80,19 @@ var DuplicatePageRouteNameError = class extends Error {
|
|
|
56
80
|
this.name = "DuplicatePageRouteNameError";
|
|
57
81
|
}
|
|
58
82
|
};
|
|
83
|
+
/** Raised when two different page files resolve to one effective URL. */
|
|
84
|
+
var DuplicatePageRoutePathError = class extends Error {
|
|
85
|
+
routePath;
|
|
86
|
+
firstFile;
|
|
87
|
+
secondFile;
|
|
88
|
+
constructor(routePath, firstFile, secondFile) {
|
|
89
|
+
super(`Two pages resolve to the same route path "${routePath}": "${firstFile}" and "${secondFile}". Each URL may identify exactly one page. Rename or move one file, or give one page an explicit route with a different path.`);
|
|
90
|
+
this.routePath = routePath;
|
|
91
|
+
this.firstFile = firstFile;
|
|
92
|
+
this.secondFile = secondFile;
|
|
93
|
+
this.name = "DuplicatePageRoutePathError";
|
|
94
|
+
}
|
|
95
|
+
};
|
|
59
96
|
function toPosix(value) {
|
|
60
97
|
return value.replace(/\\/g, "/");
|
|
61
98
|
}
|
|
@@ -74,27 +111,18 @@ function isFile(candidate) {
|
|
|
74
111
|
}
|
|
75
112
|
}
|
|
76
113
|
/**
|
|
77
|
-
* The
|
|
78
|
-
*
|
|
79
|
-
* neither has zero pages, which is a legal empty state.
|
|
114
|
+
* The page root. It is optional; a project without it has zero pages, which is
|
|
115
|
+
* a legal empty state.
|
|
80
116
|
*/
|
|
81
117
|
function discoverWebRoots(srcRoot) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
if (isDirectory(globalRoot)) roots.push(globalRoot);
|
|
85
|
-
const appDir = path.join(srcRoot, "app");
|
|
86
|
-
if (isDirectory(appDir)) for (const entry of fs.readdirSync(appDir, { withFileTypes: true }).sort(byName)) {
|
|
87
|
-
if (!entry.isDirectory()) continue;
|
|
88
|
-
const moduleWebRoot = path.join(appDir, entry.name, "web");
|
|
89
|
-
if (isDirectory(moduleWebRoot)) roots.push(moduleWebRoot);
|
|
90
|
-
}
|
|
91
|
-
return roots;
|
|
118
|
+
const webRoot = path.join(srcRoot, "web");
|
|
119
|
+
return isDirectory(webRoot) ? [webRoot] : [];
|
|
92
120
|
}
|
|
93
121
|
function byName(left, right) {
|
|
94
122
|
return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
95
123
|
}
|
|
96
124
|
/**
|
|
97
|
-
* The subject list: every `*.page.tsx` under
|
|
125
|
+
* The subject list: every `*.page.tsx` under the page root, one call for the
|
|
98
126
|
* whole graph.
|
|
99
127
|
*
|
|
100
128
|
* Unlike {@link discoverPages}, this reads no `route` or `prefix` export and
|
|
@@ -179,26 +207,238 @@ function assertUniqueRouteNames(pages, appRoot) {
|
|
|
179
207
|
fileByRouteName.set(page.routeName, relative);
|
|
180
208
|
}
|
|
181
209
|
}
|
|
182
|
-
/**
|
|
183
|
-
|
|
210
|
+
/**
|
|
211
|
+
* A page's declared `route.path` is a choice its author made deliberately, so
|
|
212
|
+
* two pages that both declare the same path are the author's call, not an
|
|
213
|
+
* accident — {@link assertUniqueRouteNames} is what keeps each of them
|
|
214
|
+
* reachable by name. What this refuses is a COLLISION NOBODY CHOSE: at least
|
|
215
|
+
* one side's path is filesystem-derived, which is exactly the case a rename
|
|
216
|
+
* or a new file can produce without anyone noticing two pages now answer the
|
|
217
|
+
* same URL. The reserved not-found path (`"*"`) is never a candidate here
|
|
218
|
+
* either way — it identifies no browsable URL, and {@link assertUniqueRouteNames}
|
|
219
|
+
* already refuses a second `404.page.tsx` by its own reserved name.
|
|
220
|
+
*/
|
|
221
|
+
function assertUniqueRoutePaths(pages, appRoot, explicitFiles) {
|
|
222
|
+
const fileByRoutePath = /* @__PURE__ */ new Map();
|
|
223
|
+
for (const page of pages) {
|
|
224
|
+
if (page.routePath === "*") continue;
|
|
225
|
+
const existing = fileByRoutePath.get(page.routePath);
|
|
226
|
+
const relative = toPosix(path.relative(appRoot, page.pageFile));
|
|
227
|
+
if (existing !== void 0 && !(explicitFiles.has(existing) && explicitFiles.has(relative))) throw new DuplicatePageRoutePathError(page.routePath, existing, relative);
|
|
228
|
+
if (existing === void 0) fileByRoutePath.set(page.routePath, relative);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
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 {
|
|
184
253
|
pageFile;
|
|
185
|
-
|
|
186
|
-
|
|
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.`);
|
|
187
262
|
this.pageFile = pageFile;
|
|
188
|
-
this.
|
|
263
|
+
this.unknownKeys = unknownKeys;
|
|
264
|
+
this.name = "UnknownMetadataKeyError";
|
|
189
265
|
}
|
|
190
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
|
+
}
|
|
191
427
|
/**
|
|
192
428
|
* The declared exports of one file, or a thrown
|
|
193
429
|
* {@link NonLiteralRouteExportError} when they cannot be read without running
|
|
194
430
|
* the application. Layouts are read once per run and remembered: a layout is
|
|
195
431
|
* the nearest one for every page beside it, and parsing it once per page would
|
|
196
432
|
* be the same answer bought repeatedly.
|
|
433
|
+
*
|
|
434
|
+
* `source` is the file's text when the caller already holds it — the page loop
|
|
435
|
+
* reads each page once and spends that read on both the route declarations and
|
|
436
|
+
* the metadata check, rather than opening the same file twice.
|
|
197
437
|
*/
|
|
198
|
-
function readDeclarations(sourceFile, cache) {
|
|
438
|
+
function readDeclarations(sourceFile, cache, source) {
|
|
199
439
|
let result = cache.get(sourceFile);
|
|
200
440
|
if (result === void 0) {
|
|
201
|
-
result = readRouteExports(sourceFile);
|
|
441
|
+
result = readRouteExports(sourceFile, source);
|
|
202
442
|
cache.set(sourceFile, result);
|
|
203
443
|
}
|
|
204
444
|
if (!result.ok) throw new NonLiteralRouteExportError(result.rejection);
|
|
@@ -254,17 +494,18 @@ function readLayoutShape(layoutFile, cache) {
|
|
|
254
494
|
return shape;
|
|
255
495
|
}
|
|
256
496
|
/**
|
|
257
|
-
* Scans
|
|
497
|
+
* Scans the page root and returns the pages in a defined total order.
|
|
258
498
|
*
|
|
259
499
|
* Zero pages is a legal result, not an error: a project may be configured
|
|
260
500
|
* with web and have nothing to serve yet. What is an error is a `*.page.tsx`
|
|
261
|
-
* with
|
|
262
|
-
*
|
|
263
|
-
* production 404s on; two pages claiming one route name, which this refuses
|
|
501
|
+
* with a route or metadata export that cannot be read statically; two pages
|
|
502
|
+
* claiming one effective path or route name, which this refuses
|
|
264
503
|
* to return at all — the alternative is an artefact in which one of them is
|
|
265
504
|
* silently unreachable; a `route` or `prefix` that cannot be read without
|
|
266
505
|
* running the application, which is refused before any page is reported at
|
|
267
|
-
* all;
|
|
506
|
+
* all; a `metadata` export declaring a key nothing reads, which no compiler
|
|
507
|
+
* catches unless the page opted into the type and which otherwise serves a page
|
|
508
|
+
* with a silently missing tag; and a page whose layout chain holds more than one RENDERING layout, which
|
|
268
509
|
* the production installer would refuse anyway — discovery refuses it first so
|
|
269
510
|
* that artefact is never produced.
|
|
270
511
|
*/
|
|
@@ -278,9 +519,27 @@ function discoverPages(options) {
|
|
|
278
519
|
const layoutShapes = /* @__PURE__ */ new Map();
|
|
279
520
|
const relativeToApp = (file) => toPosix(path.relative(appRoot, file));
|
|
280
521
|
const pages = [];
|
|
522
|
+
const explicitRouteFiles = /* @__PURE__ */ new Set();
|
|
523
|
+
let errorPage;
|
|
281
524
|
for (const webRoot of webRoots) for (const pageFile of walkFiles(webRoot, (fileName) => fileName.endsWith(".page.tsx"))) {
|
|
282
|
-
const
|
|
283
|
-
|
|
525
|
+
const pageSource = fs.readFileSync(pageFile, "utf-8");
|
|
526
|
+
assertPageHasDefaultExport(relativeToApp(pageFile), pageSource);
|
|
527
|
+
const { route } = readDeclarations(pageFile, declarations, pageSource);
|
|
528
|
+
if (isErrorPageFile(pageFile)) {
|
|
529
|
+
if (route !== void 0) throw new ErrorPageDeclaresRouteError(relativeToApp(pageFile));
|
|
530
|
+
if (errorPage !== void 0) throw new DuplicateErrorPageError(relativeToApp(errorPage.pageFile), relativeToApp(pageFile));
|
|
531
|
+
errorPage = {
|
|
532
|
+
type: "error",
|
|
533
|
+
pageFile,
|
|
534
|
+
webRoot,
|
|
535
|
+
...hasAppFile ? { appFile } : {}
|
|
536
|
+
};
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const isNotFoundPage = isNotFoundPageFile(pageFile);
|
|
540
|
+
if (isNotFoundPage && route !== void 0) throw new NotFoundPageDeclaresRouteError(relativeToApp(pageFile));
|
|
541
|
+
const unknownMetadataKeys = readMetadataKeys(relativeToApp(pageFile), pageSource);
|
|
542
|
+
if (unknownMetadataKeys.length > 0) throw new UnknownMetadataKeyError(relativeToApp(pageFile), unknownMetadataKeys);
|
|
284
543
|
const layouts = layoutChainFor(pageFile, webRoot);
|
|
285
544
|
const shapes = layouts.map((layoutFile) => readLayoutShape(layoutFile, layoutShapes));
|
|
286
545
|
const selection = selectPageLayout(layouts.map((layout, index) => ({
|
|
@@ -290,24 +549,39 @@ function discoverPages(options) {
|
|
|
290
549
|
if (selection.type === "rejected") throw new NestedLayoutsNotSupportedError(relativeToApp(pageFile), selection.layouts.map(relativeToApp));
|
|
291
550
|
const middlewareLayouts = layouts.filter((_, index) => shapes[index].hasMiddleware);
|
|
292
551
|
const layoutPrefix = layouts.reduce((composed, layoutFile) => composeRoutePath(composed, readDeclarations(layoutFile, declarations).prefix ?? "/"), "/");
|
|
552
|
+
const relativePageFile = toPosix(path.relative(webRoot, pageFile));
|
|
553
|
+
const layoutPrefixes = Object.fromEntries(layouts.flatMap((layoutFile) => {
|
|
554
|
+
const prefix = readDeclarations(layoutFile, declarations).prefix;
|
|
555
|
+
if (prefix === void 0) return [];
|
|
556
|
+
return [[toPosix(path.relative(webRoot, path.dirname(layoutFile))), prefix]];
|
|
557
|
+
}));
|
|
558
|
+
const effectiveRoutePath = isNotFoundPage ? "*" : route ? composeRoutePath(layoutPrefix, route.path) : deriveFilesystemRoutePath({
|
|
559
|
+
pageFile: relativePageFile,
|
|
560
|
+
layoutPrefixes
|
|
561
|
+
});
|
|
562
|
+
if (!isNotFoundPage && route !== void 0) explicitRouteFiles.add(relativeToApp(pageFile));
|
|
293
563
|
pages.push({
|
|
294
|
-
|
|
564
|
+
type: "page",
|
|
565
|
+
routeName: isNotFoundPage ? NOT_FOUND_ROUTE_NAME : route?.name ?? (route ? deriveFallbackRouteName({
|
|
295
566
|
routePath: route.path,
|
|
296
567
|
sourceFile: relativeToApp(pageFile)
|
|
297
|
-
}),
|
|
298
|
-
routePath:
|
|
568
|
+
}) : deriveFilesystemRouteName(relativePageFile)),
|
|
569
|
+
routePath: effectiveRoutePath,
|
|
299
570
|
pageFile,
|
|
300
571
|
webRoot,
|
|
301
|
-
layouts,
|
|
302
|
-
middlewareLayouts,
|
|
572
|
+
layouts: isNotFoundPage ? [] : layouts,
|
|
573
|
+
middlewareLayouts: isNotFoundPage ? [] : middlewareLayouts,
|
|
303
574
|
...hasAppFile ? { appFile } : {}
|
|
304
575
|
});
|
|
305
576
|
}
|
|
577
|
+
if (errorPage !== void 0) pages.push(errorPage);
|
|
306
578
|
pages.sort(comparePages);
|
|
307
|
-
|
|
579
|
+
const routablePages = pages.filter(isDiscoveredRoutablePage);
|
|
580
|
+
assertUniqueRoutePaths(routablePages, appRoot, explicitRouteFiles);
|
|
581
|
+
assertUniqueRouteNames(routablePages, appRoot);
|
|
308
582
|
return pages;
|
|
309
583
|
}
|
|
310
584
|
|
|
311
585
|
//#endregion
|
|
312
|
-
export { DuplicatePageRouteNameError, discoverPageFiles, discoverPages, discoverWebRoots, isFile, layoutChainFor, toPosix, walkFiles };
|
|
586
|
+
export { DuplicatePageRouteNameError, ErrorPageDeclaresRouteError, discoverPageFiles, discoverPages, discoverWebRoots, isDiscoveredRoutablePage, isErrorPageFile, isFile, layoutChainFor, toPosix, walkFiles };
|
|
313
587
|
//# sourceMappingURL=discover-pages.mjs.map
|