@reckona/mreact-router 0.0.65 → 0.0.67
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/package.json +13 -12
- package/src/actions.ts +1130 -0
- package/src/adapters/aws-lambda.ts +993 -0
- package/src/adapters/cloudflare.ts +1286 -0
- package/src/adapters/devtools.ts +5 -0
- package/src/adapters/edge.ts +70 -0
- package/src/adapters/node.ts +126 -0
- package/src/adapters/static.ts +61 -0
- package/src/app-router-globals.ts +19 -0
- package/src/assets.ts +113 -0
- package/src/build.ts +2948 -0
- package/src/bundle-pipeline.ts +496 -0
- package/src/cache-config.ts +35 -0
- package/src/cache-stats.ts +54 -0
- package/src/cache.ts +418 -0
- package/src/cli-options.ts +296 -0
- package/src/cli.ts +94 -0
- package/src/client.ts +3398 -0
- package/src/config.ts +146 -0
- package/src/cookies.ts +113 -0
- package/src/csp.ts +103 -0
- package/src/csrf.ts +132 -0
- package/src/deferred.ts +52 -0
- package/src/dev-server.ts +262 -0
- package/src/file-conventions.ts +88 -0
- package/src/http.ts +128 -0
- package/src/i18n.ts +98 -0
- package/src/import-policy.ts +261 -0
- package/src/index.ts +221 -0
- package/src/link.ts +47 -0
- package/src/logger.ts +149 -0
- package/src/module-runner.ts +554 -0
- package/src/multipart.ts +577 -0
- package/src/native-escape.ts +53 -0
- package/src/native-route-matcher.ts +173 -0
- package/src/navigation-state.ts +98 -0
- package/src/navigation.ts +215 -0
- package/src/prerender-store.ts +233 -0
- package/src/render.ts +5187 -0
- package/src/route-path.ts +5 -0
- package/src/route-shells.ts +47 -0
- package/src/route-source.ts +224 -0
- package/src/route-styles.ts +91 -0
- package/src/routes.ts +362 -0
- package/src/runtime-cache.ts +8 -0
- package/src/runtime-state.ts +37 -0
- package/src/security-headers.ts +134 -0
- package/src/serve.ts +1265 -0
- package/src/session.ts +238 -0
- package/src/source-jsx.ts +30 -0
- package/src/source-modules.ts +69 -0
- package/src/stream-list.ts +45 -0
- package/src/trace.ts +114 -0
- package/src/types.ts +155 -0
- package/src/upgrade.ts +8 -0
- package/src/vite-config.ts +63 -0
- package/src/vite-plugin-cache-key.ts +53 -0
- package/src/vite.ts +690 -0
- package/src/workspace-packages.ts +67 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,3398 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { dirname, extname, join, relative, sep } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
collectClientRouteModuleAnalysis,
|
|
7
|
+
formatDiagnostic,
|
|
8
|
+
type ComponentMetadata,
|
|
9
|
+
type ClientRouteModuleAnalysis,
|
|
10
|
+
type ClientRouteStaticImportReference,
|
|
11
|
+
type ClientReferenceMetadata,
|
|
12
|
+
type StaticImportReference,
|
|
13
|
+
type TopLevelExportRenderInfo,
|
|
14
|
+
} from "@reckona/mreact-compiler";
|
|
15
|
+
import {
|
|
16
|
+
collectClientRouteModuleAnalysisFromContext,
|
|
17
|
+
createCompilerModuleContext,
|
|
18
|
+
stripTypeScriptWithOxc,
|
|
19
|
+
transformCompilerModuleContext,
|
|
20
|
+
type CompilerModuleContext,
|
|
21
|
+
} from "@reckona/mreact-compiler/internal";
|
|
22
|
+
import { assetPath } from "./assets.js";
|
|
23
|
+
import {
|
|
24
|
+
bundleRouterModule,
|
|
25
|
+
bundleRouterModules,
|
|
26
|
+
type RouterCompatBuildApi,
|
|
27
|
+
type RouterBundleAssetOutput,
|
|
28
|
+
type RouterBundleChunkOutput,
|
|
29
|
+
} from "./bundle-pipeline.js";
|
|
30
|
+
import type { AppRoute } from "./routes.js";
|
|
31
|
+
import { existingRouteShellCandidates } from "./route-shells.js";
|
|
32
|
+
import { stripRouteClientOnlyExports } from "./route-source.js";
|
|
33
|
+
import { hasJsxSyntax } from "./source-jsx.js";
|
|
34
|
+
import { sourceModuleCandidates } from "./source-modules.js";
|
|
35
|
+
import { escapeHtmlQuotedAttribute as escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
|
|
36
|
+
import { workspacePackageFile } from "./workspace-packages.js";
|
|
37
|
+
import type { Plugin, PluginOption } from "vite";
|
|
38
|
+
|
|
39
|
+
const nodeBuiltinPackages = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
|
|
40
|
+
|
|
41
|
+
export interface ClientRouteManifestEntry {
|
|
42
|
+
bytes?: number;
|
|
43
|
+
css?: readonly string[];
|
|
44
|
+
path: string;
|
|
45
|
+
kind: AppRoute["kind"];
|
|
46
|
+
client: boolean;
|
|
47
|
+
devScript?: string;
|
|
48
|
+
imports?: readonly string[];
|
|
49
|
+
navigation?: boolean;
|
|
50
|
+
navigationScript?: string;
|
|
51
|
+
routeId?: string;
|
|
52
|
+
script?: string;
|
|
53
|
+
sourceMap?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface BuildClientRouteOutputOptions {
|
|
57
|
+
code: string;
|
|
58
|
+
clientBoundaryImports?: readonly string[] | undefined;
|
|
59
|
+
clientReferenceImports?: readonly ClientReferenceImport[] | undefined;
|
|
60
|
+
clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
|
|
61
|
+
filename: string;
|
|
62
|
+
minify?: boolean | undefined;
|
|
63
|
+
routePath: string;
|
|
64
|
+
sourceMap?: boolean | undefined;
|
|
65
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
66
|
+
clientNavigation?: boolean | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BuildClientRouteBatchOutput {
|
|
70
|
+
assets?: RouterBundleAssetOutput[] | undefined;
|
|
71
|
+
chunks: readonly RouterBundleChunkOutput[];
|
|
72
|
+
routes: readonly BuildClientRouteBatchRouteOutput[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BuildClientRouteBatchRouteOutput {
|
|
76
|
+
chunk: RouterBundleChunkOutput;
|
|
77
|
+
routeId: string;
|
|
78
|
+
routePath: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ClientRouteInferenceCache {
|
|
82
|
+
moduleAnalysisByFile: Map<string, Promise<ClientRouteModuleAnalysis>>;
|
|
83
|
+
moduleContextByFile: Map<string, Promise<CompilerModuleContext>>;
|
|
84
|
+
resolvedByImport: Map<string, Promise<string | undefined>>;
|
|
85
|
+
sourceByFile: Map<string, Promise<CachedClientRouteSource>>;
|
|
86
|
+
transformedSourceByFile: Map<string, Promise<CachedClientRouteSource>>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface CachedClientRouteSource {
|
|
90
|
+
signature: string;
|
|
91
|
+
source: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface ClientRouteSourceTransform {
|
|
95
|
+
cacheKey: string;
|
|
96
|
+
transform(filename: string, code: string): Promise<string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ClientRouteInferenceResult {
|
|
100
|
+
client: boolean;
|
|
101
|
+
clientBoundaryImports: string[];
|
|
102
|
+
diagnostics: ClientRouteInferenceDiagnostic[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
|
|
106
|
+
clientBoundaryExportNames: string[];
|
|
107
|
+
clientBoundaryModule: boolean;
|
|
108
|
+
nestedClientExportNames: string[];
|
|
109
|
+
clientReferenceSourceFiles: string[];
|
|
110
|
+
serverOnly: boolean;
|
|
111
|
+
serverOnlyClientRuntime: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface ClientReferenceImport {
|
|
115
|
+
exportName: string;
|
|
116
|
+
importSource: string;
|
|
117
|
+
name: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ClientRouteReferenceResult extends ClientRouteInferenceResult {
|
|
121
|
+
clientReferenceImports: ClientReferenceImport[];
|
|
122
|
+
clientReferenceManifest: ClientReferenceMetadata[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ClientRouteInferenceDiagnostic {
|
|
126
|
+
code:
|
|
127
|
+
| typeof clientBoundaryInferenceServerOnlyReferenceCode
|
|
128
|
+
| typeof clientBoundaryInferenceUnsupportedReferenceCode;
|
|
129
|
+
filename: string;
|
|
130
|
+
level: "warn";
|
|
131
|
+
localNames: string[];
|
|
132
|
+
message: string;
|
|
133
|
+
source: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const clientBoundaryInferenceServerOnlyReferenceCode =
|
|
137
|
+
"MR_CLIENT_BOUNDARY_INFERENCE_SERVER_ONLY_REFERENCE";
|
|
138
|
+
const clientBoundaryInferenceUnsupportedReferenceCode =
|
|
139
|
+
"MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE";
|
|
140
|
+
|
|
141
|
+
export async function routeToClientManifestEntry(
|
|
142
|
+
route: AppRoute,
|
|
143
|
+
): Promise<ClientRouteManifestEntry> {
|
|
144
|
+
if (route.kind !== "page") {
|
|
145
|
+
return { path: route.path, kind: route.kind, client: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const code = await readFile(route.file, "utf8");
|
|
149
|
+
const inference = await inferClientRouteModule({
|
|
150
|
+
code: stripRouteClientOnlyExports(code),
|
|
151
|
+
filename: route.file,
|
|
152
|
+
routePath: route.path,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return inference.client
|
|
156
|
+
? {
|
|
157
|
+
path: route.path,
|
|
158
|
+
kind: route.kind,
|
|
159
|
+
client: true,
|
|
160
|
+
routeId: routeIdForPath(route.path),
|
|
161
|
+
script: clientScriptForPath(route.path),
|
|
162
|
+
}
|
|
163
|
+
: { path: route.path, kind: route.kind, client: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createClientRouteInferenceCache(): ClientRouteInferenceCache {
|
|
167
|
+
return {
|
|
168
|
+
moduleAnalysisByFile: new Map(),
|
|
169
|
+
moduleContextByFile: new Map(),
|
|
170
|
+
resolvedByImport: new Map(),
|
|
171
|
+
sourceByFile: new Map(),
|
|
172
|
+
transformedSourceByFile: new Map(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function isClientRouteModule(options: {
|
|
177
|
+
appDir?: string | undefined;
|
|
178
|
+
cache?: ClientRouteInferenceCache | undefined;
|
|
179
|
+
code: string;
|
|
180
|
+
filename: string;
|
|
181
|
+
routePath?: string | undefined;
|
|
182
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
183
|
+
}): Promise<boolean> {
|
|
184
|
+
return (await inferClientRouteModule(options)).client;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function inferClientRouteModule(options: {
|
|
188
|
+
appDir?: string | undefined;
|
|
189
|
+
cache?: ClientRouteInferenceCache | undefined;
|
|
190
|
+
code: string;
|
|
191
|
+
filename: string;
|
|
192
|
+
moduleContext?: CompilerModuleContext | undefined;
|
|
193
|
+
routePath?: string | undefined;
|
|
194
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
195
|
+
}): Promise<ClientRouteInferenceResult> {
|
|
196
|
+
const cache = options.cache ?? createClientRouteInferenceCache();
|
|
197
|
+
const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
|
|
198
|
+
const code = await transformClientRouteSource({
|
|
199
|
+
code: options.code,
|
|
200
|
+
filename: options.filename,
|
|
201
|
+
sourceTransform,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const routeInference = await inferClientRouteModuleSource({
|
|
206
|
+
cache,
|
|
207
|
+
code,
|
|
208
|
+
filename: options.filename,
|
|
209
|
+
...(sourceTransform === undefined ? { moduleContext: options.moduleContext } : {}),
|
|
210
|
+
root: true,
|
|
211
|
+
seen: new Set(),
|
|
212
|
+
sourceTransform,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (options.appDir === undefined) {
|
|
216
|
+
return routeInference;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const shellInferences = await inferClientRouteShellModules({
|
|
220
|
+
appDir: options.appDir,
|
|
221
|
+
cache,
|
|
222
|
+
filename: options.filename,
|
|
223
|
+
sourceTransform,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
client: routeInference.client || shellInferences.some((inference) => inference.client),
|
|
228
|
+
clientBoundaryImports: routeInference.clientBoundaryImports,
|
|
229
|
+
diagnostics: [
|
|
230
|
+
...routeInference.diagnostics,
|
|
231
|
+
...shellInferences.flatMap((inference) => inference.diagnostics),
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`Failed to infer client route for ${options.routePath ?? "<unknown>"} (${options.filename}).\n${errorMessage(error)}`,
|
|
237
|
+
{ cause: error },
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export async function collectClientRouteReferences(options: {
|
|
243
|
+
appDir?: string | undefined;
|
|
244
|
+
cache?: ClientRouteInferenceCache | undefined;
|
|
245
|
+
code: string;
|
|
246
|
+
filename: string;
|
|
247
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
248
|
+
}): Promise<ClientRouteReferenceResult> {
|
|
249
|
+
const cache = options.cache ?? createClientRouteInferenceCache();
|
|
250
|
+
const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
|
|
251
|
+
const code = await transformClientRouteSource({
|
|
252
|
+
code: options.code,
|
|
253
|
+
filename: options.filename,
|
|
254
|
+
sourceTransform,
|
|
255
|
+
});
|
|
256
|
+
const routeModuleContext = await compilerModuleContextForSource({
|
|
257
|
+
cache,
|
|
258
|
+
code,
|
|
259
|
+
filename: options.filename,
|
|
260
|
+
});
|
|
261
|
+
const routeInference = await inferClientRouteModuleSource({
|
|
262
|
+
cache,
|
|
263
|
+
code,
|
|
264
|
+
filename: options.filename,
|
|
265
|
+
moduleContext: routeModuleContext,
|
|
266
|
+
root: true,
|
|
267
|
+
seen: new Set(),
|
|
268
|
+
sourceTransform,
|
|
269
|
+
});
|
|
270
|
+
const sources: Array<{
|
|
271
|
+
code: string;
|
|
272
|
+
filename: string;
|
|
273
|
+
inference: ClientRouteModuleInferenceResult;
|
|
274
|
+
moduleContext: CompilerModuleContext;
|
|
275
|
+
}> = [];
|
|
276
|
+
const seenSourceFiles = new Set<string>();
|
|
277
|
+
const addSource = async (sourceOptions: {
|
|
278
|
+
code: string;
|
|
279
|
+
filename: string;
|
|
280
|
+
inference?: ClientRouteModuleInferenceResult | undefined;
|
|
281
|
+
moduleContext?: CompilerModuleContext | undefined;
|
|
282
|
+
}) => {
|
|
283
|
+
if (seenSourceFiles.has(sourceOptions.filename)) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
seenSourceFiles.add(sourceOptions.filename);
|
|
288
|
+
const moduleContext =
|
|
289
|
+
sourceOptions.moduleContext ??
|
|
290
|
+
(await compilerModuleContextForSource({
|
|
291
|
+
cache,
|
|
292
|
+
code: sourceOptions.code,
|
|
293
|
+
filename: sourceOptions.filename,
|
|
294
|
+
}));
|
|
295
|
+
const inference =
|
|
296
|
+
sourceOptions.inference ??
|
|
297
|
+
(await inferClientRouteModuleSource({
|
|
298
|
+
cache,
|
|
299
|
+
code: sourceOptions.code,
|
|
300
|
+
filename: sourceOptions.filename,
|
|
301
|
+
moduleContext,
|
|
302
|
+
root: true,
|
|
303
|
+
seen: new Set(),
|
|
304
|
+
sourceTransform,
|
|
305
|
+
}));
|
|
306
|
+
sources.push({
|
|
307
|
+
code: sourceOptions.code,
|
|
308
|
+
filename: sourceOptions.filename,
|
|
309
|
+
inference,
|
|
310
|
+
moduleContext,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
for (const referenceFile of inference.clientReferenceSourceFiles) {
|
|
314
|
+
const code = stripRouteClientOnlyExports(
|
|
315
|
+
await readClientRouteSource({ cache, filename: referenceFile, sourceTransform }),
|
|
316
|
+
);
|
|
317
|
+
await addSource({ code, filename: referenceFile });
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
await addSource({
|
|
322
|
+
code,
|
|
323
|
+
filename: options.filename,
|
|
324
|
+
inference: routeInference,
|
|
325
|
+
moduleContext: routeModuleContext,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (options.appDir !== undefined) {
|
|
329
|
+
for (const shell of await clientShellFilesForPage(options.appDir, options.filename)) {
|
|
330
|
+
const code = stripRouteClientOnlyExports(
|
|
331
|
+
await readClientRouteSource({ cache, filename: shell, sourceTransform }),
|
|
332
|
+
);
|
|
333
|
+
const moduleContext = await compilerModuleContextForSource({
|
|
334
|
+
cache,
|
|
335
|
+
code,
|
|
336
|
+
filename: shell,
|
|
337
|
+
});
|
|
338
|
+
await addSource({
|
|
339
|
+
code,
|
|
340
|
+
filename: shell,
|
|
341
|
+
inference: await inferClientRouteModuleSource({
|
|
342
|
+
cache,
|
|
343
|
+
code,
|
|
344
|
+
filename: shell,
|
|
345
|
+
moduleContext,
|
|
346
|
+
root: true,
|
|
347
|
+
seen: new Set(),
|
|
348
|
+
sourceTransform,
|
|
349
|
+
}),
|
|
350
|
+
moduleContext,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const clientReferenceManifest: ClientReferenceMetadata[] = [];
|
|
356
|
+
const clientReferenceImports: ClientReferenceImport[] = [];
|
|
357
|
+
const seenReferences = new Set<string>();
|
|
358
|
+
|
|
359
|
+
for (const source of sources) {
|
|
360
|
+
const output = transformCompilerModuleContext({
|
|
361
|
+
code: source.code,
|
|
362
|
+
clientBoundaryImports: source.inference.clientBoundaryImports,
|
|
363
|
+
dev: false,
|
|
364
|
+
filename: source.filename,
|
|
365
|
+
moduleContext: source.moduleContext,
|
|
366
|
+
target: "server",
|
|
367
|
+
});
|
|
368
|
+
const fatalDiagnostics = output.diagnostics.filter(
|
|
369
|
+
(diagnostic) =>
|
|
370
|
+
diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER" &&
|
|
371
|
+
diagnostic.code !== "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if (fatalDiagnostics.length > 0) {
|
|
375
|
+
throw new Error(
|
|
376
|
+
fatalDiagnostics
|
|
377
|
+
.map((diagnostic) => formatDiagnostic(source.filename, diagnostic))
|
|
378
|
+
.join("\n"),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const reference of output.metadata.clientReferenceManifest ?? []) {
|
|
383
|
+
const key = `${reference.name}\0${reference.moduleId}\0${reference.exportName}`;
|
|
384
|
+
|
|
385
|
+
if (seenReferences.has(key)) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
seenReferences.add(key);
|
|
390
|
+
clientReferenceManifest.push(reference);
|
|
391
|
+
clientReferenceImports.push({
|
|
392
|
+
exportName: reference.exportName,
|
|
393
|
+
importSource: clientReferenceImportSource({
|
|
394
|
+
moduleId: reference.moduleId,
|
|
395
|
+
routeFile: options.filename,
|
|
396
|
+
sourceFile: source.filename,
|
|
397
|
+
}),
|
|
398
|
+
name: reference.name,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
client:
|
|
405
|
+
routeInference.client ||
|
|
406
|
+
sources.some((source) => source.filename !== options.filename && source.inference.client),
|
|
407
|
+
clientBoundaryImports: routeInference.clientBoundaryImports,
|
|
408
|
+
clientReferenceImports,
|
|
409
|
+
clientReferenceManifest,
|
|
410
|
+
diagnostics: sources.flatMap((source) => source.inference.diagnostics),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function inferClientRouteShellModules(options: {
|
|
415
|
+
appDir: string;
|
|
416
|
+
cache: ClientRouteInferenceCache;
|
|
417
|
+
filename: string;
|
|
418
|
+
sourceTransform?: ClientRouteSourceTransform | undefined;
|
|
419
|
+
}): Promise<ClientRouteInferenceResult[]> {
|
|
420
|
+
return Promise.all(
|
|
421
|
+
(await clientShellFilesForPage(options.appDir, options.filename)).map(async (shell) => {
|
|
422
|
+
const code = stripRouteClientOnlyExports(
|
|
423
|
+
await readClientRouteSource({
|
|
424
|
+
cache: options.cache,
|
|
425
|
+
filename: shell,
|
|
426
|
+
sourceTransform: options.sourceTransform,
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
return await inferClientRouteModuleSource({
|
|
430
|
+
cache: options.cache,
|
|
431
|
+
code,
|
|
432
|
+
filename: shell,
|
|
433
|
+
root: true,
|
|
434
|
+
seen: new Set(),
|
|
435
|
+
sourceTransform: options.sourceTransform,
|
|
436
|
+
});
|
|
437
|
+
}),
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function clientShellFilesForPage(appDir: string, pageFile: string): Promise<string[]> {
|
|
442
|
+
const existing = await existingRouteShellCandidates(
|
|
443
|
+
appDir,
|
|
444
|
+
pageFile,
|
|
445
|
+
async (file) => file !== pageFile && (await isFile(file)),
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
return existing.map((candidate) => candidate.file);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clientReferenceImportSource(options: {
|
|
452
|
+
moduleId: string;
|
|
453
|
+
routeFile: string;
|
|
454
|
+
sourceFile: string;
|
|
455
|
+
}): string {
|
|
456
|
+
if (!options.moduleId.startsWith(".")) {
|
|
457
|
+
return options.moduleId;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const absolute = join(dirname(options.sourceFile), options.moduleId);
|
|
461
|
+
const relativeImport = relative(dirname(options.routeFile), absolute).replaceAll(sep, "/");
|
|
462
|
+
|
|
463
|
+
return relativeImport.startsWith(".") ? relativeImport : `./${relativeImport}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function isClientRouteSource(code: string): boolean {
|
|
467
|
+
const analysis = collectClientRouteModuleAnalysis({ code });
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
analysis.hasUseClientDirective || (!analysis.hasUseServerDirective && analysis.clientRuntime)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function isExplicitClientRouteSource(
|
|
475
|
+
analysis: ClientRouteModuleAnalysis,
|
|
476
|
+
filename: string,
|
|
477
|
+
): boolean {
|
|
478
|
+
return analysis.hasUseClientDirective || isClientBoundaryFilename(filename);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function isClientBoundaryFilename(filename: string): boolean {
|
|
482
|
+
return /\.(?:client|compat)(?:\.mreact)?\.[cm]?[jt]sx?$/.test(filename);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isServerOnlyClientRouteSource(analysis: ClientRouteModuleAnalysis): boolean {
|
|
486
|
+
return analysis.hasUseServerDirective;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function isServerOnlyImportSource(source: string): boolean {
|
|
490
|
+
return nodeBuiltinPackages.has(source);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function hasServerOnlyImports(analysis: ClientRouteModuleAnalysis): boolean {
|
|
494
|
+
return analysis.staticImports.some((reference) => isServerOnlyImportSource(reference.source));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function inferClientRouteModuleSource(options: {
|
|
498
|
+
cache: ClientRouteInferenceCache;
|
|
499
|
+
code: string;
|
|
500
|
+
filename: string;
|
|
501
|
+
moduleContext?: CompilerModuleContext | undefined;
|
|
502
|
+
root: boolean;
|
|
503
|
+
seen: Set<string>;
|
|
504
|
+
sourceTransform?: ClientRouteSourceTransform | undefined;
|
|
505
|
+
}): Promise<ClientRouteModuleInferenceResult> {
|
|
506
|
+
const analysis = await clientRouteModuleAnalysisForSource(options);
|
|
507
|
+
|
|
508
|
+
if (isServerOnlyClientRouteSource(analysis)) {
|
|
509
|
+
return emptyClientRouteModuleInferenceResult({
|
|
510
|
+
serverOnly: true,
|
|
511
|
+
serverOnlyClientRuntime: analysis.clientRuntime,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (isExplicitClientRouteSource(analysis, options.filename)) {
|
|
516
|
+
return emptyClientRouteModuleInferenceResult({
|
|
517
|
+
client: true,
|
|
518
|
+
clientBoundaryModule: true,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (options.seen.has(options.filename)) {
|
|
523
|
+
return emptyClientRouteModuleInferenceResult();
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
options.seen.add(options.filename);
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const clientBoundaryImports: string[] = [];
|
|
530
|
+
const clientBoundaryExportNames = new Set<string>();
|
|
531
|
+
const nestedClientExportNames = new Set<string>();
|
|
532
|
+
const clientReferenceSourceFiles: string[] = [];
|
|
533
|
+
const diagnostics: ClientRouteInferenceDiagnostic[] = [];
|
|
534
|
+
let clientProxy = false;
|
|
535
|
+
let nestedClient = false;
|
|
536
|
+
const exportInfo = analysis.topLevelExportRenderInfo;
|
|
537
|
+
const implicitModuleClient = exportInfo.length === 0 && analysis.clientRuntime;
|
|
538
|
+
for (const info of exportInfo) {
|
|
539
|
+
if (info.clientRuntime) {
|
|
540
|
+
clientBoundaryExportNames.add(info.name);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (
|
|
544
|
+
hasServerOnlyImports(analysis) &&
|
|
545
|
+
(implicitModuleClient || clientBoundaryExportNames.size > 0)
|
|
546
|
+
) {
|
|
547
|
+
return emptyClientRouteModuleInferenceResult({
|
|
548
|
+
serverOnly: true,
|
|
549
|
+
serverOnlyClientRuntime: true,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
const jsxComponentRoots = new Set(analysis.jsxComponentRoots);
|
|
553
|
+
const componentCallRoots = new Set(analysis.componentCallRoots);
|
|
554
|
+
const renderedComponentRoots = unionSets(jsxComponentRoots, componentCallRoots);
|
|
555
|
+
const identifierReferences = new Set(analysis.identifierReferences);
|
|
556
|
+
|
|
557
|
+
for (const reference of analysis.staticImports) {
|
|
558
|
+
const renderedByJsx = isRenderedImportReference(reference, jsxComponentRoots);
|
|
559
|
+
const renderedByCall = isRenderedImportReference(reference, componentCallRoots);
|
|
560
|
+
const rendered = isRenderedImportReference(reference, renderedComponentRoots);
|
|
561
|
+
const referenced = isReferencedImportReference(reference, identifierReferences);
|
|
562
|
+
|
|
563
|
+
if (
|
|
564
|
+
!rendered &&
|
|
565
|
+
(!referenced || !hasPotentialClientBoundaryReference(reference, identifierReferences))
|
|
566
|
+
) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (reference.sideEffect && isStyleModuleSpecifier(reference.source)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const resolved = await resolveAppLocalModule({
|
|
574
|
+
allowExplicitNonSource: options.sourceTransform !== undefined,
|
|
575
|
+
cache: options.cache,
|
|
576
|
+
importer: options.filename,
|
|
577
|
+
specifier: reference.source,
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
if (resolved === undefined) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const source = await readClientRouteSource({
|
|
585
|
+
cache: options.cache,
|
|
586
|
+
filename: resolved,
|
|
587
|
+
sourceTransform: options.sourceTransform,
|
|
588
|
+
});
|
|
589
|
+
const imported = await inferClientRouteModuleSource({
|
|
590
|
+
cache: options.cache,
|
|
591
|
+
code: source,
|
|
592
|
+
filename: resolved,
|
|
593
|
+
moduleContext: await compilerModuleContextForSource({
|
|
594
|
+
cache: options.cache,
|
|
595
|
+
code: source,
|
|
596
|
+
filename: resolved,
|
|
597
|
+
}),
|
|
598
|
+
root: false,
|
|
599
|
+
seen: options.seen,
|
|
600
|
+
sourceTransform: options.sourceTransform,
|
|
601
|
+
});
|
|
602
|
+
diagnostics.push(...imported.diagnostics);
|
|
603
|
+
|
|
604
|
+
if (!imported.client) {
|
|
605
|
+
if (imported.serverOnlyClientRuntime && rendered) {
|
|
606
|
+
diagnostics.push(
|
|
607
|
+
serverOnlyClientImportReferenceDiagnostic({
|
|
608
|
+
filename: options.filename,
|
|
609
|
+
reference,
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (rendered) {
|
|
617
|
+
const importedExportNames = renderedImportedExportNames(reference, renderedComponentRoots);
|
|
618
|
+
const renderedExportNames = renderedLocalExportNames(reference, exportInfo);
|
|
619
|
+
const importedBoundary =
|
|
620
|
+
imported.clientBoundaryModule ||
|
|
621
|
+
matchesInferredExportNames(importedExportNames, imported.clientBoundaryExportNames);
|
|
622
|
+
const importedNested = matchesInferredExportNames(
|
|
623
|
+
importedExportNames,
|
|
624
|
+
imported.nestedClientExportNames,
|
|
625
|
+
);
|
|
626
|
+
if (!importedBoundary && !importedNested) {
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
nestedClient = true;
|
|
631
|
+
|
|
632
|
+
for (const exportName of renderedExportNames) {
|
|
633
|
+
if (importedBoundary || importedNested || renderedByCall) {
|
|
634
|
+
nestedClientExportNames.add(exportName);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (renderedByCall && !renderedByJsx) {
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!importedBoundary) {
|
|
643
|
+
clientReferenceSourceFiles.push(resolved);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
clientBoundaryImports.push(reference.source);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const diagnostic = unsupportedClientImportReferenceDiagnostic({
|
|
652
|
+
filename: options.filename,
|
|
653
|
+
identifierReferences,
|
|
654
|
+
reference,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
if (diagnostic !== undefined) {
|
|
658
|
+
diagnostics.push(diagnostic);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!options.root) {
|
|
663
|
+
for (const reference of analysis.staticExports) {
|
|
664
|
+
const resolved = await resolveAppLocalModule({
|
|
665
|
+
allowExplicitNonSource: options.sourceTransform !== undefined,
|
|
666
|
+
cache: options.cache,
|
|
667
|
+
importer: options.filename,
|
|
668
|
+
specifier: reference.source,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
if (resolved === undefined) {
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const source = await readClientRouteSource({
|
|
676
|
+
cache: options.cache,
|
|
677
|
+
filename: resolved,
|
|
678
|
+
sourceTransform: options.sourceTransform,
|
|
679
|
+
});
|
|
680
|
+
const exported = await inferClientRouteModuleSource({
|
|
681
|
+
cache: options.cache,
|
|
682
|
+
code: source,
|
|
683
|
+
filename: resolved,
|
|
684
|
+
moduleContext: await compilerModuleContextForSource({
|
|
685
|
+
cache: options.cache,
|
|
686
|
+
code: source,
|
|
687
|
+
filename: resolved,
|
|
688
|
+
}),
|
|
689
|
+
root: false,
|
|
690
|
+
seen: options.seen,
|
|
691
|
+
sourceTransform: options.sourceTransform,
|
|
692
|
+
});
|
|
693
|
+
diagnostics.push(...exported.diagnostics);
|
|
694
|
+
|
|
695
|
+
if (exported.clientBoundaryModule) {
|
|
696
|
+
clientProxy = true;
|
|
697
|
+
} else if (exported.clientBoundaryExportNames.length > 0) {
|
|
698
|
+
for (const exportName of reference.exportedNames) {
|
|
699
|
+
if (exported.clientBoundaryExportNames.includes(exportName)) {
|
|
700
|
+
clientBoundaryExportNames.add(exportName);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} else if (exported.client) {
|
|
704
|
+
nestedClient = true;
|
|
705
|
+
clientReferenceSourceFiles.push(resolved);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
client:
|
|
712
|
+
clientBoundaryImports.length > 0 ||
|
|
713
|
+
clientBoundaryExportNames.size > 0 ||
|
|
714
|
+
implicitModuleClient ||
|
|
715
|
+
clientProxy ||
|
|
716
|
+
nestedClient,
|
|
717
|
+
clientBoundaryImports,
|
|
718
|
+
clientBoundaryExportNames: Array.from(clientBoundaryExportNames),
|
|
719
|
+
clientBoundaryModule: clientProxy || implicitModuleClient,
|
|
720
|
+
nestedClientExportNames: Array.from(nestedClientExportNames),
|
|
721
|
+
clientReferenceSourceFiles: Array.from(new Set(clientReferenceSourceFiles)),
|
|
722
|
+
diagnostics,
|
|
723
|
+
serverOnly: false,
|
|
724
|
+
serverOnlyClientRuntime: false,
|
|
725
|
+
};
|
|
726
|
+
} finally {
|
|
727
|
+
options.seen.delete(options.filename);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function emptyClientRouteModuleInferenceResult(
|
|
732
|
+
overrides: Partial<ClientRouteModuleInferenceResult> = {},
|
|
733
|
+
): ClientRouteModuleInferenceResult {
|
|
734
|
+
return {
|
|
735
|
+
client: false,
|
|
736
|
+
clientBoundaryImports: [],
|
|
737
|
+
clientBoundaryExportNames: [],
|
|
738
|
+
clientBoundaryModule: false,
|
|
739
|
+
nestedClientExportNames: [],
|
|
740
|
+
clientReferenceSourceFiles: [],
|
|
741
|
+
diagnostics: [],
|
|
742
|
+
serverOnly: false,
|
|
743
|
+
serverOnlyClientRuntime: false,
|
|
744
|
+
...overrides,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function clientRouteModuleAnalysisForSource(options: {
|
|
749
|
+
cache: ClientRouteInferenceCache;
|
|
750
|
+
code: string;
|
|
751
|
+
filename: string;
|
|
752
|
+
moduleContext?: CompilerModuleContext | undefined;
|
|
753
|
+
}): Promise<ClientRouteModuleAnalysis> {
|
|
754
|
+
const cacheKey = sourceAnalysisCacheKey(options.filename, options.code);
|
|
755
|
+
const cached = options.cache.moduleAnalysisByFile.get(cacheKey);
|
|
756
|
+
|
|
757
|
+
if (cached !== undefined) {
|
|
758
|
+
return cached;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const analysis = Promise.resolve().then(async () =>
|
|
762
|
+
collectClientRouteModuleAnalysisFromContext(
|
|
763
|
+
options.moduleContext ??
|
|
764
|
+
(await compilerModuleContextForSource({
|
|
765
|
+
cache: options.cache,
|
|
766
|
+
code: options.code,
|
|
767
|
+
filename: options.filename,
|
|
768
|
+
})),
|
|
769
|
+
),
|
|
770
|
+
);
|
|
771
|
+
options.cache.moduleAnalysisByFile.set(cacheKey, analysis);
|
|
772
|
+
return analysis;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export async function compilerModuleContextForSource(options: {
|
|
776
|
+
cache: ClientRouteInferenceCache;
|
|
777
|
+
code: string;
|
|
778
|
+
filename: string;
|
|
779
|
+
}): Promise<CompilerModuleContext> {
|
|
780
|
+
const cacheKey = sourceAnalysisCacheKey(options.filename, options.code);
|
|
781
|
+
const cached = options.cache.moduleContextByFile.get(cacheKey);
|
|
782
|
+
|
|
783
|
+
if (cached !== undefined) {
|
|
784
|
+
return cached;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const context = Promise.resolve().then(() =>
|
|
788
|
+
createCompilerModuleContext({
|
|
789
|
+
code: options.code,
|
|
790
|
+
filename: options.filename,
|
|
791
|
+
}),
|
|
792
|
+
);
|
|
793
|
+
options.cache.moduleContextByFile.set(cacheKey, context);
|
|
794
|
+
return context;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function sourceAnalysisCacheKey(filename: string, code: string): string {
|
|
798
|
+
return `${filename}\0${hashSourceText(code)}`;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function hashSourceText(text: string): string {
|
|
802
|
+
return createHash("sha256").update(text).digest("hex");
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function isRenderedImportReference(
|
|
806
|
+
reference: StaticImportReference,
|
|
807
|
+
componentRoots: ReadonlySet<string>,
|
|
808
|
+
): boolean {
|
|
809
|
+
return (
|
|
810
|
+
reference.sideEffect || reference.localNames.some((localName) => componentRoots.has(localName))
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function isReferencedImportReference(
|
|
815
|
+
reference: StaticImportReference,
|
|
816
|
+
identifierReferences: ReadonlySet<string>,
|
|
817
|
+
): boolean {
|
|
818
|
+
return (
|
|
819
|
+
reference.sideEffect ||
|
|
820
|
+
reference.localNames.some((localName) => identifierReferences.has(localName))
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function hasPotentialClientBoundaryReference(
|
|
825
|
+
reference: ClientRouteStaticImportReference,
|
|
826
|
+
identifierReferences: ReadonlySet<string>,
|
|
827
|
+
): boolean {
|
|
828
|
+
return (
|
|
829
|
+
reference.sideEffect ||
|
|
830
|
+
reference.specifiers.some(
|
|
831
|
+
(specifier) =>
|
|
832
|
+
specifier.kind === "namespace" && identifierReferences.has(specifier.localName),
|
|
833
|
+
) ||
|
|
834
|
+
reference.localNames.some(
|
|
835
|
+
(localName) => identifierReferences.has(localName) && startsUppercase(localName),
|
|
836
|
+
)
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function isStyleModuleSpecifier(source: string): boolean {
|
|
841
|
+
const pathname = source.split(/[?#]/u, 1)[0] ?? source;
|
|
842
|
+
return styleModuleExtensions.has(extname(pathname));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const styleModuleExtensions = new Set([
|
|
846
|
+
".css",
|
|
847
|
+
".less",
|
|
848
|
+
".sass",
|
|
849
|
+
".scss",
|
|
850
|
+
".styl",
|
|
851
|
+
".stylus",
|
|
852
|
+
]);
|
|
853
|
+
|
|
854
|
+
function renderedImportedExportNames(
|
|
855
|
+
reference: ClientRouteStaticImportReference,
|
|
856
|
+
componentRoots: ReadonlySet<string>,
|
|
857
|
+
): string[] | undefined {
|
|
858
|
+
if (reference.sideEffect) {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const names = new Set<string>();
|
|
863
|
+
|
|
864
|
+
for (const specifier of reference.specifiers) {
|
|
865
|
+
if (!componentRoots.has(specifier.localName)) {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (specifier.kind === "namespace") {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
names.add(specifier.importedName);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return Array.from(names);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function renderedLocalExportNames(
|
|
880
|
+
reference: StaticImportReference,
|
|
881
|
+
exportInfo: readonly TopLevelExportRenderInfo[],
|
|
882
|
+
): string[] {
|
|
883
|
+
const localNames = new Set(reference.localNames);
|
|
884
|
+
const rendered = exportInfo
|
|
885
|
+
.filter((info) => renderedComponentRootNames(info).some((root) => localNames.has(root)))
|
|
886
|
+
.map((info) => info.name);
|
|
887
|
+
|
|
888
|
+
return rendered.length === 0 ? ["default"] : rendered;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
function renderedComponentRootNames(info: TopLevelExportRenderInfo): string[] {
|
|
892
|
+
return [...info.renderedComponentRoots, ...info.calledComponentRoots];
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function unionSets<T>(left: ReadonlySet<T>, right: ReadonlySet<T>): ReadonlySet<T> {
|
|
896
|
+
return new Set([...left, ...right]);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function matchesInferredExportNames(
|
|
900
|
+
importedExportNames: readonly string[] | undefined,
|
|
901
|
+
inferredExportNames: readonly string[],
|
|
902
|
+
): boolean {
|
|
903
|
+
if (importedExportNames === undefined) {
|
|
904
|
+
return inferredExportNames.length > 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return importedExportNames.some((name) => inferredExportNames.includes(name));
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function serverOnlyClientImportReferenceDiagnostic(options: {
|
|
911
|
+
filename: string;
|
|
912
|
+
reference: StaticImportReference;
|
|
913
|
+
}): ClientRouteInferenceDiagnostic {
|
|
914
|
+
return {
|
|
915
|
+
code: clientBoundaryInferenceServerOnlyReferenceCode,
|
|
916
|
+
filename: options.filename,
|
|
917
|
+
level: "warn",
|
|
918
|
+
localNames: options.reference.localNames,
|
|
919
|
+
message:
|
|
920
|
+
`${options.filename}: server-only component import ${JSON.stringify(options.reference.source)} ` +
|
|
921
|
+
"uses client runtime syntax but is marked with server-only semantics. Automatic client " +
|
|
922
|
+
"boundary detection skipped it. Move the interactive UI behind a .client module or add an " +
|
|
923
|
+
"explicit clientBoundaryImports entry.",
|
|
924
|
+
source: options.reference.source,
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function unsupportedClientImportReferenceDiagnostic(options: {
|
|
929
|
+
filename: string;
|
|
930
|
+
identifierReferences: ReadonlySet<string>;
|
|
931
|
+
reference: StaticImportReference;
|
|
932
|
+
}): ClientRouteInferenceDiagnostic | undefined {
|
|
933
|
+
if (options.reference.sideEffect) {
|
|
934
|
+
return undefined;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const localNames = options.reference.localNames.filter((name) =>
|
|
938
|
+
options.identifierReferences.has(name),
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
if (localNames.length === 0) {
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
code: clientBoundaryInferenceUnsupportedReferenceCode,
|
|
947
|
+
filename: options.filename,
|
|
948
|
+
level: "warn",
|
|
949
|
+
localNames,
|
|
950
|
+
message:
|
|
951
|
+
`${options.filename}: client component import ${JSON.stringify(options.reference.source)} ` +
|
|
952
|
+
`is referenced as ${localNames.map((name) => JSON.stringify(name)).join(", ")} but ` +
|
|
953
|
+
"was not rendered through a supported static JSX pattern. Automatic client boundary " +
|
|
954
|
+
"detection supports direct JSX such as <Counter />, JSX member roots such as " +
|
|
955
|
+
"<components.Counter />, and simple aliases such as const Alias = Counter. For dynamic " +
|
|
956
|
+
`registries or computed component selection, add ${JSON.stringify(options.reference.source)} ` +
|
|
957
|
+
"to clientBoundaryImports.",
|
|
958
|
+
source: options.reference.source,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function startsUppercase(value: string): boolean {
|
|
963
|
+
return /^[A-Z]/.test(value);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function formatClientRouteInferenceDiagnostic(
|
|
967
|
+
diagnostic: ClientRouteInferenceDiagnostic,
|
|
968
|
+
): string {
|
|
969
|
+
return `${diagnostic.code}: ${diagnostic.message}`;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function resolveAppLocalModule(options: {
|
|
973
|
+
allowExplicitNonSource?: boolean | undefined;
|
|
974
|
+
cache: ClientRouteInferenceCache;
|
|
975
|
+
importer: string;
|
|
976
|
+
specifier: string;
|
|
977
|
+
}): Promise<string | undefined> {
|
|
978
|
+
if (!options.specifier.startsWith(".")) {
|
|
979
|
+
return undefined;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const cacheKey = `${options.importer}\0${options.specifier}\0${
|
|
983
|
+
options.allowExplicitNonSource === true ? "explicit" : "source"
|
|
984
|
+
}`;
|
|
985
|
+
const cached = options.cache.resolvedByImport.get(cacheKey);
|
|
986
|
+
|
|
987
|
+
if (cached !== undefined) {
|
|
988
|
+
return cached;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const resolved = resolveAppLocalModuleUncached({
|
|
992
|
+
allowExplicitNonSource: options.allowExplicitNonSource === true,
|
|
993
|
+
importer: options.importer,
|
|
994
|
+
specifier: options.specifier,
|
|
995
|
+
});
|
|
996
|
+
options.cache.resolvedByImport.set(cacheKey, resolved);
|
|
997
|
+
return resolved;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function resolveAppLocalModuleUncached(options: {
|
|
1001
|
+
allowExplicitNonSource: boolean;
|
|
1002
|
+
importer: string;
|
|
1003
|
+
specifier: string;
|
|
1004
|
+
}): Promise<string | undefined> {
|
|
1005
|
+
const { importer, specifier } = options;
|
|
1006
|
+
const base = join(dirname(importer), specifier);
|
|
1007
|
+
const candidates = sourceModuleCandidates(base);
|
|
1008
|
+
|
|
1009
|
+
if (candidates.length === 0) {
|
|
1010
|
+
if (options.allowExplicitNonSource && extname(base) !== "" && (await isFile(base))) {
|
|
1011
|
+
return base;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
for (const candidate of candidates) {
|
|
1018
|
+
if (await isFile(candidate)) {
|
|
1019
|
+
return candidate;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
throw new Error(`${importer}: could not resolve app-local import ${JSON.stringify(specifier)}.`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function isFile(path: string): Promise<boolean> {
|
|
1027
|
+
try {
|
|
1028
|
+
return (await stat(path)).isFile();
|
|
1029
|
+
} catch {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
async function readCachedFile(cache: ClientRouteInferenceCache, filename: string): Promise<string> {
|
|
1035
|
+
const signature = await sourceFileSignature(filename);
|
|
1036
|
+
const cached = cache.sourceByFile.get(filename);
|
|
1037
|
+
|
|
1038
|
+
if (cached !== undefined) {
|
|
1039
|
+
const source = await cached;
|
|
1040
|
+
|
|
1041
|
+
if (source.signature === signature) {
|
|
1042
|
+
return source.source;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const source = readFile(filename, "utf8").then((value) => ({
|
|
1047
|
+
signature,
|
|
1048
|
+
source: value,
|
|
1049
|
+
}));
|
|
1050
|
+
cache.sourceByFile.set(filename, source);
|
|
1051
|
+
return (await source).source;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
async function readClientRouteSource(options: {
|
|
1055
|
+
cache: ClientRouteInferenceCache;
|
|
1056
|
+
filename: string;
|
|
1057
|
+
sourceTransform?: ClientRouteSourceTransform | undefined;
|
|
1058
|
+
}): Promise<string> {
|
|
1059
|
+
if (options.sourceTransform === undefined) {
|
|
1060
|
+
return readCachedFile(options.cache, options.filename);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const signature = await sourceFileSignature(options.filename);
|
|
1064
|
+
const cacheKey = `${options.filename}\0${options.sourceTransform.cacheKey}`;
|
|
1065
|
+
const cached = options.cache.transformedSourceByFile.get(cacheKey);
|
|
1066
|
+
|
|
1067
|
+
if (cached !== undefined) {
|
|
1068
|
+
const source = await cached;
|
|
1069
|
+
|
|
1070
|
+
if (source.signature === signature) {
|
|
1071
|
+
return source.source;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const source = readCachedFile(options.cache, options.filename).then(async (code) => ({
|
|
1076
|
+
signature,
|
|
1077
|
+
source: await options.sourceTransform!.transform(options.filename, code),
|
|
1078
|
+
}));
|
|
1079
|
+
options.cache.transformedSourceByFile.set(cacheKey, source);
|
|
1080
|
+
return (await source).source;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function transformClientRouteSource(options: {
|
|
1084
|
+
code: string;
|
|
1085
|
+
filename: string;
|
|
1086
|
+
sourceTransform?: ClientRouteSourceTransform | undefined;
|
|
1087
|
+
}): Promise<string> {
|
|
1088
|
+
return options.sourceTransform === undefined
|
|
1089
|
+
? options.code
|
|
1090
|
+
: await options.sourceTransform.transform(options.filename, options.code);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function clientRouteSourceTransformForVitePlugins(
|
|
1094
|
+
pluginOptions: readonly PluginOption[] | undefined,
|
|
1095
|
+
): ClientRouteSourceTransform | undefined {
|
|
1096
|
+
const plugins = orderVitePlugins(flattenVitePlugins(pluginOptions)).filter((plugin) =>
|
|
1097
|
+
hasViteTransformHook(plugin),
|
|
1098
|
+
);
|
|
1099
|
+
|
|
1100
|
+
if (plugins.length === 0) {
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return {
|
|
1105
|
+
cacheKey: plugins.map((plugin, index) => `${index}:${plugin.name}`).join("\0"),
|
|
1106
|
+
async transform(filename, code) {
|
|
1107
|
+
let nextCode = code;
|
|
1108
|
+
|
|
1109
|
+
for (const plugin of plugins) {
|
|
1110
|
+
const handler = viteTransformHookHandler(plugin);
|
|
1111
|
+
|
|
1112
|
+
if (handler === undefined) {
|
|
1113
|
+
continue;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
const result = await handler.call(
|
|
1117
|
+
createClientRouteViteTransformContext(plugin.name),
|
|
1118
|
+
nextCode,
|
|
1119
|
+
filename,
|
|
1120
|
+
{ ssr: false },
|
|
1121
|
+
);
|
|
1122
|
+
|
|
1123
|
+
if (typeof result === "string") {
|
|
1124
|
+
nextCode = result;
|
|
1125
|
+
} else if (result !== null && result !== undefined && typeof result === "object") {
|
|
1126
|
+
const codeResult = (result as { code?: unknown }).code;
|
|
1127
|
+
|
|
1128
|
+
if (typeof codeResult === "string") {
|
|
1129
|
+
nextCode = codeResult;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return nextCode;
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function flattenVitePlugins(pluginOptions: readonly PluginOption[] | undefined): Plugin[] {
|
|
1140
|
+
const plugins: Plugin[] = [];
|
|
1141
|
+
const visit = (option: PluginOption | null | false | undefined): void => {
|
|
1142
|
+
if (option === false || option === null || option === undefined) {
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (Array.isArray(option)) {
|
|
1147
|
+
for (const child of option) {
|
|
1148
|
+
visit(child);
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (typeof option === "object" && "then" in option) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
plugins.push(option as Plugin);
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
for (const option of pluginOptions ?? []) {
|
|
1161
|
+
visit(option);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
return plugins;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function orderVitePlugins(plugins: readonly Plugin[]): Plugin[] {
|
|
1168
|
+
return [
|
|
1169
|
+
...plugins.filter((plugin) => plugin.enforce === "pre"),
|
|
1170
|
+
...plugins.filter((plugin) => plugin.enforce !== "pre" && plugin.enforce !== "post"),
|
|
1171
|
+
...plugins.filter((plugin) => plugin.enforce === "post"),
|
|
1172
|
+
];
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function hasViteTransformHook(plugin: Plugin): boolean {
|
|
1176
|
+
return viteTransformHookHandler(plugin) !== undefined;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function viteTransformHookHandler(plugin: Plugin):
|
|
1180
|
+
| ((
|
|
1181
|
+
this: unknown,
|
|
1182
|
+
code: string,
|
|
1183
|
+
id: string,
|
|
1184
|
+
options?: { ssr?: boolean | undefined },
|
|
1185
|
+
) => unknown)
|
|
1186
|
+
| undefined {
|
|
1187
|
+
const transform = plugin.transform as unknown;
|
|
1188
|
+
|
|
1189
|
+
if (typeof transform === "function") {
|
|
1190
|
+
return transform as (
|
|
1191
|
+
this: unknown,
|
|
1192
|
+
code: string,
|
|
1193
|
+
id: string,
|
|
1194
|
+
options?: { ssr?: boolean | undefined },
|
|
1195
|
+
) => unknown;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (transform !== null && typeof transform === "object") {
|
|
1199
|
+
const handler = (transform as { handler?: unknown }).handler;
|
|
1200
|
+
|
|
1201
|
+
if (typeof handler === "function") {
|
|
1202
|
+
return handler as (
|
|
1203
|
+
this: unknown,
|
|
1204
|
+
code: string,
|
|
1205
|
+
id: string,
|
|
1206
|
+
options?: { ssr?: boolean | undefined },
|
|
1207
|
+
) => unknown;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
return undefined;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function createClientRouteViteTransformContext(pluginName: string): unknown {
|
|
1215
|
+
return {
|
|
1216
|
+
addWatchFile() {},
|
|
1217
|
+
async resolve() {
|
|
1218
|
+
return null;
|
|
1219
|
+
},
|
|
1220
|
+
error(error: unknown): never {
|
|
1221
|
+
if (error instanceof Error) {
|
|
1222
|
+
throw error;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
throw new Error(String(error));
|
|
1226
|
+
},
|
|
1227
|
+
warn() {},
|
|
1228
|
+
getCombinedSourcemap() {
|
|
1229
|
+
return null;
|
|
1230
|
+
},
|
|
1231
|
+
parse() {
|
|
1232
|
+
throw new Error(
|
|
1233
|
+
`${pluginName}: client route import analysis cannot provide Rollup parser context.`,
|
|
1234
|
+
);
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
async function sourceFileSignature(filename: string): Promise<string> {
|
|
1240
|
+
const stats = await stat(filename);
|
|
1241
|
+
|
|
1242
|
+
return `${stats.mtimeMs}\0${stats.size}`;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
export function routeIdForPath(path: string): string {
|
|
1246
|
+
if (path === "/") {
|
|
1247
|
+
return "index";
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
return path
|
|
1251
|
+
.slice(1)
|
|
1252
|
+
.replaceAll("/", "_")
|
|
1253
|
+
.replaceAll(":", "_")
|
|
1254
|
+
.replace(/[^A-Za-z0-9_$-]/g, "_");
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
export function clientScriptForPath(path: string): string {
|
|
1258
|
+
return `routes/${routeIdForPath(path)}.js`;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
export function navigationRuntimeScriptForDev(): string {
|
|
1262
|
+
return "navigation.js";
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
export function withHydrationMarkers(options: {
|
|
1266
|
+
assetBaseUrl?: string | undefined;
|
|
1267
|
+
clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
|
|
1268
|
+
html: string;
|
|
1269
|
+
props: unknown;
|
|
1270
|
+
routePath: string;
|
|
1271
|
+
script?: string | undefined;
|
|
1272
|
+
}): string {
|
|
1273
|
+
const marker = hydrationMarkerParts({
|
|
1274
|
+
assetBaseUrl: options.assetBaseUrl,
|
|
1275
|
+
clientReferenceManifest: options.clientReferenceManifest,
|
|
1276
|
+
props: options.props,
|
|
1277
|
+
routePath: options.routePath,
|
|
1278
|
+
script: options.script,
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
return `${marker.prefix}${options.html}${marker.suffix}`;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export function withRouteMarkers(options: { html: string; routePath: string }): string {
|
|
1285
|
+
const routeId = routeIdForPath(options.routePath);
|
|
1286
|
+
|
|
1287
|
+
return `<div data-mreact-route-id="${escapeHtmlAttribute(routeId)}">${options.html}</div>`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function hydrationMarkerParts(options: {
|
|
1291
|
+
assetBaseUrl?: string | undefined;
|
|
1292
|
+
clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
|
|
1293
|
+
props: unknown;
|
|
1294
|
+
routePath: string;
|
|
1295
|
+
script?: string | undefined;
|
|
1296
|
+
}): { prefix: string; suffix: string } {
|
|
1297
|
+
const routeId = routeIdForPath(options.routePath);
|
|
1298
|
+
const escapedRouteId = escapeHtmlAttribute(routeId);
|
|
1299
|
+
const propsJson = escapeScriptJson(JSON.stringify(options.props));
|
|
1300
|
+
const script = options.script ?? clientScriptForPath(options.routePath);
|
|
1301
|
+
const scriptSrc = assetPath(script, options.assetBaseUrl ?? "/_mreact/client/");
|
|
1302
|
+
const clientReferencesJson =
|
|
1303
|
+
options.clientReferenceManifest === undefined || options.clientReferenceManifest.length === 0
|
|
1304
|
+
? undefined
|
|
1305
|
+
: escapeScriptJson(JSON.stringify(options.clientReferenceManifest));
|
|
1306
|
+
|
|
1307
|
+
return {
|
|
1308
|
+
prefix: `<div data-mreact-route-id="${escapedRouteId}">`,
|
|
1309
|
+
suffix: [
|
|
1310
|
+
"</div>",
|
|
1311
|
+
`<script type="application/json" id="mreact-props-${escapedRouteId}">${propsJson}</script>`,
|
|
1312
|
+
clientReferencesJson === undefined
|
|
1313
|
+
? undefined
|
|
1314
|
+
: `<script type="application/json" id="mreact-client-references-${escapedRouteId}">${clientReferencesJson}</script>`,
|
|
1315
|
+
`<script type="module" src="${escapeHtmlAttribute(scriptSrc)}"></script>`,
|
|
1316
|
+
]
|
|
1317
|
+
.filter((part): part is string => part !== undefined)
|
|
1318
|
+
.join(""),
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
export async function buildClientRouteBundle(options: {
|
|
1323
|
+
code: string;
|
|
1324
|
+
clientBoundaryImports?: readonly string[] | undefined;
|
|
1325
|
+
clientReferenceImports?: readonly ClientReferenceImport[] | undefined;
|
|
1326
|
+
clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
|
|
1327
|
+
filename: string;
|
|
1328
|
+
routePath: string;
|
|
1329
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
1330
|
+
}): Promise<string> {
|
|
1331
|
+
return (await buildClientRouteOutput(options)).code;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
export async function buildNavigationRuntimeBundle(
|
|
1335
|
+
options: {
|
|
1336
|
+
minify?: boolean;
|
|
1337
|
+
sourceMap?: boolean;
|
|
1338
|
+
} = {},
|
|
1339
|
+
): Promise<{ code: string; map?: string }> {
|
|
1340
|
+
return buildClientRouteOutput({
|
|
1341
|
+
code: "export default undefined;",
|
|
1342
|
+
filename: "__mreact_navigation_runtime.tsx",
|
|
1343
|
+
routePath: "/__mreact_navigation_runtime",
|
|
1344
|
+
clientNavigation: true,
|
|
1345
|
+
...(options.minify === undefined ? {} : { minify: options.minify }),
|
|
1346
|
+
...(options.sourceMap === undefined ? {} : { sourceMap: options.sourceMap }),
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
export async function buildClientRouteOutput(
|
|
1351
|
+
options: BuildClientRouteOutputOptions,
|
|
1352
|
+
): Promise<{ code: string; map?: string }> {
|
|
1353
|
+
const entry = await buildClientRouteEntrySource(options);
|
|
1354
|
+
const bundled = await bundleRouterModule({
|
|
1355
|
+
code: entry.code,
|
|
1356
|
+
define: {
|
|
1357
|
+
__MREACT_CLIENT_DEVTOOLS__: "false",
|
|
1358
|
+
},
|
|
1359
|
+
filename: options.filename,
|
|
1360
|
+
minify: options.minify === true,
|
|
1361
|
+
platform: "browser",
|
|
1362
|
+
preserveExports: true,
|
|
1363
|
+
plugins: [workspaceRuntimePlugin({ routeFiles: [options.filename] })],
|
|
1364
|
+
sourceMap: options.sourceMap,
|
|
1365
|
+
vitePlugins: options.vitePlugins,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
return {
|
|
1369
|
+
code: bundled.code,
|
|
1370
|
+
...(bundled.map === undefined ? {} : { map: bundled.map }),
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
export async function buildClientRouteBatchOutput(options: {
|
|
1375
|
+
assetBaseUrl?: string | undefined;
|
|
1376
|
+
minify?: boolean;
|
|
1377
|
+
projectRoot?: string | undefined;
|
|
1378
|
+
routes: readonly BuildClientRouteOutputOptions[];
|
|
1379
|
+
sourceMap?: boolean;
|
|
1380
|
+
vitePlugins?: readonly PluginOption[] | undefined;
|
|
1381
|
+
}): Promise<BuildClientRouteBatchOutput> {
|
|
1382
|
+
const entries = await Promise.all(
|
|
1383
|
+
options.routes.map(async (route) => ({
|
|
1384
|
+
filename: route.filename,
|
|
1385
|
+
name: routeIdForPath(route.routePath),
|
|
1386
|
+
routePath: route.routePath,
|
|
1387
|
+
source: await buildClientRouteEntrySource({
|
|
1388
|
+
...route,
|
|
1389
|
+
minify: options.minify ?? route.minify,
|
|
1390
|
+
sourceMap: options.sourceMap ?? route.sourceMap,
|
|
1391
|
+
vitePlugins: options.vitePlugins ?? route.vitePlugins,
|
|
1392
|
+
}),
|
|
1393
|
+
})),
|
|
1394
|
+
);
|
|
1395
|
+
const bundled = await bundleRouterModules({
|
|
1396
|
+
base: options.assetBaseUrl ?? "/_mreact/client/",
|
|
1397
|
+
define: {
|
|
1398
|
+
__MREACT_CLIENT_DEVTOOLS__: "false",
|
|
1399
|
+
},
|
|
1400
|
+
entries: entries.map((entry) => ({
|
|
1401
|
+
code: entry.source.code,
|
|
1402
|
+
filename: entry.filename,
|
|
1403
|
+
name: entry.name,
|
|
1404
|
+
})),
|
|
1405
|
+
minify: options.minify === true,
|
|
1406
|
+
platform: "browser",
|
|
1407
|
+
plugins: [workspaceRuntimePlugin({ routeFiles: entries.map((entry) => entry.filename) })],
|
|
1408
|
+
root: options.projectRoot,
|
|
1409
|
+
sourceMap: options.sourceMap,
|
|
1410
|
+
vitePlugins: options.vitePlugins,
|
|
1411
|
+
});
|
|
1412
|
+
const entryChunks = new Map(
|
|
1413
|
+
bundled.chunks.filter((chunk) => chunk.isEntry).map((chunk) => [chunk.name, chunk]),
|
|
1414
|
+
);
|
|
1415
|
+
|
|
1416
|
+
return {
|
|
1417
|
+
...(bundled.assets === undefined ? {} : { assets: bundled.assets }),
|
|
1418
|
+
chunks: bundled.chunks,
|
|
1419
|
+
routes: entries.map((entry) => {
|
|
1420
|
+
const chunk = entryChunks.get(entry.name);
|
|
1421
|
+
|
|
1422
|
+
if (chunk === undefined) {
|
|
1423
|
+
throw new Error(`Failed to bundle client route ${entry.routePath}: missing entry chunk.`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return {
|
|
1427
|
+
chunk,
|
|
1428
|
+
routeId: entry.name,
|
|
1429
|
+
routePath: entry.routePath,
|
|
1430
|
+
};
|
|
1431
|
+
}),
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export async function buildClientRouteEntrySource(
|
|
1436
|
+
options: BuildClientRouteOutputOptions,
|
|
1437
|
+
): Promise<{ code: string }> {
|
|
1438
|
+
const moduleContext = createCompilerModuleContext({
|
|
1439
|
+
code: options.code,
|
|
1440
|
+
filename: options.filename,
|
|
1441
|
+
});
|
|
1442
|
+
const compiled = transformCompilerModuleContext({
|
|
1443
|
+
code: options.code,
|
|
1444
|
+
clientBoundaryImports: options.clientBoundaryImports ?? [],
|
|
1445
|
+
filename: options.filename,
|
|
1446
|
+
moduleContext,
|
|
1447
|
+
target: "client",
|
|
1448
|
+
dev: options.minify !== true,
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
if (compiled.diagnostics.length > 0) {
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
compiled.diagnostics
|
|
1454
|
+
.map((diagnostic) => formatDiagnostic(options.filename, diagnostic))
|
|
1455
|
+
.join("\n"),
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
const clientNavigation = options.clientNavigation ?? detectClientNavigationHint(options.code);
|
|
1460
|
+
const clientReferenceManifest =
|
|
1461
|
+
options.clientReferenceManifest ?? (await inferClientReferenceManifestForBundle(options));
|
|
1462
|
+
const compatClientReferenceNames = compatClientReferenceComponentNames(clientReferenceManifest);
|
|
1463
|
+
const clientReferenceImportBlock = emitClientReferenceImportBlock(
|
|
1464
|
+
options.clientReferenceImports ?? [],
|
|
1465
|
+
);
|
|
1466
|
+
const clientReferenceRegistry = emitClientReferenceRegistry(
|
|
1467
|
+
clientReferenceManifest,
|
|
1468
|
+
options.clientReferenceImports ?? [],
|
|
1469
|
+
compatClientReferenceNames,
|
|
1470
|
+
);
|
|
1471
|
+
const routeComponentExpression = routeComponentExpressionForComponents(
|
|
1472
|
+
compiled.metadata.components,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
const routeId = routeIdForPath(options.routePath);
|
|
1476
|
+
const routeUsesCells = detectRouteCellStateHint(compiled.code);
|
|
1477
|
+
const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
|
|
1478
|
+
const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect;
|
|
1479
|
+
const routeHasEventBindings =
|
|
1480
|
+
(compiled.metadata.eventHydrationManifest?.events.length ?? 0) > 0;
|
|
1481
|
+
const routeRequiresFullHydration =
|
|
1482
|
+
routeUsesCells || routeUsesReactiveEffect || routeHasEventBindings;
|
|
1483
|
+
const routeUsesOnlyClientReferenceBoundaries =
|
|
1484
|
+
!routeRequiresFullHydration &&
|
|
1485
|
+
clientReferenceManifest.length > 0 &&
|
|
1486
|
+
(options.clientReferenceImports?.length ?? 0) > 0;
|
|
1487
|
+
const routeHydrationCode = routeUsesOnlyClientReferenceBoundaries ? "" : compiled.code;
|
|
1488
|
+
const hydratedRouteComponentExpression = routeUsesOnlyClientReferenceBoundaries
|
|
1489
|
+
? "undefined"
|
|
1490
|
+
: routeComponentExpression;
|
|
1491
|
+
const routeStateSignature = routeUsesCells ? routeStateSignatureForSource(compiled.code) : "";
|
|
1492
|
+
const routeCellEffectImport = routeUsesCells
|
|
1493
|
+
? `import { effect as __mreactRouteEffect } from "@reckona/mreact-reactive-core";\n`
|
|
1494
|
+
: "";
|
|
1495
|
+
const routeCleanupScopeImport = routeUsesCleanupScope
|
|
1496
|
+
? `import { withCleanupScope as __mreactWithCleanupScope } from "@reckona/mreact-reactive-core/internal";\n`
|
|
1497
|
+
: "";
|
|
1498
|
+
const navigationStateDeclaration = clientNavigation
|
|
1499
|
+
? `const __mreactNavigationState = __mreactGlobal.__mreactNavigationState ??= {
|
|
1500
|
+
cache: new Map(),
|
|
1501
|
+
current: {
|
|
1502
|
+
from: null,
|
|
1503
|
+
pending: false,
|
|
1504
|
+
to: null,
|
|
1505
|
+
type: null,
|
|
1506
|
+
},
|
|
1507
|
+
installed: false,
|
|
1508
|
+
prefetchedScripts: new Set(),
|
|
1509
|
+
routePrefetchManifest: undefined,
|
|
1510
|
+
routePrefetchManifestText: undefined,
|
|
1511
|
+
viewportAnchors: new WeakSet(),
|
|
1512
|
+
viewportObserver: undefined,
|
|
1513
|
+
};`
|
|
1514
|
+
: "";
|
|
1515
|
+
const routeCellStateDeclaration = routeUsesCells
|
|
1516
|
+
? `const __mreactRouteStates = __mreactGlobal.__mreactRouteStates ??= new Map();
|
|
1517
|
+
let __mreactActiveCellRecords = undefined;
|
|
1518
|
+
let __mreactActiveCellIndex = 0;`
|
|
1519
|
+
: "";
|
|
1520
|
+
const routeCleanupStateDeclaration = routeUsesCleanupScope
|
|
1521
|
+
? `const __mreactRouteDisposers = __mreactGlobal.__mreactRouteDisposers ??= new Map();`
|
|
1522
|
+
: "";
|
|
1523
|
+
const routeCellHook = routeUsesCells
|
|
1524
|
+
? `
|
|
1525
|
+
__mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
|
|
1526
|
+
if (__mreactActiveCellRecords === undefined) {
|
|
1527
|
+
return nativeCell(initial);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const cellKey = String(__mreactActiveCellIndex);
|
|
1531
|
+
__mreactActiveCellIndex += 1;
|
|
1532
|
+
const existingRecord = __mreactActiveCellRecords.get(cellKey);
|
|
1533
|
+
const record = existingRecord ?? { value: initial };
|
|
1534
|
+
const stateCell = nativeCell(record.value);
|
|
1535
|
+
const setStateCell = stateCell.set;
|
|
1536
|
+
|
|
1537
|
+
stateCell.set = (next) => {
|
|
1538
|
+
setStateCell((previous) => {
|
|
1539
|
+
const resolved = typeof next === "function" ? next(previous) : next;
|
|
1540
|
+
record.value = resolved;
|
|
1541
|
+
return resolved;
|
|
1542
|
+
});
|
|
1543
|
+
};
|
|
1544
|
+
|
|
1545
|
+
__mreactActiveCellRecords.set(cellKey, record);
|
|
1546
|
+
return stateCell;
|
|
1547
|
+
};`
|
|
1548
|
+
: "";
|
|
1549
|
+
const routeCellHydrationStart = routeUsesCells
|
|
1550
|
+
? ` const __mreactPreviousState = __mreactRouteStates.get(__mreactRouteId);
|
|
1551
|
+
const __mreactState = __mreactPreviousState?.marker === __mreactMarker &&
|
|
1552
|
+
__mreactPreviousState?.signature === __mreactRouteStateSignature
|
|
1553
|
+
? __mreactPreviousState
|
|
1554
|
+
: {
|
|
1555
|
+
cells: new Map(),
|
|
1556
|
+
marker: __mreactMarker,
|
|
1557
|
+
signature: __mreactRouteStateSignature,
|
|
1558
|
+
};
|
|
1559
|
+
__mreactDropMismatchedRouteState(__mreactPreviousState, __mreactState);
|
|
1560
|
+
__mreactRouteStates.set(__mreactRouteId, __mreactState);
|
|
1561
|
+
__mreactState.dispose?.();
|
|
1562
|
+
|
|
1563
|
+
__mreactState.dispose = __mreactRouteEffect(() => {
|
|
1564
|
+
const __mreactRouteEffectDisposers = new Set();
|
|
1565
|
+
__mreactActiveCellRecords = __mreactState.cells;
|
|
1566
|
+
__mreactActiveCellIndex = 0;
|
|
1567
|
+
__mreactRouteDisposers.set(__mreactRouteId, () => __mreactState.dispose?.());
|
|
1568
|
+
|
|
1569
|
+
try {
|
|
1570
|
+
`
|
|
1571
|
+
: "";
|
|
1572
|
+
const routeCellHydrationEnd = routeUsesCells
|
|
1573
|
+
? ` } finally {
|
|
1574
|
+
__mreactActiveCellRecords = undefined;
|
|
1575
|
+
__mreactActiveCellIndex = 0;
|
|
1576
|
+
}
|
|
1577
|
+
return () => {
|
|
1578
|
+
for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
|
|
1579
|
+
__mreactDispose();
|
|
1580
|
+
}
|
|
1581
|
+
__mreactRouteEffectDisposers.clear();
|
|
1582
|
+
};
|
|
1583
|
+
});
|
|
1584
|
+
`
|
|
1585
|
+
: "";
|
|
1586
|
+
const routeCellHydrationIndent = routeUsesCells ? " " : " ";
|
|
1587
|
+
const routeCleanupHydrationStart = routeUsesCleanupScope && !routeUsesCells
|
|
1588
|
+
? ` __mreactDisposeRoute(__mreactRouteId);
|
|
1589
|
+
const __mreactRouteEffectDisposers = new Set();
|
|
1590
|
+
__mreactRouteDisposers.set(__mreactRouteId, () => {
|
|
1591
|
+
for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
|
|
1592
|
+
__mreactDispose();
|
|
1593
|
+
}
|
|
1594
|
+
__mreactRouteEffectDisposers.clear();
|
|
1595
|
+
});
|
|
1596
|
+
`
|
|
1597
|
+
: "";
|
|
1598
|
+
const routeCellDropFunction = routeUsesCells
|
|
1599
|
+
? `
|
|
1600
|
+
function __mreactDropMismatchedRouteState(previousState, nextState) {
|
|
1601
|
+
if (previousState === undefined || previousState === nextState) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (previousState.signature !== nextState.signature && typeof console !== "undefined") {
|
|
1606
|
+
console.warn("mreact: dropping stale route state after route cell signature changed");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
`
|
|
1610
|
+
: "";
|
|
1611
|
+
const routeCleanupFunction = routeUsesCleanupScope
|
|
1612
|
+
? `
|
|
1613
|
+
function __mreactDisposeRoute(routeId) {
|
|
1614
|
+
const __mreactDispose = __mreactRouteDisposers.get(routeId);
|
|
1615
|
+
if (__mreactDispose === undefined) {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
__mreactRouteDisposers.delete(routeId);
|
|
1619
|
+
__mreactDispose();
|
|
1620
|
+
}
|
|
1621
|
+
`
|
|
1622
|
+
: "";
|
|
1623
|
+
const routeCleanupNavigationDispose = routeUsesCleanupScope
|
|
1624
|
+
? ` if (currentRouteId !== nextRouteId) {
|
|
1625
|
+
__mreactDisposeRoute(currentRouteId);
|
|
1626
|
+
}
|
|
1627
|
+
`
|
|
1628
|
+
: "";
|
|
1629
|
+
const routeNodeResolver = routeUsesCells
|
|
1630
|
+
? `
|
|
1631
|
+
function __mreactResolveRouteNode(value) {
|
|
1632
|
+
if (
|
|
1633
|
+
value !== null &&
|
|
1634
|
+
typeof value === "object" &&
|
|
1635
|
+
value.$$typeof === Symbol.for("modular.react.element") &&
|
|
1636
|
+
typeof value.type === "function"
|
|
1637
|
+
) {
|
|
1638
|
+
return __mreactResolveRouteNode(value.type(value.props ?? {}));
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (value !== null && typeof value === "object" && value.nodeType !== undefined) {
|
|
1642
|
+
return value;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const fragment = document.createDocumentFragment();
|
|
1646
|
+
if (value !== null && value !== undefined && value !== false && value !== true) {
|
|
1647
|
+
fragment.append(document.createTextNode(String(value)));
|
|
1648
|
+
}
|
|
1649
|
+
return fragment;
|
|
1650
|
+
}
|
|
1651
|
+
`
|
|
1652
|
+
: "";
|
|
1653
|
+
const routeComponentCallExpression = routeUsesCleanupScope
|
|
1654
|
+
? "__mreactWithCleanupScope((__mreactDispose) => __mreactRouteEffectDisposers.add(__mreactDispose), () => __mreactComponent(__mreactProps))"
|
|
1655
|
+
: "__mreactComponent(__mreactProps)";
|
|
1656
|
+
const routeNodeExpression = routeUsesCells
|
|
1657
|
+
? `__mreactResolveRouteNode(${routeComponentCallExpression})`
|
|
1658
|
+
: routeComponentCallExpression;
|
|
1659
|
+
const boundaryOnlyHydrationBlock = routeRequiresFullHydration
|
|
1660
|
+
? ""
|
|
1661
|
+
: `${routeCellHydrationIndent}if (!__mreactHasNonSerializableClientBoundaries(__mreactMarker) && __mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
|
|
1662
|
+
${routeCellHydrationIndent} __mreactMarker.setAttribute("data-mreact-hydrated", "true");
|
|
1663
|
+
${routeCellHydrationIndent} return;
|
|
1664
|
+
${routeCellHydrationIndent}}
|
|
1665
|
+
`;
|
|
1666
|
+
const routeComponentGuard = `${routeCellHydrationIndent}if (__mreactComponent === undefined) {
|
|
1667
|
+
${routeCellHydrationIndent} return;
|
|
1668
|
+
${routeCellHydrationIndent}}
|
|
1669
|
+
`;
|
|
1670
|
+
const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${emitCompatClientReferenceImportBlock(compatClientReferenceNames)}${clientReferenceImportBlock}${routeHydrationCode}
|
|
1671
|
+
|
|
1672
|
+
const __mreactRouteId = ${JSON.stringify(routeId)};
|
|
1673
|
+
const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
|
|
1674
|
+
const __mreactGlobal = globalThis;
|
|
1675
|
+
${navigationStateDeclaration}
|
|
1676
|
+
${routeCellStateDeclaration}
|
|
1677
|
+
${routeCleanupStateDeclaration}
|
|
1678
|
+
${routeCellHook}
|
|
1679
|
+
${clientReferenceRegistry}
|
|
1680
|
+
${routeNodeResolver}
|
|
1681
|
+
|
|
1682
|
+
export function __mreactHydrateRoute() {
|
|
1683
|
+
__mreactApplyOutOfOrderFragments(document);
|
|
1684
|
+
const __mreactMarker = document.querySelector(\`[data-mreact-route-id="\${__mreactRouteId}"]\`);
|
|
1685
|
+
const __mreactPropsElement = document.getElementById(\`mreact-props-\${__mreactRouteId}\`);
|
|
1686
|
+
const __mreactClientReferencesElement = document.getElementById(\`mreact-client-references-\${__mreactRouteId}\`);
|
|
1687
|
+
const __mreactProps = __mreactPropsElement?.textContent === undefined
|
|
1688
|
+
? {}
|
|
1689
|
+
: JSON.parse(__mreactPropsElement.textContent);
|
|
1690
|
+
const __mreactClientReferences = __mreactClientReferencesElement?.textContent === undefined
|
|
1691
|
+
? []
|
|
1692
|
+
: JSON.parse(__mreactClientReferencesElement.textContent);
|
|
1693
|
+
const __mreactClientReferenceManifests = __mreactGlobal.__mreactClientReferenceManifests ??= new Map();
|
|
1694
|
+
__mreactClientReferenceManifests.set(__mreactRouteId, __mreactClientReferences);
|
|
1695
|
+
const __mreactComponent = ${hydratedRouteComponentExpression};
|
|
1696
|
+
|
|
1697
|
+
if (__mreactMarker === null) {
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
${routeCellHydrationStart}${routeCleanupHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
|
|
1701
|
+
${routeCellHydrationIndent}__mreactResumeRoute(__mreactMarker, __mreactNode);
|
|
1702
|
+
${routeCellHydrationIndent}__mreactHydrateClientBoundaries(__mreactMarker, __mreactClientReferences, __mreactClientReferenceComponents);
|
|
1703
|
+
${routeCellHydrationIndent}__mreactMarker.setAttribute("data-mreact-hydrated", "true");
|
|
1704
|
+
${routeCellHydrationEnd}}
|
|
1705
|
+
${routeCellDropFunction}
|
|
1706
|
+
${routeCleanupFunction}
|
|
1707
|
+
|
|
1708
|
+
__mreactHydrateRoute();
|
|
1709
|
+
${clientNavigation ? "__mreactInstallNavigation();" : ""}
|
|
1710
|
+
|
|
1711
|
+
${
|
|
1712
|
+
clientNavigation
|
|
1713
|
+
? `export function __mreactNavigateToHtml(html, url, options = {}) {
|
|
1714
|
+
__mreactSaveCurrentHistoryState();
|
|
1715
|
+
const applied = __mreactApplyNavigationHtml(html, url);
|
|
1716
|
+
|
|
1717
|
+
if (!applied) {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
__mreactPushHistoryState(url);
|
|
1722
|
+
if (options.scroll !== "preserve") {
|
|
1723
|
+
__mreactScrollTo(0, 0);
|
|
1724
|
+
}
|
|
1725
|
+
__mreactResetNavigationFocus();
|
|
1726
|
+
__mreactAnnounceNavigation();
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
function __mreactResetNavigationFocus() {
|
|
1731
|
+
if (typeof document === "undefined") {
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const focusTarget =
|
|
1736
|
+
document.querySelector("[data-mreact-focus-target]") ??
|
|
1737
|
+
document.querySelector("main") ??
|
|
1738
|
+
document.querySelector("h1") ??
|
|
1739
|
+
document.body;
|
|
1740
|
+
|
|
1741
|
+
if (!(focusTarget instanceof HTMLElement)) {
|
|
1742
|
+
return;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const hadTabIndex = focusTarget.hasAttribute("tabindex");
|
|
1746
|
+
|
|
1747
|
+
if (!hadTabIndex) {
|
|
1748
|
+
focusTarget.setAttribute("tabindex", "-1");
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
try {
|
|
1752
|
+
focusTarget.focus({ preventScroll: true });
|
|
1753
|
+
} catch {
|
|
1754
|
+
focusTarget.focus();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (!hadTabIndex) {
|
|
1758
|
+
focusTarget.addEventListener(
|
|
1759
|
+
"blur",
|
|
1760
|
+
() => focusTarget.removeAttribute("tabindex"),
|
|
1761
|
+
{ once: true },
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function __mreactAnnounceNavigation() {
|
|
1767
|
+
if (typeof document === "undefined" || document.body === null) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
const announcement = __mreactRouteAnnouncementElement();
|
|
1772
|
+
announcement.textContent = \`Loaded \${document.title || "page"}\`;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
function __mreactRouteAnnouncementElement() {
|
|
1776
|
+
const existing = document.getElementById("mreact-route-announcement");
|
|
1777
|
+
|
|
1778
|
+
if (existing instanceof HTMLElement) {
|
|
1779
|
+
return existing;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const announcement = document.createElement("div");
|
|
1783
|
+
announcement.id = "mreact-route-announcement";
|
|
1784
|
+
announcement.setAttribute("role", "status");
|
|
1785
|
+
announcement.setAttribute("aria-live", "polite");
|
|
1786
|
+
announcement.setAttribute("aria-atomic", "true");
|
|
1787
|
+
announcement.setAttribute(
|
|
1788
|
+
"style",
|
|
1789
|
+
"position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden",
|
|
1790
|
+
);
|
|
1791
|
+
document.body.appendChild(announcement);
|
|
1792
|
+
return announcement;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
export async function __mreactPrefetch(url) {
|
|
1796
|
+
if (!__mreactCanPrefetch()) {
|
|
1797
|
+
return false;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
const href = __mreactNormalizeNavigationUrl(url);
|
|
1801
|
+
|
|
1802
|
+
if (href === undefined) {
|
|
1803
|
+
return false;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
const script = __mreactRouteScriptForNavigationUrl(href);
|
|
1807
|
+
|
|
1808
|
+
if (script === undefined) {
|
|
1809
|
+
return __mreactPrefetchNavigationHtml(href);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
return __mreactPrefetchRouteScript(script);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function __mreactPrefetchNavigationHtml(href) {
|
|
1816
|
+
if (__mreactNavigationState.cache.has(href)) {
|
|
1817
|
+
return true;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
return __mreactFetchNavigationHtml(href).then((html) => {
|
|
1821
|
+
if (typeof html !== "string") {
|
|
1822
|
+
return false;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
__mreactNavigationState.cache.set(href, html);
|
|
1826
|
+
return true;
|
|
1827
|
+
}).catch(() => false);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function __mreactPrefetchRouteScript(script) {
|
|
1831
|
+
if (typeof document === "undefined") {
|
|
1832
|
+
return false;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const href = __mreactNormalizeAssetUrl(script);
|
|
1836
|
+
|
|
1837
|
+
if (href === undefined) {
|
|
1838
|
+
return false;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
if (__mreactNavigationState.prefetchedScripts.has(href)) {
|
|
1842
|
+
return true;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
for (const link of Array.from(document.querySelectorAll('link[rel="modulepreload"][href]'))) {
|
|
1846
|
+
if (link.href === href) {
|
|
1847
|
+
__mreactNavigationState.prefetchedScripts.add(href);
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const link = document.createElement("link");
|
|
1853
|
+
link.rel = "modulepreload";
|
|
1854
|
+
link.href = href;
|
|
1855
|
+
document.head.appendChild(link);
|
|
1856
|
+
__mreactNavigationState.prefetchedScripts.add(href);
|
|
1857
|
+
return true;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
export async function __mreactNavigate(url, options = {}) {
|
|
1861
|
+
const href = __mreactNormalizeNavigationUrl(url);
|
|
1862
|
+
|
|
1863
|
+
if (href === undefined) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
__mreactSetNavigationState(__mreactPendingNavigationState(href, options.type ?? "push"));
|
|
1868
|
+
|
|
1869
|
+
try {
|
|
1870
|
+
const cachedHtml = __mreactNavigationState.cache.get(href);
|
|
1871
|
+
const html = cachedHtml ?? await __mreactFetchNavigationHtml(href);
|
|
1872
|
+
|
|
1873
|
+
if (typeof html !== "string") {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
__mreactNavigationState.cache.set(href, html);
|
|
1878
|
+
return await __mreactApplyNavigationHtmlWithOptionalTransition(html, href, options);
|
|
1879
|
+
} finally {
|
|
1880
|
+
__mreactSetNavigationState(__mreactIdleNavigationState());
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
export function __mreactGetNavigationState() {
|
|
1885
|
+
return __mreactNavigationStateSnapshot(__mreactNavigationState.current);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
function __mreactPendingNavigationState(to, type) {
|
|
1889
|
+
return {
|
|
1890
|
+
from: typeof location === "undefined" ? null : location.href,
|
|
1891
|
+
pending: true,
|
|
1892
|
+
to,
|
|
1893
|
+
type: __mreactNavigationType(type),
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function __mreactIdleNavigationState() {
|
|
1898
|
+
return {
|
|
1899
|
+
from: null,
|
|
1900
|
+
pending: false,
|
|
1901
|
+
to: null,
|
|
1902
|
+
type: null,
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function __mreactSetNavigationState(state) {
|
|
1907
|
+
const snapshot = __mreactNavigationStateSnapshot(state);
|
|
1908
|
+
__mreactNavigationState.current = snapshot;
|
|
1909
|
+
__mreactApplyNavigationStateAttributes(snapshot);
|
|
1910
|
+
__mreactDispatchNavigationStateChange(snapshot);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function __mreactNavigationStateSnapshot(state) {
|
|
1914
|
+
return {
|
|
1915
|
+
from: typeof state?.from === "string" ? state.from : null,
|
|
1916
|
+
pending: state?.pending === true,
|
|
1917
|
+
to: typeof state?.to === "string" ? state.to : null,
|
|
1918
|
+
type: __mreactNavigationType(state?.type),
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function __mreactNavigationType(type) {
|
|
1923
|
+
return type === "push" || type === "replace" || type === "pop" || type === "refresh"
|
|
1924
|
+
? type
|
|
1925
|
+
: null;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function __mreactApplyNavigationStateAttributes(state) {
|
|
1929
|
+
if (typeof document === "undefined") {
|
|
1930
|
+
return;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const root = document.documentElement;
|
|
1934
|
+
|
|
1935
|
+
if (state.pending !== true) {
|
|
1936
|
+
root.removeAttribute("data-mreact-navigation-pending");
|
|
1937
|
+
root.removeAttribute("data-mreact-navigation-from");
|
|
1938
|
+
root.removeAttribute("data-mreact-navigation-to");
|
|
1939
|
+
root.removeAttribute("data-mreact-navigation-type");
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
root.setAttribute("data-mreact-navigation-pending", "true");
|
|
1944
|
+
|
|
1945
|
+
if (state.from === null) {
|
|
1946
|
+
root.removeAttribute("data-mreact-navigation-from");
|
|
1947
|
+
} else {
|
|
1948
|
+
root.setAttribute("data-mreact-navigation-from", state.from);
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
if (state.to === null) {
|
|
1952
|
+
root.removeAttribute("data-mreact-navigation-to");
|
|
1953
|
+
} else {
|
|
1954
|
+
root.setAttribute("data-mreact-navigation-to", state.to);
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
if (state.type === null) {
|
|
1958
|
+
root.removeAttribute("data-mreact-navigation-type");
|
|
1959
|
+
} else {
|
|
1960
|
+
root.setAttribute("data-mreact-navigation-type", state.type);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function __mreactDispatchNavigationStateChange(state) {
|
|
1965
|
+
if (typeof window === "undefined" || typeof CustomEvent !== "function") {
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
window.dispatchEvent(new CustomEvent("mreact:navigation-state-change", {
|
|
1970
|
+
detail: state,
|
|
1971
|
+
}));
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
async function __mreactApplyNavigationHtmlWithOptionalTransition(html, href, options) {
|
|
1975
|
+
if (!__mreactViewTransitionsAllowed(options.transition)) {
|
|
1976
|
+
return __mreactNavigateToHtml(html, href, options);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
let navigated = false;
|
|
1980
|
+
const transition = document.startViewTransition(() => {
|
|
1981
|
+
navigated = __mreactNavigateToHtml(html, href, options);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1984
|
+
try {
|
|
1985
|
+
await transition.updateCallbackDone;
|
|
1986
|
+
} catch {
|
|
1987
|
+
return navigated;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
return navigated;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
function __mreactViewTransitionsAllowed(transition) {
|
|
1994
|
+
if (
|
|
1995
|
+
transition !== "auto" ||
|
|
1996
|
+
typeof document === "undefined" ||
|
|
1997
|
+
typeof document.startViewTransition !== "function"
|
|
1998
|
+
) {
|
|
1999
|
+
return false;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
2003
|
+
return true;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
try {
|
|
2007
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
2008
|
+
} catch {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
export function __mreactInvalidateNavigationCache(path) {
|
|
2014
|
+
const normalizedPath = __mreactNormalizeNavigationPath(path);
|
|
2015
|
+
|
|
2016
|
+
if (normalizedPath === undefined) {
|
|
2017
|
+
return;
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
for (const href of Array.from(__mreactNavigationState.cache.keys())) {
|
|
2021
|
+
if (__mreactNormalizeNavigationPath(href) === normalizedPath) {
|
|
2022
|
+
__mreactNavigationState.cache.delete(href);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
function __mreactFetchNavigationHtml(href) {
|
|
2028
|
+
return fetch(href, {
|
|
2029
|
+
headers: { "x-mreact-navigation": "1" },
|
|
2030
|
+
}).then((response) => {
|
|
2031
|
+
__mreactApplyRevalidationHeader(response);
|
|
2032
|
+
if (__mreactNavigationResponseRequiresDocumentReload(response)) {
|
|
2033
|
+
return undefined;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
return response.text();
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
function __mreactNavigationResponseRequiresDocumentReload(response) {
|
|
2041
|
+
return response.status === 204 || response.headers.get("x-mreact-navigation") === "reload";
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
function __mreactApplyRevalidationHeader(response) {
|
|
2045
|
+
const header = response.headers.get("x-mreact-revalidate");
|
|
2046
|
+
|
|
2047
|
+
if (header === null || header.trim() === "") {
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
for (const path of header.split(",")) {
|
|
2052
|
+
__mreactInvalidateNavigationCache(path.trim());
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function __mreactNormalizeNavigationPath(path) {
|
|
2057
|
+
if (typeof location === "undefined") {
|
|
2058
|
+
return typeof path === "string" && path.length > 0 ? path : undefined;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
try {
|
|
2062
|
+
const url = new URL(path, location.href);
|
|
2063
|
+
const pathname = url.pathname.replace(/\\/+$/, "");
|
|
2064
|
+
|
|
2065
|
+
return pathname === "" ? "/" : pathname;
|
|
2066
|
+
} catch {
|
|
2067
|
+
return undefined;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
export function __mreactRestoreHistoryState(state) {
|
|
2072
|
+
if (state === null || state === undefined || state.__mreact !== true || typeof state.html !== "string") {
|
|
2073
|
+
return false;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const applied = __mreactApplyNavigationHtml(state.html, state.url);
|
|
2077
|
+
|
|
2078
|
+
if (!applied) {
|
|
2079
|
+
return false;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
__mreactScrollTo(Number(state.scrollX ?? 0), Number(state.scrollY ?? 0));
|
|
2083
|
+
return true;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function __mreactApplyNavigationHtml(html, url) {
|
|
2087
|
+
const template = document.createElement("template");
|
|
2088
|
+
template.innerHTML = html.replace(/^\\s*<!doctype html>/i, "");
|
|
2089
|
+
__mreactApplyOutOfOrderFragments(template.content);
|
|
2090
|
+
const nextMarker = template.content.querySelector("[data-mreact-route-id]");
|
|
2091
|
+
const currentMarker = document.querySelector("[data-mreact-route-id]");
|
|
2092
|
+
|
|
2093
|
+
if (nextMarker === null || currentMarker === null) {
|
|
2094
|
+
return false;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
const currentRouteId = currentMarker.getAttribute("data-mreact-route-id");
|
|
2098
|
+
const nextRouteId = nextMarker.getAttribute("data-mreact-route-id");
|
|
2099
|
+
|
|
2100
|
+
__mreactSyncHeadMetadata(template.content, html);
|
|
2101
|
+
__mreactResumeNode(currentMarker, nextMarker);
|
|
2102
|
+
${routeCleanupNavigationDispose} __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
|
|
2103
|
+
|
|
2104
|
+
const script = template.content.querySelector('script[type="module"][src]')?.getAttribute("src");
|
|
2105
|
+
if (script !== null && script !== undefined) {
|
|
2106
|
+
void import(/* @vite-ignore */ script).then((module) => module.__mreactHydrateRoute?.());
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
__mreactApplyOutOfOrderFragments(document);
|
|
2110
|
+
__mreactObserveViewportPrefetchAnchors(document);
|
|
2111
|
+
|
|
2112
|
+
return true;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
function __mreactSyncHeadMetadata(root, html) {
|
|
2116
|
+
__mreactSyncHtmlLang(root, html);
|
|
2117
|
+
|
|
2118
|
+
const nextHead = root.querySelector("head");
|
|
2119
|
+
|
|
2120
|
+
if ((nextHead === null && !/<head(?:\\s[^>]*)?>/i.test(html)) || document.head === null) {
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
const metadataRoot = nextHead ?? root;
|
|
2125
|
+
const selector = __mreactManagedHeadMetadataSelector();
|
|
2126
|
+
|
|
2127
|
+
for (const element of Array.from(document.head.querySelectorAll(selector))) {
|
|
2128
|
+
element.remove();
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
for (const element of Array.from(metadataRoot.querySelectorAll(selector))) {
|
|
2132
|
+
document.head.appendChild(element);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function __mreactSyncHtmlLang(root, html) {
|
|
2137
|
+
const nextHtml = root.querySelector("html");
|
|
2138
|
+
const nextLang = nextHtml?.getAttribute("lang") ?? __mreactHtmlLangFromSource(html);
|
|
2139
|
+
|
|
2140
|
+
if (nextLang === null) {
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
if (document.documentElement.lang !== nextLang) {
|
|
2145
|
+
document.documentElement.lang = nextLang;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function __mreactHtmlLangFromSource(html) {
|
|
2150
|
+
const match = /<html\\b[^>]*\\slang=(?:"([^"]*)"|'([^']*)'|([^\\s>]+))/i.exec(html);
|
|
2151
|
+
|
|
2152
|
+
return match === null ? null : match[1] ?? match[2] ?? match[3] ?? null;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function __mreactManagedHeadMetadataSelector() {
|
|
2156
|
+
return [
|
|
2157
|
+
"title",
|
|
2158
|
+
'meta[name="description"]',
|
|
2159
|
+
'link[rel="canonical"]',
|
|
2160
|
+
'meta[property="og:title"]',
|
|
2161
|
+
'meta[property="og:description"]',
|
|
2162
|
+
'meta[property="og:image"]',
|
|
2163
|
+
'link[rel="icon"]',
|
|
2164
|
+
'link[rel="apple-touch-icon"]',
|
|
2165
|
+
'meta[name="robots"]',
|
|
2166
|
+
'meta[name="theme-color"]',
|
|
2167
|
+
'meta[name="viewport"]',
|
|
2168
|
+
].join(",");
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function __mreactSyncRouteDataScripts(root, currentRouteId, nextRouteId) {
|
|
2172
|
+
const managedIds = __mreactRouteDataScriptIds(currentRouteId, nextRouteId);
|
|
2173
|
+
|
|
2174
|
+
if (managedIds.size === 0) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
for (const element of Array.from(document.querySelectorAll(__mreactRouteDataScriptSelector()))) {
|
|
2179
|
+
if (managedIds.has(element.id)) {
|
|
2180
|
+
element.remove();
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
for (const element of Array.from(root.querySelectorAll(__mreactRouteDataScriptSelector()))) {
|
|
2185
|
+
if (managedIds.has(element.id)) {
|
|
2186
|
+
document.body.appendChild(element);
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function __mreactRouteDataScriptIds(...routeIds) {
|
|
2192
|
+
const ids = new Set();
|
|
2193
|
+
|
|
2194
|
+
for (const routeId of routeIds) {
|
|
2195
|
+
if (typeof routeId !== "string" || routeId === "") {
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
ids.add(\`mreact-props-\${routeId}\`);
|
|
2200
|
+
ids.add(\`mreact-client-references-\${routeId}\`);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
return ids;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
function __mreactRouteDataScriptSelector() {
|
|
2207
|
+
return 'script[type="application/json"][id^="mreact-props-"], script[type="application/json"][id^="mreact-client-references-"]';
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function __mreactCurrentHistoryState(url) {
|
|
2211
|
+
return {
|
|
2212
|
+
__mreact: true,
|
|
2213
|
+
html: document.body.innerHTML,
|
|
2214
|
+
scrollX: Number(globalThis.scrollX ?? 0),
|
|
2215
|
+
scrollY: Number(globalThis.scrollY ?? 0),
|
|
2216
|
+
url,
|
|
2217
|
+
};
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
function __mreactPushHistoryState(url) {
|
|
2221
|
+
if (typeof history === "undefined" || url === undefined) {
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
try {
|
|
2226
|
+
history.pushState(__mreactCurrentHistoryState(url), "", url);
|
|
2227
|
+
} catch {
|
|
2228
|
+
// Ignore invalid URLs in non-browser test environments.
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function __mreactSaveCurrentHistoryState() {
|
|
2233
|
+
if (typeof history === "undefined" || typeof location === "undefined") {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
try {
|
|
2238
|
+
history.replaceState(__mreactCurrentHistoryState(location.href), "", location.href);
|
|
2239
|
+
} catch {
|
|
2240
|
+
// Ignore invalid URLs in non-browser test environments.
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
function __mreactEnableManualScrollRestoration() {
|
|
2245
|
+
if (typeof history === "undefined" || !("scrollRestoration" in history)) {
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
try {
|
|
2250
|
+
history.scrollRestoration = "manual";
|
|
2251
|
+
} catch {
|
|
2252
|
+
// Ignore read-only history implementations in non-browser runtimes.
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
function __mreactNormalizeNavigationUrl(url) {
|
|
2257
|
+
if (typeof location === "undefined") {
|
|
2258
|
+
return typeof url === "string" ? url : undefined;
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
try {
|
|
2262
|
+
return new URL(url, location.href).href;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return undefined;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
function __mreactNormalizeAssetUrl(url) {
|
|
2269
|
+
if (typeof location === "undefined") {
|
|
2270
|
+
return typeof url === "string" ? url : undefined;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
try {
|
|
2274
|
+
return new URL(url, location.href).href;
|
|
2275
|
+
} catch {
|
|
2276
|
+
return undefined;
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function __mreactRouteScriptForNavigationUrl(url) {
|
|
2281
|
+
const href = __mreactNormalizeNavigationUrl(url);
|
|
2282
|
+
|
|
2283
|
+
if (href === undefined || typeof location === "undefined") {
|
|
2284
|
+
return undefined;
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
let nextUrl;
|
|
2288
|
+
|
|
2289
|
+
try {
|
|
2290
|
+
nextUrl = new URL(href, location.href);
|
|
2291
|
+
} catch {
|
|
2292
|
+
return undefined;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (nextUrl.origin !== location.origin) {
|
|
2296
|
+
return undefined;
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
for (const route of __mreactClientRoutePrefetchManifest()) {
|
|
2300
|
+
if (__mreactRoutePathMatches(route.path, nextUrl.pathname)) {
|
|
2301
|
+
return route.script;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
return undefined;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
function __mreactClientRoutePrefetchManifest() {
|
|
2309
|
+
const element = typeof document === "undefined"
|
|
2310
|
+
? null
|
|
2311
|
+
: document.getElementById("mreact-route-prefetch-manifest");
|
|
2312
|
+
const text = element?.textContent ?? "";
|
|
2313
|
+
|
|
2314
|
+
if (
|
|
2315
|
+
__mreactNavigationState.routePrefetchManifest !== undefined &&
|
|
2316
|
+
__mreactNavigationState.routePrefetchManifestText === text
|
|
2317
|
+
) {
|
|
2318
|
+
return __mreactNavigationState.routePrefetchManifest;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
__mreactNavigationState.routePrefetchManifestText = text;
|
|
2322
|
+
|
|
2323
|
+
if (element === null) {
|
|
2324
|
+
__mreactNavigationState.routePrefetchManifest = [];
|
|
2325
|
+
return __mreactNavigationState.routePrefetchManifest;
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
try {
|
|
2329
|
+
const parsed = JSON.parse(text);
|
|
2330
|
+
__mreactNavigationState.routePrefetchManifest = Array.isArray(parsed)
|
|
2331
|
+
? parsed.filter((route) =>
|
|
2332
|
+
route !== null &&
|
|
2333
|
+
typeof route === "object" &&
|
|
2334
|
+
typeof route.path === "string" &&
|
|
2335
|
+
typeof route.script === "string"
|
|
2336
|
+
)
|
|
2337
|
+
: [];
|
|
2338
|
+
} catch {
|
|
2339
|
+
__mreactNavigationState.routePrefetchManifest = [];
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
return __mreactNavigationState.routePrefetchManifest;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function __mreactRoutePathMatches(routePath, pathname) {
|
|
2346
|
+
const routeSegments = __mreactNormalizeRoutePath(routePath);
|
|
2347
|
+
const pathSegments = __mreactNormalizeRoutePath(pathname);
|
|
2348
|
+
|
|
2349
|
+
if (routeSegments.length === 0) {
|
|
2350
|
+
return pathSegments.length === 0;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
for (const [index, segment] of routeSegments.entries()) {
|
|
2354
|
+
const value = pathSegments[index];
|
|
2355
|
+
|
|
2356
|
+
if (segment.startsWith(":...")) {
|
|
2357
|
+
return pathSegments.length >= index + 1;
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
if (value === undefined) {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if (!segment.startsWith(":") && segment !== value) {
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
return routeSegments.length === pathSegments.length;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
function __mreactNormalizeRoutePath(path) {
|
|
2373
|
+
const normalized = path.length > 1 ? path.replace(/\\/+$/, "") : path;
|
|
2374
|
+
return normalized === "/" || normalized === "" ? [] : normalized.replace(/^\\/+/, "").split("/");
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function __mreactCanPrefetch() {
|
|
2378
|
+
if (typeof navigator === "undefined") {
|
|
2379
|
+
return true;
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
const connection = navigator.connection ?? navigator.mozConnection ?? navigator.webkitConnection;
|
|
2383
|
+
const effectiveType = typeof connection?.effectiveType === "string"
|
|
2384
|
+
? connection.effectiveType.toLowerCase()
|
|
2385
|
+
: "";
|
|
2386
|
+
|
|
2387
|
+
return connection?.saveData !== true && effectiveType !== "slow-2g" && effectiveType !== "2g";
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function __mreactScrollTo(x, y) {
|
|
2391
|
+
if (typeof scrollTo === "function") {
|
|
2392
|
+
scrollTo(x, y);
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function __mreactIsHashOnlyNavigation(nextUrl) {
|
|
2397
|
+
if (typeof location === "undefined") {
|
|
2398
|
+
return false;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
return nextUrl.origin === location.origin &&
|
|
2402
|
+
nextUrl.pathname === location.pathname &&
|
|
2403
|
+
nextUrl.search === location.search &&
|
|
2404
|
+
nextUrl.hash !== "" &&
|
|
2405
|
+
nextUrl.hash !== location.hash;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function __mreactInstallNavigation() {
|
|
2409
|
+
if (__mreactNavigationState.installed || typeof document === "undefined") {
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
__mreactNavigationState.installed = true;
|
|
2414
|
+
__mreactEnableManualScrollRestoration();
|
|
2415
|
+
__mreactSaveCurrentHistoryState();
|
|
2416
|
+
addEventListener("popstate", (event) => {
|
|
2417
|
+
__mreactSaveCurrentHistoryState();
|
|
2418
|
+
if (!__mreactRestoreHistoryState(event.state)) {
|
|
2419
|
+
location.reload();
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
document.addEventListener("pointerenter", (event) => {
|
|
2423
|
+
const anchor = __mreactAnchorFromEvent(event);
|
|
2424
|
+
|
|
2425
|
+
if (anchor !== null && __mreactAnchorPrefetchMode(anchor) === "intent") {
|
|
2426
|
+
void __mreactPrefetch(anchor.href);
|
|
2427
|
+
}
|
|
2428
|
+
}, true);
|
|
2429
|
+
document.addEventListener("pointerdown", (event) => {
|
|
2430
|
+
const anchor = __mreactAnchorFromEvent(event);
|
|
2431
|
+
|
|
2432
|
+
if (anchor !== null && __mreactAnchorPrefetchMode(anchor) === "intent") {
|
|
2433
|
+
void __mreactPrefetch(anchor.href);
|
|
2434
|
+
}
|
|
2435
|
+
}, true);
|
|
2436
|
+
document.addEventListener("focusin", (event) => {
|
|
2437
|
+
const anchor = __mreactAnchorFromEvent(event);
|
|
2438
|
+
|
|
2439
|
+
if (anchor !== null && __mreactAnchorPrefetchMode(anchor) === "intent") {
|
|
2440
|
+
void __mreactPrefetch(anchor.href);
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
__mreactObserveViewportPrefetchAnchors(document);
|
|
2444
|
+
document.addEventListener("click", (event) => {
|
|
2445
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
const anchor = __mreactAnchorFromEvent(event);
|
|
2450
|
+
|
|
2451
|
+
if (anchor === null) {
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
if (anchor.dataset.mreactReload === "true") {
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
const nextUrl = new URL(anchor.href, location.href);
|
|
2460
|
+
|
|
2461
|
+
if (nextUrl.origin !== location.origin) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
if (__mreactIsHashOnlyNavigation(nextUrl)) {
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
event.preventDefault();
|
|
2470
|
+
void __mreactNavigate(nextUrl.href, {
|
|
2471
|
+
scroll: __mreactAnchorScrollMode(anchor),
|
|
2472
|
+
transition: __mreactAnchorTransitionMode(anchor),
|
|
2473
|
+
})
|
|
2474
|
+
.then((navigated) => {
|
|
2475
|
+
if (!navigated) {
|
|
2476
|
+
location.href = nextUrl.href;
|
|
2477
|
+
}
|
|
2478
|
+
}).catch(() => {
|
|
2479
|
+
location.href = nextUrl.href;
|
|
2480
|
+
});
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
function __mreactAnchorFromEvent(event) {
|
|
2485
|
+
const target = event.target;
|
|
2486
|
+
const anchor = target instanceof Element ? target.closest("a[href]") : null;
|
|
2487
|
+
|
|
2488
|
+
if (!(anchor instanceof HTMLAnchorElement) || anchor.target !== "" || anchor.hasAttribute("download")) {
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return anchor;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function __mreactAnchorPrefetchMode(anchor) {
|
|
2496
|
+
const value = anchor.dataset.mreactPrefetch;
|
|
2497
|
+
|
|
2498
|
+
if (value === "false" || value === "none") {
|
|
2499
|
+
return "none";
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
return value === "viewport" ? "viewport" : "intent";
|
|
2503
|
+
}
|
|
2504
|
+
|
|
2505
|
+
function __mreactAnchorScrollMode(anchor) {
|
|
2506
|
+
return anchor.dataset.mreactScroll === "preserve" ? "preserve" : "top";
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
function __mreactAnchorTransitionMode(anchor) {
|
|
2510
|
+
return anchor.dataset.mreactTransition === "auto" ? "auto" : "none";
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function __mreactObserveViewportPrefetchAnchors(root) {
|
|
2514
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (__mreactNavigationState.viewportObserver === undefined) {
|
|
2519
|
+
__mreactNavigationState.viewportObserver = new IntersectionObserver((entries) => {
|
|
2520
|
+
for (const entry of entries) {
|
|
2521
|
+
if (!entry.isIntersecting || !(entry.target instanceof HTMLAnchorElement)) {
|
|
2522
|
+
continue;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
__mreactNavigationState.viewportObserver?.unobserve(entry.target);
|
|
2526
|
+
void __mreactPrefetch(entry.target.href);
|
|
2527
|
+
}
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
for (const anchor of Array.from(root.querySelectorAll('a[href][data-mreact-prefetch="viewport"]'))) {
|
|
2532
|
+
if (!(anchor instanceof HTMLAnchorElement) || __mreactNavigationState.viewportAnchors.has(anchor)) {
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
__mreactNavigationState.viewportAnchors.add(anchor);
|
|
2537
|
+
__mreactNavigationState.viewportObserver.observe(anchor);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
`
|
|
2541
|
+
: ""
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2544
|
+
function __mreactApplyOutOfOrderFragments(root) {
|
|
2545
|
+
const fragments = Array.from(root.querySelectorAll("template[data-mreact-oob-fragment]"));
|
|
2546
|
+
|
|
2547
|
+
for (const fragment of fragments) {
|
|
2548
|
+
const id = fragment.getAttribute("data-mreact-oob-fragment");
|
|
2549
|
+
|
|
2550
|
+
if (id === null) {
|
|
2551
|
+
continue;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
const placeholder = Array.from(root.querySelectorAll("[data-mreact-oob-placeholder]"))
|
|
2555
|
+
.find((candidate) => candidate.getAttribute("data-mreact-oob-placeholder") === id);
|
|
2556
|
+
|
|
2557
|
+
if (placeholder === undefined) {
|
|
2558
|
+
continue;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
placeholder.replaceWith(fragment.content.cloneNode(true));
|
|
2562
|
+
fragment.remove();
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
function __mreactHydrateClientBoundaries(marker, references, components) {
|
|
2567
|
+
if (components.size === 0 && (!Array.isArray(references) || references.length === 0)) {
|
|
2568
|
+
return false;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
const placeholders = Array.from(marker.querySelectorAll("template[data-mreact-client-boundary]"));
|
|
2572
|
+
|
|
2573
|
+
if (placeholders.length === 0) {
|
|
2574
|
+
return false;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
for (const placeholder of placeholders) {
|
|
2578
|
+
const name = placeholder.getAttribute("data-mreact-client-boundary");
|
|
2579
|
+
const entry = name === null ? undefined : components.get(name);
|
|
2580
|
+
const component = typeof entry === "function" ? entry : entry?.component;
|
|
2581
|
+
const compat = entry?.compat === true;
|
|
2582
|
+
|
|
2583
|
+
if (typeof component !== "function") {
|
|
2584
|
+
return false;
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
const propsElement = __mreactClientBoundaryPropsElement(placeholder, name);
|
|
2588
|
+
const props = propsElement?.textContent === undefined || propsElement.textContent === ""
|
|
2589
|
+
? {}
|
|
2590
|
+
: JSON.parse(propsElement.textContent);
|
|
2591
|
+
|
|
2592
|
+
if (compat) {
|
|
2593
|
+
const container = document.createElement("span");
|
|
2594
|
+
container.setAttribute("data-mreact-compat-boundary", name ?? "");
|
|
2595
|
+
container.style.display = "contents";
|
|
2596
|
+
placeholder.replaceWith(container);
|
|
2597
|
+
__mreactCompatCreateRoot(container).render(__mreactCompatCreateElement(component, props));
|
|
2598
|
+
propsElement?.remove();
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
const node = component(props);
|
|
2603
|
+
|
|
2604
|
+
placeholder.replaceWith(node);
|
|
2605
|
+
propsElement?.remove();
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
return true;
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
function __mreactHasNonSerializableClientBoundaries(marker) {
|
|
2612
|
+
return marker.querySelector(
|
|
2613
|
+
'template[data-mreact-client-boundary][data-mreact-client-boundary-nonserializable="true"]',
|
|
2614
|
+
) !== null;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
function __mreactClientBoundaryPropsElement(placeholder, name) {
|
|
2618
|
+
let next = placeholder.nextSibling;
|
|
2619
|
+
|
|
2620
|
+
while (next !== null) {
|
|
2621
|
+
if (
|
|
2622
|
+
next.nodeType === Node.ELEMENT_NODE &&
|
|
2623
|
+
next.tagName === "SCRIPT" &&
|
|
2624
|
+
next.getAttribute("type") === "application/json" &&
|
|
2625
|
+
next.getAttribute("data-mreact-client-boundary-props") === name
|
|
2626
|
+
) {
|
|
2627
|
+
return next;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
if (next.nodeType === Node.ELEMENT_NODE) {
|
|
2631
|
+
return undefined;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
next = next.nextSibling;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
return undefined;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function __mreactResumeRoute(marker, nextNode) {
|
|
2641
|
+
if (nextNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
2642
|
+
marker.replaceChildren(nextNode);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
const current = __mreactRouteResumeTarget(marker, nextNode);
|
|
2647
|
+
|
|
2648
|
+
if (current === null) {
|
|
2649
|
+
marker.appendChild(nextNode);
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
__mreactResumeNode(current, nextNode);
|
|
2654
|
+
|
|
2655
|
+
if (current.parentNode !== marker) {
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
while (marker.childNodes.length > 1) {
|
|
2660
|
+
marker.lastChild?.remove();
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
function __mreactRouteResumeTarget(marker, nextNode) {
|
|
2665
|
+
const current = marker.firstChild;
|
|
2666
|
+
|
|
2667
|
+
if (
|
|
2668
|
+
current === null ||
|
|
2669
|
+
current.nodeType !== Node.ELEMENT_NODE ||
|
|
2670
|
+
nextNode.nodeType !== Node.ELEMENT_NODE ||
|
|
2671
|
+
current.tagName === nextNode.tagName ||
|
|
2672
|
+
!current.hasAttribute("data-mreact-layout-boundary")
|
|
2673
|
+
) {
|
|
2674
|
+
return current;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
return __mreactFindLayoutPageTarget(current, nextNode) ?? current;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
function __mreactFindLayoutPageTarget(current, nextNode) {
|
|
2681
|
+
for (const child of Array.from(current.childNodes)) {
|
|
2682
|
+
if (child.nodeType !== Node.ELEMENT_NODE) {
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
if (
|
|
2687
|
+
child.tagName === nextNode.tagName &&
|
|
2688
|
+
!child.hasAttribute("data-mreact-layout-boundary") &&
|
|
2689
|
+
!child.hasAttribute("data-mreact-template-boundary")
|
|
2690
|
+
) {
|
|
2691
|
+
return child;
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
if (child.hasAttribute("data-mreact-layout-boundary")) {
|
|
2695
|
+
const nested = __mreactFindLayoutPageTarget(child, nextNode);
|
|
2696
|
+
|
|
2697
|
+
if (nested !== null) {
|
|
2698
|
+
return nested;
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
function __mreactResumeNode(current, next) {
|
|
2707
|
+
if (
|
|
2708
|
+
next.nodeType === Node.COMMENT_NODE &&
|
|
2709
|
+
next.nodeValue === "mreact-async-boundary"
|
|
2710
|
+
) {
|
|
2711
|
+
// Server stream emits the resolved <Await> content; preserve the existing
|
|
2712
|
+
// DOM instead of replacing it with the client placeholder comment.
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
if (__mreactShouldReplaceNode(current, next)) {
|
|
2717
|
+
current.replaceWith(next);
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
if (current.nodeType === Node.TEXT_NODE && next.nodeType === Node.TEXT_NODE) {
|
|
2722
|
+
if (current.nodeValue !== next.nodeValue) {
|
|
2723
|
+
current.nodeValue = next.nodeValue;
|
|
2724
|
+
}
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
if (current.nodeType !== Node.ELEMENT_NODE || next.nodeType !== Node.ELEMENT_NODE) {
|
|
2729
|
+
current.replaceWith(next);
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
__mreactSyncEventBindings(current, next);
|
|
2734
|
+
__mreactSyncAttributes(current, next);
|
|
2735
|
+
__mreactSyncPropBindings(current, next);
|
|
2736
|
+
__mreactResumeChildren(current, next);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function __mreactShouldReplaceNode(current, next) {
|
|
2740
|
+
if (
|
|
2741
|
+
next.nodeType === Node.ELEMENT_NODE &&
|
|
2742
|
+
next.hasAttribute("data-mreact-template-boundary")
|
|
2743
|
+
) {
|
|
2744
|
+
return true;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
if (current.nodeType !== next.nodeType) {
|
|
2748
|
+
return true;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
return current.nodeType === Node.ELEMENT_NODE &&
|
|
2752
|
+
current.tagName !== next.tagName;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
function __mreactSyncEventBindings(current, next) {
|
|
2756
|
+
const previousDisposers = current.__mreactEventDisposers;
|
|
2757
|
+
|
|
2758
|
+
if (Array.isArray(previousDisposers)) {
|
|
2759
|
+
for (const dispose of previousDisposers) {
|
|
2760
|
+
dispose();
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
const bindings = next.__mreactEventBindings;
|
|
2765
|
+
|
|
2766
|
+
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
2767
|
+
current.__mreactEventDisposers = [];
|
|
2768
|
+
current.__mreactHasEvents = false;
|
|
2769
|
+
return;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
const disposers = [];
|
|
2773
|
+
|
|
2774
|
+
for (const binding of bindings) {
|
|
2775
|
+
if (binding.delegated === true && __mreactIsDelegatedEventType(binding.type)) {
|
|
2776
|
+
disposers.push(__mreactAddDelegatedEventListener(current, binding.type, binding.listener));
|
|
2777
|
+
} else {
|
|
2778
|
+
current.addEventListener(binding.type, binding.listener);
|
|
2779
|
+
disposers.push(() => current.removeEventListener(binding.type, binding.listener));
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
current.__mreactEventDisposers = disposers;
|
|
2784
|
+
current.__mreactHasEvents = true;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
function __mreactIsDelegatedEventType(type) {
|
|
2788
|
+
return type === "change" ||
|
|
2789
|
+
type === "click" ||
|
|
2790
|
+
type === "input" ||
|
|
2791
|
+
type === "keydown" ||
|
|
2792
|
+
type === "keyup" ||
|
|
2793
|
+
type === "pointerdown" ||
|
|
2794
|
+
type === "pointermove" ||
|
|
2795
|
+
type === "pointerup" ||
|
|
2796
|
+
type === "submit";
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
function __mreactDelegatedEventState() {
|
|
2800
|
+
globalThis.__mreactDelegatedEventState ??= {
|
|
2801
|
+
elements: new WeakMap(),
|
|
2802
|
+
roots: new WeakMap(),
|
|
2803
|
+
};
|
|
2804
|
+
return globalThis.__mreactDelegatedEventState;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
function __mreactAddDelegatedEventListener(element, type, listener) {
|
|
2808
|
+
const root = element.ownerDocument;
|
|
2809
|
+
const state = __mreactDelegatedEventState();
|
|
2810
|
+
let listenersByType = state.elements.get(element);
|
|
2811
|
+
|
|
2812
|
+
if (listenersByType === undefined) {
|
|
2813
|
+
listenersByType = new Map();
|
|
2814
|
+
state.elements.set(element, listenersByType);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
let listeners = listenersByType.get(type);
|
|
2818
|
+
|
|
2819
|
+
if (listeners === undefined) {
|
|
2820
|
+
listeners = [];
|
|
2821
|
+
listenersByType.set(type, listeners);
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
listeners.push(listener);
|
|
2825
|
+
__mreactRetainDelegatedEventRoot(root, type);
|
|
2826
|
+
|
|
2827
|
+
return () => {
|
|
2828
|
+
const state = __mreactDelegatedEventState();
|
|
2829
|
+
const currentListeners = state.elements.get(element)?.get(type);
|
|
2830
|
+
const index = currentListeners?.indexOf(listener) ?? -1;
|
|
2831
|
+
|
|
2832
|
+
if (index !== -1) {
|
|
2833
|
+
currentListeners?.splice(index, 1);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
if (currentListeners?.length === 0) {
|
|
2837
|
+
state.elements.get(element)?.delete(type);
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
__mreactReleaseDelegatedEventRoot(root, type);
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
function __mreactRetainDelegatedEventRoot(root, type) {
|
|
2845
|
+
const state = __mreactDelegatedEventState();
|
|
2846
|
+
let rootsByType = state.roots.get(root);
|
|
2847
|
+
|
|
2848
|
+
if (rootsByType === undefined) {
|
|
2849
|
+
rootsByType = new Map();
|
|
2850
|
+
state.roots.set(root, rootsByType);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
const current = rootsByType.get(type);
|
|
2854
|
+
|
|
2855
|
+
if (current !== undefined) {
|
|
2856
|
+
current.count += 1;
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
const listener = (event) => __mreactDispatchDelegatedEvent(root, type, event);
|
|
2861
|
+
rootsByType.set(type, { count: 1, listener });
|
|
2862
|
+
root.addEventListener(type, listener);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
function __mreactReleaseDelegatedEventRoot(root, type) {
|
|
2866
|
+
const rootsByType = __mreactDelegatedEventState().roots.get(root);
|
|
2867
|
+
const current = rootsByType?.get(type);
|
|
2868
|
+
|
|
2869
|
+
if (rootsByType === undefined || current === undefined) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
current.count -= 1;
|
|
2874
|
+
|
|
2875
|
+
if (current.count > 0) {
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
root.removeEventListener(type, current.listener);
|
|
2880
|
+
rootsByType.delete(type);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
function __mreactDispatchDelegatedEvent(root, type, event) {
|
|
2884
|
+
const state = __mreactDelegatedEventState();
|
|
2885
|
+
|
|
2886
|
+
for (const target of event.composedPath()) {
|
|
2887
|
+
if (target === root) {
|
|
2888
|
+
break;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2891
|
+
if (!(target instanceof HTMLElement)) {
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
const listeners = state.elements.get(target)?.get(type);
|
|
2896
|
+
|
|
2897
|
+
if (listeners === undefined || listeners.length === 0) {
|
|
2898
|
+
continue;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
const activeListeners = listeners.slice();
|
|
2902
|
+
|
|
2903
|
+
for (const listener of activeListeners) {
|
|
2904
|
+
__mreactCallWithCurrentTarget(listener, event, target);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
if (event.cancelBubble) {
|
|
2908
|
+
break;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
function __mreactCallWithCurrentTarget(listener, event, currentTarget) {
|
|
2914
|
+
const descriptor = Object.getOwnPropertyDescriptor(event, "currentTarget");
|
|
2915
|
+
|
|
2916
|
+
Object.defineProperty(event, "currentTarget", {
|
|
2917
|
+
configurable: true,
|
|
2918
|
+
value: currentTarget,
|
|
2919
|
+
});
|
|
2920
|
+
|
|
2921
|
+
try {
|
|
2922
|
+
listener.call(currentTarget, event);
|
|
2923
|
+
} finally {
|
|
2924
|
+
if (descriptor === undefined) {
|
|
2925
|
+
delete event.currentTarget;
|
|
2926
|
+
} else {
|
|
2927
|
+
Object.defineProperty(event, "currentTarget", descriptor);
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
function __mreactSyncAttributes(current, next) {
|
|
2933
|
+
for (const attribute of Array.from(current.attributes)) {
|
|
2934
|
+
if (!next.hasAttribute(attribute.name)) {
|
|
2935
|
+
current.removeAttribute(attribute.name);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
for (const attribute of Array.from(next.attributes)) {
|
|
2940
|
+
if (current.getAttribute(attribute.name) !== attribute.value) {
|
|
2941
|
+
current.setAttribute(attribute.name, attribute.value);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
function __mreactSyncPropBindings(current, next) {
|
|
2947
|
+
const previousBindings = current.__mreactPropBindings;
|
|
2948
|
+
|
|
2949
|
+
if (Array.isArray(previousBindings)) {
|
|
2950
|
+
for (const binding of previousBindings) {
|
|
2951
|
+
binding.dispose?.();
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
const bindings = next.__mreactPropBindings;
|
|
2956
|
+
|
|
2957
|
+
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
2958
|
+
current.__mreactPropBindings = [];
|
|
2959
|
+
current.__mreactHasReactiveProps = false;
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
current.__mreactPropBindings = bindings;
|
|
2964
|
+
current.__mreactHasReactiveProps = true;
|
|
2965
|
+
next.__mreactPropBindings = [];
|
|
2966
|
+
next.__mreactHasReactiveProps = false;
|
|
2967
|
+
|
|
2968
|
+
for (const binding of bindings) {
|
|
2969
|
+
binding.retarget?.(current);
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
function __mreactResumeChildren(current, next) {
|
|
2974
|
+
const nextChildren = Array.from(next.childNodes);
|
|
2975
|
+
const refreshTextBindings = next.__mreactHasEvents === true;
|
|
2976
|
+
let index = 0;
|
|
2977
|
+
|
|
2978
|
+
while (index < nextChildren.length) {
|
|
2979
|
+
const currentChild = current.childNodes[index];
|
|
2980
|
+
const nextChild = nextChildren[index];
|
|
2981
|
+
|
|
2982
|
+
if (currentChild === undefined) {
|
|
2983
|
+
current.appendChild(nextChild);
|
|
2984
|
+
index += 1;
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// Nodes owned by insertDynamic/bindText must replace the matching server
|
|
2989
|
+
// DOM so subsequent reactive updates mutate the live node/range instead of
|
|
2990
|
+
// appending beside stale SSR fallback content.
|
|
2991
|
+
const isDynamicNode = nextChild.__mreactDynamicNode === true;
|
|
2992
|
+
const isReactiveText = nextChild.__mreactReactiveText === true;
|
|
2993
|
+
|
|
2994
|
+
if (isDynamicNode) {
|
|
2995
|
+
currentChild.replaceWith(nextChild);
|
|
2996
|
+
} else if (
|
|
2997
|
+
(refreshTextBindings || isReactiveText) &&
|
|
2998
|
+
currentChild.nodeType === Node.TEXT_NODE &&
|
|
2999
|
+
nextChild.nodeType === Node.TEXT_NODE
|
|
3000
|
+
) {
|
|
3001
|
+
currentChild.replaceWith(nextChild);
|
|
3002
|
+
} else {
|
|
3003
|
+
__mreactResumeNode(currentChild, nextChild);
|
|
3004
|
+
}
|
|
3005
|
+
index += 1;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
while (current.childNodes.length > nextChildren.length) {
|
|
3009
|
+
current.lastChild?.remove();
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
`;
|
|
3013
|
+
return {
|
|
3014
|
+
code: stripTypeScriptWithOxc(entry),
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
function workspaceRuntimePlugin(options: { routeFiles: readonly string[] }) {
|
|
3019
|
+
const routeFiles = new Set(options.routeFiles);
|
|
3020
|
+
const packageFile = (monorepoDir: string, packageName: string, entry: string): string =>
|
|
3021
|
+
workspacePackageFile({
|
|
3022
|
+
currentFileUrl: import.meta.url,
|
|
3023
|
+
entry,
|
|
3024
|
+
monorepoDir,
|
|
3025
|
+
packageName,
|
|
3026
|
+
});
|
|
3027
|
+
const reactiveCorePath = packageFile("reactive-core", "@reckona/mreact-reactive-core", "index");
|
|
3028
|
+
const reactiveCoreDir = dirname(reactiveCorePath);
|
|
3029
|
+
const runtimePaths = new Map([
|
|
3030
|
+
[
|
|
3031
|
+
"@reckona/mreact-reactive-core/internal",
|
|
3032
|
+
packageFile("reactive-core", "@reckona/mreact-reactive-core", "internal"),
|
|
3033
|
+
],
|
|
3034
|
+
["@reckona/mreact-compat", packageFile("react-compat", "@reckona/mreact-compat", "index")],
|
|
3035
|
+
[
|
|
3036
|
+
"@reckona/mreact-compat/event-priority",
|
|
3037
|
+
packageFile("react-compat", "@reckona/mreact-compat", "event-priority"),
|
|
3038
|
+
],
|
|
3039
|
+
[
|
|
3040
|
+
"@reckona/mreact-compat/flight",
|
|
3041
|
+
packageFile("react-compat", "@reckona/mreact-compat", "flight"),
|
|
3042
|
+
],
|
|
3043
|
+
[
|
|
3044
|
+
"@reckona/mreact-compat/internal",
|
|
3045
|
+
packageFile("react-compat", "@reckona/mreact-compat", "internal"),
|
|
3046
|
+
],
|
|
3047
|
+
[
|
|
3048
|
+
"@reckona/mreact-compat/jsx-dev-runtime",
|
|
3049
|
+
packageFile("react-compat", "@reckona/mreact-compat", "jsx-dev-runtime"),
|
|
3050
|
+
],
|
|
3051
|
+
[
|
|
3052
|
+
"@reckona/mreact-compat/jsx-runtime",
|
|
3053
|
+
packageFile("react-compat", "@reckona/mreact-compat", "jsx-runtime"),
|
|
3054
|
+
],
|
|
3055
|
+
[
|
|
3056
|
+
"@reckona/mreact-compat/scheduler",
|
|
3057
|
+
packageFile("react-compat", "@reckona/mreact-compat", "scheduler"),
|
|
3058
|
+
],
|
|
3059
|
+
[
|
|
3060
|
+
"@reckona/mreact-reactive-dom",
|
|
3061
|
+
packageFile("reactive-dom", "@reckona/mreact-reactive-dom", "index"),
|
|
3062
|
+
],
|
|
3063
|
+
]);
|
|
3064
|
+
|
|
3065
|
+
return {
|
|
3066
|
+
name: "mreact-workspace-runtime",
|
|
3067
|
+
setup(buildApi: RouterCompatBuildApi) {
|
|
3068
|
+
buildApi.onResolve({ filter: /^\.\/devtools\.js$/ }, (args) =>
|
|
3069
|
+
args.importer?.startsWith(reactiveCoreDir) === true
|
|
3070
|
+
? { namespace: "mreact-devtools-stub", path: "devtools" }
|
|
3071
|
+
: undefined,
|
|
3072
|
+
);
|
|
3073
|
+
buildApi.onResolve({ filter: /^@reckona\/mreact-reactive-core$/ }, () => ({
|
|
3074
|
+
namespace: "mreact-hot-runtime",
|
|
3075
|
+
path: "reactive-core",
|
|
3076
|
+
}));
|
|
3077
|
+
buildApi.onResolve(
|
|
3078
|
+
{
|
|
3079
|
+
filter:
|
|
3080
|
+
/^@reckona\/mreact-(?:compat|reactive-core|reactive-dom)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler))?$/,
|
|
3081
|
+
},
|
|
3082
|
+
(args) => {
|
|
3083
|
+
const path = runtimePaths.get(args.path);
|
|
3084
|
+
|
|
3085
|
+
return path === undefined ? undefined : { path };
|
|
3086
|
+
},
|
|
3087
|
+
);
|
|
3088
|
+
buildApi.onLoad({ filter: /^reactive-core$/, namespace: "mreact-hot-runtime" }, () => ({
|
|
3089
|
+
contents: `import { cell as nativeCell } from ${JSON.stringify(reactiveCorePath)};
|
|
3090
|
+
export * from ${JSON.stringify(reactiveCorePath)};
|
|
3091
|
+
export function cell(initial) {
|
|
3092
|
+
const routeCell = globalThis.__mreactRouteCell;
|
|
3093
|
+
return typeof routeCell === "function" ? routeCell(nativeCell, initial) : nativeCell(initial);
|
|
3094
|
+
}`,
|
|
3095
|
+
loader: "ts",
|
|
3096
|
+
resolveDir: reactiveCoreDir,
|
|
3097
|
+
}));
|
|
3098
|
+
buildApi.onLoad({ filter: /^devtools$/, namespace: "mreact-devtools-stub" }, () => ({
|
|
3099
|
+
contents: `export function emitReactiveDevtoolsEvent() {}
|
|
3100
|
+
export function hasReactiveDevtoolsEmitter() { return false; }
|
|
3101
|
+
export function currentDevtoolsEmitter() { return undefined; }`,
|
|
3102
|
+
loader: "ts",
|
|
3103
|
+
}));
|
|
3104
|
+
buildApi.onLoad({ filter: /\.(?:mreact\.)?[cm]?[jt]sx$/ }, async (args) => {
|
|
3105
|
+
if (!isRouteClientDependencySourcePath(args.path, routeFiles)) {
|
|
3106
|
+
return undefined;
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
const source = await readFile(args.path, "utf8");
|
|
3110
|
+
const moduleContext = createCompilerModuleContext({
|
|
3111
|
+
code: source,
|
|
3112
|
+
filename: args.path,
|
|
3113
|
+
});
|
|
3114
|
+
|
|
3115
|
+
if (!hasJsxSyntax(moduleContext.program)) {
|
|
3116
|
+
return undefined;
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
const output = transformCompilerModuleContext({
|
|
3120
|
+
code: source,
|
|
3121
|
+
dev: true,
|
|
3122
|
+
filename: args.path,
|
|
3123
|
+
mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
|
|
3124
|
+
moduleContext,
|
|
3125
|
+
target: "client",
|
|
3126
|
+
});
|
|
3127
|
+
|
|
3128
|
+
if (output.diagnostics.length > 0) {
|
|
3129
|
+
throw new Error(
|
|
3130
|
+
output.diagnostics
|
|
3131
|
+
.map((diagnostic) => formatDiagnostic(args.path, diagnostic))
|
|
3132
|
+
.join("\n"),
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
return {
|
|
3137
|
+
contents: output.code,
|
|
3138
|
+
loader: "tsx",
|
|
3139
|
+
resolveDir: dirname(args.path),
|
|
3140
|
+
};
|
|
3141
|
+
});
|
|
3142
|
+
},
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
function isRouteClientDependencySourcePath(path: string, routeFiles: ReadonlySet<string>): boolean {
|
|
3147
|
+
return !routeFiles.has(path) && !path.includes(`${sep}node_modules${sep}`);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function isCompatSourcePath(path: string): boolean {
|
|
3151
|
+
return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(path);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/**
|
|
3155
|
+
* Detects the `export const clientNavigation = false` hint in a page module
|
|
3156
|
+
* source. Returns the hinted value, or `true` when no hint is present (i.e.,
|
|
3157
|
+
* preserve the historical "navigation runtime is always present" behavior).
|
|
3158
|
+
*
|
|
3159
|
+
* Regex-based to avoid pulling the JS parser into the build path. The pattern
|
|
3160
|
+
* accepts the common formatting variants:
|
|
3161
|
+
* export const clientNavigation = false
|
|
3162
|
+
* export const clientNavigation = false ;
|
|
3163
|
+
* export const clientNavigation: boolean = false
|
|
3164
|
+
*/
|
|
3165
|
+
export function detectClientNavigationHint(source: string): boolean {
|
|
3166
|
+
const match = source.match(
|
|
3167
|
+
/export\s+const\s+clientNavigation\s*(?::\s*[^=]+)?=\s*(true|false)\s*;?/,
|
|
3168
|
+
);
|
|
3169
|
+
return match === null ? true : match[1] === "true";
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
export function detectNavigationRuntimeHint(source: string): boolean {
|
|
3173
|
+
const match = source.match(
|
|
3174
|
+
/export\s+const\s+navigationRuntime\s*(?::\s*[^=]+)?=\s*(true|false)\s*;?/,
|
|
3175
|
+
);
|
|
3176
|
+
return match !== null && match[1] === "true";
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
function detectRouteCellStateHint(code: string): boolean {
|
|
3180
|
+
const callExpression = routeCellCallExpressionSource(code);
|
|
3181
|
+
|
|
3182
|
+
return callExpression === undefined
|
|
3183
|
+
? /\bcell\d*\s*\(/.test(code)
|
|
3184
|
+
: new RegExp(`(?:${callExpression})\\s*\\(`).test(code);
|
|
3185
|
+
}
|
|
3186
|
+
|
|
3187
|
+
function detectRouteReactiveEffectHint(code: string): boolean {
|
|
3188
|
+
return (
|
|
3189
|
+
/from\s+["']@reckona\/mreact-reactive-core["']/.test(code) &&
|
|
3190
|
+
/\beffect\s*\(/.test(code)
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
async function inferClientReferenceManifestForBundle(options: {
|
|
3195
|
+
code: string;
|
|
3196
|
+
filename: string;
|
|
3197
|
+
routePath: string;
|
|
3198
|
+
}): Promise<readonly ClientReferenceMetadata[]> {
|
|
3199
|
+
const cache = createClientRouteInferenceCache();
|
|
3200
|
+
const moduleContext = await compilerModuleContextForSource({
|
|
3201
|
+
cache,
|
|
3202
|
+
code: options.code,
|
|
3203
|
+
filename: options.filename,
|
|
3204
|
+
});
|
|
3205
|
+
const inference = await inferClientRouteModule({
|
|
3206
|
+
cache,
|
|
3207
|
+
code: options.code,
|
|
3208
|
+
filename: options.filename,
|
|
3209
|
+
moduleContext,
|
|
3210
|
+
routePath: options.routePath,
|
|
3211
|
+
});
|
|
3212
|
+
|
|
3213
|
+
if (inference.clientBoundaryImports.length === 0) {
|
|
3214
|
+
return [];
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
const output = transformCompilerModuleContext({
|
|
3218
|
+
code: options.code,
|
|
3219
|
+
clientBoundaryImports: inference.clientBoundaryImports,
|
|
3220
|
+
dev: true,
|
|
3221
|
+
filename: options.filename,
|
|
3222
|
+
moduleContext,
|
|
3223
|
+
target: "server",
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
return output.metadata.clientReferenceManifest ?? [];
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3229
|
+
function emitClientReferenceImportBlock(imports: readonly ClientReferenceImport[]): string {
|
|
3230
|
+
if (imports.length === 0) {
|
|
3231
|
+
return "";
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
return `${imports
|
|
3235
|
+
.map((reference, index) => {
|
|
3236
|
+
const localName = clientReferenceLocalName(index);
|
|
3237
|
+
|
|
3238
|
+
return isIdentifierName(reference.exportName)
|
|
3239
|
+
? `import { ${reference.exportName} as ${localName} } from ${JSON.stringify(reference.importSource)};`
|
|
3240
|
+
: `import * as ${localName} from ${JSON.stringify(reference.importSource)};`;
|
|
3241
|
+
})
|
|
3242
|
+
.join("\n")}\n`;
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
function emitClientReferenceRegistry(
|
|
3246
|
+
manifest: readonly ClientReferenceMetadata[],
|
|
3247
|
+
imports: readonly ClientReferenceImport[],
|
|
3248
|
+
compatNames: ReadonlySet<string>,
|
|
3249
|
+
): string {
|
|
3250
|
+
const importedExpressions = new Map(
|
|
3251
|
+
imports.map((reference, index) => [
|
|
3252
|
+
reference.name,
|
|
3253
|
+
isIdentifierName(reference.exportName)
|
|
3254
|
+
? clientReferenceLocalName(index)
|
|
3255
|
+
: `${clientReferenceLocalName(index)}[${JSON.stringify(reference.exportName)}]`,
|
|
3256
|
+
]),
|
|
3257
|
+
);
|
|
3258
|
+
const entries = manifest.flatMap((reference) => {
|
|
3259
|
+
const expression =
|
|
3260
|
+
importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
|
|
3261
|
+
|
|
3262
|
+
return expression === undefined
|
|
3263
|
+
? []
|
|
3264
|
+
: [
|
|
3265
|
+
compatNames.has(reference.name)
|
|
3266
|
+
? ` [${JSON.stringify(reference.name)}, { component: ${expression}, compat: true }],`
|
|
3267
|
+
: ` [${JSON.stringify(reference.name)}, ${expression}],`,
|
|
3268
|
+
];
|
|
3269
|
+
});
|
|
3270
|
+
|
|
3271
|
+
return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
function compatClientReferenceComponentNames(
|
|
3275
|
+
manifest: readonly ClientReferenceMetadata[],
|
|
3276
|
+
): ReadonlySet<string> {
|
|
3277
|
+
return new Set(
|
|
3278
|
+
manifest
|
|
3279
|
+
.filter((reference) => isCompatSourcePath(reference.moduleId))
|
|
3280
|
+
.map((reference) => reference.name),
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
function emitCompatClientReferenceImportBlock(compatNames: ReadonlySet<string>): string {
|
|
3285
|
+
return compatNames.size === 0
|
|
3286
|
+
? ""
|
|
3287
|
+
: 'import { createElement as __mreactCompatCreateElement, createRoot as __mreactCompatCreateRoot } from "@reckona/mreact-compat";\n';
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function clientReferenceLocalName(index: number): string {
|
|
3291
|
+
return `__mreactClientReference${index}`;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3294
|
+
function clientReferenceExpression(name: string): string | undefined {
|
|
3295
|
+
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(name) ? name : undefined;
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
function routeComponentExpressionForComponents(components: readonly ComponentMetadata[]): string {
|
|
3299
|
+
const candidates = uniqueStrings([
|
|
3300
|
+
...components
|
|
3301
|
+
.filter((component) => component.exportName === "default")
|
|
3302
|
+
.map((component) => component.name),
|
|
3303
|
+
"Page",
|
|
3304
|
+
"DefaultExport",
|
|
3305
|
+
]).filter(isIdentifierName);
|
|
3306
|
+
|
|
3307
|
+
return candidates.reduceRight(
|
|
3308
|
+
(next, name) => `typeof ${name} === "function" ? ${name} : ${next}`,
|
|
3309
|
+
"undefined",
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
function uniqueStrings(values: readonly string[]): string[] {
|
|
3314
|
+
return Array.from(new Set(values));
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function isIdentifierName(value: string): boolean {
|
|
3318
|
+
return /^[A-Za-z_$][\w$]*$/.test(value);
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
function routeStateSignatureForSource(code: string): string {
|
|
3322
|
+
const callExpression = routeCellCallExpressionSource(code);
|
|
3323
|
+
const callsitePattern =
|
|
3324
|
+
callExpression === undefined
|
|
3325
|
+
? /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(cell\d*|cell)\s*\(/g
|
|
3326
|
+
: new RegExp(
|
|
3327
|
+
`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*(${callExpression})\\s*\\(`,
|
|
3328
|
+
"g",
|
|
3329
|
+
);
|
|
3330
|
+
const cellCallsites = Array.from(
|
|
3331
|
+
code.matchAll(callsitePattern),
|
|
3332
|
+
(match) => `${match[1]}:${match[2]}`,
|
|
3333
|
+
);
|
|
3334
|
+
const countPattern =
|
|
3335
|
+
callExpression === undefined
|
|
3336
|
+
? /\bcell\d*\s*\(/g
|
|
3337
|
+
: new RegExp(`(?:${callExpression})\\s*\\(`, "g");
|
|
3338
|
+
const signature =
|
|
3339
|
+
cellCallsites.length > 0
|
|
3340
|
+
? cellCallsites.join("\n")
|
|
3341
|
+
: `cell-count:${(code.match(countPattern) ?? []).length}`;
|
|
3342
|
+
|
|
3343
|
+
return createHash("sha256").update(signature).digest("hex").slice(0, 16);
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
function routeCellCallExpressionSource(code: string): string | undefined {
|
|
3347
|
+
const namedImports = new Set<string>();
|
|
3348
|
+
const namespaceImports = new Set<string>();
|
|
3349
|
+
const namedImportPattern =
|
|
3350
|
+
/import\s+\{(?<imports>[^}]*)\}\s+from\s+["']@reckona\/mreact-reactive-core["']/g;
|
|
3351
|
+
|
|
3352
|
+
for (const match of code.matchAll(namedImportPattern)) {
|
|
3353
|
+
const imports = match.groups?.imports;
|
|
3354
|
+
|
|
3355
|
+
if (imports === undefined) {
|
|
3356
|
+
continue;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
for (const part of imports.split(",")) {
|
|
3360
|
+
const specifier = part.trim();
|
|
3361
|
+
const alias = /^cell\s+as\s+([A-Za-z_$][\w$]*)$/.exec(specifier);
|
|
3362
|
+
|
|
3363
|
+
if (specifier === "cell") {
|
|
3364
|
+
namedImports.add("cell");
|
|
3365
|
+
} else if (alias?.[1] !== undefined) {
|
|
3366
|
+
namedImports.add(alias[1]);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
const namespaceImportPattern =
|
|
3372
|
+
/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@reckona\/mreact-reactive-core["']/g;
|
|
3373
|
+
|
|
3374
|
+
for (const match of code.matchAll(namespaceImportPattern)) {
|
|
3375
|
+
if (match[1] !== undefined) {
|
|
3376
|
+
namespaceImports.add(match[1]);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
const alternatives = [
|
|
3381
|
+
...Array.from(namedImports, (name) => `\\b${escapeRegExp(name)}`),
|
|
3382
|
+
...Array.from(namespaceImports, (name) => `\\b${escapeRegExp(name)}\\.cell`),
|
|
3383
|
+
];
|
|
3384
|
+
|
|
3385
|
+
return alternatives.length === 0 ? undefined : `(?:${alternatives.join("|")})`;
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
function escapeRegExp(value: string): string {
|
|
3389
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
function errorMessage(error: unknown): string {
|
|
3393
|
+
return error instanceof Error ? error.message : String(error);
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
function escapeScriptJson(value: string): string {
|
|
3397
|
+
return value.replaceAll("<", "\\u003c");
|
|
3398
|
+
}
|