@warlock.js/web 5.0.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 +49 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/esm/build/contribution.d.mts +55 -0
- package/esm/build/contribution.mjs +132 -0
- package/esm/build/contribution.mjs.map +1 -0
- package/esm/build/discover-pages.mjs +294 -0
- package/esm/build/discover-pages.mjs.map +1 -0
- package/esm/build/generate-client-registry.mjs +123 -0
- package/esm/build/generate-client-registry.mjs.map +1 -0
- package/esm/build/generate-pages-barrel.mjs +330 -0
- package/esm/build/generate-pages-barrel.mjs.map +1 -0
- package/esm/build/read-route-exports.mjs +212 -0
- package/esm/build/read-route-exports.mjs.map +1 -0
- package/esm/client/build-hydrated-tree.mjs +113 -0
- package/esm/client/build-hydrated-tree.mjs.map +1 -0
- package/esm/client/hydrate-page.mjs +73 -0
- package/esm/client/hydrate-page.mjs.map +1 -0
- package/esm/client/navigation/current-route.d.mts +91 -0
- package/esm/client/navigation/current-route.mjs +66 -0
- package/esm/client/navigation/current-route.mjs.map +1 -0
- package/esm/client/navigation/fetch-page-data.mjs +97 -0
- package/esm/client/navigation/fetch-page-data.mjs.map +1 -0
- package/esm/client/navigation/navigation-root.mjs +196 -0
- package/esm/client/navigation/navigation-root.mjs.map +1 -0
- package/esm/client/navigation/prefetch.mjs +110 -0
- package/esm/client/navigation/prefetch.mjs.map +1 -0
- package/esm/client/navigation/refresh.d.mts +40 -0
- package/esm/client/navigation/refresh.mjs +146 -0
- package/esm/client/navigation/refresh.mjs.map +1 -0
- package/esm/client/navigation/verbs.d.mts +55 -0
- package/esm/client/navigation/verbs.mjs +66 -0
- package/esm/client/navigation/verbs.mjs.map +1 -0
- package/esm/client/runtime/index.d.mts +4 -0
- package/esm/client/runtime/index.mjs +4 -0
- package/esm/client/runtime/manifest.d.mts +8 -0
- package/esm/client/runtime/manifest.mjs +77 -0
- package/esm/client/runtime/manifest.mjs.map +1 -0
- package/esm/client/runtime/matcher.d.mts +7 -0
- package/esm/client/runtime/matcher.mjs +169 -0
- package/esm/client/runtime/matcher.mjs.map +1 -0
- package/esm/client/runtime/types.d.mts +22 -0
- package/esm/components/default-app.mjs +32 -0
- package/esm/components/default-app.mjs.map +1 -0
- package/esm/components/document-context.d.mts +1 -0
- package/esm/components/document-context.mjs +24 -0
- package/esm/components/document-context.mjs.map +1 -0
- package/esm/components/head.d.mts +14 -0
- package/esm/components/head.mjs +63 -0
- package/esm/components/head.mjs.map +1 -0
- package/esm/components/link.d.mts +157 -0
- package/esm/components/link.mjs +194 -0
- package/esm/components/link.mjs.map +1 -0
- package/esm/components/scripts.d.mts +14 -0
- package/esm/components/scripts.mjs +21 -0
- package/esm/components/scripts.mjs.map +1 -0
- package/esm/connector/index.d.mts +6 -0
- package/esm/connector/index.mjs +7 -0
- package/esm/context.d.mts +107 -0
- package/esm/hydration/index.d.mts +1 -0
- package/esm/hydration/index.mjs +33 -0
- package/esm/hydration/index.mjs.map +1 -0
- package/esm/hydration-payload.d.mts +1 -0
- package/esm/hydration-payload.mjs +72 -0
- package/esm/hydration-payload.mjs.map +1 -0
- package/esm/index.d.mts +35 -0
- package/esm/index.mjs +12 -0
- package/esm/loaders.d.mts +47 -0
- package/esm/metadata.d.mts +54 -0
- package/esm/props.d.mts +42 -0
- package/esm/route.d.mts +30 -0
- package/esm/routing/compose-route-path.mjs +33 -0
- package/esm/routing/compose-route-path.mjs.map +1 -0
- package/esm/routing/data-request.mjs +52 -0
- package/esm/routing/data-request.mjs.map +1 -0
- package/esm/routing/layout-policy.mjs +78 -0
- package/esm/routing/layout-policy.mjs.map +1 -0
- package/esm/routing/navigator.mjs +21 -0
- package/esm/routing/navigator.mjs.map +1 -0
- package/esm/routing/query-string.d.mts +250 -0
- package/esm/routing/query-string.mjs +340 -0
- package/esm/routing/query-string.mjs.map +1 -0
- package/esm/routing/route-identity.mjs +68 -0
- package/esm/routing/route-identity.mjs.map +1 -0
- package/esm/routing/route-table.d.mts +34 -0
- package/esm/routing/route-table.mjs +182 -0
- package/esm/routing/route-table.mjs.map +1 -0
- package/esm/routing/router-events.d.mts +159 -0
- package/esm/routing/router-events.mjs +83 -0
- package/esm/routing/router-events.mjs.map +1 -0
- package/esm/server/buffered-response.mjs +115 -0
- package/esm/server/buffered-response.mjs.map +1 -0
- package/esm/server/build-hydration-payload.mjs +27 -0
- package/esm/server/build-hydration-payload.mjs.map +1 -0
- package/esm/server/client-asset-url-prefix.d.mts +22 -0
- package/esm/server/client-asset-url-prefix.mjs +23 -0
- package/esm/server/client-asset-url-prefix.mjs.map +1 -0
- package/esm/server/create-page-module-loader.mjs +49 -0
- package/esm/server/create-page-module-loader.mjs.map +1 -0
- package/esm/server/create-page-route-handler.mjs +107 -0
- package/esm/server/create-page-route-handler.mjs.map +1 -0
- package/esm/server/dev-server.mjs +167 -0
- package/esm/server/dev-server.mjs.map +1 -0
- package/esm/server/execute-page-request.d.mts +1 -0
- package/esm/server/execute-page-request.mjs +166 -0
- package/esm/server/execute-page-request.mjs.map +1 -0
- package/esm/server/execute-page-request.types.d.mts +1 -0
- package/esm/server/hydration-client-url.d.mts +37 -0
- package/esm/server/hydration-client-url.mjs +101 -0
- package/esm/server/hydration-client-url.mjs.map +1 -0
- package/esm/server/index.mjs +13 -0
- package/esm/server/install-page-routes-from-manifest.mjs +133 -0
- package/esm/server/install-page-routes-from-manifest.mjs.map +1 -0
- package/esm/server/install-page-routes.d.mts +1 -0
- package/esm/server/install-page-routes.mjs +6 -0
- package/esm/server/install-production-page-routes.mjs +55 -0
- package/esm/server/install-production-page-routes.mjs.map +1 -0
- package/esm/server/match-page-route.mjs +48 -0
- package/esm/server/match-page-route.mjs.map +1 -0
- package/esm/server/page-context.d.mts +1 -0
- package/esm/server/page-context.mjs +33 -0
- package/esm/server/page-context.mjs.map +1 -0
- package/esm/server/page-manifest.d.mts +102 -0
- package/esm/server/page-manifest.mjs +31 -0
- package/esm/server/page-manifest.mjs.map +1 -0
- package/esm/server/render-page.mjs +214 -0
- package/esm/server/render-page.mjs.map +1 -0
- package/esm/server/resolve-page-metadata.mjs +33 -0
- package/esm/server/resolve-page-metadata.mjs.map +1 -0
- package/esm/server/resolve-validation-data.mjs +27 -0
- package/esm/server/resolve-validation-data.mjs.map +1 -0
- package/esm/server/settle-page-response.d.mts +1 -0
- package/esm/server/settle-page-response.mjs +90 -0
- package/esm/server/settle-page-response.mjs.map +1 -0
- package/esm/server/stylesheet-urls.mjs +65 -0
- package/esm/server/stylesheet-urls.mjs.map +1 -0
- package/esm/server/web-connector-factory.d.mts +25 -0
- package/esm/server/web-connector-factory.mjs +121 -0
- package/esm/server/web-connector-factory.mjs.map +1 -0
- package/esm/server/web-connector.d.mts +34 -0
- package/esm/server/web-connector.mjs +516 -0
- package/esm/server/web-connector.mjs.map +1 -0
- package/esm/shared.d.mts +22 -0
- package/esm/shared.mjs +223 -0
- package/esm/shared.mjs.map +1 -0
- package/esm/validation.d.mts +31 -0
- package/esm/vite/app-convention-aliases.mjs +43 -0
- package/esm/vite/app-convention-aliases.mjs.map +1 -0
- package/esm/vite/build-client.d.mts +38 -0
- package/esm/vite/build-client.mjs +72 -0
- package/esm/vite/build-client.mjs.map +1 -0
- package/esm/vite/gate-a-resolve.d.mts +57 -0
- package/esm/vite/gate-a-resolve.mjs +834 -0
- package/esm/vite/gate-a-resolve.mjs.map +1 -0
- package/esm/vite/gate-b-secrets.d.mts +57 -0
- package/esm/vite/gate-b-secrets.mjs +299 -0
- package/esm/vite/gate-b-secrets.mjs.map +1 -0
- package/esm/vite/gate-c-verify.d.mts +118 -0
- package/esm/vite/gate-c-verify.mjs +263 -0
- package/esm/vite/gate-c-verify.mjs.map +1 -0
- package/esm/vite/hydration-entries.d.mts +17 -0
- package/esm/vite/hydration-entries.mjs +45 -0
- package/esm/vite/hydration-entries.mjs.map +1 -0
- package/esm/vite/index.d.mts +126 -0
- package/esm/vite/index.mjs +146 -0
- package/esm/vite/index.mjs.map +1 -0
- package/esm/vite/page-registry-plugin.d.mts +43 -0
- package/esm/vite/page-registry-plugin.mjs +138 -0
- package/esm/vite/page-registry-plugin.mjs.map +1 -0
- package/esm/vite/projection.d.mts +31 -0
- package/esm/vite/projection.mjs +384 -0
- package/esm/vite/projection.mjs.map +1 -0
- package/llms-full.txt +1041 -0
- package/llms.txt +21 -0
- package/package.json +77 -0
- package/skills/add-web-to-an-app/SKILL.md +103 -0
- package/skills/create-a-page/SKILL.md +141 -0
- package/skills/load-page-data/SKILL.md +183 -0
- package/skills/navigate-on-the-client/SKILL.md +182 -0
- package/skills/serve-styles/SKILL.md +145 -0
- package/skills/use-layouts/SKILL.md +121 -0
- package/skills/write-the-root/SKILL.md +131 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PAYLOAD_SCRIPT_ID } from "./components/document-context.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/hydration-payload.ts
|
|
4
|
+
/**
|
|
5
|
+
* Exported so a payload-shape assertion can be written against the contract
|
|
6
|
+
* itself. A spec that hardcodes its own copy of this list silently becomes a
|
|
7
|
+
* claim about a PAST revision — that is exactly how the rev. 3 keys landed with
|
|
8
|
+
* two specs still asserting the rev. 2 shape.
|
|
9
|
+
*/
|
|
10
|
+
const REQUIRED_PAYLOAD_KEYS = [
|
|
11
|
+
"appData",
|
|
12
|
+
"layoutData",
|
|
13
|
+
"pageData",
|
|
14
|
+
"shared",
|
|
15
|
+
"name"
|
|
16
|
+
];
|
|
17
|
+
const ABSENT_PAYLOAD_MESSAGE = `Warlock hydration payload is absent: #${PAYLOAD_SCRIPT_ID}, owned by web/src/components/document-context.ts, was not found.`;
|
|
18
|
+
const MALFORMED_PAYLOAD_MESSAGE = `Warlock hydration payload was found at #${PAYLOAD_SCRIPT_ID} but could not be read.`;
|
|
19
|
+
function malformedPayload() {
|
|
20
|
+
throw new Error(MALFORMED_PAYLOAD_MESSAGE);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The keys that are allowed to be ABSENT but not allowed to be wrong.
|
|
24
|
+
*
|
|
25
|
+
* `metadata` and `params` are optional because the server is right not to
|
|
26
|
+
* always produce them — a page with no `metadata` export resolves none, and a
|
|
27
|
+
* payload written by a build that predates these keys carries neither. Failing
|
|
28
|
+
* a whole page over an absent accessor would turn a cosmetic gap into a blank
|
|
29
|
+
* screen, so absence is accepted and the readers default it.
|
|
30
|
+
*
|
|
31
|
+
* Present-but-not-an-object is a different claim entirely: it means something
|
|
32
|
+
* produced a payload with these names meaning something else, and every reader
|
|
33
|
+
* downstream would then be indexing a string. That is MALFORMED under the same
|
|
34
|
+
* rule the required keys live by, so it throws. Arrays included — `typeof []`
|
|
35
|
+
* is `"object"`, and an array of params is not params.
|
|
36
|
+
*/
|
|
37
|
+
const OPTIONAL_OBJECT_PAYLOAD_KEYS = ["metadata", "params"];
|
|
38
|
+
function isPlainObject(value) {
|
|
39
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
function requireHydrationPayload(value) {
|
|
42
|
+
if (!isPlainObject(value)) malformedPayload();
|
|
43
|
+
for (const key of REQUIRED_PAYLOAD_KEYS) if (!Object.prototype.hasOwnProperty.call(value, key)) malformedPayload();
|
|
44
|
+
for (const key of OPTIONAL_OBJECT_PAYLOAD_KEYS) {
|
|
45
|
+
const optional = value[key];
|
|
46
|
+
if (optional !== void 0 && !isPlainObject(optional)) malformedPayload();
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Read the fixed payload script without changing the server-rendered root.
|
|
52
|
+
*
|
|
53
|
+
* Extra fields are ignored. The gate owns the FIVE required keys — absent or
|
|
54
|
+
* malformed, both throw — plus a shape check on the two optional ones
|
|
55
|
+
* ({@link OPTIONAL_OBJECT_PAYLOAD_KEYS}); it deliberately does not require
|
|
56
|
+
* those to be present.
|
|
57
|
+
*/
|
|
58
|
+
function readHydrationPayload(documentNode) {
|
|
59
|
+
const element = documentNode.getElementById(PAYLOAD_SCRIPT_ID);
|
|
60
|
+
if (element === null) throw new Error(ABSENT_PAYLOAD_MESSAGE);
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(element.textContent ?? "");
|
|
64
|
+
} catch {
|
|
65
|
+
malformedPayload();
|
|
66
|
+
}
|
|
67
|
+
return requireHydrationPayload(parsed);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
//#endregion
|
|
71
|
+
export { readHydrationPayload };
|
|
72
|
+
//# sourceMappingURL=hydration-payload.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hydration-payload.mjs","names":[],"sources":["../../../../../../web/src/hydration-payload.ts"],"sourcesContent":["import {\n PAYLOAD_SCRIPT_ID,\n type HydrationDocumentPayloadSource,\n} from \"./components/document-context\";\n\nexport type { HydrationDocumentPayloadSource } from \"./components/document-context\";\n\n/**\n * Exported so a payload-shape assertion can be written against the contract\n * itself. A spec that hardcodes its own copy of this list silently becomes a\n * claim about a PAST revision — that is exactly how the rev. 3 keys landed with\n * two specs still asserting the rev. 2 shape.\n */\nexport const REQUIRED_PAYLOAD_KEYS = [\n \"appData\",\n \"layoutData\",\n \"pageData\",\n \"shared\",\n \"name\",\n] as const;\n\nconst ABSENT_PAYLOAD_MESSAGE =\n `Warlock hydration payload is absent: #${PAYLOAD_SCRIPT_ID}, owned by ` +\n \"web/src/components/document-context.ts, was not found.\";\nconst MALFORMED_PAYLOAD_MESSAGE =\n `Warlock hydration payload was found at #${PAYLOAD_SCRIPT_ID} but could not be read.`;\n\nfunction malformedPayload(): never {\n throw new Error(MALFORMED_PAYLOAD_MESSAGE);\n}\n\n/**\n * The keys that are allowed to be ABSENT but not allowed to be wrong.\n *\n * `metadata` and `params` are optional because the server is right not to\n * always produce them — a page with no `metadata` export resolves none, and a\n * payload written by a build that predates these keys carries neither. Failing\n * a whole page over an absent accessor would turn a cosmetic gap into a blank\n * screen, so absence is accepted and the readers default it.\n *\n * Present-but-not-an-object is a different claim entirely: it means something\n * produced a payload with these names meaning something else, and every reader\n * downstream would then be indexing a string. That is MALFORMED under the same\n * rule the required keys live by, so it throws. Arrays included — `typeof []`\n * is `\"object\"`, and an array of params is not params.\n */\nexport const OPTIONAL_OBJECT_PAYLOAD_KEYS = [\"metadata\", \"params\"] as const;\n\nfunction isPlainObject(value: unknown): boolean {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction requireHydrationPayload(value: unknown): HydrationDocumentPayloadSource {\n if (!isPlainObject(value)) malformedPayload();\n\n for (const key of REQUIRED_PAYLOAD_KEYS) {\n if (!Object.prototype.hasOwnProperty.call(value, key)) malformedPayload();\n }\n\n for (const key of OPTIONAL_OBJECT_PAYLOAD_KEYS) {\n const optional = (value as Record<string, unknown>)[key];\n\n if (optional !== undefined && !isPlainObject(optional)) malformedPayload();\n }\n\n return value as HydrationDocumentPayloadSource;\n}\n\n/**\n * Read the fixed payload script without changing the server-rendered root.\n *\n * Extra fields are ignored. The gate owns the FIVE required keys — absent or\n * malformed, both throw — plus a shape check on the two optional ones\n * ({@link OPTIONAL_OBJECT_PAYLOAD_KEYS}); it deliberately does not require\n * those to be present.\n */\nexport function readHydrationPayload(documentNode: Document): HydrationDocumentPayloadSource {\n const element = documentNode.getElementById(PAYLOAD_SCRIPT_ID);\n\n if (element === null) throw new Error(ABSENT_PAYLOAD_MESSAGE);\n\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(element.textContent ?? \"\");\n } catch {\n malformedPayload();\n }\n\n return requireHydrationPayload(parsed);\n}\n"],"mappings":";;;;;;;;;AAaA,MAAa,wBAAwB;CACnC;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,yBACJ,yCAAyC,kBAAkB;AAE7D,MAAM,4BACJ,2CAA2C,kBAAkB;AAE/D,SAAS,mBAA0B;CACjC,MAAM,IAAI,MAAM,yBAAyB;AAC3C;;;;;;;;;;;;;;;;AAiBA,MAAa,+BAA+B,CAAC,YAAY,QAAQ;AAEjE,SAAS,cAAc,OAAyB;CAC9C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;AAEA,SAAS,wBAAwB,OAAgD;CAC/E,IAAI,CAAC,cAAc,KAAK,GAAG,iBAAiB;CAE5C,KAAK,MAAM,OAAO,uBAChB,IAAI,CAAC,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG,iBAAiB;CAG1E,KAAK,MAAM,OAAO,8BAA8B;EAC9C,MAAM,WAAY,MAAkC;EAEpD,IAAI,aAAa,UAAa,CAAC,cAAc,QAAQ,GAAG,iBAAiB;CAC3E;CAEA,OAAO;AACT;;;;;;;;;AAUA,SAAgB,qBAAqB,cAAwD;CAC3F,MAAM,UAAU,aAAa,eAAe,iBAAiB;CAE7D,IAAI,YAAY,MAAM,MAAM,IAAI,MAAM,sBAAsB;CAE5D,IAAI;CAEJ,IAAI;EACF,SAAS,KAAK,MAAM,QAAQ,eAAe,EAAE;CAC/C,QAAQ;EACN,iBAAiB;CACnB;CAEA,OAAO,wBAAwB,MAAM;AACvC"}
|
package/esm/index.d.mts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppLoader, LayoutLoader, PageLoader } from "./loaders.mjs";
|
|
2
|
+
import { AppProps, LayoutProps, PageProps } from "./props.mjs";
|
|
3
|
+
import { PageMetadata } from "./metadata.mjs";
|
|
4
|
+
import { shared, useShared } from "./shared.mjs";
|
|
5
|
+
import { QueryStringInput, QueryStringLeaf, QueryStringNested, QueryStringObject, QueryStringOptions, QueryStringValue, RepeatedKeyStrategy, UnserializableQueryValueError, queryString, queryStringOf, resetQueryStringOptions, setQueryStringOptions } from "./routing/query-string.mjs";
|
|
6
|
+
import { RouteParameters, RouteQuery, href } from "./routing/route-table.mjs";
|
|
7
|
+
import { Link } from "./components/link.mjs";
|
|
8
|
+
import { getHash, navigateBack, navigateTo } from "./client/navigation/verbs.mjs";
|
|
9
|
+
import { NavigationEndPayload, NavigationErrorPayload, NavigationStartPayload, createRouterEvents, routerEvents } from "./routing/router-events.mjs";
|
|
10
|
+
import { MatchedRoute, currentRoute, previousRoute } from "./client/navigation/current-route.mjs";
|
|
11
|
+
import { refresh } from "./client/navigation/refresh.mjs";
|
|
12
|
+
import { Head } from "./components/head.mjs";
|
|
13
|
+
import { Scripts } from "./components/scripts.mjs";
|
|
14
|
+
import { PageContext } from "./context.mjs";
|
|
15
|
+
|
|
16
|
+
//#region ../web/src/index.d.ts
|
|
17
|
+
/**
|
|
18
|
+
* THE AUDIT SURFACE — everything the browser receives, declared by the app.
|
|
19
|
+
*
|
|
20
|
+
* Ships EMPTY and with NO index signature: `shared.anything` does not compile
|
|
21
|
+
* until the application augments this interface (v5/app does at
|
|
22
|
+
* `src/web/types.ts:23-59`). Required keys demand an unconditional middleware
|
|
23
|
+
* writer; optional keys may be written conditionally.
|
|
24
|
+
*
|
|
25
|
+
* Declared HERE, in the entry module, and that placement is load-bearing:
|
|
26
|
+
* applications augment the module `"@warlock.js/web"`, and TypeScript merges a
|
|
27
|
+
* module augmentation only with interfaces declared in the module that
|
|
28
|
+
* specifier resolves to — an interface re-exported through the barrel from a
|
|
29
|
+
* concern file would NOT merge (microsoft/TypeScript#18877). The published
|
|
30
|
+
* package's entry .d.ts must keep declaring it directly for the same reason.
|
|
31
|
+
*/
|
|
32
|
+
interface SharedContext {}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { type AppLoader, type AppProps, Head, type LayoutLoader, type LayoutProps, Link, type MatchedRoute, type NavigationEndPayload, type NavigationErrorPayload, type NavigationStartPayload, type PageContext, type PageLoader, type PageMetadata, type PageProps, type QueryStringInput, type QueryStringLeaf, type QueryStringNested, type QueryStringObject, type QueryStringOptions, type QueryStringValue, type RepeatedKeyStrategy, type RouteParameters, type RouteQuery, Scripts, SharedContext, UnserializableQueryValueError, createRouterEvents, currentRoute, getHash, href, navigateBack, navigateTo, previousRoute, queryString, queryStringOf, refresh, resetQueryStringOptions, routerEvents, setQueryStringOptions, shared, useShared };
|
|
35
|
+
//# sourceMappingURL=index.d.mts.map
|
package/esm/index.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { shared, useShared } from "./shared.mjs";
|
|
2
|
+
import { UnserializableQueryValueError, queryString, queryStringOf, resetQueryStringOptions, setQueryStringOptions } from "./routing/query-string.mjs";
|
|
3
|
+
import { href } from "./routing/route-table.mjs";
|
|
4
|
+
import { Link } from "./components/link.mjs";
|
|
5
|
+
import { getHash, navigateBack, navigateTo } from "./client/navigation/verbs.mjs";
|
|
6
|
+
import { createRouterEvents, routerEvents } from "./routing/router-events.mjs";
|
|
7
|
+
import { currentRoute, previousRoute } from "./client/navigation/current-route.mjs";
|
|
8
|
+
import { refresh } from "./client/navigation/refresh.mjs";
|
|
9
|
+
import { Head } from "./components/head.mjs";
|
|
10
|
+
import { Scripts } from "./components/scripts.mjs";
|
|
11
|
+
|
|
12
|
+
export { Head, Link, Scripts, UnserializableQueryValueError, createRouterEvents, currentRoute, getHash, href, navigateBack, navigateTo, previousRoute, queryString, queryStringOf, refresh, resetQueryStringOptions, routerEvents, setQueryStringOptions, shared, useShared };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { RouteDeclaration, RouteParamsOf } from "./route.mjs";
|
|
2
|
+
import { PageValidation, ValidatedOutput } from "./validation.mjs";
|
|
3
|
+
import { SharedContext } from "./index.mjs";
|
|
4
|
+
import { PageContext, WebRequest, WebResponse } from "./context.mjs";
|
|
5
|
+
|
|
6
|
+
//#region ../web/src/loaders.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* The context a PAGE loader receives, narrowed by the page's own sibling
|
|
9
|
+
* exports: `TValidation` types `request.validated()` off the schema,
|
|
10
|
+
* `TRoute` types `request.input()` off the path literal. A sibling export
|
|
11
|
+
* cannot contextually type an already-authored function — the generics are
|
|
12
|
+
* the mechanism that links them (product-details.page.tsx:20-41).
|
|
13
|
+
*/
|
|
14
|
+
type PageLoaderContext<TValidation extends PageValidation | undefined, TRoute extends RouteDeclaration | undefined> = {
|
|
15
|
+
request: WebRequest<ValidatedOutput<TValidation>, RouteParamsOf<TRoute>>;
|
|
16
|
+
response: WebResponse;
|
|
17
|
+
shared: SharedContext;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The page controller's contract. Used with `satisfies`, NEVER `:` — an
|
|
21
|
+
* annotation replaces the inferred type and `PageProps<typeof loader>` loses
|
|
22
|
+
* `data` (v5/app README §A). The return is `unknown` because `satisfies`
|
|
23
|
+
* preserves whatever the loader actually returns; the short-circuit brand and
|
|
24
|
+
* the props types do the narrowing on the way out.
|
|
25
|
+
*
|
|
26
|
+
* `TValidation` may be `undefined` — a page with no `validation` export
|
|
27
|
+
* (product-details.page.tsx:69). `TRoute` may be the config object or a bare
|
|
28
|
+
* string literal (contact-us.page.tsx:27).
|
|
29
|
+
*/
|
|
30
|
+
type PageLoader<TValidation extends PageValidation | undefined = undefined, TRoute extends RouteDeclaration | undefined = undefined> = (context: PageLoaderContext<TValidation, TRoute>) => unknown;
|
|
31
|
+
/**
|
|
32
|
+
* A layout's controller. Bare — no generics — in every v5/app use
|
|
33
|
+
* (products/web/layout.tsx:53-58, dashboard.layout.tsx:34-36): a layout has
|
|
34
|
+
* no `validation`/`route` exports to link, and the contract is the annotated
|
|
35
|
+
* type, not a parameter list (`async () => …` is still a controller with the
|
|
36
|
+
* full context available).
|
|
37
|
+
*/
|
|
38
|
+
type LayoutLoader = (context: PageContext) => unknown;
|
|
39
|
+
/**
|
|
40
|
+
* The application root's controller (root.tsx:56-58). Same shape as
|
|
41
|
+
* `LayoutLoader`; a separate name because the two revalidate and nest under
|
|
42
|
+
* different rules, and conflating them would make that drift invisible.
|
|
43
|
+
*/
|
|
44
|
+
type AppLoader = (context: PageContext) => unknown;
|
|
45
|
+
//#endregion
|
|
46
|
+
export { AppLoader, LayoutLoader, PageLoader };
|
|
47
|
+
//# sourceMappingURL=loaders.d.mts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { LoaderData, LoaderFunction } from "./props.mjs";
|
|
2
|
+
import { SharedContext } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../web/src/metadata.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the `metadata` export may produce. The pipeline injects it into
|
|
7
|
+
* `<head>` before the first byte; `<Head />` only decides placement.
|
|
8
|
+
*/
|
|
9
|
+
type MetadataOutput = {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
keywords?: string | readonly string[];
|
|
13
|
+
canonical?: string;
|
|
14
|
+
robots?: string;
|
|
15
|
+
/**
|
|
16
|
+
* `og:title`/`og:description` fall back to the top-level `title`/
|
|
17
|
+
* `description` when `openGraph` is present but the member is absent —
|
|
18
|
+
* no other member has a fallback.
|
|
19
|
+
*/
|
|
20
|
+
openGraph?: {
|
|
21
|
+
title?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
image?: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
};
|
|
27
|
+
twitter?: {
|
|
28
|
+
card?: string;
|
|
29
|
+
title?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
image?: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Static object OR a function of the loader's data (products.page.tsx:81-84,
|
|
36
|
+
* product-details.page.tsx:71-74). The function form runs server-side, after
|
|
37
|
+
* the loader, with the same `data` the component will receive — which is why
|
|
38
|
+
* it can describe the page instead of guessing at it.
|
|
39
|
+
*
|
|
40
|
+
* **`data` is always present, and that is now true rather than merely
|
|
41
|
+
* declared.** The function form runs only when the loader resolved; when it
|
|
42
|
+
* rejected, the framework emits `ERROR_PAGE_METADATA` and this never runs
|
|
43
|
+
* (`server/resolve-page-metadata.ts`, which explains why at length). An earlier
|
|
44
|
+
* revision passed `{ data: undefined, error }` on the boundary path while
|
|
45
|
+
* declaring `data` non-optional — every page that read `data` unguarded then
|
|
46
|
+
* threw a `TypeError` that replaced the loader's real error.
|
|
47
|
+
*/
|
|
48
|
+
type PageMetadata<TLoader extends LoaderFunction | undefined = undefined> = MetadataOutput | ((context: {
|
|
49
|
+
data: LoaderData<TLoader>;
|
|
50
|
+
shared: Readonly<SharedContext>;
|
|
51
|
+
}) => MetadataOutput);
|
|
52
|
+
//#endregion
|
|
53
|
+
export { PageMetadata };
|
|
54
|
+
//# sourceMappingURL=metadata.d.mts.map
|
package/esm/props.d.mts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SharedContext } from "./index.mjs";
|
|
2
|
+
import { LoaderShortCircuit } from "./context.mjs";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
//#region ../web/src/props.d.ts
|
|
6
|
+
/** Any loader authored with `satisfies` — the concrete function type. */
|
|
7
|
+
type LoaderFunction = (...args: any[]) => unknown;
|
|
8
|
+
/**
|
|
9
|
+
* The loader's literal return shape MINUS the branded short-circuits:
|
|
10
|
+
* `response.notFound()` / `response.redirect()` never union into `data`
|
|
11
|
+
* (product-details.page.tsx:45-57, login.page.tsx:71-84). No loader → no
|
|
12
|
+
* data.
|
|
13
|
+
*/
|
|
14
|
+
type LoaderData<TLoader> = TLoader extends LoaderFunction ? Exclude<Awaited<ReturnType<TLoader>>, LoaderShortCircuit> : undefined;
|
|
15
|
+
/**
|
|
16
|
+
* What the pipeline hands a page component: its own loader's data plus the
|
|
17
|
+
* per-request payload. Never `request` or `response` — the component also
|
|
18
|
+
* renders on a machine where neither exists.
|
|
19
|
+
*/
|
|
20
|
+
type PageProps<TLoader extends LoaderFunction | undefined = undefined> = {
|
|
21
|
+
data: LoaderData<TLoader>;
|
|
22
|
+
shared: Readonly<SharedContext>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* A layout additionally receives the subtree it wraps. Usable bare —
|
|
26
|
+
* `LayoutProps` with no generic — for a layout with no loader
|
|
27
|
+
* (main/web/layout.tsx:26).
|
|
28
|
+
*/
|
|
29
|
+
type LayoutProps<TLoader extends LoaderFunction | undefined = undefined> = {
|
|
30
|
+
data: LoaderData<TLoader>;
|
|
31
|
+
shared: Readonly<SharedContext>;
|
|
32
|
+
children: ReactNode;
|
|
33
|
+
};
|
|
34
|
+
/** The root component's props (root.tsx:76). */
|
|
35
|
+
type AppProps<TLoader extends LoaderFunction | undefined = undefined> = {
|
|
36
|
+
data: LoaderData<TLoader>;
|
|
37
|
+
shared: Readonly<SharedContext>;
|
|
38
|
+
children: ReactNode;
|
|
39
|
+
};
|
|
40
|
+
//#endregion
|
|
41
|
+
export { AppProps, LayoutProps, LoaderData, LoaderFunction, PageProps };
|
|
42
|
+
//# sourceMappingURL=props.d.mts.map
|
package/esm/route.d.mts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region ../web/src/route.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The `route` export's accepted shapes: the configured object
|
|
4
|
+
* (`{ path, name } as const` — the `as const` is what keeps `path` a literal
|
|
5
|
+
* a conditional type can parse, product-details.page.tsx:6-14) or the bare
|
|
6
|
+
* path string for the 2-line minimum page (contact-us.page.tsx:27, where the
|
|
7
|
+
* name is derived).
|
|
8
|
+
*/
|
|
9
|
+
type RouteDeclaration = string | {
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly name?: string;
|
|
12
|
+
};
|
|
13
|
+
type RoutePath<TRoute> = TRoute extends string ? TRoute : TRoute extends {
|
|
14
|
+
readonly path: infer TPath extends string;
|
|
15
|
+
} ? TPath : never;
|
|
16
|
+
/**
|
|
17
|
+
* Parameter names out of a path literal: `"/:id"` → `"id"`,
|
|
18
|
+
* `"/a/:x/:y"` → `"x" | "y"`, `"/"` and `"*"` → `never` (so `request.input()`
|
|
19
|
+
* is uncallable where no parameter exists to read).
|
|
20
|
+
*/
|
|
21
|
+
type PathParams<TPath extends string> = TPath extends `${string}:${infer TRest}` ? TRest extends `${infer TParam}/${infer TTail}` ? TParam | PathParams<`/${TTail}`> : TRest : never;
|
|
22
|
+
/**
|
|
23
|
+
* `undefined` (no route generic supplied) keeps `input()` loose rather than
|
|
24
|
+
* uncallable — the contract only narrows when the loader links its route via
|
|
25
|
+
* `satisfies PageLoader<…, typeof route>`.
|
|
26
|
+
*/
|
|
27
|
+
type RouteParamsOf<TRoute> = [TRoute] extends [undefined] ? string : PathParams<RoutePath<TRoute>>;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { RouteDeclaration, RouteParamsOf };
|
|
30
|
+
//# sourceMappingURL=route.d.mts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region ../web/src/routing/compose-route-path.ts
|
|
2
|
+
/**
|
|
3
|
+
* Route-path composition — the single, pure rule for turning a layout's
|
|
4
|
+
* declared `prefix` and a page's declared `route.path` into the page's
|
|
5
|
+
* effective, registered path. Previously three hand-written copies: the dev
|
|
6
|
+
* installer (`web/src/server/install-page-routes.ts`), the production
|
|
7
|
+
* manifest installer (`web/src/server/install-page-routes-from-manifest.ts`)
|
|
8
|
+
* and build discovery (`web/src/build/discover-pages.ts`) each carried this
|
|
9
|
+
* exact rule so build and boot could not quietly disagree about it; all
|
|
10
|
+
* three now delegate here instead.
|
|
11
|
+
*
|
|
12
|
+
* DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing
|
|
13
|
+
* here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module
|
|
14
|
+
* receives canonical values and trusts nothing about them beyond the input
|
|
15
|
+
* contract asserted below — it asserts rather than trusts, but it never
|
|
16
|
+
* repairs.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* A page's effective path: its nearest layout's `prefix` followed by its own
|
|
20
|
+
* declared `route.path`. A root prefix ("/") and a root path ("/") each
|
|
21
|
+
* contribute nothing, so neither adds a slash of its own; every other case
|
|
22
|
+
* joins on the slash `routePath` already starts with, which is what keeps a
|
|
23
|
+
* double slash out of the result, and an empty concatenation is the site
|
|
24
|
+
* root.
|
|
25
|
+
*/
|
|
26
|
+
function composeRoutePath(layoutPrefix, routePath) {
|
|
27
|
+
const composed = `${layoutPrefix === "/" ? "" : layoutPrefix.replace(/\/+$/, "")}${routePath === "/" ? "" : routePath}`;
|
|
28
|
+
return composed === "" ? "/" : composed;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { composeRoutePath };
|
|
33
|
+
//# sourceMappingURL=compose-route-path.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-route-path.mjs","names":[],"sources":["../../../../../../../web/src/routing/compose-route-path.ts"],"sourcesContent":["/**\n * Route-path composition — the single, pure rule for turning a layout's\n * declared `prefix` and a page's declared `route.path` into the page's\n * effective, registered path. Previously three hand-written copies: the dev\n * installer (`web/src/server/install-page-routes.ts`), the production\n * manifest installer (`web/src/server/install-page-routes-from-manifest.ts`)\n * and build discovery (`web/src/build/discover-pages.ts`) each carried this\n * exact rule so build and boot could not quietly disagree about it; all\n * three now delegate here instead.\n *\n * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing\n * here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module\n * receives canonical values and trusts nothing about them beyond the input\n * contract asserted below — it asserts rather than trusts, but it never\n * repairs.\n */\n\n/**\n * A page's effective path: its nearest layout's `prefix` followed by its own\n * declared `route.path`. A root prefix (\"/\") and a root path (\"/\") each\n * contribute nothing, so neither adds a slash of its own; every other case\n * joins on the slash `routePath` already starts with, which is what keeps a\n * double slash out of the result, and an empty concatenation is the site\n * root.\n */\nexport function composeRoutePath(layoutPrefix: string, routePath: string): string {\n const normalizedPrefix = layoutPrefix === \"/\" ? \"\" : layoutPrefix.replace(/\\/+$/, \"\");\n const normalizedRoute = routePath === \"/\" ? \"\" : routePath;\n const composed = `${normalizedPrefix}${normalizedRoute}`;\n\n return composed === \"\" ? \"/\" : composed;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,cAAsB,WAA2B;CAGhF,MAAM,WAAW,GAFQ,iBAAiB,MAAM,KAAK,aAAa,QAAQ,QAAQ,EAAE,IAC5D,cAAc,MAAM,KAAK;CAGjD,OAAO,aAAa,KAAK,MAAM;AACjC"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region ../web/src/routing/data-request.ts
|
|
2
|
+
/**
|
|
3
|
+
* The marker that turns a page request into a DATA request.
|
|
4
|
+
*
|
|
5
|
+
* A client navigation needs exactly what a full page load needs — middleware,
|
|
6
|
+
* validation, loaders, redirects, cookies, the settled status — and differs in
|
|
7
|
+
* one respect only: it wants the hydration payload as JSON instead of a
|
|
8
|
+
* rendered document. So it is deliberately NOT a separate `/_loader` route.
|
|
9
|
+
*
|
|
10
|
+
* WHY NOT A SEPARATE ROUTE. A `/_loader?path=/products/42` endpoint has to
|
|
11
|
+
* resolve that path to a page itself, which is a SECOND implementation of route
|
|
12
|
+
* semantics living beside the server's. This codebase already refuses that
|
|
13
|
+
* bargain for the browser — the hydration payload carries the route `name` so
|
|
14
|
+
* the client never re-matches — and the same reasoning applies here with more
|
|
15
|
+
* force: a loader endpoint that disagreed with the real route about params,
|
|
16
|
+
* prefixes or which page owns a path would answer a different request than the
|
|
17
|
+
* one the user navigated to. Same URL, same route, same matcher, same pipeline;
|
|
18
|
+
* only the final representation differs.
|
|
19
|
+
*
|
|
20
|
+
* WHY A HEADER AND NOT `?_data=1`. The query string belongs to the page — it is
|
|
21
|
+
* what `validation` and loaders read. Injecting a framework key into it means a
|
|
22
|
+
* page with strict query validation rejects its own client navigations, and
|
|
23
|
+
* every loader that echoes its query starts leaking a private flag.
|
|
24
|
+
*
|
|
25
|
+
* Responses to a data request must carry `Vary: <this header>` so a shared
|
|
26
|
+
* cache can never hand a document to a client that asked for JSON, or the
|
|
27
|
+
* reverse. Page responses are `private, no-store` today, which makes that
|
|
28
|
+
* theoretical — `Vary` is what keeps it theoretical if the caching policy
|
|
29
|
+
* changes.
|
|
30
|
+
*/
|
|
31
|
+
const WARLOCK_DATA_REQUEST_HEADER = "x-warlock-data";
|
|
32
|
+
/**
|
|
33
|
+
* Declared explicitly because the payload goes on the wire ALREADY SERIALIZED,
|
|
34
|
+
* as a string, and core only auto-picks `application/json` for object bodies.
|
|
35
|
+
* See the send site for why it must be a string.
|
|
36
|
+
*/
|
|
37
|
+
const DATA_RESPONSE_CONTENT_TYPE = "application/json";
|
|
38
|
+
/**
|
|
39
|
+
* Whether a request asked for the payload rather than the document.
|
|
40
|
+
*
|
|
41
|
+
* Presence-based on purpose: a proxy that rewrites the value, or a client on a
|
|
42
|
+
* newer version that sends something more specific, still means "data". Only an
|
|
43
|
+
* absent or empty header means "render the document".
|
|
44
|
+
*/
|
|
45
|
+
function isDataRequest(headerValue) {
|
|
46
|
+
const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
47
|
+
return typeof value === "string" && value.length > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { DATA_RESPONSE_CONTENT_TYPE, WARLOCK_DATA_REQUEST_HEADER, isDataRequest };
|
|
52
|
+
//# sourceMappingURL=data-request.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-request.mjs","names":[],"sources":["../../../../../../../web/src/routing/data-request.ts"],"sourcesContent":["/**\n * The marker that turns a page request into a DATA request.\n *\n * A client navigation needs exactly what a full page load needs — middleware,\n * validation, loaders, redirects, cookies, the settled status — and differs in\n * one respect only: it wants the hydration payload as JSON instead of a\n * rendered document. So it is deliberately NOT a separate `/_loader` route.\n *\n * WHY NOT A SEPARATE ROUTE. A `/_loader?path=/products/42` endpoint has to\n * resolve that path to a page itself, which is a SECOND implementation of route\n * semantics living beside the server's. This codebase already refuses that\n * bargain for the browser — the hydration payload carries the route `name` so\n * the client never re-matches — and the same reasoning applies here with more\n * force: a loader endpoint that disagreed with the real route about params,\n * prefixes or which page owns a path would answer a different request than the\n * one the user navigated to. Same URL, same route, same matcher, same pipeline;\n * only the final representation differs.\n *\n * WHY A HEADER AND NOT `?_data=1`. The query string belongs to the page — it is\n * what `validation` and loaders read. Injecting a framework key into it means a\n * page with strict query validation rejects its own client navigations, and\n * every loader that echoes its query starts leaking a private flag.\n *\n * Responses to a data request must carry `Vary: <this header>` so a shared\n * cache can never hand a document to a client that asked for JSON, or the\n * reverse. Page responses are `private, no-store` today, which makes that\n * theoretical — `Vary` is what keeps it theoretical if the caching policy\n * changes.\n */\nexport const WARLOCK_DATA_REQUEST_HEADER = \"x-warlock-data\";\n\n/**\n * The value the client sends. Any non-empty value is honoured on the way in —\n * the header's PRESENCE is the signal — but the client sends this one so the\n * traffic is self-describing in a log or a network panel.\n */\nexport const WARLOCK_DATA_REQUEST_VALUE = \"1\";\n\n/**\n * Declared explicitly because the payload goes on the wire ALREADY SERIALIZED,\n * as a string, and core only auto-picks `application/json` for object bodies.\n * See the send site for why it must be a string.\n */\nexport const DATA_RESPONSE_CONTENT_TYPE = \"application/json\";\n\n/**\n * Whether a request asked for the payload rather than the document.\n *\n * Presence-based on purpose: a proxy that rewrites the value, or a client on a\n * newer version that sends something more specific, still means \"data\". Only an\n * absent or empty header means \"render the document\".\n */\nexport function isDataRequest(headerValue: string | string[] | undefined): boolean {\n const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;\n\n return typeof value === \"string\" && value.length > 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,8BAA8B;;;;;;AAc3C,MAAa,6BAA6B;;;;;;;;AAS1C,SAAgB,cAAc,aAAqD;CACjF,MAAM,QAAQ,MAAM,QAAQ,WAAW,IAAI,YAAY,KAAK;CAE5D,OAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//#region ../web/src/routing/layout-policy.ts
|
|
2
|
+
/**
|
|
3
|
+
* Raised when a `chain` passed to {@link selectPageLayout} contains an empty
|
|
4
|
+
* layout identifier. An empty string is not a layout identifier a caller could
|
|
5
|
+
* have meant; this module refuses it rather than silently treating it as
|
|
6
|
+
* absent or as a real selection.
|
|
7
|
+
*/
|
|
8
|
+
var EmptyLayoutChainEntryError = class extends Error {
|
|
9
|
+
chain;
|
|
10
|
+
constructor(chain) {
|
|
11
|
+
super(`layout-policy: chain [${chain.map((entry) => `"${entry}"`).join(", ")}] contains an empty string. Every element of a layout chain passed to selectPageLayout must be a non-empty layout identifier — omit the entry entirely rather than passing an empty string for it.`);
|
|
12
|
+
this.chain = chain;
|
|
13
|
+
this.name = "EmptyLayoutChainEntryError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The single error contract for a `rejected` selection — the one class and
|
|
18
|
+
* one message every caller of {@link selectPageLayout} raises when it refuses
|
|
19
|
+
* a page whose path holds more than one RENDERING layout. `pageFile` and
|
|
20
|
+
* `layoutFiles` are the caller's context (its audience-appropriate identifiers
|
|
21
|
+
* for the page and the rejected rendering layouts — app-root-relative POSIX
|
|
22
|
+
* paths, in practice); the category and wording are this module's.
|
|
23
|
+
*
|
|
24
|
+
* The wording names the rendering layouts and ONLY the rendering layouts, and
|
|
25
|
+
* it does not offer removal as a remedy. Its predecessor said "remove or
|
|
26
|
+
* consolidate the extra layout", which, on a chain whose second element was a
|
|
27
|
+
* `middleware`-only authorization boundary, instructed the reader to delete
|
|
28
|
+
* their security guard to make the build pass.
|
|
29
|
+
*/
|
|
30
|
+
var NestedLayoutsNotSupportedError = class extends Error {
|
|
31
|
+
pageFile;
|
|
32
|
+
layoutFiles;
|
|
33
|
+
constructor(pageFile, layoutFiles) {
|
|
34
|
+
super(`"${pageFile}" has more than one layout on its path that renders: ${layoutFiles.map((file) => `"${file}"`).join(", ")}. Pages currently support at most one RENDERING layout — a layout with a default export — and nesting more than one is not yet supported. Layouts that render nothing, such as a \`prefix\`- or \`middleware\`-only layout, do not count against this and may nest freely. To fix: consolidate the rendering layouts named above into one — and do not remove a middleware-only layout to satisfy this, since none of them is what this refuses.`);
|
|
35
|
+
this.pageFile = pageFile;
|
|
36
|
+
this.layoutFiles = layoutFiles;
|
|
37
|
+
this.name = "NestedLayoutsNotSupportedError";
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** The classified form of an entry, whichever way the caller spelled it. */
|
|
41
|
+
function toEntry(entry) {
|
|
42
|
+
return typeof entry === "string" ? {
|
|
43
|
+
layout: entry,
|
|
44
|
+
renders: true
|
|
45
|
+
} : entry;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Selects which layout, if any, a page RENDERS inside, given its layout chain
|
|
49
|
+
* as enumerated outermost-first.
|
|
50
|
+
*
|
|
51
|
+
* Only entries with `renders: true` are counted: no rendering layout yields
|
|
52
|
+
* `{ type: "none" }`; exactly one yields `{ type: "selected"; layout }`; more
|
|
53
|
+
* than one yields `{ type: "rejected"; layouts }` carrying just those rendering
|
|
54
|
+
* layouts — this function throws nothing for that case; see the module doc for
|
|
55
|
+
* why rejection is data, not a throw.
|
|
56
|
+
*
|
|
57
|
+
* Throws {@link EmptyLayoutChainEntryError} when any entry's identifier is an
|
|
58
|
+
* empty string — the one input-contract violation this module refuses rather
|
|
59
|
+
* than passes through as a selection.
|
|
60
|
+
*/
|
|
61
|
+
function selectPageLayout(chain) {
|
|
62
|
+
const entries = chain.map(toEntry);
|
|
63
|
+
if (entries.some((entry) => entry.layout === "")) throw new EmptyLayoutChainEntryError(entries.map((entry) => entry.layout));
|
|
64
|
+
const rendering = entries.filter((entry) => entry.renders).map((entry) => entry.layout);
|
|
65
|
+
if (rendering.length === 0) return { type: "none" };
|
|
66
|
+
if (rendering.length === 1) return {
|
|
67
|
+
type: "selected",
|
|
68
|
+
layout: rendering[0]
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
type: "rejected",
|
|
72
|
+
layouts: rendering
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { NestedLayoutsNotSupportedError, selectPageLayout };
|
|
78
|
+
//# sourceMappingURL=layout-policy.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-policy.mjs","names":[],"sources":["../../../../../../../web/src/routing/layout-policy.ts"],"sourcesContent":["/**\n * Layout policy — the single, pure rule for turning a page's ENUMERATED\n * layout chain into a selection decision.\n *\n * Enumeration and selection are deliberately separate concerns. Discovery\n * ({@link \"../build/discover-pages.ts\"}'s `layoutChainFor`) walks a page's\n * directory ancestry and reports every `layout.tsx` it finds, outermost\n * first, honestly and unfiltered — it does not decide whether that chain is\n * usable. This module is the one place that decision is made: given a chain,\n * how many layouts does composition get to use, and which one(s)?\n *\n * THE RULE COUNTS RENDERING LAYOUTS, NOT FILES. A layout that contributes no\n * element to the document — one with no default export, carrying only\n * `prefix`, `middleware` or other named exports — is not a second wrapper and\n * never was. Counting files instead of wrappers made an authorization boundary\n * indistinguishable from a nested layout, and the resulting refusal told app\n * authors to delete the boundary to make the build pass. Nested RENDERING\n * layouts remain unsupported; everything else composes freely.\n *\n * DIRECTORY CONTRACT — applies to everything in `web/src/routing/`: nothing\n * here may import `node:fs`, `node:path`, `vite`, or `fastify`. This module\n * receives a canonical chain and trusts nothing about it beyond the input\n * contract asserted below — it asserts rather than trusts, but it never\n * repairs. It therefore never DECIDES what renders either: that answer needs\n * the filesystem, so the caller — which has it — classifies each entry and\n * passes the classification in. Discovery owns the fact; this module owns the\n * rule. The `layout` identifiers are opaque (paths, in practice) and this\n * module never inspects their shape; it only counts and selects.\n *\n * REJECTION IS DATA, NOT A THROW: a chain with two or more rendering layouts\n * does not make {@link selectPageLayout} raise — it returns the rejected\n * rendering layouts, in order. What a rejection MEANS to the user is\n * nonetheless fixed here: {@link NestedLayoutsNotSupportedError} is the single\n * error contract for it — one class, one message shape, built from the\n * rejection data plus caller-supplied page identity. Callers decide only WHEN\n * to raise it and supply that context; none of them wraps the rejection in a\n * category or wording of its own. A shared policy whose failure semantics fork\n * per caller is shared in the happy path and forked in the sad one — and the\n * sad path is the one users meet.\n */\n\n/**\n * One layout in a chain, with the caller's answer to the only question this\n * module needs about it: does it render?\n *\n * `renders` is true when the layout's module has a default export — the thing\n * that puts an element in the document. Everything else it exports (`prefix`,\n * `middleware`, helpers) is invisible to this rule.\n */\nexport type LayoutChainEntry = {\n /** Opaque identifier for the layout — an app-root-relative POSIX path, in practice. */\n layout: string;\n /** Whether this layout contributes an element to the document. */\n renders: boolean;\n};\n\n/**\n * A chain as {@link selectPageLayout} accepts it: classified entries, or bare\n * identifiers for a caller that has not classified its chain yet.\n *\n * TRANSITIONAL: a bare string is read as a RENDERING layout, which is the\n * conservative reading (it can only make the rule stricter, never looser) and\n * reproduces this module's pre-classification behaviour exactly. It exists so\n * the boot-time manifest installer — which holds loaded modules rather than\n * source files — keeps working unchanged until it classifies too; remove the\n * string arm once every caller passes {@link LayoutChainEntry}s.\n */\nexport type LayoutChainInput = readonly (string | LayoutChainEntry)[];\n\n/**\n * The policy's decision for one page's layout chain:\n *\n * - `\"none\"` — no layout on the chain renders; the page composes against no\n * layout. A chain of three middleware-only layouts lands here exactly as an\n * empty chain does, because neither has a wrapper in it.\n * - `\"selected\"` — exactly one layout renders; `layout` is that element, which,\n * being the only rendering one, is simultaneously the outermost and the\n * nearest rendering layout — there is no distinction to draw between the two\n * when there is only one.\n * - `\"rejected\"` — more than one layout renders; `layouts` carries the\n * RENDERING layouts only, outermost-first, in chain order, so a consumer can\n * name every layout actually at fault without naming the guards between them.\n */\nexport type LayoutPolicyResult =\n | { type: \"none\" }\n | { type: \"selected\"; layout: string }\n | { type: \"rejected\"; layouts: readonly string[] };\n\n/**\n * Raised when a `chain` passed to {@link selectPageLayout} contains an empty\n * layout identifier. An empty string is not a layout identifier a caller could\n * have meant; this module refuses it rather than silently treating it as\n * absent or as a real selection.\n */\nexport class EmptyLayoutChainEntryError extends Error {\n public constructor(public readonly chain: readonly string[]) {\n super(\n `layout-policy: chain [${chain.map((entry) => `\"${entry}\"`).join(\", \")}] contains an empty ` +\n \"string. Every element of a layout chain passed to selectPageLayout must be a non-empty \" +\n \"layout identifier — omit the entry entirely rather than passing an empty string for it.\",\n );\n this.name = \"EmptyLayoutChainEntryError\";\n }\n}\n\n/**\n * The single error contract for a `rejected` selection — the one class and\n * one message every caller of {@link selectPageLayout} raises when it refuses\n * a page whose path holds more than one RENDERING layout. `pageFile` and\n * `layoutFiles` are the caller's context (its audience-appropriate identifiers\n * for the page and the rejected rendering layouts — app-root-relative POSIX\n * paths, in practice); the category and wording are this module's.\n *\n * The wording names the rendering layouts and ONLY the rendering layouts, and\n * it does not offer removal as a remedy. Its predecessor said \"remove or\n * consolidate the extra layout\", which, on a chain whose second element was a\n * `middleware`-only authorization boundary, instructed the reader to delete\n * their security guard to make the build pass.\n */\nexport class NestedLayoutsNotSupportedError extends Error {\n public constructor(\n public readonly pageFile: string,\n public readonly layoutFiles: readonly string[],\n ) {\n super(\n `\"${pageFile}\" has more than one layout on its path that renders: ` +\n `${layoutFiles.map((file) => `\"${file}\"`).join(\", \")}. Pages currently support at most one ` +\n \"RENDERING layout — a layout with a default export — and nesting more than one is not yet \" +\n \"supported. Layouts that render nothing, such as a `prefix`- or `middleware`-only layout, \" +\n \"do not count against this and may nest freely. To fix: consolidate the rendering layouts \" +\n \"named above into one — and do not remove a middleware-only layout to satisfy this, since \" +\n \"none of them is what this refuses.\",\n );\n this.name = \"NestedLayoutsNotSupportedError\";\n }\n}\n\n/** The classified form of an entry, whichever way the caller spelled it. */\nfunction toEntry(entry: string | LayoutChainEntry): LayoutChainEntry {\n return typeof entry === \"string\" ? { layout: entry, renders: true } : entry;\n}\n\n/**\n * Selects which layout, if any, a page RENDERS inside, given its layout chain\n * as enumerated outermost-first.\n *\n * Only entries with `renders: true` are counted: no rendering layout yields\n * `{ type: \"none\" }`; exactly one yields `{ type: \"selected\"; layout }`; more\n * than one yields `{ type: \"rejected\"; layouts }` carrying just those rendering\n * layouts — this function throws nothing for that case; see the module doc for\n * why rejection is data, not a throw.\n *\n * Throws {@link EmptyLayoutChainEntryError} when any entry's identifier is an\n * empty string — the one input-contract violation this module refuses rather\n * than passes through as a selection.\n */\nexport function selectPageLayout(chain: LayoutChainInput): LayoutPolicyResult {\n const entries = chain.map(toEntry);\n\n if (entries.some((entry) => entry.layout === \"\")) {\n throw new EmptyLayoutChainEntryError(entries.map((entry) => entry.layout));\n }\n\n const rendering = entries.filter((entry) => entry.renders).map((entry) => entry.layout);\n\n if (rendering.length === 0) {\n return { type: \"none\" };\n }\n\n if (rendering.length === 1) {\n return { type: \"selected\", layout: rendering[0] };\n }\n\n return { type: \"rejected\", layouts: rendering };\n}\n"],"mappings":";;;;;;;AA8FA,IAAa,6BAAb,cAAgD,MAAM;CACjB;CAAnC,AAAO,YAAY,AAAgB,OAA0B;EAC3D,MACE,yBAAyB,MAAM,KAAK,UAAU,IAAI,MAAM,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,mMAGzE;EALiC;EAMjC,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;;AAgBA,IAAa,iCAAb,cAAoD,MAAM;CAEtC;CACA;CAFlB,AAAO,YACL,AAAgB,UAChB,AAAgB,aAChB;EACA,MACE,IAAI,SAAS,uDACR,YAAY,KAAK,SAAS,IAAI,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,ibAMzD;EAXgB;EACA;EAWhB,KAAK,OAAO;CACd;AACF;;AAGA,SAAS,QAAQ,OAAoD;CACnE,OAAO,OAAO,UAAU,WAAW;EAAE,QAAQ;EAAO,SAAS;CAAK,IAAI;AACxE;;;;;;;;;;;;;;;AAgBA,SAAgB,iBAAiB,OAA6C;CAC5E,MAAM,UAAU,MAAM,IAAI,OAAO;CAEjC,IAAI,QAAQ,MAAM,UAAU,MAAM,WAAW,EAAE,GAC7C,MAAM,IAAI,2BAA2B,QAAQ,KAAK,UAAU,MAAM,MAAM,CAAC;CAG3E,MAAM,YAAY,QAAQ,QAAQ,UAAU,MAAM,OAAO,CAAC,CAAC,KAAK,UAAU,MAAM,MAAM;CAEtF,IAAI,UAAU,WAAW,GACvB,OAAO,EAAE,MAAM,OAAO;CAGxB,IAAI,UAAU,WAAW,GACvB,OAAO;EAAE,MAAM;EAAY,QAAQ,UAAU;CAAG;CAGlD,OAAO;EAAE,MAAM;EAAY,SAAS;CAAU;AAChD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region ../web/src/routing/navigator.ts
|
|
2
|
+
let navigator;
|
|
3
|
+
/**
|
|
4
|
+
* Installed by the navigation runtime at mount, and torn down with `undefined`.
|
|
5
|
+
*
|
|
6
|
+
* @returns the previous navigator, so a caller that installs one can restore
|
|
7
|
+
* what was there — the same shape the other `connect*` seams in this package
|
|
8
|
+
* use, and what makes a test able to leave the module as it found it.
|
|
9
|
+
*/
|
|
10
|
+
function connectNavigator(next) {
|
|
11
|
+
const previous = navigator;
|
|
12
|
+
navigator = next;
|
|
13
|
+
return previous;
|
|
14
|
+
}
|
|
15
|
+
function currentNavigator() {
|
|
16
|
+
return navigator;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { connectNavigator, currentNavigator };
|
|
21
|
+
//# sourceMappingURL=navigator.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigator.mjs","names":[],"sources":["../../../../../../../web/src/routing/navigator.ts"],"sourcesContent":["/**\n * The one seam between `<Link>` and the client navigation runtime.\n *\n * `<Link>` is UNIVERSAL — the same component renders on the server and in the\n * browser — so it cannot import the runtime: doing so would drag `fetch`, the\n * page registry and React state into the server render, and into the bundle of\n * any app that never navigates client-side.\n *\n * So the runtime REGISTERS itself here instead, and `<Link>` asks. On the\n * server nothing has registered, `currentNavigator()` is `undefined`, and the\n * anchor behaves as a plain anchor — which is exactly right, because on the\n * server it IS a plain anchor.\n *\n * This is also what makes the feature progressively enhancing rather than\n * load-bearing: until the hydration bundle has run and registered, every link\n * on the page still works as an ordinary link.\n */\n\n/**\n * @returns `true` if the runtime accepted the navigation and the caller should\n * suppress the browser's default. `false` means \"not mine\" — let the browser do\n * what it was going to do.\n */\nexport type Navigator = (url: string, options?: { replace?: boolean }) => boolean;\n\nlet navigator: Navigator | undefined;\n\n/**\n * Installed by the navigation runtime at mount, and torn down with `undefined`.\n *\n * @returns the previous navigator, so a caller that installs one can restore\n * what was there — the same shape the other `connect*` seams in this package\n * use, and what makes a test able to leave the module as it found it.\n */\nexport function connectNavigator(next: Navigator | undefined): Navigator | undefined {\n const previous = navigator;\n\n navigator = next;\n\n return previous;\n}\n\nexport function currentNavigator(): Navigator | undefined {\n return navigator;\n}\n"],"mappings":";AAyBA,IAAI;;;;;;;;AASJ,SAAgB,iBAAiB,MAAoD;CACnF,MAAM,WAAW;CAEjB,YAAY;CAEZ,OAAO;AACT;AAEA,SAAgB,mBAA0C;CACxD,OAAO;AACT"}
|