@reckona/mreact-router 0.0.195 → 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 { dirname, extname, 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) {
@@ -1318,6 +1708,12 @@ export async function buildNavigationRuntimeBundle(options = {}) {
1318
1708
  export async function buildClientRouteOutput(options) {
1319
1709
  const entry = await buildClientRouteEntrySource(options);
1320
1710
  const dropConsoleFunctions = options.dropConsoleFunctions ?? resolveClientConsolePureFunctions(options.dropClientConsole);
1711
+ const sourceRegionModulePaths = options.debugLabels === true ? undefined : new Set([options.filename]);
1712
+ const runtimePlugin = workspaceRuntimePlugin({
1713
+ debugLabels: options.debugLabels === true,
1714
+ routeFiles: [options.filename],
1715
+ sourceRegionModulePaths,
1716
+ });
1321
1717
  const bundled = await bundleRouterModule({
1322
1718
  code: entry.code,
1323
1719
  cacheDir: options.cacheDir,
@@ -1329,7 +1725,8 @@ export async function buildClientRouteOutput(options) {
1329
1725
  minify: options.minify === true,
1330
1726
  platform: "browser",
1331
1727
  preserveExports: true,
1332
- plugins: [workspaceRuntimePlugin({ routeFiles: [options.filename] })],
1728
+ sourceRegionModulePaths,
1729
+ plugins: [runtimePlugin],
1333
1730
  sourceMap: options.sourceMap,
1334
1731
  vitePlugins: options.vitePlugins,
1335
1732
  });
@@ -1350,6 +1747,15 @@ export async function buildClientRouteBatchOutput(options) {
1350
1747
  vitePlugins: options.vitePlugins ?? route.vitePlugins,
1351
1748
  }),
1352
1749
  })));
1750
+ const debugLabels = options.routes.some((route) => route.debugLabels === true);
1751
+ const sourceRegionModulePaths = debugLabels
1752
+ ? undefined
1753
+ : new Set(entries.map((entry) => entry.filename));
1754
+ const runtimePlugin = workspaceRuntimePlugin({
1755
+ debugLabels,
1756
+ routeFiles: entries.map((entry) => entry.filename),
1757
+ sourceRegionModulePaths,
1758
+ });
1353
1759
  const bundled = await bundleRouterModules({
1354
1760
  base: options.assetBaseUrl ?? "/_mreact/client/",
1355
1761
  cacheDir: options.cacheDir,
@@ -1363,7 +1769,8 @@ export async function buildClientRouteBatchOutput(options) {
1363
1769
  })),
1364
1770
  minify: options.minify === true,
1365
1771
  platform: "browser",
1366
- plugins: [workspaceRuntimePlugin({ routeFiles: entries.map((entry) => entry.filename) })],
1772
+ sourceRegionModulePaths,
1773
+ plugins: [runtimePlugin],
1367
1774
  root: options.projectRoot,
1368
1775
  sourceMap: options.sourceMap,
1369
1776
  dropConsoleFunctions: options.dropConsoleFunctions,
@@ -1392,13 +1799,20 @@ export async function buildClientRouteEntrySource(options) {
1392
1799
  filename: options.filename,
1393
1800
  });
1394
1801
  const routeSourceAnalysis = collectClientRouteModuleAnalysisFromContext(moduleContext);
1802
+ const compilerFilename = options.debugLabels === true ? basename(options.filename) : options.filename;
1803
+ const compilerModuleContext = compilerFilename === options.filename
1804
+ ? moduleContext
1805
+ : createCompilerModuleContext({
1806
+ code: options.code,
1807
+ filename: compilerFilename,
1808
+ });
1395
1809
  const compiled = transformCompilerModuleContext({
1396
1810
  code: options.code,
1397
1811
  clientBoundaryImports: options.clientBoundaryImports ?? [],
1398
- filename: options.filename,
1399
- moduleContext,
1812
+ filename: compilerFilename,
1813
+ moduleContext: compilerModuleContext,
1400
1814
  target: "client",
1401
- dev: options.minify !== true,
1815
+ dev: options.debugLabels === true,
1402
1816
  });
