@reckona/mreact-router 0.0.31 → 0.0.33
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/dist/build.d.ts.map +1 -1
- package/dist/build.js +59 -89
- 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 +433 -98
- package/dist/client.js.map +1 -1
- package/dist/module-runner.d.ts +3 -1
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +25 -5
- package/dist/module-runner.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +48 -32
- 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 +5 -13
- 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 +10 -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
|
|
|
@@ -1677,7 +1979,6 @@ function __mreactResumeChildren(current, next) {
|
|
|
1677
1979
|
};
|
|
1678
1980
|
}
|
|
1679
1981
|
function workspaceRuntimePlugin(options) {
|
|
1680
|
-
const routeDir = dirname(options.routeFile);
|
|
1681
1982
|
const packageFile = (monorepoDir, packageName, entry) => workspacePackageFile({
|
|
1682
1983
|
currentFileUrl: import.meta.url,
|
|
1683
1984
|
entry,
|
|
@@ -1750,14 +2051,19 @@ export function currentDevtoolsEmitter() { return undefined; }`,
|
|
|
1750
2051
|
loader: "ts",
|
|
1751
2052
|
}));
|
|
1752
2053
|
buildApi.onLoad({ filter: /\.(?:mreact\.)?[cm]?[jt]sx$/ }, async (args) => {
|
|
1753
|
-
if (!
|
|
2054
|
+
if (!isRouteClientDependencySourcePath(args.path, options.routeFile)) {
|
|
1754
2055
|
return undefined;
|
|
1755
2056
|
}
|
|
1756
2057
|
const source = await readFile(args.path, "utf8");
|
|
1757
|
-
const
|
|
2058
|
+
const moduleContext = createCompilerModuleContext({
|
|
2059
|
+
code: source,
|
|
2060
|
+
filename: args.path,
|
|
2061
|
+
});
|
|
2062
|
+
const output = transformCompilerModuleContext({
|
|
1758
2063
|
code: source,
|
|
1759
2064
|
dev: true,
|
|
1760
2065
|
filename: args.path,
|
|
2066
|
+
moduleContext,
|
|
1761
2067
|
target: "client",
|
|
1762
2068
|
});
|
|
1763
2069
|
if (output.diagnostics.length > 0) {
|
|
@@ -1774,8 +2080,8 @@ export function currentDevtoolsEmitter() { return undefined; }`,
|
|
|
1774
2080
|
},
|
|
1775
2081
|
};
|
|
1776
2082
|
}
|
|
1777
|
-
function
|
|
1778
|
-
return path
|
|
2083
|
+
function isRouteClientDependencySourcePath(path, routeFile) {
|
|
2084
|
+
return path !== routeFile && !path.includes(`${sep}node_modules${sep}`);
|
|
1779
2085
|
}
|
|
1780
2086
|
/**
|
|
1781
2087
|
* Detects the `export const clientNavigation = false` hint in a page module
|
|
@@ -1803,32 +2109,61 @@ function detectRouteCellStateHint(code) {
|
|
|
1803
2109
|
: new RegExp(`(?:${callExpression})\\s*\\(`).test(code);
|
|
1804
2110
|
}
|
|
1805
2111
|
async function inferClientReferenceManifestForBundle(options) {
|
|
2112
|
+
const cache = createClientRouteInferenceCache();
|
|
2113
|
+
const moduleContext = await compilerModuleContextForSource({
|
|
2114
|
+
cache,
|
|
2115
|
+
code: options.code,
|
|
2116
|
+
filename: options.filename,
|
|
2117
|
+
});
|
|
1806
2118
|
const inference = await inferClientRouteModule({
|
|
2119
|
+
cache,
|
|
1807
2120
|
code: options.code,
|
|
1808
2121
|
filename: options.filename,
|
|
2122
|
+
moduleContext,
|
|
1809
2123
|
routePath: options.routePath,
|
|
1810
2124
|
});
|
|
1811
2125
|
if (inference.clientBoundaryImports.length === 0) {
|
|
1812
2126
|
return [];
|
|
1813
2127
|
}
|
|
1814
|
-
const output =
|
|
2128
|
+
const output = transformCompilerModuleContext({
|
|
1815
2129
|
code: options.code,
|
|
1816
2130
|
clientBoundaryImports: inference.clientBoundaryImports,
|
|
1817
2131
|
dev: true,
|
|
1818
2132
|
filename: options.filename,
|
|
2133
|
+
moduleContext,
|
|
1819
2134
|
target: "server",
|
|
1820
2135
|
});
|
|
1821
2136
|
return output.metadata.clientReferenceManifest ?? [];
|
|
1822
2137
|
}
|
|
1823
|
-
function
|
|
2138
|
+
function emitClientReferenceImportBlock(imports) {
|
|
2139
|
+
if (imports.length === 0) {
|
|
2140
|
+
return "";
|
|
2141
|
+
}
|
|
2142
|
+
return `${imports.map((reference, index) => {
|
|
2143
|
+
const localName = clientReferenceLocalName(index);
|
|
2144
|
+
return isIdentifierName(reference.exportName)
|
|
2145
|
+
? `import { ${reference.exportName} as ${localName} } from ${JSON.stringify(reference.importSource)};`
|
|
2146
|
+
: `import * as ${localName} from ${JSON.stringify(reference.importSource)};`;
|
|
2147
|
+
}).join("\n")}\n`;
|
|
2148
|
+
}
|
|
2149
|
+
function emitClientReferenceRegistry(manifest, imports) {
|
|
2150
|
+
const importedExpressions = new Map(imports.map((reference, index) => [
|
|
2151
|
+
reference.name,
|
|
2152
|
+
isIdentifierName(reference.exportName)
|
|
2153
|
+
? clientReferenceLocalName(index)
|
|
2154
|
+
: `${clientReferenceLocalName(index)}[${JSON.stringify(reference.exportName)}]`,
|
|
2155
|
+
]));
|
|
1824
2156
|
const entries = manifest.flatMap((reference) => {
|
|
1825
|
-
const expression = clientReferenceExpression(reference.name);
|
|
2157
|
+
const expression = importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
|
|
1826
2158
|
return expression === undefined
|
|
1827
2159
|
? []
|
|
1828
2160
|
: [` [${JSON.stringify(reference.name)}, ${expression}],`];
|
|
1829
2161
|
});
|
|
1830
2162
|
return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
|
|
1831
2163
|
}
|
|
2164
|
+
function clientReferenceLocalName(index) {
|
|
2165
|
+
return `__mreactClientReference${index}`;
|
|
2166
|
+
}
|
|
1832
2167
|
function clientReferenceExpression(name) {
|
|
1833
2168
|
return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*$/.test(name) ? name : undefined;
|
|
1834
2169
|
}
|