@reckona/mreact-router 0.0.196 → 0.0.197

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,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { builtinModules } from "node:module";
4
- import { basename, dirname, extname, isAbsolute, join, relative, sep } from "node:path";
4
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { analyzeBoundaryGraph, collectClientRouteModuleAnalysis, formatDiagnostic, } from "@reckona/mreact-compiler";
6
6
  import { collectClientRouteModuleAnalysisFromContext, createCompilerModuleContext, hasUnguardedBrowserGlobalReference, readTopLevelBooleanExport, readTopLevelBooleanExportFromContext, stripTypeScriptWithOxc, transformCompilerModuleContext, } from "@reckona/mreact-compiler/internal";
7
7
  import { assetPath } from "./assets.js";
@@ -54,6 +54,16 @@ export async function isClientRouteModule(options) {
54
54
  export async function inferClientRouteModule(options) {
55
55
  const cache = options.cache ?? createClientRouteInferenceCache();
56
56
  const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
57
+ const componentCollector = options.collectComponents
58
+ ? {
59
+ clientReachable: new Set(),
60
+ components: new Map(),
61
+ localExportNamesByFile: new Map(),
62
+ pendingExportValidations: [],
63
+ reachable: new Set(),
64
+ staticExportEdges: [],
65
+ }
66
+ : undefined;
57
67
  const code = await transformClientRouteSource({
58
68
  code: options.code,
59
69
  filename: options.filename,
@@ -63,9 +73,11 @@ export async function inferClientRouteModule(options) {
63
73
  const routeInference = await inferClientRouteModuleSource({
64
74
  cache,
65
75
  code,
76
+ componentCollector,
66
77
  filename: options.filename,
67
78
  ...(sourceTransform === undefined ? { moduleContext: options.moduleContext } : {}),
68
79
  root: true,
80
+ routeEntry: true,
69
81
  seen: new Set(),
70
82
  sourceTransform,
71
83
  });
@@ -78,21 +90,38 @@ export async function inferClientRouteModule(options) {
78
90
  }))
79
91
  : routeInference;
80
92
  if (options.appDir === undefined) {
81
- return withClientRouteDiagnosticPath(mergedRouteInference, options.routePath);
93
+ const exportDiagnostics = finalizeClientRouteComponentExportValidations(componentCollector);
94
+ return withClientRouteDiagnosticPath({
95
+ ...mergedRouteInference,
96
+ ...(componentCollector === undefined
97
+ ? {}
98
+ : {
99
+ components: normalizedClientRouteComponents(componentCollector, options.filename),
100
+ }),
101
+ diagnostics: [...mergedRouteInference.diagnostics, ...exportDiagnostics],
102
+ }, options.routePath);
82
103
  }
83
104
  const shellInferences = await inferClientRouteShellModules({
84
105
  appDir: options.appDir,
85
106
  cache,
107
+ componentCollector,
86
108
  filename: options.filename,
87
109
  sourceTransform,
88
110
  });
111
+ const exportDiagnostics = finalizeClientRouteComponentExportValidations(componentCollector);
89
112
  return withClientRouteDiagnosticPath({
90
113
  client: mergedRouteInference.client || shellInferences.some((inference) => inference.client),
91
114
  clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
92
115
  clientBoundaryFallbackImports: mergedRouteInference.clientBoundaryFallbackImports,
116
+ ...(componentCollector === undefined
117
+ ? {}
118
+ : {
119
+ components: normalizedClientRouteComponents(componentCollector, options.filename),
120
+ }),
93
121
  diagnostics: [
94
122
  ...mergedRouteInference.diagnostics,
95
123
  ...shellInferences.flatMap((inference) => inference.diagnostics),
124
+ ...exportDiagnostics,
96
125
  ],
97
126
  }, options.routePath);
98
127
  }
@@ -161,6 +190,7 @@ export async function collectClientRouteReferences(options) {
161
190
  filename: options.filename,
162
191
  moduleContext: routeModuleContext,
163
192
  root: true,
193
+ routeEntry: true,
164
194
  seen: new Set(),
165
195
  sourceTransform,
166
196
  });
@@ -184,6 +214,7 @@ export async function collectClientRouteReferences(options) {
184
214
  filename: sourceOptions.filename,
185
215
  moduleContext,
186
216
  root: true,
217
+ routeEntry: false,
187
218
  seen: new Set(),
188
219
  sourceTransform,
189
220
  }));
