@reckona/mreact-router 0.0.196 → 0.0.198

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/src/client.ts 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 {
6
6
  analyzeBoundaryGraph,
7
7
  collectClientRouteModuleAnalysis,
@@ -10,6 +10,7 @@ import {
10
10
  type BoundaryGraphResult,
11
11
  type ClientRouteModuleAnalysis,
12
12
  type ClientRouteStaticImportReference,
13
+ type StaticExportReference,
13
14
  type StaticImportReference,
14
15
  type TopLevelExportRenderInfo,
15
16
  } from "@reckona/mreact-compiler";
@@ -118,10 +119,37 @@ export interface ClientRouteInferenceResult {
118
119
  client: boolean;
119
120
  clientBoundaryImports: string[];
120
121
  clientBoundaryFallbackImports: string[];
122
+ components?: ClientRouteComponent[] | undefined;
121
123
  diagnostics: ClientRouteInferenceDiagnostic[];
122
124
  }
123
125
 
126
+ export type ClientRouteComponentClassification =
127
+ | "client-boundary"
128
+ | "client-route"
129
+ | "server-only"
130
+ | "server-render"
131
+ | "shared"
132
+ | "unknown";
133
+
134
+ export type ClientRouteComponentOrigin =
135
+ | "client-filename"
136
+ | "compat-filename"
137
+ | "inferred-client-runtime"
138
+ | "server-only-import"
139
+ | "server-render"
140
+ | "unresolved-reference"
141
+ | "use-client-directive"
142
+ | "use-server-directive";
143
+
144
+ export interface ClientRouteComponent {
145
+ classification: ClientRouteComponentClassification;
146
+ exportName: string;
147
+ file: string;
148
+ origin: ClientRouteComponentOrigin;
149
+ }
150
+
124
151
  interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
152
+ availableExportNames: string[];
125
153
  boundaryGraphFallbackCandidate: boolean;
126
154
  boundaryGraphFallbackRequired: boolean;
127
155
  clientBoundaryExportNames: string[];
@@ -134,6 +162,28 @@ interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
134
162
  usesNavigationLink: boolean;
135
163
  }
136
164
 