1403
1817
  if (compiled.diagnostics.length > 0) {
1404
1818
  throw new Error(compiled.diagnostics
@@ -1416,7 +1830,8 @@ export async function buildClientRouteEntrySource(options) {
1416
1830
  const routeId = routeIdForPath(options.routePath);
1417
1831
  const routeUsesCells = detectRouteCellStateHint(compiled.code);
1418
1832
  const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
1419
- const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect;
1833
+ const routeUsesDomRefs = compiled.metadata.imports.some((entry) => entry.source === "@reckona/mreact-reactive-dom" && entry.specifiers.includes("bindDomRef"));
1834
+ const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect || routeUsesDomRefs;
1420
1835
  const routeExplicitlyRequiresHydration = isExplicitClientRouteSource(routeSourceAnalysis, options.filename);
1421
1836
  const routeHasEventBindings = (compiled.metadata.eventHydrationManifest?.events.length ?? 0) > 0;
1422
1837
  const routeCapturesEventBindings = compiled.code.includes("__mreactEventBindings");
@@ -1427,6 +1842,7 @@ export async function buildClientRouteEntrySource(options) {
1427
1842
  const routeRequiresFullHydration = routeExplicitlyRequiresHydration ||
1428
1843
  routeUsesCells ||
1429
1844
  routeUsesReactiveEffect ||
1845
+ routeUsesDomRefs ||
1430
1846
  routeHasEventBindings;
1431
1847
  const routeUsesOnlyClientReferenceBoundaries = !routeRequiresFullHydration &&
1432
1848
  clientReferenceManifest.length > 0 &&
@@ -1447,7 +1863,7 @@ export async function buildClientRouteEntrySource(options) {
1447
1863
  ? `import { bindCapturedEvent as __mreactBindCapturedEvent } from "@reckona/mreact-reactive-dom/internal";\n`
1448
1864
  : "";
1449
1865
  const routeReactiveDomMetadataImport = !routeUsesOnlyClientReferenceBoundaries
1450
- ? `${routeCapturedEventImport}import { withEventBindingMetadata as __mreactWithEventBindingMetadata, withPropBindingMetadata as __mreactWithPropBindingMetadata } from "@reckona/mreact-reactive-dom";\n`
1866
+ ? `${routeCapturedEventImport}import { ${routeUsesDomRefs ? "getDomRefBindings as __mreactGetDomRefBindings, " : ""}withEventBindingMetadata as __mreactWithEventBindingMetadata, withPropBindingMetadata as __mreactWithPropBindingMetadata } from "@reckona/mreact-reactive-dom";\n`
1451
1867
  : "";
1452
1868
  const navigationStateDeclaration = inlineClientNavigation
1453
1869
  ? `const __mreactNavigationState = __mreactGlobal.__mreactNavigationState ??= {
@@ -1723,9 +2139,10 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
1723
2139
  __mreactActiveCellIndex = 0;
1724
2140
  }
1725
2141
  return () => {
1726
- for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
1727
- __mreactDispose();
1728
- }
2142
+ __mreactRunLifecycleTasks(
2143
+ Array.from(__mreactRouteEffectDisposers),
2144
+ (__mreactDispose) => __mreactDispose(),
2145
+ );
1729
2146
  __mreactRouteEffectDisposers.clear();
1730
2147
  };
1731
2148
  });
@@ -1736,13 +2153,33 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
1736
2153
  ? ` __mreactDisposeRoute(__mreactRouteId);
1737
2154
  const __mreactRouteEffectDisposers = new Set();
1738
2155
  __mreactRouteDisposers.set(__mreactRouteId, () => {
1739
- for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
1740
- __mreactDispose();
1741
- }
2156
+ __mreactRunLifecycleTasks(
2157
+ Array.from(__mreactRouteEffectDisposers),
2158
+ (__mreactDispose) => __mreactDispose(),
2159
+ );
1742
2160
  __mreactRouteEffectDisposers.clear();
1743
2161
  });
1744
2162
  `
1745
2163
  : "";
2164
+ const routeLifecycleFunctions = `
2165
+ function __mreactRunLifecycleTasks(values, run) {
2166
+ let firstError;
2167
+
2168
+ for (const value of values) {
2169
+ try {
2170
+ run(value);
2171
+ } catch (error) {
2172
+ firstError ??= error;
2173
+ }
2174
+ }
2175
+
2176
+ if (firstError !== undefined) {
2177
+ queueMicrotask(() => {
2178
+ throw firstError;
2179
+ });
2180
+ }
2181
+ }
2182
+ `;
1746
2183
  const routeCellDropFunction = routeUsesCells
1747
2184
  ? `
1748
2185
  function __mreactDropMismatchedRouteState(previousState, nextState) {
@@ -1768,12 +2205,18 @@ function __mreactDisposeRoute(routeId) {
1768
2205
  }
1769
2206
  `
1770
2207
  : "";
1771
- const routeCleanupNavigationDispose = routeUsesCleanupScope
1772
- ? ` if (currentRouteId !== nextRouteId) {
1773
- __mreactDisposeRoute(currentRouteId);
2208
+ const routeCleanupNavigationDispose = ` if (currentRouteId !== nextRouteId) {
2209
+ const __mreactRegisteredRouteDisposers = __mreactGlobal.__mreactRouteDisposers;
2210
+ const __mreactRegisteredRouteDispose = __mreactRegisteredRouteDisposers?.get(currentRouteId);
2211
+ if (__mreactRegisteredRouteDispose !== undefined) {
2212
+ __mreactRegisteredRouteDisposers.delete(currentRouteId);
2213
+ __mreactRunLifecycleTasks(
2214
+ [__mreactRegisteredRouteDispose],
2215
+ (__mreactDispose) => __mreactDispose(),
2216
+ );
2217
+ }
1774
2218
  }
1775
- `
1776
- : "";
2219
+ `;
1777
2220
  const routeNodeResolver = routeUsesCells
1778
2221
  ? `
1779
2222
  function __mreactResolveRouteNode(value) {
@@ -1812,9 +2255,7 @@ function __mreactResolveRouteNode(value) {
1812
2255
  const previousDisposers = current.__mreactEventDisposers;
1813
2256
 
1814
2257
  if (Array.isArray(previousDisposers)) {
1815
- for (const dispose of previousDisposers) {
1816
- dispose();
1817
- }
2258
+ __mreactRunLifecycleTasks(previousDisposers, (dispose) => dispose());
1818
2259
  }
1819
2260
 
1820
2261
  const rawBindings = next.__mreactEventBindings;
@@ -1843,14 +2284,26 @@ function __mreactResolveRouteNode(value) {
1843
2284
  const previousDisposers = current.__mreactEventDisposers;
1844
2285
 
1845
2286
  if (Array.isArray(previousDisposers)) {
1846
- for (const dispose of previousDisposers) {
1847
- dispose();
1848
- }
2287
+ __mreactRunLifecycleTasks(previousDisposers, (dispose) => dispose());
1849
2288
  }
1850
2289
 
1851
2290
  current.__mreactEventDisposers = [];
1852
2291
  current.__mreactHasEvents = false;
1853
2292
  }
2293
+ `;
2294
+ const routeDomRefBindingSyncFunction = routeUsesDomRefs
2295
+ ? `function __mreactSyncDomRefBindings(current, next) {
2296
+ __mreactRunLifecycleTasks(
2297
+ Array.from(__mreactGetDomRefBindings(current)),
2298
+ (binding) => binding.dispose(),
2299
+ );
2300
+ __mreactRunLifecycleTasks(
2301
+ Array.from(__mreactGetDomRefBindings(next)),
2302
+ (binding) => binding.retarget(current),
2303
+ );
2304
+ }
2305
+ `
2306
+ : `function __mreactSyncDomRefBindings() {}
1854
2307
  `;
1855
2308
  const boundaryOnlyHydrationBlock = routeRequiresFullHydration
1856
2309
  ? ""
@@ -1909,6 +2362,7 @@ ${routeCellHydrationIndent}__mreactMarker.setAttribute(__mreactRouteHydratedAttr
1909
2362
  ${routeCellHydrationIndent}__mreactMarkRouteHydrated();
1910
2363
  ${routeCellHydrationEnd}}
1911
2364
  ${routeCellDropFunction}
2365
+ ${routeLifecycleFunctions}
1912
2366
  ${routeCleanupFunction}
1913
2367
 
1914
2368
  function __mreactMarkRouteHydrated() {
@@ -2495,13 +2949,14 @@ function __mreactApplyNavigationHtml(html, url) {
2495
2949
  const currentRouteId = currentMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
2496
2950
  const nextRouteId = nextMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
2497
2951
 
2952
+ ${routeCleanupNavigationDispose}
2498
2953
  __mreactMarkRouteHydrating();
2499
2954
  __mreactSyncHeadMetadata(template.content, html);
2500
2955
  if (!__mreactApplyNavigationShellHtml(currentMarker, nextMarker)) {
2501
2956
  __mreactUnmountCompatBoundaries(currentMarker);
2502
2957
  __mreactResumeNode(currentMarker, nextMarker);
2503
2958
  }
2504
- ${routeCleanupNavigationDispose} __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
2959
+ __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
2505
2960
 
2506
2961
  const script = template.content.querySelector('script[type="module"][src]')?.getAttribute("src");
2507
2962
  if (script !== null && script !== undefined) {
@@ -3658,6 +4113,7 @@ function __mreactResumeNode(current, next) {
3658
4113
  }
3659
4114
 
3660
4115
  __mreactSyncEventBindings(current, next);
4116
+ __mreactSyncDomRefBindings(current, next);
3661
4117
  __mreactSyncAttributes(current, next);
3662
4118
  __mreactSyncPropBindings(current, next);
3663
4119
  __mreactResumeChildren(current, next);
@@ -3680,6 +4136,7 @@ function __mreactShouldReplaceNode(current, next) {
3680
4136
  }
3681
4137
 
3682
4138
  ${routeEventBindingSyncFunction}
4139
+ ${routeDomRefBindingSyncFunction}
3683
4140
 
3684
4141
  function __mreactSyncAttributes(current, next) {
3685
4142
  for (const attribute of Array.from(current.attributes)) {
@@ -3699,9 +4156,7 @@ function __mreactSyncPropBindings(current, next) {
3699
4156
  const previousBindings = current.__mreactPropBindings;
3700
4157
 
3701
4158
  if (Array.isArray(previousBindings)) {
3702
- for (const binding of previousBindings) {
3703
- binding.dispose?.();
3704
- }
4159
+ __mreactRunLifecycleTasks(previousBindings, (binding) => binding.dispose?.());
3705
4160
  }
3706
4161
 
3707
4162
  const bindings = next.__mreactPropBindings;
@@ -3717,9 +4172,7 @@ function __mreactSyncPropBindings(current, next) {
3717
4172
  next.__mreactPropBindings = [];
3718
4173
  next.__mreactHasReactiveProps = false;
3719
4174
 
3720
- for (const binding of bindings) {
3721
- binding.retarget?.(current);
3722
- }
4175
+ __mreactRunLifecycleTasks(bindings, (binding) => binding.retarget?.(current));
3723
4176
  }
3724
4177
 
3725
4178
  function __mreactResumeChildren(current, next) {
@@ -3885,22 +4338,33 @@ export function invalidateReactiveDevtoolsCache() {}
3885
4338
  export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`,
3886
4339
  loader: "ts",
3887
4340
  }));
4341
+ if (options.sourceRegionModulePaths !== undefined) {
4342
+ buildApi.onLoad({ filter: /.*/ }, (args) => {
4343
+ if (isAbsolute(args.path) &&
4344
+ isRouteClientDependencySourcePath(args.path, routeFiles) &&
4345
+ !runtimePackageDirs.some((runtimePackageDir) => args.path.startsWith(`${runtimePackageDir}${sep}`))) {
4346
+ options.sourceRegionModulePaths?.add(args.path);
4347
+ }
4348
+ return undefined;
4349
+ });
4350
+ }
3888
4351
  buildApi.onLoad({ filter: /\.(?:mreact\.)?[cm]?[jt]sx$/ }, async (args) => {
3889
4352
  if (!isRouteClientDependencySourcePath(args.path, routeFiles)) {
3890
4353
  return undefined;
3891
4354
  }
3892
4355
  const source = await readFile(args.path, "utf8");
4356
+ const compilerFilename = options.debugLabels ? basename(args.path) : args.path;
3893
4357
  const moduleContext = createCompilerModuleContext({
3894
4358
  code: source,
3895
- filename: args.path,
4359
+ filename: compilerFilename,
3896
4360
  });
3897
4361
  if (!hasJsxSyntax(moduleContext.program)) {
3898
4362
  return undefined;
3899
4363
  }
3900
4364
  const output = transformCompilerModuleContext({
3901
4365
  code: source,
3902
- dev: true,
3903
- filename: args.path,
4366
+ dev: options.debugLabels,
4367
+ filename: compilerFilename,
3904
4368
  mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
3905
4369
  moduleContext,
3906
4370
  target: "client",