@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,157 @@
|
|
|
1
|
+
import { AnchorHTMLAttributes, ComponentType, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/components/link.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* `<Link>` is SUGAR over `href()`, and deliberately thin.
|
|
6
|
+
*
|
|
7
|
+
* `href(name, params, query)` is the durable primitive — it serves emails,
|
|
8
|
+
* redirects, `Location` headers and every non-React caller, none of which can
|
|
9
|
+
* render a component. This file adds one thing to it: an anchor element.
|
|
10
|
+
*
|
|
11
|
+
* It renders a real `<a href>`. Client-side interception is a later slice of
|
|
12
|
+
* the navigation runtime and lands here without changing this API, which is the
|
|
13
|
+
* point of routing everything through `href` first: navigation becomes a
|
|
14
|
+
* BEHAVIOUR change, not an API change.
|
|
15
|
+
*
|
|
16
|
+
* ── Parity with `@mongez/react-router` ───────────────────────────────────────
|
|
17
|
+
* `href`, `newTab`, `email`, `tel`, `component` and `prefetch` are spelled
|
|
18
|
+
* exactly as MRR spells them, so a component moved across keeps compiling.
|
|
19
|
+
* `params` and `query` are ours and have no MRR equivalent: they pair with the
|
|
20
|
+
* typed `href()` helper, which is what makes a route NAME — rather than a URL —
|
|
21
|
+
* the thing a call site names.
|
|
22
|
+
*
|
|
23
|
+
* ── The semantic divergence this file bridges ────────────────────────────────
|
|
24
|
+
* MRR's `to` is a PATH. Ours was a route NAME, and only a name — which meant a
|
|
25
|
+
* component moved across from MRR compiled and then threw at render, because
|
|
26
|
+
* `"/products"` is not the name of anything. The two packages disagreed about
|
|
27
|
+
* what the most-used prop in either of them MEANS.
|
|
28
|
+
*
|
|
29
|
+
* Since 2026-08-24 (owner ruling) `to`/`href` accept BOTH, discriminated by
|
|
30
|
+
* SHAPE — see {@link isLiteralUrl}. That is what makes MRR code portable, and
|
|
31
|
+
* it costs nothing at a Warlock call site, because the two grammars cannot
|
|
32
|
+
* collide: a route name never begins with `/` and never carries a `scheme:`.
|
|
33
|
+
* The ruling RESTS on that, so this file asserts it rather than trusting it
|
|
34
|
+
* ({@link RouteNameShapeCollisionError}).
|
|
35
|
+
*/
|
|
36
|
+
type AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href">;
|
|
37
|
+
/**
|
|
38
|
+
* Where the link goes. Every field is documented here once; which COMBINATIONS
|
|
39
|
+
* are legal is decided by {@link LinkDestination}.
|
|
40
|
+
*/
|
|
41
|
+
type LinkDestinationProps = {
|
|
42
|
+
/**
|
|
43
|
+
* A route NAME, or a literal URL — told apart by SHAPE.
|
|
44
|
+
*
|
|
45
|
+
* `"products.details"` is a NAME and is resolved through the route table. A
|
|
46
|
+
* page that moves changes its URL and keeps its name, so every call site
|
|
47
|
+
* survives the move; a dead name throws at render naming the routes that do
|
|
48
|
+
* exist, rather than rendering an anchor that 404s. This is the form to
|
|
49
|
+
* prefer, and the only one `params` and `query` apply to.
|
|
50
|
+
*
|
|
51
|
+
* `"/pricing"`, `"https://stripe.com"`, `"mailto:sales@example.com"` and any
|
|
52
|
+
* other `scheme:` are LITERAL — passed through to the element untouched, with
|
|
53
|
+
* no route lookup at all. An app links out, and a route name is not a thing
|
|
54
|
+
* you can have for a page that is not yours.
|
|
55
|
+
*/
|
|
56
|
+
to?: string;
|
|
57
|
+
/**
|
|
58
|
+
* An alias of {@link to}, for parity with `@mongez/react-router`. Identical
|
|
59
|
+
* in every respect, including which shapes it accepts.
|
|
60
|
+
*/
|
|
61
|
+
href?: string; /** Renders a `mailto:` link. Not an in-app navigation. */
|
|
62
|
+
email?: string; /** Renders a `tel:` link. Not an in-app navigation. */
|
|
63
|
+
tel?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Values for the route's `:param` segments, e.g. `{ id }` for
|
|
66
|
+
* `"/products/:id"`. Only meaningful with a route NAME.
|
|
67
|
+
*/
|
|
68
|
+
params?: Record<string, unknown>;
|
|
69
|
+
/**
|
|
70
|
+
* Query string values; an `undefined` value is omitted. Only meaningful with
|
|
71
|
+
* a route NAME.
|
|
72
|
+
*/
|
|
73
|
+
query?: Record<string, unknown>;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* EXACTLY ONE destination, enforced by the type.
|
|
77
|
+
*
|
|
78
|
+
* The alternative — a documented precedence such as "`to` wins over `href`" —
|
|
79
|
+
* is silent by construction: the losing prop goes on compiling and goes on
|
|
80
|
+
* reading like it works at the call site, and the anchor points at the wrong
|
|
81
|
+
* page. Refusing the pair costs a call site one edit and can never be
|
|
82
|
+
* misread. The runtime refuses it as well, because a JavaScript caller and a
|
|
83
|
+
* cast both get past this.
|
|
84
|
+
*/
|
|
85
|
+
type LinkDestination = {
|
|
86
|
+
to: string;
|
|
87
|
+
href?: never;
|
|
88
|
+
email?: never;
|
|
89
|
+
tel?: never;
|
|
90
|
+
} | {
|
|
91
|
+
href: string;
|
|
92
|
+
to?: never;
|
|
93
|
+
email?: never;
|
|
94
|
+
tel?: never;
|
|
95
|
+
} | {
|
|
96
|
+
email: string;
|
|
97
|
+
to?: never;
|
|
98
|
+
href?: never;
|
|
99
|
+
tel?: never;
|
|
100
|
+
} | {
|
|
101
|
+
tel: string;
|
|
102
|
+
to?: never;
|
|
103
|
+
href?: never;
|
|
104
|
+
email?: never;
|
|
105
|
+
};
|
|
106
|
+
type LinkProps = AnchorProps & LinkDestinationProps & LinkDestination & {
|
|
107
|
+
/**
|
|
108
|
+
* Open in a new browsing context: `target="_blank"` plus the `rel` that
|
|
109
|
+
* stops the opened page from reaching back through `window.opener`.
|
|
110
|
+
*
|
|
111
|
+
* A caller's own `target`/`rel` win — this only fills in what was not said.
|
|
112
|
+
*/
|
|
113
|
+
newTab?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Render as something other than `<a>` — a tag name or a component.
|
|
116
|
+
*
|
|
117
|
+
* It receives the resolved `href`, the click handler and every remaining
|
|
118
|
+
* prop, so a design-system anchor keeps client-side navigation as long as
|
|
119
|
+
* it spreads what it is given onto the element it renders.
|
|
120
|
+
*
|
|
121
|
+
* `ComponentType<any>` is MRR's signature, kept verbatim: the component is
|
|
122
|
+
* the caller's and its props are unknowable from here.
|
|
123
|
+
*/
|
|
124
|
+
component?: ComponentType<any> | string;
|
|
125
|
+
/**
|
|
126
|
+
* Fetch this page's data when the pointer or the keyboard reaches the link,
|
|
127
|
+
* so the click that follows swaps without a round trip.
|
|
128
|
+
*
|
|
129
|
+
* A GUESS, and treated as one everywhere: it is never awaited, a failure is
|
|
130
|
+
* silent, and the click behaves exactly as it would without it. Opt-in per
|
|
131
|
+
* link rather than on by default, because every prefetch is a request the
|
|
132
|
+
* user did not ask for and someone pays for the bandwidth.
|
|
133
|
+
*
|
|
134
|
+
* IGNORED for anything that is not an in-app navigation — an external URL,
|
|
135
|
+
* `mailto:`, `tel:`, `newTab`, any explicit `target`. Prefetching those
|
|
136
|
+
* would mean issuing a cross-origin request to a third party on hover,
|
|
137
|
+
* which is not a thing a link component may decide to do.
|
|
138
|
+
*/
|
|
139
|
+
prefetch?: boolean;
|
|
140
|
+
};
|
|
141
|
+
declare function Link({
|
|
142
|
+
to,
|
|
143
|
+
href: hrefAlias,
|
|
144
|
+
email,
|
|
145
|
+
tel,
|
|
146
|
+
params,
|
|
147
|
+
query,
|
|
148
|
+
newTab,
|
|
149
|
+
prefetch,
|
|
150
|
+
component: Component,
|
|
151
|
+
children,
|
|
152
|
+
onClick,
|
|
153
|
+
...elementProps
|
|
154
|
+
}: LinkProps): ReactElement;
|
|
155
|
+
//#endregion
|
|
156
|
+
export { Link };
|
|
157
|
+
//# sourceMappingURL=link.d.mts.map
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { href, knownRouteNames } from "../routing/route-table.mjs";
|
|
2
|
+
import { prefetchPageData } from "../client/navigation/prefetch.mjs";
|
|
3
|
+
import { currentNavigator } from "../routing/navigator.mjs";
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
|
|
6
|
+
//#region ../web/src/components/link.ts
|
|
7
|
+
const DESTINATION_PROPS = [
|
|
8
|
+
"to",
|
|
9
|
+
"href",
|
|
10
|
+
"email",
|
|
11
|
+
"tel"
|
|
12
|
+
];
|
|
13
|
+
var AmbiguousLinkDestinationError = class extends Error {
|
|
14
|
+
providedProps;
|
|
15
|
+
constructor(providedProps) {
|
|
16
|
+
super(`Warlock <Link> was given ${providedProps.map((name) => JSON.stringify(name)).join(" and ")}, but a link goes to exactly one place. There is no precedence between them on purpose: one of the two would silently win, and the call site would go on naming a destination that never renders. Delete the one you did not mean.`);
|
|
17
|
+
this.providedProps = providedProps;
|
|
18
|
+
this.name = "AmbiguousLinkDestinationError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var MissingLinkDestinationError = class extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(`Warlock <Link> was given no destination. Pass exactly one of ${DESTINATION_PROPS.map((name) => JSON.stringify(name)).join(", ")}. It is not defaulted to the current page: an anchor with an empty \`href\` renders as a working link and reloads the page when clicked, which is a harder fault to see than this message.`);
|
|
24
|
+
this.name = "MissingLinkDestinationError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var RouteArgumentsOnLiteralUrlError = class extends Error {
|
|
28
|
+
url;
|
|
29
|
+
providedProps;
|
|
30
|
+
constructor(url, providedProps) {
|
|
31
|
+
super(`Warlock <Link> was given ${providedProps.map((name) => JSON.stringify(name)).join(" and ")} alongside the literal URL "${url}". Those apply to a route NAME, which is resolved through the route table; a literal URL is passed through exactly as written, so they would have been dropped and the link would have pointed at an unfiltered page that still looked right at the call site. Put the values in the URL, or name the route.`);
|
|
32
|
+
this.url = url;
|
|
33
|
+
this.providedProps = providedProps;
|
|
34
|
+
this.name = "RouteArgumentsOnLiteralUrlError";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* The ruling's one assumption, broken. See the module doc comment: telling a
|
|
39
|
+
* literal URL from a route NAME by shape is only safe while no route is NAMED
|
|
40
|
+
* like a URL, and nothing in the route pipeline validates a hand-declared
|
|
41
|
+
* `route.name`. So the collision is checked at the one place it could do harm,
|
|
42
|
+
* where it is a loud refusal instead of an anchor that silently points
|
|
43
|
+
* somewhere else.
|
|
44
|
+
*/
|
|
45
|
+
var RouteNameShapeCollisionError = class extends Error {
|
|
46
|
+
routeName;
|
|
47
|
+
constructor(routeName) {
|
|
48
|
+
super(`Warlock route table: a route is NAMED ${JSON.stringify(routeName)}, which is shaped like a URL. <Link> tells a literal URL from a route name by shape — a destination starting with \`/\` or carrying a \`scheme:\` is passed through untouched — so this name can never be resolved, and every link to it would silently point at that path instead. Rename the route (\`route = { path, name }\`) to a dotted name such as ${JSON.stringify(routeName.replace(/^\/+/, "").replace(/\//g, ".") || "index")}.`);
|
|
49
|
+
this.routeName = routeName;
|
|
50
|
+
this.name = "RouteNameShapeCollisionError";
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Any RFC 3986 scheme — `https:`, `mailto:`, `tel:`, `whatsapp:`, an app's own
|
|
55
|
+
* custom one. Matched generically rather than as a list of known schemes: a
|
|
56
|
+
* list would silently resolve `bitcoin:...` through the route table, which is
|
|
57
|
+
* the exact failure this ruling exists to remove, and it would have to grow
|
|
58
|
+
* forever.
|
|
59
|
+
*/
|
|
60
|
+
const SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
61
|
+
/**
|
|
62
|
+
* Whether this destination is a URL to be used as written, rather than a route
|
|
63
|
+
* name to resolve.
|
|
64
|
+
*
|
|
65
|
+
* The whole discriminator, and deliberately the whole of it: two cheap shape
|
|
66
|
+
* tests, no parsing, no matching. Anything more would be a SECOND route matcher
|
|
67
|
+
* living beside the server's, which this codebase refuses everywhere it comes
|
|
68
|
+
* up — a matcher that disagreed with the real one would produce links to pages
|
|
69
|
+
* that do not exist.
|
|
70
|
+
*/
|
|
71
|
+
function isLiteralUrl(destination) {
|
|
72
|
+
return destination.startsWith("/") || SCHEME_PATTERN.test(destination);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a literal URL addresses THIS app.
|
|
76
|
+
*
|
|
77
|
+
* A path is ours. A `scheme:` is not — including `https:` to our own origin,
|
|
78
|
+
* which would need `window.location` to recognise and would make the answer
|
|
79
|
+
* depend on where the code is running. And `//host/path` is PROTOCOL-RELATIVE:
|
|
80
|
+
* it starts with a slash and is nonetheless another origin, which is precisely
|
|
81
|
+
* the case a "starts with `/`" test alone would hand to the navigator, where it
|
|
82
|
+
* becomes a `pushState` to a foreign origin — a SecurityError — or a
|
|
83
|
+
* speculative fetch of a third-party host.
|
|
84
|
+
*/
|
|
85
|
+
function addressesThisApp(url) {
|
|
86
|
+
return url.startsWith("/") && !url.startsWith("//");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Refuses the one table that would make {@link isLiteralUrl} wrong.
|
|
90
|
+
*
|
|
91
|
+
* Reached only for a destination already judged literal, so the cost is a scan
|
|
92
|
+
* of the published names for links that were never going to hit the table
|
|
93
|
+
* anyway — and zero for the route-name form, which is the common one. The right
|
|
94
|
+
* permanent home for this is `publishRouteTable`, at boot, once (see the report
|
|
95
|
+
* on this card).
|
|
96
|
+
*/
|
|
97
|
+
function assertNotARouteName(url) {
|
|
98
|
+
if (knownRouteNames().includes(url)) throw new RouteNameShapeCollisionError(url);
|
|
99
|
+
}
|
|
100
|
+
const ROUTE_ARGUMENT_PROPS = ["params", "query"];
|
|
101
|
+
function resolveDestination(props) {
|
|
102
|
+
const provided = DESTINATION_PROPS.filter((name) => props[name] !== void 0);
|
|
103
|
+
if (provided.length > 1) throw new AmbiguousLinkDestinationError(provided);
|
|
104
|
+
if (provided.length === 0) throw new MissingLinkDestinationError();
|
|
105
|
+
if (props.email !== void 0) return {
|
|
106
|
+
url: `mailto:${props.email}`,
|
|
107
|
+
isInApp: false
|
|
108
|
+
};
|
|
109
|
+
if (props.tel !== void 0) return {
|
|
110
|
+
url: `tel:${props.tel}`,
|
|
111
|
+
isInApp: false
|
|
112
|
+
};
|
|
113
|
+
const destination = props.to ?? props.href;
|
|
114
|
+
if (isLiteralUrl(destination)) {
|
|
115
|
+
const routeArguments = ROUTE_ARGUMENT_PROPS.filter((name) => props[name] !== void 0);
|
|
116
|
+
if (routeArguments.length > 0) throw new RouteArgumentsOnLiteralUrlError(destination, routeArguments);
|
|
117
|
+
assertNotARouteName(destination);
|
|
118
|
+
return {
|
|
119
|
+
url: destination,
|
|
120
|
+
isInApp: addressesThisApp(destination)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
url: href(destination, props.params, props.query),
|
|
125
|
+
isInApp: true
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Whether this click should be left entirely to the browser.
|
|
130
|
+
*
|
|
131
|
+
* Every case here is a click that MEANS something other than "go there in this
|
|
132
|
+
* tab", and intercepting any of them would take away behaviour the user
|
|
133
|
+
* explicitly asked for:
|
|
134
|
+
*
|
|
135
|
+
* - a modifier or middle button: open in a new tab/window, or download
|
|
136
|
+
* - `download`: save the resource, do not render it
|
|
137
|
+
* - already prevented: something upstream in the tree handled this click
|
|
138
|
+
*
|
|
139
|
+
* Left button with no modifiers is the only click that means plain navigation.
|
|
140
|
+
* The `target` case is decided before this, from the RESOLVED target, because
|
|
141
|
+
* `newTab` sets it after the caller's props are read.
|
|
142
|
+
*/
|
|
143
|
+
function isPlainLeftClick(event) {
|
|
144
|
+
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.defaultPrevented;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A target other than `_self` names ANOTHER browsing context — `_blank`, but
|
|
148
|
+
* also `_parent`, `_top` and any named frame. Client navigation rewrites the
|
|
149
|
+
* history of THIS one, so none of them are ours to intercept.
|
|
150
|
+
*/
|
|
151
|
+
function opensAnotherContext(target) {
|
|
152
|
+
return target !== void 0 && target !== "_self";
|
|
153
|
+
}
|
|
154
|
+
function Link({ to, href: hrefAlias, email, tel, params, query, newTab, prefetch, component: Component = "a", children, onClick, ...elementProps }) {
|
|
155
|
+
const { url, isInApp } = resolveDestination({
|
|
156
|
+
to,
|
|
157
|
+
href: hrefAlias,
|
|
158
|
+
email,
|
|
159
|
+
tel,
|
|
160
|
+
params,
|
|
161
|
+
query
|
|
162
|
+
});
|
|
163
|
+
const target = elementProps.target ?? (newTab === true ? "_blank" : void 0);
|
|
164
|
+
const rel = elementProps.rel ?? (target === "_blank" ? "noopener noreferrer" : void 0);
|
|
165
|
+
const handleClick = (event) => {
|
|
166
|
+
onClick?.(event);
|
|
167
|
+
if (!isInApp || opensAnotherContext(target)) return;
|
|
168
|
+
if (!isPlainLeftClick(event)) return;
|
|
169
|
+
if (currentNavigator()?.(url) !== true) return;
|
|
170
|
+
event.preventDefault();
|
|
171
|
+
};
|
|
172
|
+
const prefetchHandlers = prefetch === true && isInApp && !opensAnotherContext(target) ? {
|
|
173
|
+
onMouseEnter: (event) => {
|
|
174
|
+
elementProps.onMouseEnter?.(event);
|
|
175
|
+
prefetchPageData(url);
|
|
176
|
+
},
|
|
177
|
+
onFocus: (event) => {
|
|
178
|
+
elementProps.onFocus?.(event);
|
|
179
|
+
prefetchPageData(url);
|
|
180
|
+
}
|
|
181
|
+
} : void 0;
|
|
182
|
+
return createElement(Component, {
|
|
183
|
+
...elementProps,
|
|
184
|
+
...prefetchHandlers,
|
|
185
|
+
target,
|
|
186
|
+
rel,
|
|
187
|
+
href: url,
|
|
188
|
+
onClick: handleClick
|
|
189
|
+
}, children);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
export { Link };
|
|
194
|
+
//# sourceMappingURL=link.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"link.mjs","names":[],"sources":["../../../../../../../web/src/components/link.ts"],"sourcesContent":["import { createElement } from \"react\";\nimport type {\n AnchorHTMLAttributes,\n ComponentType,\n FocusEvent,\n MouseEvent,\n ReactElement,\n} from \"react\";\nimport { prefetchPageData } from \"../client/navigation/prefetch\";\nimport { currentNavigator } from \"../routing/navigator\";\nimport { href, knownRouteNames } from \"../routing/route-table\";\n\n/**\n * `<Link>` is SUGAR over `href()`, and deliberately thin.\n *\n * `href(name, params, query)` is the durable primitive — it serves emails,\n * redirects, `Location` headers and every non-React caller, none of which can\n * render a component. This file adds one thing to it: an anchor element.\n *\n * It renders a real `<a href>`. Client-side interception is a later slice of\n * the navigation runtime and lands here without changing this API, which is the\n * point of routing everything through `href` first: navigation becomes a\n * BEHAVIOUR change, not an API change.\n *\n * ── Parity with `@mongez/react-router` ───────────────────────────────────────\n * `href`, `newTab`, `email`, `tel`, `component` and `prefetch` are spelled\n * exactly as MRR spells them, so a component moved across keeps compiling.\n * `params` and `query` are ours and have no MRR equivalent: they pair with the\n * typed `href()` helper, which is what makes a route NAME — rather than a URL —\n * the thing a call site names.\n *\n * ── The semantic divergence this file bridges ────────────────────────────────\n * MRR's `to` is a PATH. Ours was a route NAME, and only a name — which meant a\n * component moved across from MRR compiled and then threw at render, because\n * `\"/products\"` is not the name of anything. The two packages disagreed about\n * what the most-used prop in either of them MEANS.\n *\n * Since 2026-08-24 (owner ruling) `to`/`href` accept BOTH, discriminated by\n * SHAPE — see {@link isLiteralUrl}. That is what makes MRR code portable, and\n * it costs nothing at a Warlock call site, because the two grammars cannot\n * collide: a route name never begins with `/` and never carries a `scheme:`.\n * The ruling RESTS on that, so this file asserts it rather than trusting it\n * ({@link RouteNameShapeCollisionError}).\n */\n\ntype AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, \"href\">;\n\n/**\n * Where the link goes. Every field is documented here once; which COMBINATIONS\n * are legal is decided by {@link LinkDestination}.\n */\ntype LinkDestinationProps = {\n /**\n * A route NAME, or a literal URL — told apart by SHAPE.\n *\n * `\"products.details\"` is a NAME and is resolved through the route table. A\n * page that moves changes its URL and keeps its name, so every call site\n * survives the move; a dead name throws at render naming the routes that do\n * exist, rather than rendering an anchor that 404s. This is the form to\n * prefer, and the only one `params` and `query` apply to.\n *\n * `\"/pricing\"`, `\"https://stripe.com\"`, `\"mailto:sales@example.com\"` and any\n * other `scheme:` are LITERAL — passed through to the element untouched, with\n * no route lookup at all. An app links out, and a route name is not a thing\n * you can have for a page that is not yours.\n */\n to?: string;\n /**\n * An alias of {@link to}, for parity with `@mongez/react-router`. Identical\n * in every respect, including which shapes it accepts.\n */\n href?: string;\n /** Renders a `mailto:` link. Not an in-app navigation. */\n email?: string;\n /** Renders a `tel:` link. Not an in-app navigation. */\n tel?: string;\n /**\n * Values for the route's `:param` segments, e.g. `{ id }` for\n * `\"/products/:id\"`. Only meaningful with a route NAME.\n */\n params?: Record<string, unknown>;\n /**\n * Query string values; an `undefined` value is omitted. Only meaningful with\n * a route NAME.\n */\n query?: Record<string, unknown>;\n};\n\n/**\n * EXACTLY ONE destination, enforced by the type.\n *\n * The alternative — a documented precedence such as \"`to` wins over `href`\" —\n * is silent by construction: the losing prop goes on compiling and goes on\n * reading like it works at the call site, and the anchor points at the wrong\n * page. Refusing the pair costs a call site one edit and can never be\n * misread. The runtime refuses it as well, because a JavaScript caller and a\n * cast both get past this.\n */\ntype LinkDestination =\n | { to: string; href?: never; email?: never; tel?: never }\n | { href: string; to?: never; email?: never; tel?: never }\n | { email: string; to?: never; href?: never; tel?: never }\n | { tel: string; to?: never; href?: never; email?: never };\n\nexport type LinkProps = AnchorProps &\n LinkDestinationProps &\n LinkDestination & {\n /**\n * Open in a new browsing context: `target=\"_blank\"` plus the `rel` that\n * stops the opened page from reaching back through `window.opener`.\n *\n * A caller's own `target`/`rel` win — this only fills in what was not said.\n */\n newTab?: boolean;\n /**\n * Render as something other than `<a>` — a tag name or a component.\n *\n * It receives the resolved `href`, the click handler and every remaining\n * prop, so a design-system anchor keeps client-side navigation as long as\n * it spreads what it is given onto the element it renders.\n *\n * `ComponentType<any>` is MRR's signature, kept verbatim: the component is\n * the caller's and its props are unknowable from here.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n component?: ComponentType<any> | string;\n /**\n * Fetch this page's data when the pointer or the keyboard reaches the link,\n * so the click that follows swaps without a round trip.\n *\n * A GUESS, and treated as one everywhere: it is never awaited, a failure is\n * silent, and the click behaves exactly as it would without it. Opt-in per\n * link rather than on by default, because every prefetch is a request the\n * user did not ask for and someone pays for the bandwidth.\n *\n * IGNORED for anything that is not an in-app navigation — an external URL,\n * `mailto:`, `tel:`, `newTab`, any explicit `target`. Prefetching those\n * would mean issuing a cross-origin request to a third party on hover,\n * which is not a thing a link component may decide to do.\n */\n prefetch?: boolean;\n };\n\nconst DESTINATION_PROPS = [\"to\", \"href\", \"email\", \"tel\"] as const;\n\nexport class AmbiguousLinkDestinationError extends Error {\n public constructor(public readonly providedProps: readonly string[]) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")}, but a link goes to exactly one place. There is no ` +\n \"precedence between them on purpose: one of the two would silently win, and the \" +\n \"call site would go on naming a destination that never renders. Delete the one \" +\n \"you did not mean.\",\n );\n this.name = \"AmbiguousLinkDestinationError\";\n }\n}\n\nexport class MissingLinkDestinationError extends Error {\n public constructor() {\n super(\n `Warlock <Link> was given no destination. Pass exactly one of ${DESTINATION_PROPS.map(\n name => JSON.stringify(name),\n ).join(\", \")}. It is not defaulted to the current page: an anchor with an empty ` +\n \"`href` renders as a working link and reloads the page when clicked, which is a \" +\n \"harder fault to see than this message.\",\n );\n this.name = \"MissingLinkDestinationError\";\n }\n}\n\nexport class RouteArgumentsOnLiteralUrlError extends Error {\n public constructor(\n public readonly url: string,\n public readonly providedProps: readonly string[],\n ) {\n super(\n `Warlock <Link> was given ${providedProps\n .map(name => JSON.stringify(name))\n .join(\" and \")} alongside the literal URL \"${url}\". Those apply to a route NAME, ` +\n \"which is resolved through the route table; a literal URL is passed through exactly \" +\n \"as written, so they would have been dropped and the link would have pointed at an \" +\n \"unfiltered page that still looked right at the call site. Put the values in the URL, \" +\n \"or name the route.\",\n );\n this.name = \"RouteArgumentsOnLiteralUrlError\";\n }\n}\n\n/**\n * The ruling's one assumption, broken. See the module doc comment: telling a\n * literal URL from a route NAME by shape is only safe while no route is NAMED\n * like a URL, and nothing in the route pipeline validates a hand-declared\n * `route.name`. So the collision is checked at the one place it could do harm,\n * where it is a loud refusal instead of an anchor that silently points\n * somewhere else.\n */\nexport class RouteNameShapeCollisionError extends Error {\n public constructor(public readonly routeName: string) {\n super(\n `Warlock route table: a route is NAMED ${JSON.stringify(routeName)}, which is shaped ` +\n \"like a URL. <Link> tells a literal URL from a route name by shape — a destination \" +\n \"starting with `/` or carrying a `scheme:` is passed through untouched — so this name \" +\n \"can never be resolved, and every link to it would silently point at that path \" +\n \"instead. Rename the route (`route = { path, name }`) to a dotted name such as \" +\n `${JSON.stringify(routeName.replace(/^\\/+/, \"\").replace(/\\//g, \".\") || \"index\")}.`,\n );\n this.name = \"RouteNameShapeCollisionError\";\n }\n}\n\ntype Destination = {\n /** What lands on the element's `href`. */\n url: string;\n /**\n * Whether this URL is a page in THIS app — the only kind the client\n * navigation runtime may be asked about, and the only kind that may be\n * prefetched. `mailto:`, `tel:` and an external URL hand off to another\n * application or another origin entirely, so intercepting any of them would\n * break it, and speculatively fetching one would be a cross-origin request\n * the developer never asked for.\n */\n isInApp: boolean;\n};\n\n/**\n * Any RFC 3986 scheme — `https:`, `mailto:`, `tel:`, `whatsapp:`, an app's own\n * custom one. Matched generically rather than as a list of known schemes: a\n * list would silently resolve `bitcoin:...` through the route table, which is\n * the exact failure this ruling exists to remove, and it would have to grow\n * forever.\n */\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;\n\n/**\n * Whether this destination is a URL to be used as written, rather than a route\n * name to resolve.\n *\n * The whole discriminator, and deliberately the whole of it: two cheap shape\n * tests, no parsing, no matching. Anything more would be a SECOND route matcher\n * living beside the server's, which this codebase refuses everywhere it comes\n * up — a matcher that disagreed with the real one would produce links to pages\n * that do not exist.\n */\nfunction isLiteralUrl(destination: string): boolean {\n return destination.startsWith(\"/\") || SCHEME_PATTERN.test(destination);\n}\n\n/**\n * Whether a literal URL addresses THIS app.\n *\n * A path is ours. A `scheme:` is not — including `https:` to our own origin,\n * which would need `window.location` to recognise and would make the answer\n * depend on where the code is running. And `//host/path` is PROTOCOL-RELATIVE:\n * it starts with a slash and is nonetheless another origin, which is precisely\n * the case a \"starts with `/`\" test alone would hand to the navigator, where it\n * becomes a `pushState` to a foreign origin — a SecurityError — or a\n * speculative fetch of a third-party host.\n */\nfunction addressesThisApp(url: string): boolean {\n return url.startsWith(\"/\") && !url.startsWith(\"//\");\n}\n\n/**\n * Refuses the one table that would make {@link isLiteralUrl} wrong.\n *\n * Reached only for a destination already judged literal, so the cost is a scan\n * of the published names for links that were never going to hit the table\n * anyway — and zero for the route-name form, which is the common one. The right\n * permanent home for this is `publishRouteTable`, at boot, once (see the report\n * on this card).\n */\nfunction assertNotARouteName(url: string): void {\n if (knownRouteNames().includes(url)) throw new RouteNameShapeCollisionError(url);\n}\n\nconst ROUTE_ARGUMENT_PROPS = [\"params\", \"query\"] as const;\n\nfunction resolveDestination(props: LinkDestinationProps): Destination {\n const provided = DESTINATION_PROPS.filter(name => props[name] !== undefined);\n\n if (provided.length > 1) throw new AmbiguousLinkDestinationError(provided);\n\n if (provided.length === 0) throw new MissingLinkDestinationError();\n\n if (props.email !== undefined) return { url: `mailto:${props.email}`, isInApp: false };\n\n if (props.tel !== undefined) return { url: `tel:${props.tel}`, isInApp: false };\n\n const destination = (props.to ?? props.href) as string;\n\n /*\n LITERAL: `/pricing`, `https://stripe.com`, `mailto:…`, `whatsapp://…`. It\n goes to the element exactly as written and the route table is never\n consulted — there is nothing to look up, and looking anyway is what used to\n throw `UnknownRouteNameError` on every link out of the application.\n */\n if (isLiteralUrl(destination)) {\n const routeArguments = ROUTE_ARGUMENT_PROPS.filter(name => props[name] !== undefined);\n\n if (routeArguments.length > 0) {\n throw new RouteArgumentsOnLiteralUrlError(destination, routeArguments);\n }\n\n assertNotARouteName(destination);\n\n return { url: destination, isInApp: addressesThisApp(destination) };\n }\n\n /*\n A NAME, resolved against the route table published at boot from the SAME\n discovery result the server registered its routes from. The previous version\n of this file restated six URLs in a literal map, so linking to any seventh\n page in the application threw — the map was the limit on what could be\n linked, and nothing said so at the call site.\n */\n return { url: href(destination, props.params, props.query), isInApp: true };\n}\n\n/**\n * Whether this click should be left entirely to the browser.\n *\n * Every case here is a click that MEANS something other than \"go there in this\n * tab\", and intercepting any of them would take away behaviour the user\n * explicitly asked for:\n *\n * - a modifier or middle button: open in a new tab/window, or download\n * - `download`: save the resource, do not render it\n * - already prevented: something upstream in the tree handled this click\n *\n * Left button with no modifiers is the only click that means plain navigation.\n * The `target` case is decided before this, from the RESOLVED target, because\n * `newTab` sets it after the caller's props are read.\n */\nfunction isPlainLeftClick(event: MouseEvent<HTMLAnchorElement>): boolean {\n return (\n event.button === 0 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey &&\n !event.defaultPrevented\n );\n}\n\n/**\n * A target other than `_self` names ANOTHER browsing context — `_blank`, but\n * also `_parent`, `_top` and any named frame. Client navigation rewrites the\n * history of THIS one, so none of them are ours to intercept.\n */\nfunction opensAnotherContext(target: string | undefined): boolean {\n return target !== undefined && target !== \"_self\";\n}\n\nexport function Link({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n newTab,\n prefetch,\n component: Component = \"a\",\n children,\n onClick,\n ...elementProps\n}: LinkProps): ReactElement {\n const { url, isInApp } = resolveDestination({\n to,\n href: hrefAlias,\n email,\n tel,\n params,\n query,\n });\n\n const target = elementProps.target ?? (newTab === true ? \"_blank\" : undefined);\n\n // Only a DEFAULT: a caller that wrote its own `rel` (`\"me noopener\"`,\n // `\"external\"`) meant it, and overwriting it would delete a value the page\n // depends on to say something this component knows nothing about.\n const rel =\n elementProps.rel ?? (target === \"_blank\" ? \"noopener noreferrer\" : undefined);\n\n const handleClick = (event: MouseEvent<HTMLAnchorElement>): void => {\n // The caller's handler runs FIRST and unconditionally — it may be doing\n // analytics, closing a menu, or calling `preventDefault()` to veto the\n // navigation outright. Deciding before it ran would let this component\n // navigate away from a click the application had already cancelled.\n onClick?.(event);\n\n // `mailto:`, `tel:` and anything aimed at another browsing context leave\n // this page standing. The runtime is not consulted at all — asking it would\n // spend a page-data fetch on a click that was never going to navigate here.\n if (!isInApp || opensAnotherContext(target)) return;\n\n if (!isPlainLeftClick(event)) return;\n\n /*\n Asked for per click, never captured at render: the runtime registers\n itself when the hydration bundle mounts, which is AFTER the first render\n of every anchor on the page. A value read at render time would be\n `undefined` forever for exactly the links present at hydration — that is,\n all of them.\n\n Absent (server render, or before hydration) the anchor is left alone and\n does what an anchor does. That is the whole progressive-enhancement story:\n links work before this code runs, and work better after.\n */\n if (currentNavigator()?.(url) !== true) return;\n\n event.preventDefault();\n };\n\n /*\n The SAME gate the click uses, asked before any speculative request exists:\n only a destination this app would have navigated to itself may be fetched\n ahead of time. `mailto:`, `tel:`, an external URL and anything aimed at\n another browsing context are all clicks that leave this page, and none of\n them has page data to fetch.\n */\n const prefetchesOnInteraction =\n prefetch === true && isInApp && !opensAnotherContext(target);\n\n /*\n Attached ONLY when prefetching — a link without the prop keeps whatever\n handlers the caller passed, on the element, unwrapped.\n\n Hover AND focus, because a keyboard user never generates the first one and\n would otherwise be the only visitor who never gets the optimisation.\n\n Fire-and-forget by construction: `prefetchPageData` never rejects and is\n never awaited, so nothing here can delay the event or surface a failure. It\n is also safe to reach on the server — it no-ops without a browser — which is\n why this file can import it directly rather than through a `connect*` seam\n like the navigator's. The navigator needs a seam because the runtime behind\n it drags React state and the page registry into the server bundle; the\n prefetch cache is a `Map` and a `fetch` call, inert until an event fires.\n */\n const prefetchHandlers = prefetchesOnInteraction\n ? {\n onMouseEnter: (event: MouseEvent<HTMLAnchorElement>): void => {\n elementProps.onMouseEnter?.(event);\n void prefetchPageData(url);\n },\n onFocus: (event: FocusEvent<HTMLAnchorElement>): void => {\n elementProps.onFocus?.(event);\n void prefetchPageData(url);\n },\n }\n : undefined;\n\n return createElement(\n Component,\n { ...elementProps, ...prefetchHandlers, target, rel, href: url, onClick: handleClick },\n children,\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/components/scripts.d.ts
|
|
4
|
+
type ScriptsProps = {
|
|
5
|
+
/** Per-request CSP nonce for the inline payload script (root.tsx:119). */nonce?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* OPTIONAL placement override for the serialized loader data and `shared`.
|
|
9
|
+
* Hydration module emission is separate wiring.
|
|
10
|
+
*/
|
|
11
|
+
declare function Scripts(props: ScriptsProps): ReactElement;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { Scripts };
|
|
14
|
+
//# sourceMappingURL=scripts.d.mts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PAYLOAD_SCRIPT_ID, escapePayload, useDocumentContext } from "./document-context.mjs";
|
|
2
|
+
import { createElement } from "react";
|
|
3
|
+
|
|
4
|
+
//#region ../web/src/components/scripts.ts
|
|
5
|
+
/**
|
|
6
|
+
* OPTIONAL placement override for the serialized loader data and `shared`.
|
|
7
|
+
* Hydration module emission is separate wiring.
|
|
8
|
+
*/
|
|
9
|
+
function Scripts(props) {
|
|
10
|
+
const { payload, nonce } = useDocumentContext("Scripts");
|
|
11
|
+
return createElement("script", {
|
|
12
|
+
id: PAYLOAD_SCRIPT_ID,
|
|
13
|
+
type: "application/json",
|
|
14
|
+
nonce: props.nonce ?? nonce,
|
|
15
|
+
dangerouslySetInnerHTML: { __html: escapePayload(JSON.stringify(payload)) }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { Scripts };
|
|
21
|
+
//# sourceMappingURL=scripts.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scripts.mjs","names":[],"sources":["../../../../../../../web/src/components/scripts.ts"],"sourcesContent":["import { createElement, type ReactElement } from \"react\";\nimport { escapePayload, PAYLOAD_SCRIPT_ID, useDocumentContext } from \"./document-context\";\n\nexport type ScriptsProps = {\n /** Per-request CSP nonce for the inline payload script (root.tsx:119). */\n nonce?: string;\n};\n\n/**\n * OPTIONAL placement override for the serialized loader data and `shared`.\n * Hydration module emission is separate wiring.\n */\nexport function Scripts(props: ScriptsProps): ReactElement {\n const { payload, nonce } = useDocumentContext(\"Scripts\");\n\n // Explicit prop wins: v5/app's root.tsx passes `shared.nonce` today\n // (root.tsx:119) and must keep working unchanged. Only an absent prop falls\n // back to the framework's nonce slot (document-context.ts).\n const resolvedNonce = props.nonce ?? nonce;\n\n // `dangerouslySetInnerHTML`, not children: the serializer's escaped output must\n // reach the document byte-for-byte. React's default child-text escaping\n // (HTML-entity escaping) would double-process it and corrupt the JSON\n // (spike P7's escaping contract).\n return createElement(\"script\", {\n id: PAYLOAD_SCRIPT_ID,\n type: \"application/json\",\n nonce: resolvedNonce,\n dangerouslySetInnerHTML: { __html: escapePayload(JSON.stringify(payload)) },\n });\n}\n"],"mappings":";;;;;;;;AAYA,SAAgB,QAAQ,OAAmC;CACzD,MAAM,EAAE,SAAS,UAAU,mBAAmB,SAAS;CAWvD,OAAO,cAAc,UAAU;EAC7B,IAAI;EACJ,MAAM;EACN,OAToB,MAAM,SAAS;EAUnC,yBAAyB,EAAE,QAAQ,cAAc,KAAK,UAAU,OAAO,CAAC,EAAE;CAC5E,CAAC;AACH"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { WebBuildOptions, WebPackageRootResolutionError, createWebBuildContribution } from "../build/contribution.mjs";
|
|
2
|
+
import { PageManifest, PageManifestLayoutEntry, PageManifestPageEntry, consumePageManifest, providePageManifest } from "../server/page-manifest.mjs";
|
|
3
|
+
import { CLIENT_ASSET_URL_PREFIX } from "../server/client-asset-url-prefix.mjs";
|
|
4
|
+
import { ResolveHydrationClientUrlOptions, WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, resolveHydrationClientUrl } from "../server/hydration-client-url.mjs";
|
|
5
|
+
import { webConnector } from "../server/web-connector-factory.mjs";
|
|
6
|
+
export { CLIENT_ASSET_URL_PREFIX, type PageManifest, type PageManifestLayoutEntry, type PageManifestPageEntry, type ResolveHydrationClientUrlOptions, type WebBuildOptions, WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, WebPackageRootResolutionError, consumePageManifest, createWebBuildContribution, providePageManifest, resolveHydrationClientUrl, webConnector };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { WebPackageRootResolutionError, createWebBuildContribution } from "../build/contribution.mjs";
|
|
2
|
+
import { consumePageManifest, providePageManifest } from "../server/page-manifest.mjs";
|
|
3
|
+
import { CLIENT_ASSET_URL_PREFIX } from "../server/client-asset-url-prefix.mjs";
|
|
4
|
+
import { WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, resolveHydrationClientUrl } from "../server/hydration-client-url.mjs";
|
|
5
|
+
import { webConnector } from "../server/web-connector-factory.mjs";
|
|
6
|
+
|
|
7
|
+
export { CLIENT_ASSET_URL_PREFIX, WebClientAssetPrefixViolationError, WebClientManifestEntryMissingError, WebClientManifestMalformedError, WebClientManifestMissingError, WebPackageRootResolutionError, consumePageManifest, createWebBuildContribution, providePageManifest, resolveHydrationClientUrl, webConnector };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SharedContext } from "./index.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../web/src/context.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The brand behind loader short-circuits.
|
|
6
|
+
*
|
|
7
|
+
* Core's `Response#notFound()` / `#redirect()` return an unbranded
|
|
8
|
+
* `Response`/`this` (core/src/http/response.ts:862-866, 1028-1034). The page
|
|
9
|
+
* contract needs those returns to NOT union into `PageProps["data"]`
|
|
10
|
+
* (v5/app/src/app/products/web/product-details.page.tsx:45-57), so web brands
|
|
11
|
+
* them on ITS OWN response surface — the one a loader's context carries —
|
|
12
|
+
* without touching core. `LoaderData` excludes exactly this brand.
|
|
13
|
+
*/
|
|
14
|
+
declare const loaderShortCircuit: unique symbol;
|
|
15
|
+
interface LoaderShortCircuit {
|
|
16
|
+
readonly [loaderShortCircuit]: true;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The request as a loader sees it. Structural and deliberately minimal for M1:
|
|
20
|
+
* exactly the members the v5/app loaders exercise, typed by the page's own
|
|
21
|
+
* `validation` and `route` generics.
|
|
22
|
+
*
|
|
23
|
+
* TODO(Stream A.6 — core ctx migration): once core ships
|
|
24
|
+
* `HttpContext { request, response }` and the declared `request.user` /
|
|
25
|
+
* `request.locals` / `request.nonce`, this narrows to a refinement of core's `Request`
|
|
26
|
+
* instead of a standalone structural type.
|
|
27
|
+
*/
|
|
28
|
+
interface WebRequest<TValidated = Record<string, never>, TParams extends string = string> {
|
|
29
|
+
/**
|
|
30
|
+
* The validated inputs, typed off the page's `validation.schema` with
|
|
31
|
+
* `Infer.Output` semantics — `.default()` fields stay required
|
|
32
|
+
* (v5/app/src/app/auth/schema/login.schema.ts:29-38). The key-list parameter
|
|
33
|
+
* is optional and selects a subset, mirroring core
|
|
34
|
+
* (core/src/http/request.ts:610).
|
|
35
|
+
*/
|
|
36
|
+
validated(inputs?: readonly (keyof TValidated | (string & {}))[]): TValidated;
|
|
37
|
+
/**
|
|
38
|
+
* Typed FROM THE ROUTE: `TParams` is derived from the route path literal, so
|
|
39
|
+
* `request.input("id")` types against `path: "/:id"` and a key that is not a
|
|
40
|
+
* route parameter is a compile error
|
|
41
|
+
* (v5/app/src/web/__type-tests__/route-params.type-test.ts).
|
|
42
|
+
*/
|
|
43
|
+
input(key: TParams, defaultValue?: unknown): string;
|
|
44
|
+
/**
|
|
45
|
+
* The authenticated user, when middleware resolved one.
|
|
46
|
+
*
|
|
47
|
+
* `any`, deliberately, and recorded as a deviation in the M1 report: the
|
|
48
|
+
* reference app hands `request.user` to services typed against the APP's own
|
|
49
|
+
* model (`navService.forUser(user: User)`,
|
|
50
|
+
* v5/app/src/web/layouts/dashboard.layout.tsx:35) and reads `.id` with no
|
|
51
|
+
* null-check (settings.page.tsx:65). No type this package can declare is
|
|
52
|
+
* assignable to an app-owned model class; the real declaration has to come
|
|
53
|
+
* from core's own `Request.user` and lands with Stream A.6.
|
|
54
|
+
*
|
|
55
|
+
* Optional, matching core (`Request.user?`, core/src/http/request.ts:94) —
|
|
56
|
+
* a required member here would reject core's real `Request` at the seam.
|
|
57
|
+
* `any` absorbs the `undefined`, so loader-side `request.user.id` reads
|
|
58
|
+
* keep compiling (the recorded M1 deviation stands).
|
|
59
|
+
*/
|
|
60
|
+
user?: any;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The response as a loader sees it. A loader may mutate the response during
|
|
64
|
+
* render (headers, status) and may SHORT-CIRCUIT it — the short-circuiting
|
|
65
|
+
* methods return the branded type that never reaches `PageProps["data"]`.
|
|
66
|
+
*
|
|
67
|
+
* TODO(Stream A.6): same story as `WebRequest` — becomes a refinement of
|
|
68
|
+
* core's `Response` once the ctx object lands in core.
|
|
69
|
+
*/
|
|
70
|
+
interface WebResponse {
|
|
71
|
+
/** Buffered, committed in tree order after every loader settles. */
|
|
72
|
+
header(key: string, value: string): WebResponse;
|
|
73
|
+
/** The not-found page's loader's real output is the status (not-found.page.tsx:25). */
|
|
74
|
+
setStatusCode(statusCode: number): WebResponse;
|
|
75
|
+
/** A short-circuit, not a value — never unions into `data`. */
|
|
76
|
+
redirect(url: string, statusCode?: number): LoaderShortCircuit;
|
|
77
|
+
/** 301 flavour of the same short-circuit. */
|
|
78
|
+
permanentRedirect(url: string): LoaderShortCircuit;
|
|
79
|
+
/** A 404 is an ANSWER, not an incident (product-details.page.tsx:45-57). */
|
|
80
|
+
notFound(body?: unknown): LoaderShortCircuit;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The one context object every handler receives.
|
|
84
|
+
*
|
|
85
|
+
* TODO(Stream A.6): core is to OWN `HttpContext { request, response }` and the
|
|
86
|
+
* re-parameterised `Middleware<TContext extends HttpContext>`. Core today has
|
|
87
|
+
* no `HttpContext` and its `Middleware` is positional `(request, response)`
|
|
88
|
+
* (core/src/router/types.ts:17), so importing the type from
|
|
89
|
+
* `@warlock.js/core` cannot resolve to the v5 shape yet — this structural
|
|
90
|
+
* declaration stands in until core declares it.
|
|
91
|
+
*/
|
|
92
|
+
interface HttpContext {
|
|
93
|
+
request: WebRequest;
|
|
94
|
+
response: WebResponse;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The web layer's context: `HttpContext` plus the per-request payload. The
|
|
98
|
+
* keys are framework-owned and the list is closed — `request`, `response`,
|
|
99
|
+
* `shared`. App data goes on `request.locals` (private) or `shared` (public),
|
|
100
|
+
* never onto the context itself.
|
|
101
|
+
*/
|
|
102
|
+
interface PageContext extends HttpContext {
|
|
103
|
+
shared: SharedContext;
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { LoaderShortCircuit, PageContext, WebRequest, WebResponse };
|
|
107
|
+
//# sourceMappingURL=context.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { publishRouteTable } from "../routing/route-table.mjs";
|
|
2
|
+
import { buildHydratedTree } from "../client/build-hydrated-tree.mjs";
|
|
3
|
+
import { hydratePage } from "../client/hydrate-page.mjs";
|
|
4
|
+
import { NavigationRoot } from "../client/navigation/navigation-root.mjs";
|
|
5
|
+
import { createElement } from "react";
|
|
6
|
+
import { pages } from "virtual:warlock/pages";
|
|
7
|
+
|
|
8
|
+
//#region ../web/src/hydration/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* The client entry: registry in, hydration out. No page names, no app paths.
|
|
11
|
+
*
|
|
12
|
+
* The page graph arrives from the Vite virtual module the build plugin serves,
|
|
13
|
+
* so this file works for ANY page in ANY app. The previous version imported one
|
|
14
|
+
* page and one layout by relative path into the monorepo's reference app, which
|
|
15
|
+
* both hydrated every URL as the home page and made `@warlock.js/web`
|
|
16
|
+
* uninstallable anywhere outside this checkout. Neither a page name nor an app
|
|
17
|
+
* path appears here now, and nothing about the composition lives here either -
|
|
18
|
+
* that is `buildHydratedTree`, which takes the registry as an argument and is
|
|
19
|
+
* therefore testable without a bundler.
|
|
20
|
+
*/
|
|
21
|
+
publishRouteTable(pages, "hydration client entry");
|
|
22
|
+
hydratePage(async (payload) => {
|
|
23
|
+
return createElement(NavigationRoot, {
|
|
24
|
+
pages,
|
|
25
|
+
initialPayload: payload,
|
|
26
|
+
initialTree: await buildHydratedTree(pages, payload),
|
|
27
|
+
buildTree: buildHydratedTree
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
export { };
|
|
33
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../web/src/hydration/index.ts"],"sourcesContent":["/**\n * The client entry: registry in, hydration out. No page names, no app paths.\n *\n * The page graph arrives from the Vite virtual module the build plugin serves,\n * so this file works for ANY page in ANY app. The previous version imported one\n * page and one layout by relative path into the monorepo's reference app, which\n * both hydrated every URL as the home page and made `@warlock.js/web`\n * uninstallable anywhere outside this checkout. Neither a page name nor an app\n * path appears here now, and nothing about the composition lives here either -\n * that is `buildHydratedTree`, which takes the registry as an argument and is\n * therefore testable without a bundler.\n */\nimport { createElement } from \"react\";\nimport { pages } from \"virtual:warlock/pages\";\nimport { buildHydratedTree } from \"../client/build-hydrated-tree\";\nimport { hydratePage } from \"../client/hydrate-page\";\nimport { NavigationRoot } from \"../client/navigation/navigation-root\";\nimport { publishRouteTable } from \"../routing/route-table\";\n\n/*\n BEFORE the mount, not after: `<Link>` resolves its URL through the route table\n during render, and the first render is the hydration render. Publishing\n afterwards would make every anchor in the initial tree throw.\n\n The registry entries already carry `name` and `path` - the same pair the\n server registered its routes from, out of the same discovery result - so the\n browser's table cannot drift from the server's without the two being built\n from different page graphs, which hydration already refuses.\n*/\npublishRouteTable(pages, \"hydration client entry\");\n\n/*\n The hydrated tree is wrapped in `NavigationRoot` so the page can be REPLACED\n later without a document load. The first render is still exactly the tree the\n server produced - `NavigationRoot` renders `initialTree` verbatim and adds no\n markup of its own - so hydration still matches the server byte for byte, and\n the wrapper only starts to matter on the first navigation.\n\n NO MRR HISTORY BRIDGE IS INSTALLED HERE YET, and that is a pending decision\n rather than an oversight. This runtime drives `window.history` itself and is\n complete without MRR. Handing history to `@mongez/react-router` as well means\n `@warlock.js/web` importing it, which puts MRR in the bundle of EVERY app that\n uses this package - a packaging choice (dependency vs. peer vs. app-level\n opt-in) with consequences for apps that never navigate through MRR. The bridge\n itself is written and documented in\n `conversations/2026-08-24-production-ssr-session.md`; it lands the moment that\n choice is made.\n*/\nhydratePage(async (payload) => {\n const tree = await buildHydratedTree(pages, payload);\n\n return createElement(NavigationRoot, {\n pages,\n initialPayload: payload,\n initialTree: tree,\n buildTree: buildHydratedTree,\n });\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA6BA,kBAAkB,OAAO,wBAAwB;AAmBjD,YAAY,OAAO,YAAY;CAG7B,OAAO,cAAc,gBAAgB;EACnC;EACA,gBAAgB;EAChB,aAAa,MALI,kBAAkB,OAAO,OAAO;EAMjD,WAAW;CACb,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|