@bleedingdev/modern-js-renderer-core 0.0.0-bootstrap → 3.9.3-ultramodern.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/cjs/abort.js +65 -0
- package/dist/cjs/adapter.js +62 -0
- package/dist/cjs/data/body.js +78 -0
- package/dist/cjs/data/client.js +409 -0
- package/dist/cjs/data/codec.js +168 -0
- package/dist/cjs/data/index.js +125 -0
- package/dist/cjs/data/private.js +77 -0
- package/dist/cjs/data/routes.js +149 -0
- package/dist/cjs/data/server.js +791 -0
- package/dist/cjs/data/static.js +110 -0
- package/dist/cjs/data/tree.js +231 -0
- package/dist/cjs/data/types.js +56 -0
- package/dist/cjs/document.js +184 -0
- package/dist/cjs/entry-client.js +142 -0
- package/dist/cjs/entry-server.js +253 -0
- package/dist/cjs/federation.js +80 -0
- package/dist/cjs/identity.js +120 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/localization.js +48 -0
- package/dist/cjs/router.js +293 -0
- package/dist/cjs/server/assets.js +56 -0
- package/dist/cjs/server/dispatch.js +351 -0
- package/dist/cjs/server/index.js +57 -0
- package/dist/cjs/server/types.js +18 -0
- package/dist/cjs/server/worker.js +73 -0
- package/dist/cjs/session/cache.js +172 -0
- package/dist/cjs/session/index.js +63 -0
- package/dist/cjs/session/request.js +269 -0
- package/dist/cjs/session/types.js +18 -0
- package/dist/esm/abort.mjs +27 -0
- package/dist/esm/adapter.mjs +21 -0
- package/dist/esm/data/body.mjs +40 -0
- package/dist/esm/data/client.mjs +368 -0
- package/dist/esm/data/codec.mjs +112 -0
- package/dist/esm/data/index.mjs +7 -0
- package/dist/esm/data/private.mjs +39 -0
- package/dist/esm/data/routes.mjs +108 -0
- package/dist/esm/data/server.mjs +723 -0
- package/dist/esm/data/static.mjs +51 -0
- package/dist/esm/data/tree.mjs +193 -0
- package/dist/esm/data/types.mjs +6 -0
- package/dist/esm/document.mjs +131 -0
- package/dist/esm/entry-client.mjs +104 -0
- package/dist/esm/entry-server.mjs +215 -0
- package/dist/esm/federation.mjs +36 -0
- package/dist/esm/identity.mjs +63 -0
- package/dist/esm/index.mjs +1 -0
- package/dist/esm/localization.mjs +10 -0
- package/dist/esm/router.mjs +225 -0
- package/dist/esm/server/assets.mjs +15 -0
- package/dist/esm/server/dispatch.mjs +307 -0
- package/dist/esm/server/index.mjs +3 -0
- package/dist/esm/server/types.mjs +0 -0
- package/dist/esm/server/worker.mjs +35 -0
- package/dist/esm/session/cache.mjs +116 -0
- package/dist/esm/session/index.mjs +3 -0
- package/dist/esm/session/request.mjs +231 -0
- package/dist/esm/session/types.mjs +0 -0
- package/dist/esm-node/abort.mjs +28 -0
- package/dist/esm-node/adapter.mjs +22 -0
- package/dist/esm-node/data/body.mjs +41 -0
- package/dist/esm-node/data/client.mjs +369 -0
- package/dist/esm-node/data/codec.mjs +113 -0
- package/dist/esm-node/data/index.mjs +8 -0
- package/dist/esm-node/data/private.mjs +40 -0
- package/dist/esm-node/data/routes.mjs +109 -0
- package/dist/esm-node/data/server.mjs +724 -0
- package/dist/esm-node/data/static.mjs +52 -0
- package/dist/esm-node/data/tree.mjs +194 -0
- package/dist/esm-node/data/types.mjs +7 -0
- package/dist/esm-node/document.mjs +132 -0
- package/dist/esm-node/entry-client.mjs +105 -0
- package/dist/esm-node/entry-server.mjs +216 -0
- package/dist/esm-node/federation.mjs +37 -0
- package/dist/esm-node/identity.mjs +64 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/localization.mjs +11 -0
- package/dist/esm-node/router.mjs +226 -0
- package/dist/esm-node/server/assets.mjs +16 -0
- package/dist/esm-node/server/dispatch.mjs +308 -0
- package/dist/esm-node/server/index.mjs +4 -0
- package/dist/esm-node/server/types.mjs +1 -0
- package/dist/esm-node/server/worker.mjs +36 -0
- package/dist/esm-node/session/cache.mjs +117 -0
- package/dist/esm-node/session/index.mjs +4 -0
- package/dist/esm-node/session/request.mjs +232 -0
- package/dist/esm-node/session/types.mjs +1 -0
- package/dist/types/abort.d.ts +6 -0
- package/dist/types/adapter.d.ts +195 -0
- package/dist/types/data/body.d.ts +2 -0
- package/dist/types/data/client.d.ts +17 -0
- package/dist/types/data/codec.d.ts +14 -0
- package/dist/types/data/index.d.ts +8 -0
- package/dist/types/data/private.d.ts +6 -0
- package/dist/types/data/routes.d.ts +28 -0
- package/dist/types/data/server.d.ts +59 -0
- package/dist/types/data/static.d.ts +31 -0
- package/dist/types/data/tree.d.ts +3 -0
- package/dist/types/data/types.d.ts +104 -0
- package/dist/types/document.d.ts +76 -0
- package/dist/types/entry-client.d.ts +53 -0
- package/dist/types/entry-server.d.ts +63 -0
- package/dist/types/federation.d.ts +56 -0
- package/dist/types/identity.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/localization.d.ts +58 -0
- package/dist/types/router.d.ts +201 -0
- package/dist/types/server/assets.d.ts +14 -0
- package/dist/types/server/dispatch.d.ts +11 -0
- package/dist/types/server/index.d.ts +4 -0
- package/dist/types/server/types.d.ts +64 -0
- package/dist/types/server/worker.d.ts +25 -0
- package/dist/types/session/cache.d.ts +25 -0
- package/dist/types/session/index.d.ts +4 -0
- package/dist/types/session/request.d.ts +8 -0
- package/dist/types/session/types.d.ts +48 -0
- package/package.json +159 -4
- package/src/abort.ts +40 -0
- package/src/adapter.ts +255 -0
- package/src/data/body.ts +43 -0
- package/src/data/client.ts +501 -0
- package/src/data/codec.ts +247 -0
- package/src/data/index.ts +8 -0
- package/src/data/private.ts +68 -0
- package/src/data/routes.ts +201 -0
- package/src/data/server.ts +977 -0
- package/src/data/static.ts +105 -0
- package/src/data/tree.ts +244 -0
- package/src/data/types.ts +98 -0
- package/src/document.ts +338 -0
- package/src/entry-client.ts +236 -0
- package/src/entry-server.ts +393 -0
- package/src/federation.ts +111 -0
- package/src/identity.ts +128 -0
- package/src/index.ts +7 -0
- package/src/localization.ts +77 -0
- package/src/router.ts +475 -0
- package/src/server/assets.ts +60 -0
- package/src/server/dispatch.ts +523 -0
- package/src/server/index.ts +16 -0
- package/src/server/types.ts +94 -0
- package/src/server/worker.ts +94 -0
- package/src/session/cache.ts +177 -0
- package/src/session/index.ts +11 -0
- package/src/session/request.ts +324 -0
- package/src/session/types.ts +60 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { DocumentInlineData } from './document';
|
|
2
|
+
import type { NativeLocationRewrite } from './router';
|
|
3
|
+
|
|
4
|
+
/** `{ [namespace]: { [key]: value } }` for one language. */
|
|
5
|
+
export type NativeI18nResources = Record<string, Record<string, unknown>>;
|
|
6
|
+
|
|
7
|
+
export type NativeRequestLanguage =
|
|
8
|
+
| {
|
|
9
|
+
readonly kind: 'language';
|
|
10
|
+
readonly language: string;
|
|
11
|
+
/** Request headers the language was detected from, for `Vary`. */
|
|
12
|
+
readonly vary?: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: 'redirect';
|
|
16
|
+
readonly language: string;
|
|
17
|
+
readonly location: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A per-request or per-document translation instance, such as i18next's. */
|
|
21
|
+
export interface NativeI18nInstance {
|
|
22
|
+
readonly language: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Localized routing a native entry composes. `createNativeI18n()` from
|
|
27
|
+
* `@modern-js/i18n-runtime-extensions/native` implements it with i18next.
|
|
28
|
+
*/
|
|
29
|
+
export interface NativeEntryI18n<
|
|
30
|
+
Instance extends NativeI18nInstance = NativeI18nInstance,
|
|
31
|
+
LocalisedUrls = unknown,
|
|
32
|
+
> {
|
|
33
|
+
readonly languages: readonly string[];
|
|
34
|
+
readonly localisedUrls?: LocalisedUrls;
|
|
35
|
+
/** The language of a request URL, or the redirect to its localized URL. */
|
|
36
|
+
resolveRequest(request: Request): NativeRequestLanguage;
|
|
37
|
+
redirect(location: string): Response;
|
|
38
|
+
/** A new, fully isolated instance for one request or one browser document. */
|
|
39
|
+
create(language: string, resources?: NativeI18nResources): Promise<Instance>;
|
|
40
|
+
/** The router rewrite: matching stays on canonical paths. */
|
|
41
|
+
rewrite(getLanguage: () => string): NativeLocationRewrite;
|
|
42
|
+
/** The document handoff of a language and, with an instance, its bundles. */
|
|
43
|
+
handoff(language: string, instance?: Instance): DocumentInlineData;
|
|
44
|
+
/** The server's language and bundles, read before the browser router starts. */
|
|
45
|
+
clientHandoff(): { language: string; resources?: NativeI18nResources };
|
|
46
|
+
/**
|
|
47
|
+
* History navigation to another language prefix switches the instance.
|
|
48
|
+
* Returns the cleanup that detaches it when the client entry is disposed.
|
|
49
|
+
*/
|
|
50
|
+
syncWithRouter(router: object, instance: Instance): () => void;
|
|
51
|
+
/**
|
|
52
|
+
* Keep the document language and persisted language in step with the
|
|
53
|
+
* instance, for routed and component-only entries alike. Returns the
|
|
54
|
+
* cleanup that detaches it when the client entry is disposed.
|
|
55
|
+
*/
|
|
56
|
+
persist?(instance: Instance): () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What a renderer needs to provide localization to its application view. */
|
|
60
|
+
export interface NativeI18nView<Instance, LocalisedUrls> {
|
|
61
|
+
readonly instance: Instance;
|
|
62
|
+
readonly languages: readonly string[];
|
|
63
|
+
readonly localisedUrls?: LocalisedUrls;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nativeI18nView<Instance extends NativeI18nInstance, Urls>(
|
|
67
|
+
i18n: NativeEntryI18n<Instance, Urls>,
|
|
68
|
+
instance: Instance,
|
|
69
|
+
): NativeI18nView<Instance, Urls> {
|
|
70
|
+
return {
|
|
71
|
+
instance,
|
|
72
|
+
languages: i18n.languages,
|
|
73
|
+
...(i18n.localisedUrls === undefined
|
|
74
|
+
? {}
|
|
75
|
+
: { localisedUrls: i18n.localisedUrls }),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import { untilAborted } from './abort';
|
|
2
|
+
import type {
|
|
3
|
+
DataHandler,
|
|
4
|
+
DataHandlerInput,
|
|
5
|
+
DataOperation,
|
|
6
|
+
DataOutcome,
|
|
7
|
+
DecodedDataOutcome,
|
|
8
|
+
FileSystemRouteIR,
|
|
9
|
+
PublicDataOutcome,
|
|
10
|
+
SelectedDataRoute,
|
|
11
|
+
} from './data';
|
|
12
|
+
import {
|
|
13
|
+
DataProtocolError,
|
|
14
|
+
parsePublicData,
|
|
15
|
+
serializePublicData,
|
|
16
|
+
toTanstackPath,
|
|
17
|
+
} from './data';
|
|
18
|
+
import type { RendererIdentity } from './identity';
|
|
19
|
+
import type { RequestSession } from './session';
|
|
20
|
+
|
|
21
|
+
// Renderer-neutral glue between filesystem routes and a native TanStack router.
|
|
22
|
+
// Router shapes stay structural: each adapter pins its own router-core copy,
|
|
23
|
+
// and router-core's class types are nominal across copies.
|
|
24
|
+
|
|
25
|
+
export interface FileSystemDataModule<Context = unknown> {
|
|
26
|
+
loader?: DataHandler<Context>;
|
|
27
|
+
action?: DataHandler<Context>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RouteOutcome = DataOutcome | DecodedDataOutcome;
|
|
31
|
+
|
|
32
|
+
export interface FileSystemRouteOptions<Context = unknown, Router = unknown> {
|
|
33
|
+
/** The request owned by this router; carries RequestSession.signal. */
|
|
34
|
+
request?: Request;
|
|
35
|
+
/** Retires every route load, as a disposed browser entry does. */
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
/** The exact request owner validates native public snapshots (Solid). */
|
|
38
|
+
session?: RequestSession;
|
|
39
|
+
/** Private data-handler input; never part of native route context. */
|
|
40
|
+
context?: Context;
|
|
41
|
+
/** Resolve the native router once its route tree is installed (Octane). */
|
|
42
|
+
getRouter?: () => Router;
|
|
43
|
+
loadRoute?: (
|
|
44
|
+
route: FileSystemRouteIR,
|
|
45
|
+
input: DataHandlerInput<Context>,
|
|
46
|
+
) => Promise<RouteOutcome>;
|
|
47
|
+
/** The server resolves HTTP policy from blocking outcomes before rendering. */
|
|
48
|
+
onOutcome?: (routeId: string, outcome: RouteOutcome) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A route boundary receives only the public error projection and its status. */
|
|
52
|
+
export class RouteDataError extends Error {
|
|
53
|
+
readonly status: number;
|
|
54
|
+
readonly data: unknown;
|
|
55
|
+
readonly routeId: string;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
routeId: string,
|
|
59
|
+
outcome: Extract<DataOutcome | PublicDataOutcome, { kind: 'error' }>,
|
|
60
|
+
) {
|
|
61
|
+
super(outcome.error.message);
|
|
62
|
+
this.name = outcome.error.name;
|
|
63
|
+
this.routeId = routeId;
|
|
64
|
+
this.status =
|
|
65
|
+
'response' in outcome ? outcome.response.status : outcome.status;
|
|
66
|
+
this.data = outcome.data;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The renderer binding's own redirect and not-found constructors. */
|
|
71
|
+
export interface NativeRouteSignals {
|
|
72
|
+
redirect(options: {
|
|
73
|
+
href: string;
|
|
74
|
+
statusCode: number;
|
|
75
|
+
headers?: HeadersInit;
|
|
76
|
+
}): unknown;
|
|
77
|
+
notFound(options: { data: unknown }): unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Turn a data outcome into loader data, or throw it into the native redirect,
|
|
82
|
+
* not-found or error boundary. `project` maps every public value first.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveRouteData(
|
|
85
|
+
native: NativeRouteSignals,
|
|
86
|
+
routeId: string,
|
|
87
|
+
outcome: DataOutcome | PublicDataOutcome,
|
|
88
|
+
project: (value: unknown) => unknown = value => value,
|
|
89
|
+
): unknown {
|
|
90
|
+
switch (outcome.kind) {
|
|
91
|
+
case 'success':
|
|
92
|
+
return project(outcome.value);
|
|
93
|
+
case 'deferred': {
|
|
94
|
+
// The critical record makes the same codec round trip as on the wire:
|
|
95
|
+
// a copy that keeps its cycles and prototype, never the loader's own
|
|
96
|
+
// (possibly shared) object, then receives the deferred promises.
|
|
97
|
+
const value = parsePublicData(
|
|
98
|
+
serializePublicData(outcome.critical),
|
|
99
|
+
) as Record<string, unknown>;
|
|
100
|
+
for (const [key, promise] of Object.entries(outcome.deferred))
|
|
101
|
+
Object.defineProperty(value, key, {
|
|
102
|
+
value: promise,
|
|
103
|
+
enumerable: true,
|
|
104
|
+
writable: true,
|
|
105
|
+
configurable: true,
|
|
106
|
+
});
|
|
107
|
+
return project(value);
|
|
108
|
+
}
|
|
109
|
+
case 'redirect':
|
|
110
|
+
throw native.redirect({
|
|
111
|
+
href: outcome.location,
|
|
112
|
+
...('response' in outcome ? { headers: outcome.response.headers } : {}),
|
|
113
|
+
statusCode:
|
|
114
|
+
'response' in outcome ? outcome.response.status : outcome.status,
|
|
115
|
+
});
|
|
116
|
+
case 'not-found':
|
|
117
|
+
throw native.notFound({ data: project(outcome.value) });
|
|
118
|
+
case 'error': {
|
|
119
|
+
const error = project(outcome.error);
|
|
120
|
+
if (
|
|
121
|
+
!error ||
|
|
122
|
+
typeof error !== 'object' ||
|
|
123
|
+
!('name' in error) ||
|
|
124
|
+
typeof error.name !== 'string' ||
|
|
125
|
+
!('message' in error) ||
|
|
126
|
+
typeof error.message !== 'string'
|
|
127
|
+
)
|
|
128
|
+
throw new DataProtocolError('Invalid public route error');
|
|
129
|
+
throw Object.freeze(
|
|
130
|
+
new RouteDataError(routeId, {
|
|
131
|
+
...outcome,
|
|
132
|
+
error: { name: error.name, message: error.message },
|
|
133
|
+
data: project(outcome.data),
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Validate the filesystem shape: one application root and unique ids. */
|
|
141
|
+
export function splitFileSystemRoutes(routes: readonly FileSystemRouteIR[]): {
|
|
142
|
+
root: FileSystemRouteIR | undefined;
|
|
143
|
+
children: readonly FileSystemRouteIR[];
|
|
144
|
+
} {
|
|
145
|
+
const roots = routes.filter(route => route.isRoot);
|
|
146
|
+
if (roots.length > 1)
|
|
147
|
+
throw new Error('A filesystem route tree requires one application root');
|
|
148
|
+
const root = roots[0];
|
|
149
|
+
const ids = new Set<string>();
|
|
150
|
+
const visit = (route: FileSystemRouteIR, nested: boolean) => {
|
|
151
|
+
if (nested && route.isRoot)
|
|
152
|
+
throw new Error('An application root cannot be a child route');
|
|
153
|
+
if (ids.has(route.id))
|
|
154
|
+
throw new Error(`Duplicate filesystem route id: ${route.id}`);
|
|
155
|
+
ids.add(route.id);
|
|
156
|
+
for (const child of route.children) visit(child, true);
|
|
157
|
+
};
|
|
158
|
+
for (const route of routes) visit(route, false);
|
|
159
|
+
return {
|
|
160
|
+
root,
|
|
161
|
+
children: root
|
|
162
|
+
? [...root.children, ...routes.filter(route => route !== root)]
|
|
163
|
+
: routes,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Native path (or pathless id) options for one filesystem route. */
|
|
168
|
+
export function nativeRoutePath(
|
|
169
|
+
route: FileSystemRouteIR,
|
|
170
|
+
): { path: string } | { id: string } {
|
|
171
|
+
const path = route.index ? '/' : route.path;
|
|
172
|
+
return path ? { path: toTanstackPath(path) } : { id: route.id };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface NativeLoaderMatch {
|
|
176
|
+
params: Record<string, string>;
|
|
177
|
+
location: { publicHref: string };
|
|
178
|
+
abortController: AbortController;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Invoke a route's data handler for one native loader generation. The request
|
|
183
|
+
* carries the public URL (basepath included) and aborts with either owner.
|
|
184
|
+
*/
|
|
185
|
+
export async function loadFileSystemRoute<Context>(
|
|
186
|
+
route: FileSystemRouteIR,
|
|
187
|
+
loadRoute: NonNullable<FileSystemRouteOptions<Context>['loadRoute']>,
|
|
188
|
+
options: FileSystemRouteOptions<Context>,
|
|
189
|
+
{ params, location, abortController }: NativeLoaderMatch,
|
|
190
|
+
): Promise<{ outcome: RouteOutcome; request: Request; signal: AbortSignal }> {
|
|
191
|
+
const base =
|
|
192
|
+
options.request?.url ??
|
|
193
|
+
(typeof window === 'undefined' ? undefined : window.location.href);
|
|
194
|
+
if (!base) throw new Error('A server route loader requires its request');
|
|
195
|
+
const owners = [
|
|
196
|
+
abortController.signal,
|
|
197
|
+
...(options.request ? [options.request.signal] : []),
|
|
198
|
+
...(options.signal ? [options.signal] : []),
|
|
199
|
+
];
|
|
200
|
+
const signal = owners.length > 1 ? AbortSignal.any(owners) : owners[0]!;
|
|
201
|
+
signal.throwIfAborted();
|
|
202
|
+
const request = new Request(new URL(location.publicHref, base), {
|
|
203
|
+
headers: options.request?.headers,
|
|
204
|
+
signal,
|
|
205
|
+
});
|
|
206
|
+
// A custom loader that ignores its signal must not hold an aborted
|
|
207
|
+
// navigation or server request.
|
|
208
|
+
const outcome = await untilAborted(
|
|
209
|
+
loadRoute(route, {
|
|
210
|
+
request,
|
|
211
|
+
routeId: route.id,
|
|
212
|
+
params,
|
|
213
|
+
context: options.context as Context,
|
|
214
|
+
}),
|
|
215
|
+
signal,
|
|
216
|
+
);
|
|
217
|
+
return { outcome, request, signal };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Renderer adapters race their authored router hooks with the same rule. */
|
|
221
|
+
export { untilAborted };
|
|
222
|
+
|
|
223
|
+
/** Location input accepted by a native router's parseLocation. */
|
|
224
|
+
export interface NativeHistoryLocation {
|
|
225
|
+
href: string;
|
|
226
|
+
pathname: string;
|
|
227
|
+
search: string;
|
|
228
|
+
hash: string;
|
|
229
|
+
state: { __TSR_index: number };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface NativeRouteMatch {
|
|
233
|
+
routeId: string;
|
|
234
|
+
params: Readonly<Record<string, string>>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface NativeRouteMatcher<Location, Match> {
|
|
238
|
+
parseLocation(location: NativeHistoryLocation): Location;
|
|
239
|
+
matchRoutes(location: Location): Match[];
|
|
240
|
+
routesById: Readonly<
|
|
241
|
+
Record<string, { options: { staticData?: unknown } } | undefined>
|
|
242
|
+
>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Match a public request URL through the router's own location parsing, so the
|
|
247
|
+
* basepath (a router rewrite) is removed exactly as navigation removes it.
|
|
248
|
+
*/
|
|
249
|
+
export function matchApplicationRoutes<Location, Match>(
|
|
250
|
+
router: NativeRouteMatcher<Location, Match>,
|
|
251
|
+
url: URL,
|
|
252
|
+
): Match[] {
|
|
253
|
+
return router.matchRoutes(
|
|
254
|
+
router.parseLocation({
|
|
255
|
+
href: url.pathname + url.search + url.hash,
|
|
256
|
+
pathname: url.pathname,
|
|
257
|
+
search: url.search,
|
|
258
|
+
hash: url.hash,
|
|
259
|
+
state: { __TSR_index: 0 },
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The filesystem route id a native route was created for. */
|
|
265
|
+
function applicationRouteId(
|
|
266
|
+
router: Pick<NativeRouteMatcher<unknown, unknown>, 'routesById'>,
|
|
267
|
+
nativeRouteId: string,
|
|
268
|
+
): string | undefined {
|
|
269
|
+
const staticData = router.routesById[nativeRouteId]?.options.staticData;
|
|
270
|
+
const id =
|
|
271
|
+
staticData && typeof staticData === 'object'
|
|
272
|
+
? Reflect.get(staticData, 'ultramodernRouteId')
|
|
273
|
+
: undefined;
|
|
274
|
+
return typeof id === 'string' ? id : undefined;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The filesystem route ids a public request URL matches, outermost first. */
|
|
278
|
+
export function matchApplicationRouteIds<
|
|
279
|
+
Location,
|
|
280
|
+
Match extends { routeId: string },
|
|
281
|
+
>(router: NativeRouteMatcher<Location, Match>, url: URL): string[] {
|
|
282
|
+
return matchApplicationRoutes(router, url).flatMap(match => {
|
|
283
|
+
const id = applicationRouteId(router, match.routeId);
|
|
284
|
+
return id === undefined ? [] : [id];
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** A requested data id must belong to the native router's match for this URL. */
|
|
289
|
+
export function selectApplicationDataRoute<
|
|
290
|
+
Context,
|
|
291
|
+
Location,
|
|
292
|
+
Match extends NativeRouteMatch,
|
|
293
|
+
>(
|
|
294
|
+
router: NativeRouteMatcher<Location, Match>,
|
|
295
|
+
request: Request,
|
|
296
|
+
requestedRouteId: string,
|
|
297
|
+
operation: DataOperation,
|
|
298
|
+
handlers: Readonly<Record<string, FileSystemDataModule<Context>>>,
|
|
299
|
+
): SelectedDataRoute<Context> | undefined {
|
|
300
|
+
const match = matchApplicationRoutes(router, new URL(request.url)).find(
|
|
301
|
+
candidate =>
|
|
302
|
+
applicationRouteId(router, candidate.routeId) === requestedRouteId,
|
|
303
|
+
);
|
|
304
|
+
const handler = handlers[requestedRouteId]?.[operation];
|
|
305
|
+
if (!match || typeof handler !== 'function') return undefined;
|
|
306
|
+
return { routeId: requestedRouteId, params: match.params, handler };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** One route's source module namespaces, as the generated application imports them. */
|
|
310
|
+
export interface NativeRouteSources {
|
|
311
|
+
readonly component?: { readonly default?: unknown };
|
|
312
|
+
readonly pendingComponent?: { readonly default?: unknown };
|
|
313
|
+
readonly errorComponent?: { readonly default?: unknown };
|
|
314
|
+
readonly notFoundComponent?: { readonly default?: unknown };
|
|
315
|
+
readonly head?: { readonly head?: unknown; readonly default?: unknown };
|
|
316
|
+
readonly search?: {
|
|
317
|
+
readonly validateSearch?: unknown;
|
|
318
|
+
readonly default?: unknown;
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The generated module of a file-system routed entry: route sources and data. */
|
|
323
|
+
export interface NativeRoutedApplication {
|
|
324
|
+
/** The analyzed public prefix the router strips before matching. */
|
|
325
|
+
readonly basePath: string;
|
|
326
|
+
readonly routeIR: readonly FileSystemRouteIR[];
|
|
327
|
+
readonly routeModules: Readonly<Record<string, NativeRouteSources>>;
|
|
328
|
+
readonly dataModules: Readonly<Record<string, FileSystemDataModule>>;
|
|
329
|
+
/** Client graph only: routes whose loader runs on the server. */
|
|
330
|
+
readonly serverDataRoutes?: readonly string[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** The generated module of an entry that renders one component. */
|
|
334
|
+
export interface NativeComponentApplication {
|
|
335
|
+
readonly default: unknown;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type NativeApplicationModule =
|
|
339
|
+
| NativeRoutedApplication
|
|
340
|
+
| NativeComponentApplication;
|
|
341
|
+
|
|
342
|
+
export function isRoutedApplication(
|
|
343
|
+
application: NativeApplicationModule,
|
|
344
|
+
): application is NativeRoutedApplication {
|
|
345
|
+
return 'routeIR' in application;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Structurally identical to router-core's `LocationRewrite`. */
|
|
349
|
+
export interface NativeLocationRewrite {
|
|
350
|
+
input?: (args: { url: URL }) => undefined | string | URL;
|
|
351
|
+
output?: (args: { url: URL }) => undefined | string | URL;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface NativeRouterOptions {
|
|
355
|
+
readonly identity: RendererIdentity;
|
|
356
|
+
readonly loadRoute: NonNullable<FileSystemRouteOptions['loadRoute']>;
|
|
357
|
+
/** The server request this router renders; a browser router omits it. */
|
|
358
|
+
readonly request?: Request;
|
|
359
|
+
/** Retires every route load of this router, e.g. on entry disposal. */
|
|
360
|
+
readonly signal?: AbortSignal;
|
|
361
|
+
/** Private data-handler input; never part of native route context. */
|
|
362
|
+
readonly context?: object;
|
|
363
|
+
readonly onOutcome?: (routeId: string, outcome: RouteOutcome) => void;
|
|
364
|
+
readonly session?: RequestSession;
|
|
365
|
+
/** The document CSP nonce the router stamps on its emitted scripts. */
|
|
366
|
+
readonly nonce?: string;
|
|
367
|
+
readonly rewrite?: NativeLocationRewrite;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Router options a renderer passes to its native router constructor. */
|
|
371
|
+
export interface NativeApplicationRouterOptions<Route> {
|
|
372
|
+
readonly routeTree: Route;
|
|
373
|
+
readonly basepath: string;
|
|
374
|
+
readonly context: {
|
|
375
|
+
readonly ultramodern: Readonly<{
|
|
376
|
+
rendererIdentity: Readonly<RendererIdentity>;
|
|
377
|
+
}>;
|
|
378
|
+
};
|
|
379
|
+
readonly rewrite?: NativeLocationRewrite;
|
|
380
|
+
readonly ssr?: { readonly nonce: string };
|
|
381
|
+
/** A server router starts at the request location in memory history. */
|
|
382
|
+
readonly location?: { readonly origin: string; readonly href: string };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The renderer's own route tree and router constructors. */
|
|
386
|
+
export interface NativeRouterFactory<Route, Router> {
|
|
387
|
+
routeTree(
|
|
388
|
+
routes: readonly FileSystemRouteIR[],
|
|
389
|
+
modules: Readonly<Record<string, Record<string, unknown>>>,
|
|
390
|
+
options: FileSystemRouteOptions<object, Router>,
|
|
391
|
+
): Route;
|
|
392
|
+
router(options: NativeApplicationRouterOptions<Route>): Router;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const viewFields = [
|
|
396
|
+
'component',
|
|
397
|
+
'pendingComponent',
|
|
398
|
+
'errorComponent',
|
|
399
|
+
'notFoundComponent',
|
|
400
|
+
] as const;
|
|
401
|
+
|
|
402
|
+
/** Resolve one route's module namespaces into native route options. */
|
|
403
|
+
function resolveRouteSources(
|
|
404
|
+
sources: NativeRouteSources,
|
|
405
|
+
): Record<string, unknown> {
|
|
406
|
+
const module: Record<string, unknown> = {};
|
|
407
|
+
for (const field of viewFields) {
|
|
408
|
+
const source = sources[field];
|
|
409
|
+
if (source) module[field] = source.default;
|
|
410
|
+
}
|
|
411
|
+
if (sources.head) module.head = sources.head.head ?? sources.head.default;
|
|
412
|
+
if (sources.search) {
|
|
413
|
+
const validateSearch =
|
|
414
|
+
sources.search.validateSearch ?? sources.search.default;
|
|
415
|
+
if (typeof validateSearch !== 'function')
|
|
416
|
+
throw new Error(
|
|
417
|
+
'A native search module must export validateSearch or a default validator',
|
|
418
|
+
);
|
|
419
|
+
module.validateSearch = validateSearch;
|
|
420
|
+
}
|
|
421
|
+
return module;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Create the native router of a generated routed application. */
|
|
425
|
+
export function createNativeRouter<Route, Router>(
|
|
426
|
+
application: NativeRoutedApplication,
|
|
427
|
+
options: NativeRouterOptions,
|
|
428
|
+
factory: NativeRouterFactory<Route, Router>,
|
|
429
|
+
): Router {
|
|
430
|
+
const context = options.context ?? {};
|
|
431
|
+
if (Object.hasOwn(context, 'ultramodern'))
|
|
432
|
+
throw new Error('The native router context reserves ultramodern metadata');
|
|
433
|
+
const modules = Object.fromEntries(
|
|
434
|
+
Object.entries(application.routeModules).map(([id, sources]) => [
|
|
435
|
+
id,
|
|
436
|
+
resolveRouteSources(sources),
|
|
437
|
+
]),
|
|
438
|
+
);
|
|
439
|
+
let router: Router | undefined;
|
|
440
|
+
const routeTree = factory.routeTree(application.routeIR, modules, {
|
|
441
|
+
...(options.request ? { request: options.request } : {}),
|
|
442
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
443
|
+
context,
|
|
444
|
+
...(options.onOutcome ? { onOutcome: options.onOutcome } : {}),
|
|
445
|
+
...(options.session ? { session: options.session } : {}),
|
|
446
|
+
getRouter: () => {
|
|
447
|
+
if (!router) throw new Error('The native router is not created yet');
|
|
448
|
+
return router;
|
|
449
|
+
},
|
|
450
|
+
loadRoute: options.loadRoute,
|
|
451
|
+
});
|
|
452
|
+
const url = options.request ? new URL(options.request.url) : undefined;
|
|
453
|
+
router = factory.router({
|
|
454
|
+
routeTree,
|
|
455
|
+
basepath: application.basePath,
|
|
456
|
+
context: {
|
|
457
|
+
ultramodern: Object.freeze({
|
|
458
|
+
rendererIdentity: Object.freeze({ ...options.identity }),
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
461
|
+
// Matching stays on canonical paths; public URLs carry the language.
|
|
462
|
+
...(options.rewrite ? { rewrite: options.rewrite } : {}),
|
|
463
|
+
// Router-emitted scripts carry the document's CSP nonce.
|
|
464
|
+
...(options.nonce === undefined ? {} : { ssr: { nonce: options.nonce } }),
|
|
465
|
+
...(url
|
|
466
|
+
? {
|
|
467
|
+
location: {
|
|
468
|
+
origin: url.origin,
|
|
469
|
+
href: url.pathname + url.search + url.hash,
|
|
470
|
+
},
|
|
471
|
+
}
|
|
472
|
+
: {}),
|
|
473
|
+
});
|
|
474
|
+
return router;
|
|
475
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { collectDocumentAssets, type DocumentAsset } from '../document';
|
|
2
|
+
import {
|
|
3
|
+
assertRendererIdentity,
|
|
4
|
+
type Renderer,
|
|
5
|
+
type RendererIdentity,
|
|
6
|
+
} from '../identity';
|
|
7
|
+
|
|
8
|
+
export const RENDERER_ASSET_MANIFEST_FILE = 'renderer-assets.json';
|
|
9
|
+
|
|
10
|
+
export interface NativeClientAssetManifest {
|
|
11
|
+
readonly schema: 'ultramodern-renderer-assets';
|
|
12
|
+
readonly version: 1;
|
|
13
|
+
readonly renderer: Exclude<Renderer, 'react'>;
|
|
14
|
+
readonly entries: Readonly<
|
|
15
|
+
Record<
|
|
16
|
+
string,
|
|
17
|
+
{
|
|
18
|
+
readonly rendererIdentity: RendererIdentity;
|
|
19
|
+
readonly assets: readonly DocumentAsset[];
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read actual emitted initial assets, never infer URLs from source filenames. */
|
|
26
|
+
export function validateNativeClientAssetManifest(
|
|
27
|
+
value: unknown,
|
|
28
|
+
expectedIdentity: RendererIdentity,
|
|
29
|
+
): readonly DocumentAsset[] {
|
|
30
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Missing native client asset manifest. Rebuild the application before serving.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
const manifest = value as Partial<NativeClientAssetManifest>;
|
|
36
|
+
if (
|
|
37
|
+
manifest.schema !== 'ultramodern-renderer-assets' ||
|
|
38
|
+
manifest.version !== 1 ||
|
|
39
|
+
expectedIdentity.renderer === 'react' ||
|
|
40
|
+
manifest.renderer !== expectedIdentity.renderer
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Native client asset manifest conflicts with the selected renderer or schema.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const entry = manifest.entries?.[expectedIdentity.entryName];
|
|
47
|
+
if (!entry?.rendererIdentity || !Array.isArray(entry.assets)) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Native client asset manifest is missing entry ${expectedIdentity.entryName}.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
assertRendererIdentity(entry.rendererIdentity, expectedIdentity);
|
|
53
|
+
const assets = collectDocumentAssets(entry.assets);
|
|
54
|
+
if (!assets.some(asset => asset.kind === 'script')) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Native client asset manifest has no application script for ${expectedIdentity.entryName}.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return assets;
|
|
60
|
+
}
|