@@ -227,6 +258,7 @@ export async function collectClientRouteReferences(options) {
227
258
  filename: shell,
228
259
  moduleContext,
229
260
  root: true,
261
+ routeEntry: false,
230
262
  seen: new Set(),
231
263
  sourceTransform,
232
264
  }),
@@ -343,8 +375,10 @@ async function inferClientRouteShellModules(options) {
343
375
  return await inferClientRouteModuleSource({
344
376
  cache: options.cache,
345
377
  code,
378
+ componentCollector: options.componentCollector,
346
379
  filename: shell,
347
380
  root: true,
381
+ routeEntry: false,
348
382
  seen: new Set(),
349
383
  sourceTransform: options.sourceTransform,
350
384
  });
@@ -366,6 +400,256 @@ export function isClientRouteSource(code) {
366
400
  const analysis = collectClientRouteModuleAnalysis({ code });
367
401
  return (analysis.hasUseClientDirective || (!analysis.hasUseServerDirective && analysis.clientRuntime));
368
402
  }
403
+ function collectClientRouteComponentsForModule(collector, options) {
404
+ if (collector === undefined) {
405
+ return;
406
+ }
407
+ if (options.root) {
408
+ markClientRouteComponentReachable(collector, options.filename, ["default"]);
409
+ }
410
+ const localExportNames = collector.localExportNamesByFile.get(options.filename) ?? new Set();
411
+ for (const info of options.analysis.topLevelExportRenderInfo) {
412
+ localExportNames.add(info.name);
413
+ }
414
+ collector.localExportNamesByFile.set(options.filename, localExportNames);
415
+ const serverOnly = options.analysis.hasUseServerDirective || hasServerOnlyImports(options.analysis);
416
+ const explicitClient = isExplicitClientRouteSource(options.analysis, options.filename);
417
+ for (const info of options.analysis.topLevelExportRenderInfo) {
418
+ const component = {
419
+ classification: serverOnly
420
+ ? "server-only"
421
+ : explicitClient || info.clientRuntime
422
+ ? options.routeEntry && info.name === "default"
423
+ ? "client-route"
424
+ : "client-boundary"
425
+ : "server-render",
426
+ exportName: info.name,
427
+ file: options.filename,
428
+ origin: clientRouteComponentOrigin({
429
+ analysis: options.analysis,
430
+ filename: options.filename,
431
+ info,
432
+ serverOnly,
433
+ }),
434
+ };
435
+ collector.components.set(clientRouteComponentKey(component.file, component.exportName), component);
436
+ }
437
+ for (const info of options.analysis.topLevelExportRenderInfo) {
438
+ if (!isClientRouteComponentReachable(collector, options.filename, info.name)) {
439
+ continue;
440
+ }
441
+ const renderedNames = new Set([
442
+ ...(options.analysis.reachableExportRenderedComponentNames[info.name] ?? []),
443
+ ...(options.analysis.reachableExportRenderedComponentRoots[info.name] ?? []),
444
+ ]);
445
+ for (const rendered of options.analysis.topLevelExportRenderInfo) {
446
+ if (rendered.name !== "default" &&
447
+ (renderedNames.has(rendered.name) || renderedNames.has(rendered.localName ?? rendered.name))) {
448
+ markClientRouteComponentReachable(collector, options.filename, [rendered.name], {
449
+ clientExecution: clientRouteComponentRunsOnClient(collector, options.filename, info.name),
450
+ });
451
+ }
452
+ }
453
+ }
454
+ }
455
+ function clientRouteComponentOrigin(options) {
456
+ if (options.analysis.hasUseServerDirective) {
457
+ return "use-server-directive";
458
+ }
459
+ if (options.serverOnly) {
460
+ return "server-only-import";
461
+ }
462
+ if (options.analysis.hasUseClientDirective) {
463
+ return "use-client-directive";
464
+ }
465
+ if (/\.compat(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
466
+ return "compat-filename";
467
+ }
468
+ if (/\.client(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
469
+ return "client-filename";
470
+ }
471
+ return options.info.clientRuntime ? "inferred-client-runtime" : "server-render";
472
+ }
473
+ function normalizedClientRouteComponents(collector, routeFile) {
474
+ return Array.from(collector.components.values())
475
+ .filter((component) => collector.reachable.has(clientRouteComponentKey(component.file, component.exportName)) ||
476
+ collector.reachable.has(clientRouteComponentKey(component.file, "*")))
477
+ .map((component) => component.classification === "server-render" &&
478
+ (collector.clientReachable.has(clientRouteComponentKey(component.file, component.exportName)) ||
479
+ collector.clientReachable.has(clientRouteComponentKey(component.file, "*")))
480
+ ? { ...component, classification: "shared" }
481
+ : component)
482
+ .sort((left, right) => {
483
+ const leftRoute = left.file === routeFile && left.exportName === "default";
484
+ const rightRoute = right.file === routeFile && right.exportName === "default";
485
+ if (leftRoute !== rightRoute) {
486
+ return leftRoute ? -1 : 1;
487
+ }
488
+ return left.file === right.file
489
+ ? left.exportName === right.exportName
490
+ ? left.classification.localeCompare(right.classification)
491
+ : left.exportName.localeCompare(right.exportName)
492
+ : left.file.localeCompare(right.file);
493
+ });
494
+ }
495
+ function clientRouteComponentKey(file, exportName) {
496
+ return `${file}\0${exportName}`;
497
+ }
498
+ function markClientRouteComponentReachable(collector, file, exportNames, options = {}) {
499
+ if (collector === undefined) {
500
+ return;
501
+ }
502
+ if (exportNames === undefined) {
503
+ collector.reachable.add(clientRouteComponentKey(file, "*"));
504
+ if (options.clientExecution === true) {
505
+ collector.clientReachable.add(clientRouteComponentKey(file, "*"));
506
+ }
507
+ return;
508
+ }
509
+ for (const exportName of exportNames) {
510
+ const key = clientRouteComponentKey(file, exportName);
511
+ collector.reachable.add(key);
512
+ if (options.clientExecution === true) {
513
+ collector.clientReachable.add(key);
514
+ }
515
+ }
516
+ }
517
+ function clientRouteComponentRunsOnClient(collector, file, exportName) {
518
+ if (collector === undefined) {
519
+ return false;
520
+ }
521
+ const key = clientRouteComponentKey(file, exportName);
522
+ const component = collector.components.get(key);
523
+ return (component?.classification === "client-boundary" ||
524
+ component?.classification === "client-route" ||
525
+ component?.classification === "shared" ||
526
+ collector.clientReachable.has(key) ||
527
+ collector.clientReachable.has(clientRouteComponentKey(file, "*")));
528
+ }
529
+ function isClientRouteComponentReachable(collector, file, exportName) {
530
+ return (collector === undefined ||
531
+ collector.reachable.has(clientRouteComponentKey(file, exportName)) ||
532
+ collector.reachable.has(clientRouteComponentKey(file, "*")));
533
+ }
534
+ function propagateClientRouteComponentStaticExport(collector, file, resolved, reference) {
535
+ if (collector === undefined) {
536
+ return;
537
+ }
538
+ const wildcardReachable = collector.reachable.has(clientRouteComponentKey(file, "*"));
539
+ const wildcardClientExecution = clientRouteComponentRunsOnClient(collector, file, "*");
540
+ if (reference.exportAll) {
541
+ if (wildcardReachable) {
542
+ markClientRouteComponentReachable(collector, resolved, undefined, {
543
+ clientExecution: wildcardClientExecution,
544
+ });
545
+ }
546
+ for (const key of collector.reachable) {
547
+ const [reachableFile, exportName] = key.split("\0");
548
+ if (reachableFile === file && exportName !== undefined && exportName !== "*") {
549
+ markClientRouteComponentReachable(collector, resolved, [exportName], {
550
+ clientExecution: clientRouteComponentRunsOnClient(collector, file, exportName),
551
+ });
552
+ }
553
+ }
554
+ return;
555
+ }
556
+ for (const specifier of reference.specifiers) {
557
+ if (wildcardReachable ||
558
+ collector.reachable.has(clientRouteComponentKey(file, specifier.exportedName))) {
559
+ markClientRouteComponentReachable(collector, resolved, [specifier.localName], {
560
+ clientExecution: wildcardClientExecution ||
561
+ clientRouteComponentRunsOnClient(collector, file, specifier.exportedName),
562
+ });
563
+ }
564
+ }
565
+ if (reference.specifiers.length === 0) {
566
+ for (const exportName of reference.exportedNames) {
567
+ if (wildcardReachable || collector.reachable.has(clientRouteComponentKey(file, exportName))) {
568
+ markClientRouteComponentReachable(collector, resolved, [exportName], {
569
+ clientExecution: wildcardClientExecution ||
570
+ clientRouteComponentRunsOnClient(collector, file, exportName),
571
+ });
572
+ }
573
+ }
574
+ }
575
+ }
576
+ function collectUnknownClientRouteComponents(collector, options) {
577
+ if (collector === undefined) {
578
+ return;
579
+ }
580
+ for (const exportName of options.exportNames ?? ["*"]) {
581
+ const component = {
582
+ classification: "unknown",
583
+ exportName,
584
+ file: options.file,
585
+ origin: "unresolved-reference",
586
+ };
587
+ collector.components.set(clientRouteComponentKey(component.file, component.exportName), component);
588
+ markClientRouteComponentReachable(collector, component.file, [component.exportName]);
589
+ }
590
+ }
591
+ function collectClientRoutePendingExportValidation(collector, options) {
592
+ if (collector === undefined || options.exportNames.length === 0) {
593
+ return;
594
+ }
595
+ collector.pendingExportValidations.push(options);
596
+ }
597
+ function collectClientRouteStaticExportEdge(collector, options) {
598
+ collector?.staticExportEdges.push(options);
599
+ }
600
+ function finalizeClientRouteComponentExportValidations(collector) {
601
+ if (collector === undefined) {
602
+ return [];
603
+ }
604
+ const exportNamesByFile = new Map();
605
+ for (const [file, names] of collector.localExportNamesByFile) {
606
+ exportNamesByFile.set(file, new Set(names));
607
+ }
608
+ let changed = true;
609
+ while (changed) {
610
+ changed = false;
611
+ for (const edge of collector.staticExportEdges) {
612
+ const exportedNames = exportNamesByFile.get(edge.file) ?? new Set();
613
+ const sourceNames = exportNamesByFile.get(edge.resolved) ?? new Set();
614
+ const namesToAdd = edge.reference.exportAll
615
+ ? new Set([...sourceNames].filter((exportName) => exportName !== "default"))
616
+ : new Set(edge.reference.specifiers
617
+ .filter((specifier) => sourceNames.has(specifier.localName))
618
+ .map((specifier) => specifier.exportedName));
619
+ for (const exportName of namesToAdd) {
620
+ if (!exportedNames.has(exportName)) {
621
+ exportedNames.add(exportName);
622
+ changed = true;
623
+ }
624
+ }
625
+ exportNamesByFile.set(edge.file, exportedNames);
626
+ }
627
+ }
628
+ const diagnostics = [];
629
+ const seen = new Set();
630
+ for (const pending of collector.pendingExportValidations) {
631
+ const availableExportNames = exportNamesByFile.get(pending.file) ?? new Set();
632
+ const missingExportNames = pending.exportNames.filter((exportName) => !availableExportNames.has(exportName));
633
+ if (missingExportNames.length === 0) {
634
+ continue;
635
+ }
636
+ const key = `${pending.importer}\0${pending.source}\0${missingExportNames.join("\0")}`;
637
+ if (seen.has(key)) {
638
+ continue;
639
+ }
640
+ seen.add(key);
641
+ collectUnknownClientRouteComponents(collector, {
642
+ exportNames: missingExportNames,
643
+ file: pending.file,
644
+ });
645
+ diagnostics.push(unresolvedClientRouteReferenceDiagnostic({
646
+ exportNames: missingExportNames,
647
+ filename: pending.importer,
648
+ source: pending.source,
649
+ }));
650
+ }
651
+ return diagnostics;
652
+ }
369
653
  function isExplicitClientRouteSource(analysis, filename) {
370
654
  return analysis.hasUseClientDirective || isClientBoundaryFilename(filename);
371
655
  }
@@ -384,22 +668,35 @@ function hasServerOnlyImports(analysis) {
384
668
  async function inferClientRouteModuleSource(options) {
385
669
  const analysis = await clientRouteModuleAnalysisForSource(options);
386
670
  const usesNavigationLinkLocal = detectLinkComponentUsage(analysis);
387
- if (isServerOnlyClientRouteSource(analysis)) {
388
- return emptyClientRouteModuleInferenceResult({
671
+ collectClientRouteComponentsForModule(options.componentCollector, {
672
+ analysis,
673
+ filename: options.filename,
674
+ root: options.root,
675
+ routeEntry: options.routeEntry,
676
+ });
677
+ const forcedInference = isServerOnlyClientRouteSource(analysis)
678
+ ? emptyClientRouteModuleInferenceResult({
679
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
389
680
  navigationLinkExportNames: detectLinkComponentExportNames(analysis),
390
681
  serverOnly: true,
391
682
  serverOnlyClientRuntime: analysis.clientRuntime,
392
683
  usesNavigationLink: usesNavigationLinkLocal,
393
- });
394
- }
395
- if (isExplicitClientRouteSource(analysis, options.filename)) {
396
- return emptyClientRouteModuleInferenceResult({
397
- client: true,
398
- clientBoundaryModule: true,
399
- });
684
+ })
685
+ : isExplicitClientRouteSource(analysis, options.filename)
686
+ ? emptyClientRouteModuleInferenceResult({
687
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
688
+ client: true,
689
+ clientBoundaryModule: true,
690
+ })
691
+ : undefined;
692
+ if (forcedInference !== undefined && options.componentCollector === undefined) {
693
+ return forcedInference;
400
694
  }
401
695
  if (options.seen.has(options.filename)) {
402
- return emptyClientRouteModuleInferenceResult();
696
+ return (forcedInference ??
697
+ emptyClientRouteModuleInferenceResult({
698
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
699
+ }));
403
700
  }
404
701
  options.seen.add(options.filename);
405
702
  try {
@@ -409,6 +706,7 @@ async function inferClientRouteModuleSource(options) {
409
706
  const nestedClientExportNames = new Set();
410
707
  const clientReferenceSourceFiles = [];
411
708
  const diagnostics = [];
709
+ const availableExportNames = new Set(analysis.topLevelExportRenderInfo.map((info) => info.name));
412
710
  let boundaryGraphFallbackRequired = false;
413
711
  let clientProxy = false;
414
712
  let nestedClient = false;
@@ -422,8 +720,11 @@ async function inferClientRouteModuleSource(options) {
422
720
  }
423
721
  }
424
722
  if (hasServerOnlyImports(analysis) &&
425
- (implicitModuleClient || clientBoundaryExportNames.size > 0)) {
723
+ (implicitModuleClient || clientBoundaryExportNames.size > 0) &&
724
+ options.componentCollector === undefined) {
426
725
  return emptyClientRouteModuleInferenceResult({
726
+ availableExportNames: Array.from(availableExportNames),
727
+ navigationLinkExportNames: detectLinkComponentExportNames(analysis),
427
728
  serverOnly: true,
428
729
  serverOnlyClientRuntime: true,
429
730
  });
@@ -450,15 +751,46 @@ async function inferClientRouteModuleSource(options) {
450
751
  if (reference.sideEffect && isStyleModuleSpecifier(reference.source)) {
451
752
  continue;
452
753
  }
754
+ const renderingExportNames = rendered ? renderedLocalExportNames(reference, exportInfo) : [];
755
+ const renderedImportedNames = rendered
756
+ ? renderedImportedExportNames(reference, renderedComponentRoots)
757
+ : [];
758
+ const renderedFromReachableExport = rendered &&
759
+ renderingExportNames.some((exportName) => isClientRouteComponentReachable(options.componentCollector, options.filename, exportName));
453
760
  const resolved = await resolveAppLocalModule({
454
761
  allowExplicitNonSource: options.sourceTransform !== undefined,
455
762
  cache: options.cache,
456
763
  importer: options.filename,
457
764
  specifier: reference.source,
765
+ tolerateUnresolved: options.componentCollector !== undefined,
458
766
  });
459
767
  if (resolved === undefined) {
768
+ if (options.componentCollector !== undefined &&
769
+ renderedFromReachableExport &&
770
+ reference.source.startsWith(".")) {
771
+ collectUnknownClientRouteComponents(options.componentCollector, {
772
+ exportNames: renderedImportedNames,
773
+ file: resolve(dirname(options.filename), reference.source),
774
+ });
775
+ diagnostics.push(unresolvedClientRouteReferenceDiagnostic({
776
+ filename: options.filename,
777
+ source: reference.source,
778
+ }));
779
+ }
460
780
  continue;
461
781
  }
782
+ if (renderedFromReachableExport) {
783
+ const clientExecution = renderingExportNames.some((exportName) => clientRouteComponentRunsOnClient(options.componentCollector, options.filename, exportName));
784
+ markClientRouteComponentReachable(options.componentCollector, resolved, renderedImportedNames, { clientExecution });
785
+ if (renderedImportedNames !== undefined) {
786
+ collectClientRoutePendingExportValidation(options.componentCollector, {
787
+ exportNames: renderedImportedNames,
788
+ file: resolved,
789
+ importer: options.filename,
790
+ source: reference.source,
791
+ });
792
+ }
793
+ }
462
794
  const source = await readClientRouteSource({
463
795
  cache: options.cache,
464
796
  filename: resolved,
@@ -467,6 +799,7 @@ async function inferClientRouteModuleSource(options) {
467
799
  const imported = await inferClientRouteModuleSource({
468
800
  cache: options.cache,
469
801
  code: source,
802
+ componentCollector: options.componentCollector,
470
803
  filename: resolved,
471
804
  moduleContext: await compilerModuleContextForSource({
472
805
  cache: options.cache,
@@ -474,6 +807,7 @@ async function inferClientRouteModuleSource(options) {
474
807
  filename: resolved,
475
808
  }),
476
809
  root: false,
810
+ routeEntry: false,
477
811
  seen: options.seen,
478
812
  sourceTransform: options.sourceTransform,
479
813
  });
@@ -505,8 +839,8 @@ async function inferClientRouteModuleSource(options) {
505
839
  continue;
506
840
  }
507
841
  if (rendered) {
508
- const importedExportNames = renderedImportedExportNames(reference, renderedComponentRoots);
509
- const renderedExportNames = renderedLocalExportNames(reference, exportInfo);
842
+ const importedExportNames = renderedImportedNames;
843
+ const renderedExportNames = renderingExportNames;
510
844
  const importedBoundary = imported.clientBoundaryModule ||
511
845
  matchesInferredExportNames(importedExportNames, imported.clientBoundaryExportNames);
512
846
  const importedNested = matchesInferredExportNames(importedExportNames, imported.nestedClientExportNames);
@@ -553,10 +887,23 @@ async function inferClientRouteModuleSource(options) {
553
887
  cache: options.cache,
554
888
  importer: options.filename,
555
889
  specifier: reference.source,
890
+ tolerateUnresolved: options.componentCollector !== undefined,
556
891
  });
557
892
  if (resolved === undefined) {
893
+ if (options.componentCollector !== undefined) {
894
+ diagnostics.push(unresolvedClientRouteReferenceDiagnostic({
895
+ filename: options.filename,
896
+ source: reference.source,
897
+ }));
898
+ }
558
899
  continue;
559
900
  }
901
+ propagateClientRouteComponentStaticExport(options.componentCollector, options.filename, resolved, reference);
902
+ collectClientRouteStaticExportEdge(options.componentCollector, {
903
+ file: options.filename,
904
+ reference,
905
+ resolved,
906
+ });
560
907
  const source = await readClientRouteSource({
561
908
  cache: options.cache,
562
909
  filename: resolved,
@@ -565,6 +912,7 @@ async function inferClientRouteModuleSource(options) {
565
912
  const exported = await inferClientRouteModuleSource({
566
913
  cache: options.cache,
567
914
  code: source,
915
+ componentCollector: options.componentCollector,
568
916
  filename: resolved,
569
917
  moduleContext: await compilerModuleContextForSource({
570
918
  cache: options.cache,
@@ -572,10 +920,24 @@ async function inferClientRouteModuleSource(options) {
572
920
  filename: resolved,
573
921
  }),
574
922
  root: false,
923
+ routeEntry: false,
575
924
  seen: options.seen,
576
925
  sourceTransform: options.sourceTransform,
577
926
  });
578
927
  diagnostics.push(...exported.diagnostics);
928
+ if (reference.exportAll) {
929
+ for (const exportName of exported.availableExportNames) {
930
+ availableExportNames.add(exportName);
931
+ }
932
+ }
933
+ else {
934
+ for (const specifier of reference.specifiers) {
935
+ availableExportNames.add(specifier.exportedName);
936
+ }
937
+ for (const exportName of reference.exportedNames) {
938
+ availableExportNames.add(exportName);
939
+ }
940
+ }
579
941
  // A re-export renders nothing itself, so it does not set the module's
580
942
  // own `usesNavigationLink`; it only forwards per-export `Link` usage so
581
943
  // an importer that renders this name can decide precisely. Map the
@@ -620,7 +982,8 @@ async function inferClientRouteModuleSource(options) {
620
982
  }
621
983
  }
622
984
  }
623
- return {
985
+ const inferred = {
986
+ availableExportNames: Array.from(availableExportNames),
624
987
  boundaryGraphFallbackCandidate: analysis.staticExports.length > 0 || boundaryGraphFallbackRequired,
625
988
  boundaryGraphFallbackRequired,
626
989
  client: clientBoundaryImports.length > 0 ||
@@ -640,6 +1003,15 @@ async function inferClientRouteModuleSource(options) {
640
1003
  serverOnlyClientRuntime: false,
641
1004
  usesNavigationLink,
642
1005
  };
1006
+ return forcedInference === undefined
1007
+ ? inferred
1008
+ : {
1009
+ ...forcedInference,
1010
+ availableExportNames: inferred.availableExportNames,
1011
+ diagnostics,
1012
+ navigationLinkExportNames: inferred.navigationLinkExportNames,
1013
+ usesNavigationLink: inferred.usesNavigationLink,
1014
+ };
643
1015
  }
644
1016
  finally {
645
1017
  options.seen.delete(options.filename);
@@ -647,6 +1019,7 @@ async function inferClientRouteModuleSource(options) {
647
1019
  }
648
1020
  function emptyClientRouteModuleInferenceResult(overrides = {}) {
649
1021
  return {
1022
+ availableExportNames: [],
650
1023
  boundaryGraphFallbackCandidate: false,
651
1024
  boundaryGraphFallbackRequired: false,
652
1025
  client: false,
@@ -883,7 +1256,7 @@ function matchingBraceEnd(source, openBraceIndex) {
883
1256
  }
884
1257
  continue;
885
1258
  }
886
- if (char === "\"" || char === "'" || char === "`") {
1259
+ if (char === '"' || char === "'" || char === "`") {
887
1260
  quote = char;
888
1261
  continue;
889
1262
  }
@@ -940,8 +1313,7 @@ function addDestructuredCallbackNames(names, destructured) {
940
1313
  const property = destructuredBindingName(rawProperty);
941
1314
  const alias = destructuredBindingName(rawAlias);
942
1315
  const name = alias ?? property;
943
- if (name !== undefined &&
944
- (isCallbackPropName(property) || isCallbackPropName(name))) {
1316
+ if (name !== undefined && (isCallbackPropName(property) || isCallbackPropName(name))) {
945
1317
  names.add(name);
946
1318
  }
947
1319
  }
@@ -1001,6 +1373,20 @@ function serverOnlyClientImportReferenceDiagnostic(options) {
1001
1373
  source: options.reference.source,
1002
1374
  };
1003
1375
  }
1376
+ function unresolvedClientRouteReferenceDiagnostic(options) {
1377
+ const exportSuffix = options.exportNames === undefined || options.exportNames.length === 0
1378
+ ? ""
1379
+ : ` (${options.exportNames.map((name) => JSON.stringify(name)).join(", ")})`;
1380
+ return {
1381
+ code: "MR_CLIENT_BOUNDARY_INFERENCE_UNRESOLVED_REFERENCE",
1382
+ filename: options.filename,
1383
+ level: "warn",
1384
+ localNames: [...(options.exportNames ?? [])],
1385
+ message: `${options.filename}: rendered component reference ${JSON.stringify(options.source)}${exportSuffix} ` +
1386
+ "could not be resolved to an exported component.",
1387
+ source: options.source,
1388
+ };
1389
+ }
1004
1390
  function functionCallInteractiveImportDiagnostic(options) {
1005
1391
  const localNames = options.reference.localNames.filter(startsUppercase);
1006
1392
  const component = localNames[0] ?? options.reference.localNames[0] ?? options.reference.source;
@@ -1066,7 +1452,7 @@ async function resolveAppLocalModule(options) {
1066
1452
  if (!options.specifier.startsWith(".")) {
1067
1453
  return undefined;
1068
1454
  }
1069
- const cacheKey = `${options.importer}\0${options.specifier}\0${options.allowExplicitNonSource === true ? "explicit" : "source"}`;
1455
+ const cacheKey = `${options.importer}\0${options.specifier}\0${options.allowExplicitNonSource === true ? "explicit" : "source"}\0${options.tolerateUnresolved === true ? "tolerant" : "strict"}`;
1070
1456
  const cached = options.cache.resolvedByImport.get(cacheKey);
1071
1457
  if (cached !== undefined) {
1072
1458
  return cached;
@@ -1075,6 +1461,7 @@ async function resolveAppLocalModule(options) {
1075
1461
  allowExplicitNonSource: options.allowExplicitNonSource === true,
1076
1462
  importer: options.importer,
1077
1463
  specifier: options.specifier,
1464
+ tolerateUnresolved: options.tolerateUnresolved === true,
1078
1465
  });
1079
1466
  options.cache.resolvedByImport.set(cacheKey, resolved);
1080
1467
  return resolved;
@@ -1094,6 +1481,9 @@ async function resolveAppLocalModuleUncached(options) {
1094
1481
  return candidate;
1095
1482
  }
1096
1483
  }
1484
+ if (options.tolerateUnresolved) {
1485
+ return undefined;
1486
+ }
1097
1487
  throw new Error(`${importer}: could not resolve app-local import ${JSON.stringify(specifier)}.`);
1098
1488
  }
1099
1489
  async function isFile(path) {
@@ -1440,8 +1830,7 @@ export async function buildClientRouteEntrySource(options) {
1440
1830
  const routeId = routeIdForPath(options.routePath);
1441
1831
  const routeUsesCells = detectRouteCellStateHint(compiled.code);
1442
1832
  const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
1443
- const routeUsesDomRefs = compiled.metadata.imports.some((entry) => entry.source === "@reckona/mreact-reactive-dom" &&
1444
- entry.specifiers.includes("bindDomRef"));
1833
+ const routeUsesDomRefs = compiled.metadata.imports.some((entry) => entry.source === "@reckona/mreact-reactive-dom" && entry.specifiers.includes("bindDomRef"));
1445
1834
  const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect || routeUsesDomRefs;
1446
1835
  const routeExplicitlyRequiresHydration = isExplicitClientRouteSource(routeSourceAnalysis, options.filename);
1447
1836
  const routeHasEventBindings = (compiled.metadata.eventHydrationManifest?.events.length ?? 0) > 0;