@reckona/mreact-router 0.0.31 → 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/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 { dirname, extname, join } from "node:path";
4
- import { collectIdentifierReferenceNames, collectJsxComponentRootNames, collectStaticExportReferences, collectStaticImportReferences, formatDiagnostic, hasClientRuntimeSyntax, transform, } from "@reckona/mreact-compiler";
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
- exportsByFile: new Map(),
33
- importsByFile: new Map(),
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
- return await inferClientRouteModuleSource({
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
- return hasClientRuntimeSyntax({ code });
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
- if (isClientRouteSource(options.code)) {
61
- return { client: true, clientBoundaryImports: [], diagnostics: [] };
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 { client: false, clientBoundaryImports: [], diagnostics: [] };
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
- const jsxComponentRoots = new Set(collectJsxComponentRootNames({
72
- code: options.code,
73
- filename: options.filename,
74
- }));
75
- const identifierReferences = new Set(collectIdentifierReferenceNames({
76
- code: options.code,
77
- filename: options.filename,
78
- }));
79
- for (const reference of await staticImportReferencesForSource(options)) {
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 await staticExportReferencesForSource(options)) {
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.client) {
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 || clientProxy,
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
- async function staticImportReferencesForSource(options) {
154
- const cached = options.cache.importsByFile.get(options.filename);
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 imports = Promise.resolve().then(() => collectStaticImportReferences({
159
- code: options.code,
160
- filename: options.filename,
161
- }));
162
- options.cache.importsByFile.set(options.filename, imports);
163
- return imports;
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 staticExportReferencesForSource(options) {
166
- const cached = options.cache.exportsByFile.get(options.filename);
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 exports = Promise.resolve().then(() => collectStaticExportReferences({
452
+ const context = Promise.resolve().then(() => createCompilerModuleContext({
171
453
  code: options.code,
172
454
  filename: options.filename,
173
455
  }));
174
- options.cache.exportsByFile.set(options.filename, exports);
175
- return exports;
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: "MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE",
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
- return cached;
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 compiled = transform({
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 clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest);
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(__mreactMarker, __mreactClientReferences, __mreactClientReferenceComponents)) {
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 output = transform({
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 = transform({
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 emitClientReferenceRegistry(manifest) {
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
  }