@reckona/mreact-router 0.0.30 → 0.0.32
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/README.md +2 -2
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +21 -21
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +141 -94
- package/dist/build.js.map +1 -1
- package/dist/client.d.ts +38 -5
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +430 -94
- package/dist/client.js.map +1 -1
- package/dist/module-runner.d.ts +7 -1
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +41 -5
- package/dist/module-runner.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +137 -45
- package/dist/render.js.map +1 -1
- package/dist/route-shells.d.ts +9 -0
- package/dist/route-shells.d.ts.map +1 -0
- package/dist/route-shells.js +26 -0
- package/dist/route-shells.js.map +1 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +104 -31
- package/dist/serve.js.map +1 -1
- package/dist/source-modules.d.ts +2 -0
- package/dist/source-modules.d.ts.map +1 -0
- package/dist/source-modules.js +60 -0
- package/dist/source-modules.js.map +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +7 -4
- package/dist/vite.js.map +1 -1
- package/package.json +9 -8
package/dist/client.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { collectClientRouteModuleAnalysis, formatDiagnostic, } from "@reckona/mreact-compiler";
|
|
6
|
+
import { collectClientRouteModuleAnalysisFromContext, createCompilerModuleContext, transformCompilerModuleContext, } from "@reckona/mreact-compiler/internal";
|
|
5
7
|
import { assetPath } from "./assets.js";
|
|
6
8
|
import { bundleRouterModule } from "./bundle-pipeline.js";
|
|
9
|
+
import { existingRouteShellCandidates } from "./route-shells.js";
|
|
7
10
|
import { stripRouteClientOnlyExports } from "./route-source.js";
|
|
11
|
+
import { sourceModuleCandidates } from "./source-modules.js";
|
|
8
12
|
import { escapeHtmlQuotedAttribute as escapeHtmlAttribute } from "@reckona/mreact-shared/html-escape";
|
|
9
13
|
import { workspacePackageFile } from "./workspace-packages.js";
|
|
14
|
+
const nodeBuiltinPackages = new Set(builtinModules.flatMap((name) => [name, `node:${name}`]));
|
|
15
|
+
const clientBoundaryInferenceServerOnlyReferenceCode = "MR_CLIENT_BOUNDARY_INFERENCE_SERVER_ONLY_REFERENCE";
|
|
16
|
+
const clientBoundaryInferenceUnsupportedReferenceCode = "MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE";
|
|
10
17
|
export async function routeToClientManifestEntry(route) {
|
|
11
18
|
if (route.kind === "server") {
|
|
12
19
|
return { path: route.path, kind: route.kind, client: false };
|
|
@@ -29,8 +36,8 @@ export async function routeToClientManifestEntry(route) {
|
|
|
29
36
|
}
|
|
30
37
|
export function createClientRouteInferenceCache() {
|
|
31
38
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
moduleAnalysisByFile: new Map(),
|
|
40
|
+
moduleContextByFile: new Map(),
|
|
34
41
|
resolvedByImport: new Map(),
|
|
35
42
|
sourceByFile: new Map(),
|
|
36
43
|
};
|
|
@@ -41,42 +48,244 @@ export async function isClientRouteModule(options) {
|
|
|
41
48
|
export async function inferClientRouteModule(options) {
|
|
42
49
|
const cache = options.cache ?? createClientRouteInferenceCache();
|
|
43
50
|
try {
|
|
44
|
-
|
|
51
|
+
const routeInference = await inferClientRouteModuleSource({
|
|
45
52
|
cache,
|
|
46
53
|
code: options.code,
|
|
47
54
|
filename: options.filename,
|
|
55
|
+
moduleContext: options.moduleContext,
|
|
48
56
|
root: true,
|
|
49
57
|
seen: new Set(),
|
|
50
58
|
});
|
|
59
|
+
if (options.appDir === undefined) {
|
|
60
|
+
return routeInference;
|
|
61
|
+
}
|
|
62
|
+
const shellInferences = await inferClientRouteShellModules({
|
|
63
|
+
appDir: options.appDir,
|
|
64
|
+
cache,
|
|
65
|
+
filename: options.filename,
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
client: routeInference.client || shellInferences.some((inference) => inference.client),
|
|
69
|
+
clientBoundaryImports: routeInference.clientBoundaryImports,
|
|
70
|
+
diagnostics: [
|
|
71
|
+
...routeInference.diagnostics,
|
|
72
|
+
...shellInferences.flatMap((inference) => inference.diagnostics),
|
|
73
|
+
],
|
|
74
|
+
};
|
|
51
75
|
}
|
|
52
76
|
catch (error) {
|
|
53
77
|
throw new Error(`Failed to infer client route for ${options.routePath ?? "<unknown>"} (${options.filename}).\n${errorMessage(error)}`, { cause: error });
|
|
54
78
|
}
|
|
55
79
|
}
|
|
80
|
+
export async function collectClientRouteReferences(options) {
|
|
81
|
+
const cache = options.cache ?? createClientRouteInferenceCache();
|
|
82
|
+
const routeModuleContext = await compilerModuleContextForSource({
|
|
83
|
+
cache,
|
|
84
|
+
code: options.code,
|
|
85
|
+
filename: options.filename,
|
|
86
|
+
});
|
|
87
|
+
const routeInference = await inferClientRouteModuleSource({
|
|
88
|
+
cache,
|
|
89
|
+
code: options.code,
|
|
90
|
+
filename: options.filename,
|
|
91
|
+
moduleContext: routeModuleContext,
|
|
92
|
+
root: true,
|
|
93
|
+
seen: new Set(),
|
|
94
|
+
});
|
|
95
|
+
const sources = [];
|
|
96
|
+
const seenSourceFiles = new Set();
|
|
97
|
+
const addSource = async (sourceOptions) => {
|
|
98
|
+
if (seenSourceFiles.has(sourceOptions.filename)) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
seenSourceFiles.add(sourceOptions.filename);
|
|
102
|
+
const moduleContext = sourceOptions.moduleContext ??
|
|
103
|
+
await compilerModuleContextForSource({
|
|
104
|
+
cache,
|
|
105
|
+
code: sourceOptions.code,
|
|
106
|
+
filename: sourceOptions.filename,
|
|
107
|
+
});
|
|
108
|
+
const inference = sourceOptions.inference ??
|
|
109
|
+
(await inferClientRouteModuleSource({
|
|
110
|
+
cache,
|
|
111
|
+
code: sourceOptions.code,
|
|
112
|
+
filename: sourceOptions.filename,
|
|
113
|
+
moduleContext,
|
|
114
|
+
root: true,
|
|
115
|
+
seen: new Set(),
|
|
116
|
+
}));
|
|
117
|
+
sources.push({
|
|
118
|
+
code: sourceOptions.code,
|
|
119
|
+
filename: sourceOptions.filename,
|
|
120
|
+
inference,
|
|
121
|
+
moduleContext,
|
|
122
|
+
});
|
|
123
|
+
for (const referenceFile of inference.clientReferenceSourceFiles) {
|
|
124
|
+
const code = stripRouteClientOnlyExports(await readCachedFile(cache, referenceFile));
|
|
125
|
+
await addSource({ code, filename: referenceFile });
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
await addSource({
|
|
129
|
+
code: options.code,
|
|
130
|
+
filename: options.filename,
|
|
131
|
+
inference: routeInference,
|
|
132
|
+
moduleContext: routeModuleContext,
|
|
133
|
+
});
|
|
134
|
+
if (options.appDir !== undefined) {
|
|
135
|
+
for (const shell of await clientShellFilesForPage(options.appDir, options.filename)) {
|
|
136
|
+
const code = stripRouteClientOnlyExports(await readCachedFile(cache, shell));
|
|
137
|
+
const moduleContext = await compilerModuleContextForSource({
|
|
138
|
+
cache,
|
|
139
|
+
code,
|
|
140
|
+
filename: shell,
|
|
141
|
+
});
|
|
142
|
+
await addSource({
|
|
143
|
+
code,
|
|
144
|
+
filename: shell,
|
|
145
|
+
inference: await inferClientRouteModuleSource({
|
|
146
|
+
cache,
|
|
147
|
+
code,
|
|
148
|
+
filename: shell,
|
|
149
|
+
moduleContext,
|
|
150
|
+
root: true,
|
|
151
|
+
seen: new Set(),
|
|
152
|
+
}),
|
|
153
|
+
moduleContext,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const clientReferenceManifest = [];
|
|
158
|
+
const clientReferenceImports = [];
|
|
159
|
+
const seenReferences = new Set();
|
|
160
|
+
for (const source of sources) {
|
|
161
|
+
const output = transformCompilerModuleContext({
|
|
162
|
+
code: source.code,
|
|
163
|
+
clientBoundaryImports: source.inference.clientBoundaryImports,
|
|
164
|
+
dev: false,
|
|
165
|
+
filename: source.filename,
|
|
166
|
+
moduleContext: source.moduleContext,
|
|
167
|
+
target: "server",
|
|
168
|
+
});
|
|
169
|
+
const fatalDiagnostics = output.diagnostics.filter((diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER" &&
|
|
170
|
+
diagnostic.code !== "MR_UNSUPPORTED_AWAIT_INNER_COMPONENT");
|
|
171
|
+
if (fatalDiagnostics.length > 0) {
|
|
172
|
+
throw new Error(fatalDiagnostics.map((diagnostic) => formatDiagnostic(source.filename, diagnostic)).join("\n"));
|
|
173
|
+
}
|
|
174
|
+
for (const reference of output.metadata.clientReferenceManifest ?? []) {
|
|
175
|
+
const key = `${reference.name}\0${reference.moduleId}\0${reference.exportName}`;
|
|
176
|
+
if (seenReferences.has(key)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
seenReferences.add(key);
|
|
180
|
+
clientReferenceManifest.push(reference);
|
|
181
|
+
clientReferenceImports.push({
|
|
182
|
+
exportName: reference.exportName,
|
|
183
|
+
importSource: clientReferenceImportSource({
|
|
184
|
+
moduleId: reference.moduleId,
|
|
185
|
+
routeFile: options.filename,
|
|
186
|
+
sourceFile: source.filename,
|
|
187
|
+
}),
|
|
188
|
+
name: reference.name,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
client: routeInference.client ||
|
|
194
|
+
sources.some((source) => source.filename !== options.filename && source.inference.client),
|
|
195
|
+
clientBoundaryImports: routeInference.clientBoundaryImports,
|
|
196
|
+
clientReferenceImports,
|
|
197
|
+
clientReferenceManifest,
|
|
198
|
+
diagnostics: sources.flatMap((source) => source.inference.diagnostics),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function inferClientRouteShellModules(options) {
|
|
202
|
+
return Promise.all((await clientShellFilesForPage(options.appDir, options.filename)).map(async (shell) => {
|
|
203
|
+
const code = stripRouteClientOnlyExports(await readCachedFile(options.cache, shell));
|
|
204
|
+
return await inferClientRouteModuleSource({
|
|
205
|
+
cache: options.cache,
|
|
206
|
+
code,
|
|
207
|
+
filename: shell,
|
|
208
|
+
root: true,
|
|
209
|
+
seen: new Set(),
|
|
210
|
+
});
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
async function clientShellFilesForPage(appDir, pageFile) {
|
|
214
|
+
const existing = await existingRouteShellCandidates(appDir, pageFile, async (file) => file !== pageFile && await isFile(file));
|
|
215
|
+
return existing.map((candidate) => candidate.file);
|
|
216
|
+
}
|
|
217
|
+
function clientReferenceImportSource(options) {
|
|
218
|
+
if (!options.moduleId.startsWith(".")) {
|
|
219
|
+
return options.moduleId;
|
|
220
|
+
}
|
|
221
|
+
const absolute = join(dirname(options.sourceFile), options.moduleId);
|
|
222
|
+
const relativeImport = relative(dirname(options.routeFile), absolute).replaceAll(sep, "/");
|
|
223
|
+
return relativeImport.startsWith(".") ? relativeImport : `./${relativeImport}`;
|
|
224
|
+
}
|
|
56
225
|
export function isClientRouteSource(code) {
|
|
57
|
-
|
|
226
|
+
const analysis = collectClientRouteModuleAnalysis({ code });
|
|
227
|
+
return (analysis.hasUseClientDirective ||
|
|
228
|
+
(!analysis.hasUseServerDirective && analysis.clientRuntime));
|
|
229
|
+
}
|
|
230
|
+
function isExplicitClientRouteSource(analysis, filename) {
|
|
231
|
+
return analysis.hasUseClientDirective || isClientBoundaryFilename(filename);
|
|
232
|
+
}
|
|
233
|
+
function isClientBoundaryFilename(filename) {
|
|
234
|
+
return /\.client(?:\.mreact)?\.[cm]?[jt]sx?$/.test(filename);
|
|
235
|
+
}
|
|
236
|
+
function isServerOnlyClientRouteSource(analysis) {
|
|
237
|
+
return analysis.hasUseServerDirective;
|
|
238
|
+
}
|
|
239
|
+
function isServerOnlyImportSource(source) {
|
|
240
|
+
return nodeBuiltinPackages.has(source);
|
|
241
|
+
}
|
|
242
|
+
function hasServerOnlyImports(analysis) {
|
|
243
|
+
return analysis.staticImports.some((reference) => isServerOnlyImportSource(reference.source));
|
|
58
244
|
}
|
|
59
245
|
async function inferClientRouteModuleSource(options) {
|
|
60
|
-
|
|
61
|
-
|
|
246
|
+
const analysis = await clientRouteModuleAnalysisForSource(options);
|
|
247
|
+
if (isServerOnlyClientRouteSource(analysis)) {
|
|
248
|
+
return emptyClientRouteModuleInferenceResult({
|
|
249
|
+
serverOnly: true,
|
|
250
|
+
serverOnlyClientRuntime: analysis.clientRuntime,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (isExplicitClientRouteSource(analysis, options.filename)) {
|
|
254
|
+
return emptyClientRouteModuleInferenceResult({
|
|
255
|
+
client: true,
|
|
256
|
+
clientBoundaryModule: true,
|
|
257
|
+
});
|
|
62
258
|
}
|
|
63
259
|
if (options.seen.has(options.filename)) {
|
|
64
|
-
return
|
|
260
|
+
return emptyClientRouteModuleInferenceResult();
|
|
65
261
|
}
|
|
66
262
|
options.seen.add(options.filename);
|
|
67
263
|
try {
|
|
68
264
|
const clientBoundaryImports = [];
|
|
265
|
+
const clientBoundaryExportNames = new Set();
|
|
266
|
+
const nestedClientExportNames = new Set();
|
|
267
|
+
const clientReferenceSourceFiles = [];
|
|
69
268
|
const diagnostics = [];
|
|
70
269
|
let clientProxy = false;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
270
|
+
let nestedClient = false;
|
|
271
|
+
const exportInfo = analysis.topLevelExportRenderInfo;
|
|
272
|
+
const implicitModuleClient = exportInfo.length === 0 &&
|
|
273
|
+
analysis.clientRuntime;
|
|
274
|
+
for (const info of exportInfo) {
|
|
275
|
+
if (info.clientRuntime) {
|
|
276
|
+
clientBoundaryExportNames.add(info.name);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (hasServerOnlyImports(analysis) &&
|
|
280
|
+
(implicitModuleClient || clientBoundaryExportNames.size > 0)) {
|
|
281
|
+
return emptyClientRouteModuleInferenceResult({
|
|
282
|
+
serverOnly: true,
|
|
283
|
+
serverOnlyClientRuntime: true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const jsxComponentRoots = new Set(analysis.jsxComponentRoots);
|
|
287
|
+
const identifierReferences = new Set(analysis.identifierReferences);
|
|
288
|
+
for (const reference of analysis.staticImports) {
|
|
80
289
|
const rendered = isRenderedImportReference(reference, jsxComponentRoots);
|
|
81
290
|
const referenced = isReferencedImportReference(reference, identifierReferences);
|
|
82
291
|
if (!rendered &&
|
|
@@ -96,14 +305,43 @@ async function inferClientRouteModuleSource(options) {
|
|
|
96
305
|
cache: options.cache,
|
|
97
306
|
code: source,
|
|
98
307
|
filename: resolved,
|
|
308
|
+
moduleContext: await compilerModuleContextForSource({
|
|
309
|
+
cache: options.cache,
|
|
310
|
+
code: source,
|
|
311
|
+
filename: resolved,
|
|
312
|
+
}),
|
|
99
313
|
root: false,
|
|
100
314
|
seen: options.seen,
|
|
101
315
|
});
|
|
102
316
|
diagnostics.push(...imported.diagnostics);
|
|
103
317
|
if (!imported.client) {
|
|
318
|
+
if (imported.serverOnlyClientRuntime && rendered) {
|
|
319
|
+
diagnostics.push(serverOnlyClientImportReferenceDiagnostic({
|
|
320
|
+
filename: options.filename,
|
|
321
|
+
reference,
|
|
322
|
+
}));
|
|
323
|
+
}
|
|
104
324
|
continue;
|
|
105
325
|
}
|
|
106
326
|
if (rendered) {
|
|
327
|
+
const importedExportNames = renderedImportedExportNames(reference, jsxComponentRoots);
|
|
328
|
+
const renderedExportNames = renderedLocalExportNames(reference, exportInfo);
|
|
329
|
+
const importedBoundary = imported.clientBoundaryModule ||
|
|
330
|
+
matchesInferredExportNames(importedExportNames, imported.clientBoundaryExportNames);
|
|
331
|
+
const importedNested = matchesInferredExportNames(importedExportNames, imported.nestedClientExportNames);
|
|
332
|
+
if (!importedBoundary && !importedNested) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
nestedClient = true;
|
|
336
|
+
for (const exportName of renderedExportNames) {
|
|
337
|
+
if (importedBoundary || importedNested) {
|
|
338
|
+
nestedClientExportNames.add(exportName);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!importedBoundary) {
|
|
342
|
+
clientReferenceSourceFiles.push(resolved);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
107
345
|
clientBoundaryImports.push(reference.source);
|
|
108
346
|
continue;
|
|
109
347
|
}
|
|
@@ -117,7 +355,7 @@ async function inferClientRouteModuleSource(options) {
|
|
|
117
355
|
}
|
|
118
356
|
}
|
|
119
357
|
if (!options.root) {
|
|
120
|
-
for (const reference of
|
|
358
|
+
for (const reference of analysis.staticExports) {
|
|
121
359
|
const resolved = await resolveAppLocalModule({
|
|
122
360
|
cache: options.cache,
|
|
123
361
|
importer: options.filename,
|
|
@@ -131,48 +369,98 @@ async function inferClientRouteModuleSource(options) {
|
|
|
131
369
|
cache: options.cache,
|
|
132
370
|
code: source,
|
|
133
371
|
filename: resolved,
|
|
372
|
+
moduleContext: await compilerModuleContextForSource({
|
|
373
|
+
cache: options.cache,
|
|
374
|
+
code: source,
|
|
375
|
+
filename: resolved,
|
|
376
|
+
}),
|
|
134
377
|
root: false,
|
|
135
378
|
seen: options.seen,
|
|
136
379
|
});
|
|
137
380
|
diagnostics.push(...exported.diagnostics);
|
|
138
|
-
if (exported.
|
|
381
|
+
if (exported.clientBoundaryModule) {
|
|
139
382
|
clientProxy = true;
|
|
140
383
|
}
|
|
384
|
+
else if (exported.clientBoundaryExportNames.length > 0) {
|
|
385
|
+
for (const exportName of reference.exportedNames) {
|
|
386
|
+
if (exported.clientBoundaryExportNames.includes(exportName)) {
|
|
387
|
+
clientBoundaryExportNames.add(exportName);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else if (exported.client) {
|
|
392
|
+
nestedClient = true;
|
|
393
|
+
clientReferenceSourceFiles.push(resolved);
|
|
394
|
+
}
|
|
141
395
|
}
|
|
142
396
|
}
|
|
143
397
|
return {
|
|
144
|
-
client: clientBoundaryImports.length > 0 ||
|
|
398
|
+
client: clientBoundaryImports.length > 0 ||
|
|
399
|
+
clientBoundaryExportNames.size > 0 ||
|
|
400
|
+
implicitModuleClient ||
|
|
401
|
+
clientProxy ||
|
|
402
|
+
nestedClient,
|
|
145
403
|
clientBoundaryImports,
|
|
404
|
+
clientBoundaryExportNames: Array.from(clientBoundaryExportNames),
|
|
405
|
+
clientBoundaryModule: clientProxy || implicitModuleClient,
|
|
406
|
+
nestedClientExportNames: Array.from(nestedClientExportNames),
|
|
407
|
+
clientReferenceSourceFiles: Array.from(new Set(clientReferenceSourceFiles)),
|
|
146
408
|
diagnostics,
|
|
409
|
+
serverOnly: false,
|
|
410
|
+
serverOnlyClientRuntime: false,
|
|
147
411
|
};
|
|
148
412
|
}
|
|
149
413
|
finally {
|
|
150
414
|
options.seen.delete(options.filename);
|
|
151
415
|
}
|
|
152
416
|
}
|
|
153
|
-
|
|
154
|
-
|
|
417
|
+
function emptyClientRouteModuleInferenceResult(overrides = {}) {
|
|
418
|
+
return {
|
|
419
|
+
client: false,
|
|
420
|
+
clientBoundaryImports: [],
|
|
421
|
+
clientBoundaryExportNames: [],
|
|
422
|
+
clientBoundaryModule: false,
|
|
423
|
+
nestedClientExportNames: [],
|
|
424
|
+
clientReferenceSourceFiles: [],
|
|
425
|
+
diagnostics: [],
|
|
426
|
+
serverOnly: false,
|
|
427
|
+
serverOnlyClientRuntime: false,
|
|
428
|
+
...overrides,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
async function clientRouteModuleAnalysisForSource(options) {
|
|
432
|
+
const cacheKey = sourceAnalysisCacheKey(options.filename, options.code);
|
|
433
|
+
const cached = options.cache.moduleAnalysisByFile.get(cacheKey);
|
|
155
434
|
if (cached !== undefined) {
|
|
156
435
|
return cached;
|
|
157
436
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
437
|
+
const analysis = Promise.resolve().then(async () => collectClientRouteModuleAnalysisFromContext(options.moduleContext ??
|
|
438
|
+
await compilerModuleContextForSource({
|
|
439
|
+
cache: options.cache,
|
|
440
|
+
code: options.code,
|
|
441
|
+
filename: options.filename,
|
|
442
|
+
})));
|
|
443
|
+
options.cache.moduleAnalysisByFile.set(cacheKey, analysis);
|
|
444
|
+
return analysis;
|
|
164
445
|
}
|
|
165
|
-
async function
|
|
166
|
-
const
|
|
446
|
+
export async function compilerModuleContextForSource(options) {
|
|
447
|
+
const cacheKey = sourceAnalysisCacheKey(options.filename, options.code);
|
|
448
|
+
const cached = options.cache.moduleContextByFile.get(cacheKey);
|
|
167
449
|
if (cached !== undefined) {
|
|
168
450
|
return cached;
|
|
169
451
|
}
|
|
170
|
-
const
|
|
452
|
+
const context = Promise.resolve().then(() => createCompilerModuleContext({
|
|
171
453
|
code: options.code,
|
|
172
454
|
filename: options.filename,
|
|
173
455
|
}));
|
|
174
|
-
options.cache.
|
|
175
|
-
return
|
|
456
|
+
options.cache.moduleContextByFile.set(cacheKey, context);
|
|
457
|
+
return context;
|
|
458
|
+
}
|
|
459
|
+
function sourceAnalysisCacheKey(filename, code) {
|
|
460
|
+
return `${filename}\0${hashSourceText(code)}`;
|
|
461
|
+
}
|
|
462
|
+
function hashSourceText(text) {
|
|
463
|
+
return createHash("sha256").update(text).digest("hex");
|
|
176
464
|
}
|
|
177
465
|
function isRenderedImportReference(reference, jsxComponentRoots) {
|
|
178
466
|
return (reference.sideEffect ||
|
|
@@ -184,8 +472,51 @@ function isReferencedImportReference(reference, identifierReferences) {
|
|
|
184
472
|
}
|
|
185
473
|
function hasPotentialClientBoundaryReference(reference, identifierReferences) {
|
|
186
474
|
return (reference.sideEffect ||
|
|
475
|
+
reference.specifiers.some((specifier) => specifier.kind === "namespace" && identifierReferences.has(specifier.localName)) ||
|
|
187
476
|
reference.localNames.some((localName) => identifierReferences.has(localName) && startsUppercase(localName)));
|
|
188
477
|
}
|
|
478
|
+
function renderedImportedExportNames(reference, jsxComponentRoots) {
|
|
479
|
+
if (reference.sideEffect) {
|
|
480
|
+
return undefined;
|
|
481
|
+
}
|
|
482
|
+
const names = new Set();
|
|
483
|
+
for (const specifier of reference.specifiers) {
|
|
484
|
+
if (!jsxComponentRoots.has(specifier.localName)) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (specifier.kind === "namespace") {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
names.add(specifier.importedName);
|
|
491
|
+
}
|
|
492
|
+
return Array.from(names);
|
|
493
|
+
}
|
|
494
|
+
function renderedLocalExportNames(reference, exportInfo) {
|
|
495
|
+
const localNames = new Set(reference.localNames);
|
|
496
|
+
const rendered = exportInfo
|
|
497
|
+
.filter((info) => info.renderedComponentRoots.some((root) => localNames.has(root)))
|
|
498
|
+
.map((info) => info.name);
|
|
499
|
+
return rendered.length === 0 ? ["default"] : rendered;
|
|
500
|
+
}
|
|
501
|
+
function matchesInferredExportNames(importedExportNames, inferredExportNames) {
|
|
502
|
+
if (importedExportNames === undefined) {
|
|
503
|
+
return inferredExportNames.length > 0;
|
|
504
|
+
}
|
|
505
|
+
return importedExportNames.some((name) => inferredExportNames.includes(name));
|
|
506
|
+
}
|
|
507
|
+
function serverOnlyClientImportReferenceDiagnostic(options) {
|
|
508
|
+
return {
|
|
509
|
+
code: clientBoundaryInferenceServerOnlyReferenceCode,
|
|
510
|
+
filename: options.filename,
|
|
511
|
+
level: "warn",
|
|
512
|
+
localNames: options.reference.localNames,
|
|
513
|
+
message: `${options.filename}: server-only component import ${JSON.stringify(options.reference.source)} ` +
|
|
514
|
+
"uses client runtime syntax but is marked with server-only semantics. Automatic client " +
|
|
515
|
+
"boundary detection skipped it. Move the interactive UI behind a .client module or add an " +
|
|
516
|
+
"explicit clientBoundaryImports entry.",
|
|
517
|
+
source: options.reference.source,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
189
520
|
function unsupportedClientImportReferenceDiagnostic(options) {
|
|
190
521
|
if (options.reference.sideEffect) {
|
|
191
522
|
return undefined;
|
|
@@ -195,7 +526,7 @@ function unsupportedClientImportReferenceDiagnostic(options) {
|
|
|
195
526
|
return undefined;
|
|
196
527
|
}
|
|
197
528
|
return {
|
|
198
|
-
code:
|
|
529
|
+
code: clientBoundaryInferenceUnsupportedReferenceCode,
|
|
199
530
|
filename: options.filename,
|
|
200
531
|
level: "warn",
|
|
201
532
|
localNames,
|
|
@@ -241,52 +572,6 @@ async function resolveAppLocalModuleUncached(importer, specifier) {
|
|
|
241
572
|
}
|
|
242
573
|
throw new Error(`${importer}: could not resolve app-local import ${JSON.stringify(specifier)}.`);
|
|
243
574
|
}
|
|
244
|
-
function sourceModuleCandidates(base) {
|
|
245
|
-
if (hasSourceModuleExtension(base)) {
|
|
246
|
-
return [base, ...typescriptSourceModuleCandidates(base)];
|
|
247
|
-
}
|
|
248
|
-
if (extname(base) !== "") {
|
|
249
|
-
return [];
|
|
250
|
-
}
|
|
251
|
-
return [
|
|
252
|
-
`${base}.ts`,
|
|
253
|
-
`${base}.tsx`,
|
|
254
|
-
`${base}.mreact.tsx`,
|
|
255
|
-
`${base}.js`,
|
|
256
|
-
`${base}.jsx`,
|
|
257
|
-
`${base}.mjs`,
|
|
258
|
-
`${base}.mts`,
|
|
259
|
-
`${base}.cjs`,
|
|
260
|
-
`${base}.cts`,
|
|
261
|
-
join(base, "index.ts"),
|
|
262
|
-
join(base, "index.tsx"),
|
|
263
|
-
join(base, "index.mreact.tsx"),
|
|
264
|
-
join(base, "index.js"),
|
|
265
|
-
join(base, "index.jsx"),
|
|
266
|
-
join(base, "index.mjs"),
|
|
267
|
-
join(base, "index.mts"),
|
|
268
|
-
join(base, "index.cjs"),
|
|
269
|
-
join(base, "index.cts"),
|
|
270
|
-
];
|
|
271
|
-
}
|
|
272
|
-
function hasSourceModuleExtension(path) {
|
|
273
|
-
return /\.(?:mreact\.tsx|tsx?|jsx?|mjs|mts|cjs|cts)$/.test(path);
|
|
274
|
-
}
|
|
275
|
-
function typescriptSourceModuleCandidates(path) {
|
|
276
|
-
if (path.endsWith(".js")) {
|
|
277
|
-
return [`${path.slice(0, -3)}.ts`, `${path.slice(0, -3)}.tsx`];
|
|
278
|
-
}
|
|
279
|
-
if (path.endsWith(".jsx")) {
|
|
280
|
-
return [`${path.slice(0, -4)}.tsx`];
|
|
281
|
-
}
|
|
282
|
-
if (path.endsWith(".mjs")) {
|
|
283
|
-
return [`${path.slice(0, -4)}.mts`];
|
|
284
|
-
}
|
|
285
|
-
if (path.endsWith(".cjs")) {
|
|
286
|
-
return [`${path.slice(0, -4)}.cts`];
|
|
287
|
-
}
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
575
|
async function isFile(path) {
|
|
291
576
|
try {
|
|
292
577
|
return (await stat(path)).isFile();
|
|
@@ -295,14 +580,25 @@ async function isFile(path) {
|
|
|
295
580
|
return false;
|
|
296
581
|
}
|
|
297
582
|
}
|
|
298
|
-
function readCachedFile(cache, filename) {
|
|
583
|
+
async function readCachedFile(cache, filename) {
|
|
584
|
+
const signature = await sourceFileSignature(filename);
|
|
299
585
|
const cached = cache.sourceByFile.get(filename);
|
|
300
586
|
if (cached !== undefined) {
|
|
301
|
-
|
|
587
|
+
const source = await cached;
|
|
588
|
+
if (source.signature === signature) {
|
|
589
|
+
return source.source;
|
|
590
|
+
}
|
|
302
591
|
}
|
|
303
|
-
const source = readFile(filename, "utf8")
|
|
592
|
+
const source = readFile(filename, "utf8").then((value) => ({
|
|
593
|
+
signature,
|
|
594
|
+
source: value,
|
|
595
|
+
}));
|
|
304
596
|
cache.sourceByFile.set(filename, source);
|
|
305
|
-
return source;
|
|
597
|
+
return (await source).source;
|
|
598
|
+
}
|
|
599
|
+
async function sourceFileSignature(filename) {
|
|
600
|
+
const stats = await stat(filename);
|
|
601
|
+
return `${stats.mtimeMs}\0${stats.size}`;
|
|
306
602
|
}
|
|
307
603
|
export function routeIdForPath(path) {
|
|
308
604
|
if (path === "/") {
|
|
@@ -371,9 +667,14 @@ export async function buildNavigationRuntimeBundle(options = {}) {
|
|
|
371
667
|
});
|
|
372
668
|
}
|
|
373
669
|
export async function buildClientRouteOutput(options) {
|
|
374
|
-
const
|
|
670
|
+
const moduleContext = createCompilerModuleContext({
|
|
671
|
+
code: options.code,
|
|
672
|
+
filename: options.filename,
|
|
673
|
+
});
|
|
674
|
+
const compiled = transformCompilerModuleContext({
|
|
375
675
|
code: options.code,
|
|
376
676
|
filename: options.filename,
|
|
677
|
+
moduleContext,
|
|
377
678
|
target: "client",
|
|
378
679
|
dev: options.minify !== true,
|
|
379
680
|
});
|
|
@@ -384,7 +685,8 @@ export async function buildClientRouteOutput(options) {
|
|
|
384
685
|
}
|
|
385
686
|
const clientNavigation = options.clientNavigation ?? detectClientNavigationHint(options.code);
|
|
386
687
|
const clientReferenceManifest = options.clientReferenceManifest ?? (await inferClientReferenceManifestForBundle(options));
|
|
387
|
-
const
|
|
688
|
+
const clientReferenceImportBlock = emitClientReferenceImportBlock(options.clientReferenceImports ?? []);
|
|
689
|
+
const clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest, options.clientReferenceImports ?? []);
|
|
388
690
|
const routeComponentExpression = routeComponentExpressionForComponents(compiled.metadata.components);
|
|
389
691
|
const routeId = routeIdForPath(options.routePath);
|
|
390
692
|
const routeUsesCells = detectRouteCellStateHint(compiled.code);
|
|
@@ -476,7 +778,7 @@ function __mreactDropMismatchedRouteState(previousState, nextState) {
|
|
|
476
778
|
}
|
|
477
779
|
`
|
|
478
780
|
: "";
|
|
479
|
-
const entry = `${compiled.code}
|
|
781
|
+
const entry = `${clientReferenceImportBlock}${compiled.code}
|
|
480
782
|
|
|
481
783
|
const __mreactRouteId = ${JSON.stringify(routeId)};
|
|
482
784
|
const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
|
|
@@ -504,7 +806,7 @@ export function __mreactHydrateRoute() {
|
|
|
504
806
|
if (__mreactMarker === null || __mreactComponent === undefined) {
|
|
505
807
|
return;
|
|
506
808
|
}
|
|
507
|
-
${routeCellHydrationStart}${routeCellHydrationIndent}if (__mreactHydrateClientBoundaries(
|
|
809
|
+
${routeCellHydrationStart}${routeCellHydrationIndent}if (__mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
|
|
508
810
|
${routeCellHydrationIndent} __mreactMarker.setAttribute("data-mreact-hydrated", "true");
|
|
509
811
|
${routeCellHydrationIndent} return;
|
|
510
812
|
${routeCellHydrationIndent}}
|
|
@@ -1267,7 +1569,7 @@ function __mreactApplyOutOfOrderFragments(root) {
|
|
|
1267
1569
|
}
|
|
1268
1570
|
|
|
1269
1571
|
function __mreactHydrateClientBoundaries(marker, references, components) {
|
|
1270
|
-
if (!Array.isArray(references) || references.length === 0) {
|
|
1572
|
+
if (components.size === 0 && (!Array.isArray(references) || references.length === 0)) {
|
|
1271
1573
|
return false;
|
|
1272
1574
|
}
|
|
1273
1575
|
|
|
@@ -1754,10 +2056,15 @@ export function currentDevtoolsEmitter() { return undefined; }`,
|
|
|
1754
2056
|
return undefined;
|
|
1755
2057
|
}
|
|
1756
2058
|
const source = await readFile(args.path, "utf8");
|
|
1757
|
-
const
|
|
2059
|
+
const moduleContext = createCompilerModuleContext({
|
|
2060
|
+
code: source,
|
|
2061
|
+
filename: args.path,
|
|
2062
|
+
});
|
|
2063
|
+
const output = transformCompilerModuleContext({
|
|
1758
2064
|
code: source,
|
|
1759
2065
|
dev: true,
|
|
1760
2066
|
filename: args.path,
|
|
2067
|
+
moduleContext,
|
|
1761
2068
|
target: "client",
|
|
1762
2069
|
});
|
|
1763
2070
|
if (output.diagnostics.length > 0) {
|
|
@@ -1803,32 +2110,61 @@ function detectRouteCellStateHint(code) {
|
|
|
1803
2110
|
: new RegExp(`(?:${callExpression})\\s*\\(`).test(code);
|
|
1804
2111
|
}
|
|
1805
2112
|
async function inferClientReferenceManifestForBundle(options) {
|
|
2113
|
+
const cache = createClientRouteInferenceCache();
|
|
2114
|
+
const moduleContext = await compilerModuleContextForSource({
|
|
2115
|
+
cache,
|
|
2116
|
+
code: options.code,
|
|
2117
|
+
filename: options.filename,
|
|
2118
|
+
});
|
|
1806
2119
|
const inference = await inferClientRouteModule({
|
|
2120
|
+
cache,
|
|
1807
2121
|
code: options.code,
|
|
1808
2122
|
filename: options.filename,
|
|
2123
|
+
moduleContext,
|
|
1809
2124
|
routePath: options.routePath,
|
|
1810
2125
|
});
|
|
1811
2126
|
if (inference.clientBoundaryImports.length === 0) {
|
|
1812
2127
|
return [];
|
|
1813
2128
|
}
|
|
1814
|
-
const output =
|
|
2129
|
+
const output = transformCompilerModuleContext({
|
|
1815
2130
|
code: options.code,
|
|
1816
2131
|
clientBoundaryImports: inference.clientBoundaryImports,
|
|
1817
2132
|
dev: true,
|
|
1818
2133
|
filename: options.filename,
|
|
2134
|
+
moduleContext,
|
|
1819
2135
|
target: "server",
|
|
1820
2136
|
});
|
|
1821
2137
|
return output.metadata.clientReferenceManifest ?? [];
|
|
1822
2138
|
}
|
|
1823
|
-
function
|
|
2139
|
+
function emitClientReferenceImportBlock(imports) {
|
|
2140
|
+
if (imports.length === 0) {
|
|
2141
|
+
return "";
|
|
2142
|
+
}
|
|
2143
|
+
return `${imports.map((reference, index) => {
|
|
2144
|
+
const localName = clientReferenceLocalName(index);
|
|
2145
|
+
return isIdentifierName(reference.exportName)
|
|
2146
|
+
? `import { ${reference.exportName} as ${localName} } from ${JSON.stringify(reference.importSource)};`
|
|
2147
|
+
: `import * as ${localName} from ${JSON.stringify(reference.importSource)};`;
|
|
2148
|
+
}).join("\n")}\n`;
|
|
2149
|
+
}
|
|
2150
|
+
function emitClientReferenceRegistry(manifest, imports) {
|
|
2151
|
+
const importedExpressions = new Map(imports.map((reference, index) => [
|
|
2152
|
+
reference.name,
|
|
2153
|
+
isIdentifierName(reference.exportName)
|
|
2154
|
+
? clientReferenceLocalName(index)
|
|
2155
|
+
: `${clientReferenceLocalName(index)}[${JSON.stringify(reference.exportName)}]`,
|
|
2156
|
+
]));
|
|
1824
2157
|
const entries = manifest.flatMap((reference) => {
|
|
1825
|
-
const expression = clientReferenceExpression(reference.name);
|
|
2158
|
+
const expression = importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
|
|
1826
2159
|
return expression === undefined
|
|
1827
2160
|
? []
|
|
1828
2161
|
: [` [${JSON.stringify(reference.name)}, ${expression}],`];
|
|
1829
2162
|
});
|
|
1830
2163
|
return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
|
|
1831
2164
|
}
|
|
2165
|
+
function clientReferenceLocalName(index) {
|
|
2166
|
+
return `__mreactClientReference${index}`;
|
|
2167
|
+
}
|
|
1832
2168
|
function clientReferenceExpression(name) {
|
|
1833
2169
|
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(name) ? name : undefined;
|
|
1834
2170
|
}
|