165
+ interface ClientRouteComponentCollector {
166
+ clientReachable: Set<string>;
167
+ components: Map<string, ClientRouteComponent>;
168
+ localExportNamesByFile: Map<string, Set<string>>;
169
+ pendingExportValidations: ClientRoutePendingExportValidation[];
170
+ reachable: Set<string>;
171
+ staticExportEdges: ClientRouteStaticExportEdge[];
172
+ }
173
+
174
+ interface ClientRoutePendingExportValidation {
175
+ exportNames: string[];
176
+ file: string;
177
+ importer: string;
178
+ source: string;
179
+ }
180
+
181
+ interface ClientRouteStaticExportEdge {
182
+ file: string;
183
+ reference: StaticExportReference;
184
+ resolved: string;
185
+ }
186
+
137
187
  export interface ClientReferenceImport {
138
188
  exportName: string;
139
189
  importSource: string;
@@ -148,10 +198,11 @@ export interface ClientRouteReferenceResult extends ClientRouteInferenceResult {
148
198
 
149
199
  export interface ClientRouteInferenceDiagnostic {
150
200
  code:
151
- | typeof clientBoundaryInferenceServerOnlyReferenceCode
152
- | typeof clientBoundaryInferenceFunctionCallInteractiveCode
153
- | typeof clientBoundaryInferenceUnsupportedReferenceCode
154
- | typeof navigationRuntimeLinkDisabledCode;
201
+ | "MR_CLIENT_BOUNDARY_INFERENCE_SERVER_ONLY_REFERENCE"
202
+ | "MR_CLIENT_BOUNDARY_INFERENCE_FUNCTION_CALL_INTERACTIVE"
203
+ | "MR_CLIENT_BOUNDARY_INFERENCE_UNRESOLVED_REFERENCE"
204
+ | "MR_CLIENT_BOUNDARY_INFERENCE_UNSUPPORTED_REFERENCE"
205
+ | "MR_NAVIGATION_RUNTIME_LINK_DISABLED";
155
206
  filename: string;
156
207
  level: "warn";
157
208
  localNames: string[];
@@ -218,6 +269,7 @@ export async function inferClientRouteModule(options: {
218
269
  appDir?: string | undefined;
219
270
  cache?: ClientRouteInferenceCache | undefined;
220
271
  code: string;
272
+ collectComponents?: boolean | undefined;
221
273
  filename: string;
222
274
  moduleContext?: CompilerModuleContext | undefined;
223
275
  routePath?: string | undefined;
@@ -225,6 +277,16 @@ export async function inferClientRouteModule(options: {
225
277
  }): Promise<ClientRouteInferenceResult> {
226
278
  const cache = options.cache ?? createClientRouteInferenceCache();
227
279
  const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
280
+ const componentCollector: ClientRouteComponentCollector | undefined = options.collectComponents
281
+ ? {
282
+ clientReachable: new Set(),
283
+ components: new Map(),
284
+ localExportNamesByFile: new Map(),
285
+ pendingExportValidations: [],
286
+ reachable: new Set(),
287
+ staticExportEdges: [],
288
+ }
289
+ : undefined;
228
290
  const code = await transformClientRouteSource({
229
291
  code: options.code,
230
292
  filename: options.filename,
@@ -235,9 +297,11 @@ export async function inferClientRouteModule(options: {
235
297
  const routeInference = await inferClientRouteModuleSource({
236
298
  cache,
237
299
  code,
300
+ componentCollector,
238
301
  filename: options.filename,
239
302
  ...(sourceTransform === undefined ? { moduleContext: options.moduleContext } : {}),
240
303
  root: true,
304
+ routeEntry: true,
241
305
  seen: new Set(),
242
306
  sourceTransform,
243
307
  });
@@ -254,15 +318,29 @@ export async function inferClientRouteModule(options: {
254
318
  : routeInference;
255
319
 
256
320
  if (options.appDir === undefined) {
257
- return withClientRouteDiagnosticPath(mergedRouteInference, options.routePath);
321
+ const exportDiagnostics = finalizeClientRouteComponentExportValidations(componentCollector);
322
+ return withClientRouteDiagnosticPath(
323
+ {
324
+ ...mergedRouteInference,
325
+ ...(componentCollector === undefined
326
+ ? {}
327
+ : {
328
+ components: normalizedClientRouteComponents(componentCollector, options.filename),
329
+ }),
330
+ diagnostics: [...mergedRouteInference.diagnostics, ...exportDiagnostics],
331
+ },
332
+ options.routePath,
333
+ );
258
334
  }
259
335
 
260
336
  const shellInferences = await inferClientRouteShellModules({
261
337
  appDir: options.appDir,
262
338
  cache,
339
+ componentCollector,
263
340
  filename: options.filename,
264
341
  sourceTransform,
265
342
  });
343
+ const exportDiagnostics = finalizeClientRouteComponentExportValidations(componentCollector);
266
344
 
267
345
  return withClientRouteDiagnosticPath(
268
346
  {
@@ -270,9 +348,15 @@ export async function inferClientRouteModule(options: {
270
348
  mergedRouteInference.client || shellInferences.some((inference) => inference.client),
271
349
  clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
272
350
  clientBoundaryFallbackImports: mergedRouteInference.clientBoundaryFallbackImports,
351
+ ...(componentCollector === undefined
352
+ ? {}
353
+ : {
354
+ components: normalizedClientRouteComponents(componentCollector, options.filename),
355
+ }),
273
356
  diagnostics: [
274
357
  ...mergedRouteInference.diagnostics,
275
358
  ...shellInferences.flatMap((inference) => inference.diagnostics),
359
+ ...exportDiagnostics,
276
360
  ],
277
361
  },
278
362
  options.routePath,
@@ -377,6 +461,7 @@ export async function collectClientRouteReferences(options: {
377
461
  filename: options.filename,
378
462
  moduleContext: routeModuleContext,
379
463
  root: true,
464
+ routeEntry: true,
380
465
  seen: new Set(),
381
466
  sourceTransform,
382
467
  });
@@ -413,6 +498,7 @@ export async function collectClientRouteReferences(options: {
413
498
  filename: sourceOptions.filename,
414
499
  moduleContext,
415
500
  root: true,
501
+ routeEntry: false,
416
502
  seen: new Set(),
417
503
  sourceTransform,
418
504
  }));
@@ -459,6 +545,7 @@ export async function collectClientRouteReferences(options: {
459
545
  filename: shell,
460
546
  moduleContext,
461
547
  root: true,
548
+ routeEntry: false,
462
549
  seen: new Set(),
463
550
  sourceTransform,
464
551
  }),
@@ -604,6 +691,7 @@ export function navigationRuntimeLinkDisabledDiagnostic(options: {
604
691
  async function inferClientRouteShellModules(options: {
605
692
  appDir: string;
606
693
  cache: ClientRouteInferenceCache;
694
+ componentCollector?: ClientRouteComponentCollector | undefined;
607
695
  filename: string;
608
696
  sourceTransform?: ClientRouteSourceTransform | undefined;
609
697
  }): Promise<ClientRouteInferenceResult[]> {
@@ -620,8 +708,10 @@ async function inferClientRouteShellModules(options: {
620
708
  return await inferClientRouteModuleSource({
621
709
  cache: options.cache,
622
710
  code,
711
+ componentCollector: options.componentCollector,
623
712
  filename: shell,
624
713
  root: true,
714
+ routeEntry: false,
625
715
  seen: new Set(),
626
716
  sourceTransform: options.sourceTransform,
627
717
  });
@@ -662,6 +752,380 @@ export function isClientRouteSource(code: string): boolean {
662
752
  );
663
753
  }
664
754
 
755
+ function collectClientRouteComponentsForModule(
756
+ collector: ClientRouteComponentCollector | undefined,
757
+ options: {
758
+ analysis: ClientRouteModuleAnalysis;
759
+ filename: string;
760
+ root: boolean;
761
+ routeEntry: boolean;
762
+ },
763
+ ): void {
764
+ if (collector === undefined) {
765
+ return;
766
+ }
767
+
768
+ if (options.root) {
769
+ markClientRouteComponentReachable(collector, options.filename, ["default"]);
770
+ }
771
+
772
+ const localExportNames =
773
+ collector.localExportNamesByFile.get(options.filename) ?? new Set<string>();
774
+ for (const info of options.analysis.topLevelExportRenderInfo) {
775
+ localExportNames.add(info.name);
776
+ }
777
+ collector.localExportNamesByFile.set(options.filename, localExportNames);
778
+
779
+ const serverOnly =
780
+ options.analysis.hasUseServerDirective || hasServerOnlyImports(options.analysis);
781
+ const explicitClient = isExplicitClientRouteSource(options.analysis, options.filename);
782
+
783
+ for (const info of options.analysis.topLevelExportRenderInfo) {
784
+ const component = {
785
+ classification: serverOnly
786
+ ? "server-only"
787
+ : explicitClient || info.clientRuntime
788
+ ? options.routeEntry && info.name === "default"
789
+ ? "client-route"
790
+ : "client-boundary"
791
+ : "server-render",
792
+ exportName: info.name,
793
+ file: options.filename,
794
+ origin: clientRouteComponentOrigin({
795
+ analysis: options.analysis,
796
+ filename: options.filename,
797
+ info,
798
+ serverOnly,
799
+ }),
800
+ } satisfies ClientRouteComponent;
801
+ collector.components.set(
802
+ clientRouteComponentKey(component.file, component.exportName),
803
+ component,
804
+ );
805
+ }
806
+
807
+ for (const info of options.analysis.topLevelExportRenderInfo) {
808
+ if (!isClientRouteComponentReachable(collector, options.filename, info.name)) {
809
+ continue;
810
+ }
811
+
812
+ const renderedNames = new Set([
813
+ ...(options.analysis.reachableExportRenderedComponentNames[info.name] ?? []),
814
+ ...(options.analysis.reachableExportRenderedComponentRoots[info.name] ?? []),
815
+ ]);
816
+
817
+ for (const rendered of options.analysis.topLevelExportRenderInfo) {
818
+ if (
819
+ rendered.name !== "default" &&
820
+ (renderedNames.has(rendered.name) || renderedNames.has(rendered.localName ?? rendered.name))
821
+ ) {
822
+ markClientRouteComponentReachable(collector, options.filename, [rendered.name], {
823
+ clientExecution: clientRouteComponentRunsOnClient(collector, options.filename, info.name),
824
+ });
825
+ }
826
+ }
827
+ }
828
+ }
829
+
830
+ function clientRouteComponentOrigin(options: {
831
+ analysis: ClientRouteModuleAnalysis;
832
+ filename: string;
833
+ info: TopLevelExportRenderInfo;
834
+ serverOnly: boolean;
835
+ }): ClientRouteComponentOrigin {
836
+ if (options.analysis.hasUseServerDirective) {
837
+ return "use-server-directive";
838
+ }
839
+
840
+ if (options.serverOnly) {
841
+ return "server-only-import";
842
+ }
843
+
844
+ if (options.analysis.hasUseClientDirective) {
845
+ return "use-client-directive";
846
+ }
847
+
848
+ if (/\.compat(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
849
+ return "compat-filename";
850
+ }
851
+
852
+ if (/\.client(?:\.mreact)?\.[cm]?[jt]sx?$/.test(options.filename)) {
853
+ return "client-filename";
854
+ }
855
+
856
+ return options.info.clientRuntime ? "inferred-client-runtime" : "server-render";
857
+ }
858
+
859
+ function normalizedClientRouteComponents(
860
+ collector: ClientRouteComponentCollector,
861
+ routeFile: string,
862
+ ): ClientRouteComponent[] {
863
+ return Array.from(collector.components.values())
864
+ .filter(
865
+ (component) =>
866
+ collector.reachable.has(clientRouteComponentKey(component.file, component.exportName)) ||
867
+ collector.reachable.has(clientRouteComponentKey(component.file, "*")),
868
+ )
869
+ .map((component) =>
870
+ component.classification === "server-render" &&
871
+ (collector.clientReachable.has(
872
+ clientRouteComponentKey(component.file, component.exportName),
873
+ ) ||
874
+ collector.clientReachable.has(clientRouteComponentKey(component.file, "*")))
875
+ ? { ...component, classification: "shared" as const }
876
+ : component,
877
+ )
878
+ .sort((left, right) => {
879
+ const leftRoute = left.file === routeFile && left.exportName === "default";
880
+ const rightRoute = right.file === routeFile && right.exportName === "default";
881
+
882
+ if (leftRoute !== rightRoute) {
883
+ return leftRoute ? -1 : 1;
884
+ }
885
+
886
+ return left.file === right.file
887
+ ? left.exportName === right.exportName
888
+ ? left.classification.localeCompare(right.classification)
889
+ : left.exportName.localeCompare(right.exportName)
890
+ : left.file.localeCompare(right.file);
891
+ });
892
+ }
893
+
894
+ function clientRouteComponentKey(file: string, exportName: string): string {
895
+ return `${file}\0${exportName}`;
896
+ }
897
+
898
+ function markClientRouteComponentReachable(
899
+ collector: ClientRouteComponentCollector | undefined,
900
+ file: string,
901
+ exportNames: readonly string[] | undefined,
902
+ options: { clientExecution?: boolean | undefined } = {},
903
+ ): void {
904
+ if (collector === undefined) {
905
+ return;
906
+ }
907
+
908
+ if (exportNames === undefined) {
909
+ collector.reachable.add(clientRouteComponentKey(file, "*"));
910
+ if (options.clientExecution === true) {
911
+ collector.clientReachable.add(clientRouteComponentKey(file, "*"));
912
+ }
913
+ return;
914
+ }
915
+
916
+ for (const exportName of exportNames) {
917
+ const key = clientRouteComponentKey(file, exportName);
918
+ collector.reachable.add(key);
919
+ if (options.clientExecution === true) {
920
+ collector.clientReachable.add(key);
921
+ }
922
+ }
923
+ }
924
+
925
+ function clientRouteComponentRunsOnClient(
926
+ collector: ClientRouteComponentCollector | undefined,
927
+ file: string,
928
+ exportName: string,
929
+ ): boolean {
930
+ if (collector === undefined) {
931
+ return false;
932
+ }
933
+
934
+ const key = clientRouteComponentKey(file, exportName);
935
+ const component = collector.components.get(key);
936
+ return (
937
+ component?.classification === "client-boundary" ||
938
+ component?.classification === "client-route" ||
939
+ component?.classification === "shared" ||
940
+ collector.clientReachable.has(key) ||
941
+ collector.clientReachable.has(clientRouteComponentKey(file, "*"))
942
+ );
943
+ }
944
+
945
+ function isClientRouteComponentReachable(
946
+ collector: ClientRouteComponentCollector | undefined,
947
+ file: string,
948
+ exportName: string,
949
+ ): boolean {
950
+ return (
951
+ collector === undefined ||
952
+ collector.reachable.has(clientRouteComponentKey(file, exportName)) ||
953
+ collector.reachable.has(clientRouteComponentKey(file, "*"))
954
+ );
955
+ }
956
+
957
+ function propagateClientRouteComponentStaticExport(
958
+ collector: ClientRouteComponentCollector | undefined,
959
+ file: string,
960
+ resolved: string,
961
+ reference: StaticExportReference,
962
+ ): void {
963
+ if (collector === undefined) {
964
+ return;
965
+ }
966
+
967
+ const wildcardReachable = collector.reachable.has(clientRouteComponentKey(file, "*"));
968
+ const wildcardClientExecution = clientRouteComponentRunsOnClient(collector, file, "*");
969
+
970
+ if (reference.exportAll) {
971
+ if (wildcardReachable) {
972
+ markClientRouteComponentReachable(collector, resolved, undefined, {
973
+ clientExecution: wildcardClientExecution,
974
+ });
975
+ }
976
+
977
+ for (const key of collector.reachable) {
978
+ const [reachableFile, exportName] = key.split("\0");
979
+
980
+ if (reachableFile === file && exportName !== undefined && exportName !== "*") {
981
+ markClientRouteComponentReachable(collector, resolved, [exportName], {
982
+ clientExecution: clientRouteComponentRunsOnClient(collector, file, exportName),
983
+ });
984
+ }
985
+ }
986
+ return;
987
+ }
988
+
989
+ for (const specifier of reference.specifiers) {
990
+ if (
991
+ wildcardReachable ||
992
+ collector.reachable.has(clientRouteComponentKey(file, specifier.exportedName))
993
+ ) {
994
+ markClientRouteComponentReachable(collector, resolved, [specifier.localName], {
995
+ clientExecution:
996
+ wildcardClientExecution ||
997
+ clientRouteComponentRunsOnClient(collector, file, specifier.exportedName),
998
+ });
999
+ }
1000
+ }
1001
+
1002
+ if (reference.specifiers.length === 0) {
1003
+ for (const exportName of reference.exportedNames) {
1004
+ if (wildcardReachable || collector.reachable.has(clientRouteComponentKey(file, exportName))) {
1005
+ markClientRouteComponentReachable(collector, resolved, [exportName], {
1006
+ clientExecution:
1007
+ wildcardClientExecution ||
1008
+ clientRouteComponentRunsOnClient(collector, file, exportName),
1009
+ });
1010
+ }
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ function collectUnknownClientRouteComponents(
1016
+ collector: ClientRouteComponentCollector | undefined,
1017
+ options: {
1018
+ exportNames: readonly string[] | undefined;
1019
+ file: string;
1020
+ },
1021
+ ): void {
1022
+ if (collector === undefined) {
1023
+ return;
1024
+ }
1025
+
1026
+ for (const exportName of options.exportNames ?? ["*"]) {
1027
+ const component = {
1028
+ classification: "unknown",
1029
+ exportName,
1030
+ file: options.file,
1031
+ origin: "unresolved-reference",
1032
+ } satisfies ClientRouteComponent;
1033
+ collector.components.set(
1034
+ clientRouteComponentKey(component.file, component.exportName),
1035
+ component,
1036
+ );
1037
+ markClientRouteComponentReachable(collector, component.file, [component.exportName]);
1038
+ }
1039
+ }
1040
+
1041
+ function collectClientRoutePendingExportValidation(
1042
+ collector: ClientRouteComponentCollector | undefined,
1043
+ options: ClientRoutePendingExportValidation,
1044
+ ): void {
1045
+ if (collector === undefined || options.exportNames.length === 0) {
1046
+ return;
1047
+ }
1048
+
1049
+ collector.pendingExportValidations.push(options);
1050
+ }
1051
+
1052
+ function collectClientRouteStaticExportEdge(
1053
+ collector: ClientRouteComponentCollector | undefined,
1054
+ options: ClientRouteStaticExportEdge,
1055
+ ): void {
1056
+ collector?.staticExportEdges.push(options);
1057
+ }
1058
+
1059
+ function finalizeClientRouteComponentExportValidations(
1060
+ collector: ClientRouteComponentCollector | undefined,
1061
+ ): ClientRouteInferenceDiagnostic[] {
1062
+ if (collector === undefined) {
1063
+ return [];
1064
+ }
1065
+
1066
+ const exportNamesByFile = new Map<string, Set<string>>();
1067
+ for (const [file, names] of collector.localExportNamesByFile) {
1068
+ exportNamesByFile.set(file, new Set(names));
1069
+ }
1070
+
1071
+ let changed = true;
1072
+ while (changed) {
1073
+ changed = false;
1074
+
1075
+ for (const edge of collector.staticExportEdges) {
1076
+ const exportedNames = exportNamesByFile.get(edge.file) ?? new Set<string>();
1077
+ const sourceNames = exportNamesByFile.get(edge.resolved) ?? new Set<string>();
1078
+ const namesToAdd = edge.reference.exportAll
1079
+ ? new Set([...sourceNames].filter((exportName) => exportName !== "default"))
1080
+ : new Set(
1081
+ edge.reference.specifiers
1082
+ .filter((specifier) => sourceNames.has(specifier.localName))
1083
+ .map((specifier) => specifier.exportedName),
1084
+ );
1085
+
1086
+ for (const exportName of namesToAdd) {
1087
+ if (!exportedNames.has(exportName)) {
1088
+ exportedNames.add(exportName);
1089
+ changed = true;
1090
+ }
1091
+ }
1092
+
1093
+ exportNamesByFile.set(edge.file, exportedNames);
1094
+ }
1095
+ }
1096
+
1097
+ const diagnostics: ClientRouteInferenceDiagnostic[] = [];
1098
+ const seen = new Set<string>();
1099
+ for (const pending of collector.pendingExportValidations) {
1100
+ const availableExportNames = exportNamesByFile.get(pending.file) ?? new Set<string>();
1101
+ const missingExportNames = pending.exportNames.filter(
1102
+ (exportName) => !availableExportNames.has(exportName),
1103
+ );
1104
+ if (missingExportNames.length === 0) {
1105
+ continue;
1106
+ }
1107
+
1108
+ const key = `${pending.importer}\0${pending.source}\0${missingExportNames.join("\0")}`;
1109
+ if (seen.has(key)) {
1110
+ continue;
1111
+ }
1112
+ seen.add(key);
1113
+ collectUnknownClientRouteComponents(collector, {
1114
+ exportNames: missingExportNames,
1115
+ file: pending.file,
1116
+ });
1117
+ diagnostics.push(
1118
+ unresolvedClientRouteReferenceDiagnostic({
1119
+ exportNames: missingExportNames,
1120
+ filename: pending.importer,
1121
+ source: pending.source,
1122
+ }),
1123
+ );
1124
+ }
1125
+
1126
+ return diagnostics;
1127
+ }
1128
+
665
1129
  function isExplicitClientRouteSource(
666
1130
  analysis: ClientRouteModuleAnalysis,
667
1131
  filename: string,
@@ -688,33 +1152,49 @@ function hasServerOnlyImports(analysis: ClientRouteModuleAnalysis): boolean {
688
1152
  async function inferClientRouteModuleSource(options: {
689
1153
  cache: ClientRouteInferenceCache;
690
1154
  code: string;
1155
+ componentCollector?: ClientRouteComponentCollector | undefined;
691
1156
  filename: string;
692
1157
  moduleContext?: CompilerModuleContext | undefined;
693
1158
  root: boolean;
1159
+ routeEntry: boolean;
694
1160
  seen: Set<string>;
695
1161
  sourceTransform?: ClientRouteSourceTransform | undefined;
696
1162
  }): Promise<ClientRouteModuleInferenceResult> {
697
1163
  const analysis = await clientRouteModuleAnalysisForSource(options);
698
1164
  const usesNavigationLinkLocal = detectLinkComponentUsage(analysis);
1165
+ collectClientRouteComponentsForModule(options.componentCollector, {
1166
+ analysis,
1167
+ filename: options.filename,
1168
+ root: options.root,
1169
+ routeEntry: options.routeEntry,
1170
+ });
1171
+ const forcedInference = isServerOnlyClientRouteSource(analysis)
1172
+ ? emptyClientRouteModuleInferenceResult({
1173
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
1174
+ navigationLinkExportNames: detectLinkComponentExportNames(analysis),
1175
+ serverOnly: true,
1176
+ serverOnlyClientRuntime: analysis.clientRuntime,
1177
+ usesNavigationLink: usesNavigationLinkLocal,
1178
+ })
1179
+ : isExplicitClientRouteSource(analysis, options.filename)
1180
+ ? emptyClientRouteModuleInferenceResult({
1181
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
1182
+ client: true,
1183
+ clientBoundaryModule: true,
1184
+ })
1185
+ : undefined;
699
1186
 
700
- if (isServerOnlyClientRouteSource(analysis)) {
701
- return emptyClientRouteModuleInferenceResult({
702
- navigationLinkExportNames: detectLinkComponentExportNames(analysis),
703
- serverOnly: true,
704
- serverOnlyClientRuntime: analysis.clientRuntime,
705
- usesNavigationLink: usesNavigationLinkLocal,
706
- });
707
- }
708
-
709
- if (isExplicitClientRouteSource(analysis, options.filename)) {
710
- return emptyClientRouteModuleInferenceResult({
711
- client: true,
712
- clientBoundaryModule: true,
713
- });
1187
+ if (forcedInference !== undefined && options.componentCollector === undefined) {
1188
+ return forcedInference;
714
1189
  }
715
1190
 
716
1191
  if (options.seen.has(options.filename)) {
717
- return emptyClientRouteModuleInferenceResult();
1192
+ return (
1193
+ forcedInference ??
1194
+ emptyClientRouteModuleInferenceResult({
1195
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
1196
+ })
1197
+ );
718
1198
  }
719
1199
 
720
1200
  options.seen.add(options.filename);
@@ -726,11 +1206,14 @@ async function inferClientRouteModuleSource(options: {
726
1206
  const nestedClientExportNames = new Set<string>();
727
1207
  const clientReferenceSourceFiles: string[] = [];
728
1208
  const diagnostics: ClientRouteInferenceDiagnostic[] = [];
1209
+ const availableExportNames = new Set(
1210
+ analysis.topLevelExportRenderInfo.map((info) => info.name),
1211
+ );
729
1212
  let boundaryGraphFallbackRequired = false;
730
1213
  let clientProxy = false;
731
1214
  let nestedClient = false;
732
1215
  let usesNavigationLink = usesNavigationLinkLocal;
733
- const navigationLinkExportNames = new Set<string>(detectLinkComponentExportNames(analysis));
1216
+ const navigationLinkExportNames = new Set(detectLinkComponentExportNames(analysis));
734
1217
  const exportInfo = analysis.topLevelExportRenderInfo;
735
1218
  const implicitModuleClient = exportInfo.length === 0 && analysis.clientRuntime;
736
1219
  for (const info of exportInfo) {
@@ -740,9 +1223,12 @@ async function inferClientRouteModuleSource(options: {
740
1223
  }
741
1224
  if (
742
1225
  hasServerOnlyImports(analysis) &&
743
- (implicitModuleClient || clientBoundaryExportNames.size > 0)
1226
+ (implicitModuleClient || clientBoundaryExportNames.size > 0) &&
1227
+ options.componentCollector === undefined
744
1228
  ) {
745
1229
  return emptyClientRouteModuleInferenceResult({
1230
+ availableExportNames: Array.from(availableExportNames),
1231
+ navigationLinkExportNames: detectLinkComponentExportNames(analysis),
746
1232
  serverOnly: true,
747
1233
  serverOnlyClientRuntime: true,
748
1234
  });
@@ -775,17 +1261,67 @@ async function inferClientRouteModuleSource(options: {
775
1261
  continue;
776
1262
  }
777
1263
 
1264
+ const renderingExportNames = rendered ? renderedLocalExportNames(reference, exportInfo) : [];
1265
+ const renderedImportedNames = rendered
1266
+ ? renderedImportedExportNames(reference, renderedComponentRoots)
1267
+ : [];
1268
+ const renderedFromReachableExport =
1269
+ rendered &&
1270
+ renderingExportNames.some((exportName) =>
1271
+ isClientRouteComponentReachable(options.componentCollector, options.filename, exportName),
1272
+ );
778
1273
  const resolved = await resolveAppLocalModule({
779
1274
  allowExplicitNonSource: options.sourceTransform !== undefined,
780
1275
  cache: options.cache,
781
1276
  importer: options.filename,
782
1277
  specifier: reference.source,
1278
+ tolerateUnresolved: options.componentCollector !== undefined,
783
1279
  });
784
1280
 
785
1281
  if (resolved === undefined) {
1282
+ if (
1283
+ options.componentCollector !== undefined &&
1284
+ renderedFromReachableExport &&
1285
+ reference.source.startsWith(".")
1286
+ ) {
1287
+ collectUnknownClientRouteComponents(options.componentCollector, {
1288
+ exportNames: renderedImportedNames,
1289
+ file: resolve(dirname(options.filename), reference.source),
1290
+ });
1291
+ diagnostics.push(
1292
+ unresolvedClientRouteReferenceDiagnostic({
1293
+ filename: options.filename,
1294
+ source: reference.source,
1295
+ }),
1296
+ );
1297
+ }
786
1298
  continue;
787
1299
  }
788
1300
 
1301
+ if (renderedFromReachableExport) {
1302
+ const clientExecution = renderingExportNames.some((exportName) =>
1303
+ clientRouteComponentRunsOnClient(
1304
+ options.componentCollector,
1305
+ options.filename,
1306
+ exportName,
1307
+ ),
1308
+ );
1309
+ markClientRouteComponentReachable(
1310
+ options.componentCollector,
1311
+ resolved,
1312
+ renderedImportedNames,
1313
+ { clientExecution },
1314
+ );
1315
+ if (renderedImportedNames !== undefined) {
1316
+ collectClientRoutePendingExportValidation(options.componentCollector, {
1317
+ exportNames: renderedImportedNames,
1318
+ file: resolved,
1319
+ importer: options.filename,
1320
+ source: reference.source,
1321
+ });
1322
+ }
1323
+ }
1324
+
789
1325
  const source = await readClientRouteSource({
790
1326
  cache: options.cache,
791
1327
  filename: resolved,
@@ -794,6 +1330,7 @@ async function inferClientRouteModuleSource(options: {
794
1330
  const imported = await inferClientRouteModuleSource({
795
1331
  cache: options.cache,
796
1332
  code: source,
1333
+ componentCollector: options.componentCollector,
797
1334
  filename: resolved,
798
1335
  moduleContext: await compilerModuleContextForSource({
799
1336
  cache: options.cache,
@@ -801,6 +1338,7 @@ async function inferClientRouteModuleSource(options: {
801
1338
  filename: resolved,
802
1339
  }),
803
1340
  root: false,
1341
+ routeEntry: false,
804
1342
  seen: options.seen,
805
1343
  sourceTransform: options.sourceTransform,
806
1344
  });
@@ -844,8 +1382,8 @@ async function inferClientRouteModuleSource(options: {
844
1382
  }
845
1383
 
846
1384
  if (rendered) {
847
- const importedExportNames = renderedImportedExportNames(reference, renderedComponentRoots);
848
- const renderedExportNames = renderedLocalExportNames(reference, exportInfo);
1385
+ const importedExportNames = renderedImportedNames;
1386
+ const renderedExportNames = renderingExportNames;
849
1387
  const importedBoundary =
850
1388
  imported.clientBoundaryModule ||
851
1389
  matchesInferredExportNames(importedExportNames, imported.clientBoundaryExportNames);
@@ -908,12 +1446,33 @@ async function inferClientRouteModuleSource(options: {
908
1446
  cache: options.cache,
909
1447
  importer: options.filename,
910
1448
  specifier: reference.source,
1449
+ tolerateUnresolved: options.componentCollector !== undefined,
911
1450
  });
912
1451
 
913
1452
  if (resolved === undefined) {
1453
+ if (options.componentCollector !== undefined) {
1454
+ diagnostics.push(
1455
+ unresolvedClientRouteReferenceDiagnostic({
1456
+ filename: options.filename,
1457
+ source: reference.source,
1458
+ }),
1459
+ );
1460
+ }
914
1461
  continue;
915
1462
  }
916
1463
 
1464
+ propagateClientRouteComponentStaticExport(
1465
+ options.componentCollector,
1466
+ options.filename,
1467
+ resolved,
1468
+ reference,
1469
+ );
1470
+ collectClientRouteStaticExportEdge(options.componentCollector, {
1471
+ file: options.filename,
1472
+ reference,
1473
+ resolved,
1474
+ });
1475
+
917
1476
  const source = await readClientRouteSource({
918
1477
  cache: options.cache,
919
1478
  filename: resolved,
@@ -922,6 +1481,7 @@ async function inferClientRouteModuleSource(options: {
922
1481
  const exported = await inferClientRouteModuleSource({
923
1482
  cache: options.cache,
924
1483
  code: source,
1484
+ componentCollector: options.componentCollector,
925
1485
  filename: resolved,
926
1486
  moduleContext: await compilerModuleContextForSource({
927
1487
  cache: options.cache,
@@ -929,10 +1489,23 @@ async function inferClientRouteModuleSource(options: {
929
1489
  filename: resolved,
930
1490
  }),
931
1491
  root: false,
1492
+ routeEntry: false,
932
1493
  seen: options.seen,
933
1494
  sourceTransform: options.sourceTransform,
934
1495
  });
935
1496
  diagnostics.push(...exported.diagnostics);
1497
+ if (reference.exportAll) {
1498
+ for (const exportName of exported.availableExportNames) {
1499
+ availableExportNames.add(exportName);
1500
+ }
1501
+ } else {
1502
+ for (const specifier of reference.specifiers) {
1503
+ availableExportNames.add(specifier.exportedName);
1504
+ }
1505
+ for (const exportName of reference.exportedNames) {
1506
+ availableExportNames.add(exportName);
1507
+ }
1508
+ }
936
1509
  // A re-export renders nothing itself, so it does not set the module's
937
1510
  // own `usesNavigationLink`; it only forwards per-export `Link` usage so
938
1511
  // an importer that renders this name can decide precisely. Map the
@@ -974,7 +1547,8 @@ async function inferClientRouteModuleSource(options: {
974
1547
  }
975
1548
  }
976
1549
 
977
- return {
1550
+ const inferred = {
1551
+ availableExportNames: Array.from(availableExportNames),
978
1552
  boundaryGraphFallbackCandidate:
979
1553
  analysis.staticExports.length > 0 || boundaryGraphFallbackRequired,
980
1554
  boundaryGraphFallbackRequired,
@@ -996,6 +1570,15 @@ async function inferClientRouteModuleSource(options: {
996
1570
  serverOnlyClientRuntime: false,
997
1571
  usesNavigationLink,
998
1572
  };
1573
+ return forcedInference === undefined
1574
+ ? inferred
1575
+ : {
1576
+ ...forcedInference,
1577
+ availableExportNames: inferred.availableExportNames,
1578
+ diagnostics,
1579
+ navigationLinkExportNames: inferred.navigationLinkExportNames,
1580
+ usesNavigationLink: inferred.usesNavigationLink,
1581
+ };
999
1582
  } finally {
1000
1583
  options.seen.delete(options.filename);
1001
1584
  }
@@ -1005,6 +1588,7 @@ function emptyClientRouteModuleInferenceResult(
1005
1588
  overrides: Partial<ClientRouteModuleInferenceResult> = {},
1006
1589
  ): ClientRouteModuleInferenceResult {
1007
1590
  return {
1591
+ availableExportNames: [],
1008
1592
  boundaryGraphFallbackCandidate: false,
1009
1593
  boundaryGraphFallbackRequired: false,
1010
1594
  client: false,
@@ -1426,7 +2010,7 @@ function removeSafeCallbackHandlerAttributes(
1426
2010
 
1427
2011
  function matchingBraceEnd(source: string, openBraceIndex: number): number | undefined {
1428
2012
  let depth = 0;
1429
- let quote: "\"" | "'" | "`" | undefined;
2013
+ let quote: '"' | "'" | "`" | undefined;
1430
2014
  let escaped = false;
1431
2015
 
1432
2016
  for (let index = openBraceIndex; index < source.length; index += 1) {
@@ -1443,7 +2027,7 @@ function matchingBraceEnd(source: string, openBraceIndex: number): number | unde
1443
2027
  continue;
1444
2028
  }
1445
2029
 
1446
- if (char === "\"" || char === "'" || char === "`") {
2030
+ if (char === '"' || char === "'" || char === "`") {
1447
2031
  quote = char;
1448
2032
  continue;
1449
2033
  }
@@ -1492,9 +2076,7 @@ function destructuredPropsCallbackNames(source: string): Set<string> {
1492
2076
  addDestructuredCallbackNames(names, match[1] ?? "");
1493
2077
  }
1494
2078
 
1495
- for (const match of source.matchAll(
1496
- /\{[^{}]*:\s*\{([^{}]+)\}\s*(?:=\s*\{\})?[^{}]*\}/gu,
1497
- )) {
2079
+ for (const match of source.matchAll(/\{[^{}]*:\s*\{([^{}]+)\}\s*(?:=\s*\{\})?[^{}]*\}/gu)) {
1498
2080
  addDestructuredCallbackNames(names, match[1] ?? "");
1499
2081
  }
1500
2082
 
@@ -1532,10 +2114,7 @@ function addDestructuredCallbackNames(names: Set<string>, destructured: string):
1532
2114
  const alias = destructuredBindingName(rawAlias);
1533
2115
  const name = alias ?? property;
1534
2116
 
1535
- if (
1536
- name !== undefined &&
1537
- (isCallbackPropName(property) || isCallbackPropName(name))
1538
- ) {
2117
+ if (name !== undefined && (isCallbackPropName(property) || isCallbackPropName(name))) {
1539
2118
  names.add(name);
1540
2119
  }
1541
2120
  }
@@ -1624,6 +2203,28 @@ function serverOnlyClientImportReferenceDiagnostic(options: {
1624
2203
  };
1625
2204
  }
1626
2205
 
2206
+ function unresolvedClientRouteReferenceDiagnostic(options: {
2207
+ exportNames?: readonly string[] | undefined;
2208
+ filename: string;
2209
+ source: string;
2210
+ }): ClientRouteInferenceDiagnostic {
2211
+ const exportSuffix =
2212
+ options.exportNames === undefined || options.exportNames.length === 0
2213
+ ? ""
2214
+ : ` (${options.exportNames.map((name) => JSON.stringify(name)).join(", ")})`;
2215
+
2216
+ return {
2217
+ code: "MR_CLIENT_BOUNDARY_INFERENCE_UNRESOLVED_REFERENCE",
2218
+ filename: options.filename,
2219
+ level: "warn",
2220
+ localNames: [...(options.exportNames ?? [])],
2221
+ message:
2222
+ `${options.filename}: rendered component reference ${JSON.stringify(options.source)}${exportSuffix} ` +
2223
+ "could not be resolved to an exported component.",
2224
+ source: options.source,
2225
+ };
2226
+ }
2227
+
1627
2228
  function functionCallInteractiveImportDiagnostic(options: {
1628
2229
  filename: string;
1629
2230
  reference: StaticImportReference;
@@ -1719,6 +2320,7 @@ async function resolveAppLocalModule(options: {
1719
2320
  cache: ClientRouteInferenceCache;
1720
2321
  importer: string;
1721
2322
  specifier: string;
2323
+ tolerateUnresolved?: boolean | undefined;
1722
2324
  }): Promise<string | undefined> {
1723
2325
  if (!options.specifier.startsWith(".")) {
1724
2326
  return undefined;
@@ -1726,7 +2328,7 @@ async function resolveAppLocalModule(options: {
1726
2328
 
1727
2329
  const cacheKey = `${options.importer}\0${options.specifier}\0${
1728
2330
  options.allowExplicitNonSource === true ? "explicit" : "source"
1729
- }`;
2331
+ }\0${options.tolerateUnresolved === true ? "tolerant" : "strict"}`;
1730
2332
  const cached = options.cache.resolvedByImport.get(cacheKey);
1731
2333
 
1732
2334
  if (cached !== undefined) {
@@ -1737,6 +2339,7 @@ async function resolveAppLocalModule(options: {
1737
2339
  allowExplicitNonSource: options.allowExplicitNonSource === true,
1738
2340
  importer: options.importer,
1739
2341
  specifier: options.specifier,
2342
+ tolerateUnresolved: options.tolerateUnresolved === true,
1740
2343
  });
1741
2344
  options.cache.resolvedByImport.set(cacheKey, resolved);
1742
2345
  return resolved;
@@ -1746,6 +2349,7 @@ async function resolveAppLocalModuleUncached(options: {
1746
2349
  allowExplicitNonSource: boolean;
1747
2350
  importer: string;
1748
2351
  specifier: string;
2352
+ tolerateUnresolved: boolean;
1749
2353
  }): Promise<string | undefined> {
1750
2354
  const { importer, specifier } = options;
1751
2355
  const base = join(dirname(importer), specifier);
@@ -1765,6 +2369,10 @@ async function resolveAppLocalModuleUncached(options: {
1765
2369
  }
1766
2370
  }
1767
2371
 
2372
+ if (options.tolerateUnresolved) {
2373
+ return undefined;
2374
+ }
2375
+
1768
2376
  throw new Error(`${importer}: could not resolve app-local import ${JSON.stringify(specifier)}.`);
1769
2377
  }
1770
2378
 
@@ -2268,8 +2876,7 @@ export async function buildClientRouteEntrySource(
2268
2876
  const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
2269
2877
  const routeUsesDomRefs = compiled.metadata.imports.some(
2270
2878
  (entry) =>
2271
- entry.source === "@reckona/mreact-reactive-dom" &&
2272
- entry.specifiers.includes("bindDomRef"),
2879
+ entry.source === "@reckona/mreact-reactive-dom" && entry.specifiers.includes("bindDomRef"),
2273
2880
  );
2274
2881
  const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect || routeUsesDomRefs;
2275
2882
  const routeExplicitlyRequiresHydration = isExplicitClientRouteSource(
@@ -4813,8 +5420,8 @@ export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`,
4813
5420
  if (
4814
5421
  isAbsolute(args.path) &&
4815
5422
  isRouteClientDependencySourcePath(args.path, routeFiles) &&
4816
- !runtimePackageDirs.some(
4817
- (runtimePackageDir) => args.path.startsWith(`${runtimePackageDir}${sep}`),
5423
+ !runtimePackageDirs.some((runtimePackageDir) =>
5424
+ args.path.startsWith(`${runtimePackageDir}${sep}`),
4818
5425
  )
4819
5426
  ) {
4820
5427
  options.sourceRegionModulePaths?.add(args.path);