@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/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 { dirname, extname, 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";
@@ -68,6 +69,7 @@ export interface ClientRouteManifestEntry {
68
69
  export interface BuildClientRouteOutputOptions {
69
70
  cacheDir?: string | undefined;
70
71
  code: string;
72
+ debugLabels?: boolean | undefined;
71
73
  clientBoundaryImports?: readonly string[] | undefined;
72
74
  clientReferenceImports?: readonly ClientReferenceImport[] | undefined;
73
75
  clientReferenceManifest?: readonly ClientReferenceMetadata[] | undefined;
@@ -117,10 +119,37 @@ export interface ClientRouteInferenceResult {
117
119
  client: boolean;
118
120
  clientBoundaryImports: string[];
119
121
  clientBoundaryFallbackImports: string[];
122
+ components?: ClientRouteComponent[] | undefined;
120
123
  diagnostics: ClientRouteInferenceDiagnostic[];
121
124
  }
122
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
+
123
151
  interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
152
+ availableExportNames: string[];
124
153
  boundaryGraphFallbackCandidate: boolean;
125
154
  boundaryGraphFallbackRequired: boolean;
126
155
  clientBoundaryExportNames: string[];
@@ -133,6 +162,28 @@ interface ClientRouteModuleInferenceResult extends ClientRouteInferenceResult {
133
162
  usesNavigationLink: boolean;
134
163
  }
135
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
+
136
187
  export interface ClientReferenceImport {
137
188
  exportName: string;
138
189
  importSource: string;
@@ -147,10 +198,11 @@ export interface ClientRouteReferenceResult extends ClientRouteInferenceResult {
147
198
 
148
199
  export interface ClientRouteInferenceDiagnostic {
149
200
  code:
150
- | typeof clientBoundaryInferenceServerOnlyReferenceCode
151
- | typeof clientBoundaryInferenceFunctionCallInteractiveCode
152
- | typeof clientBoundaryInferenceUnsupportedReferenceCode
153
- | 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";
154
206
  filename: string;
155
207
  level: "warn";
156
208
  localNames: string[];
@@ -217,6 +269,7 @@ export async function inferClientRouteModule(options: {
217
269
  appDir?: string | undefined;
218
270
  cache?: ClientRouteInferenceCache | undefined;
219
271
  code: string;
272
+ collectComponents?: boolean | undefined;
220
273
  filename: string;
221
274
  moduleContext?: CompilerModuleContext | undefined;
222
275
  routePath?: string | undefined;
@@ -224,6 +277,16 @@ export async function inferClientRouteModule(options: {
224
277
  }): Promise<ClientRouteInferenceResult> {
225
278
  const cache = options.cache ?? createClientRouteInferenceCache();
226
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;
227
290
  const code = await transformClientRouteSource({
228
291
  code: options.code,
229
292
  filename: options.filename,
@@ -234,9 +297,11 @@ export async function inferClientRouteModule(options: {
234
297
  const routeInference = await inferClientRouteModuleSource({
235
298
  cache,
236
299
  code,
300
+ componentCollector,
237
301
  filename: options.filename,
238
302
  ...(sourceTransform === undefined ? { moduleContext: options.moduleContext } : {}),
239
303
  root: true,
304
+ routeEntry: true,
240
305
  seen: new Set(),
241
306
  sourceTransform,
242
307
  });
@@ -253,15 +318,29 @@ export async function inferClientRouteModule(options: {
253
318
  : routeInference;
254
319
 
255
320
  if (options.appDir === undefined) {
256
- 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
+ );
257
334
  }
258
335
 
259
336
  const shellInferences = await inferClientRouteShellModules({
260
337
  appDir: options.appDir,
261
338
  cache,
339
+ componentCollector,
262
340
  filename: options.filename,
263
341
  sourceTransform,
264
342
  });
343
+ const exportDiagnostics = finalizeClientRouteComponentExportValidations(componentCollector);
265
344
 
266
345
  return withClientRouteDiagnosticPath(
267
346
  {
@@ -269,9 +348,15 @@ export async function inferClientRouteModule(options: {
269
348
  mergedRouteInference.client || shellInferences.some((inference) => inference.client),
270
349
  clientBoundaryImports: mergedRouteInference.clientBoundaryImports,
271
350
  clientBoundaryFallbackImports: mergedRouteInference.clientBoundaryFallbackImports,
351
+ ...(componentCollector === undefined
352
+ ? {}
353
+ : {
354
+ components: normalizedClientRouteComponents(componentCollector, options.filename),
355
+ }),
272
356
  diagnostics: [
273
357
  ...mergedRouteInference.diagnostics,
274
358
  ...shellInferences.flatMap((inference) => inference.diagnostics),
359
+ ...exportDiagnostics,
275
360
  ],
276
361
  },
277
362
  options.routePath,
@@ -376,6 +461,7 @@ export async function collectClientRouteReferences(options: {
376
461
  filename: options.filename,
377
462
  moduleContext: routeModuleContext,
378
463
  root: true,
464
+ routeEntry: true,
379
465
  seen: new Set(),
380
466
  sourceTransform,
381
467
  });
@@ -412,6 +498,7 @@ export async function collectClientRouteReferences(options: {
412
498
  filename: sourceOptions.filename,
413
499
  moduleContext,
414
500
  root: true,
501
+ routeEntry: false,
415
502
  seen: new Set(),
416
503
  sourceTransform,
417
504
  }));
@@ -458,6 +545,7 @@ export async function collectClientRouteReferences(options: {
458
545
  filename: shell,
459
546
  moduleContext,
460
547
  root: true,
548
+ routeEntry: false,
461
549
  seen: new Set(),
462
550
  sourceTransform,
463
551
  }),
@@ -603,6 +691,7 @@ export function navigationRuntimeLinkDisabledDiagnostic(options: {
603
691
  async function inferClientRouteShellModules(options: {
604
692
  appDir: string;
605
693
  cache: ClientRouteInferenceCache;
694
+ componentCollector?: ClientRouteComponentCollector | undefined;
606
695
  filename: string;
607
696
  sourceTransform?: ClientRouteSourceTransform | undefined;
608
697
  }): Promise<ClientRouteInferenceResult[]> {
@@ -619,8 +708,10 @@ async function inferClientRouteShellModules(options: {
619
708
  return await inferClientRouteModuleSource({
620
709
  cache: options.cache,
621
710
  code,
711
+ componentCollector: options.componentCollector,
622
712
  filename: shell,
623
713
  root: true,
714
+ routeEntry: false,
624
715
  seen: new Set(),
625
716
  sourceTransform: options.sourceTransform,
626
717
  });
@@ -661,6 +752,380 @@ export function isClientRouteSource(code: string): boolean {
661
752
  );
662
753
  }
663
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
+
664
1129
  function isExplicitClientRouteSource(
665
1130
  analysis: ClientRouteModuleAnalysis,
666
1131
  filename: string,
@@ -687,33 +1152,49 @@ function hasServerOnlyImports(analysis: ClientRouteModuleAnalysis): boolean {
687
1152
  async function inferClientRouteModuleSource(options: {
688
1153
  cache: ClientRouteInferenceCache;
689
1154
  code: string;
1155
+ componentCollector?: ClientRouteComponentCollector | undefined;
690
1156
  filename: string;
691
1157
  moduleContext?: CompilerModuleContext | undefined;
692
1158
  root: boolean;
1159
+ routeEntry: boolean;
693
1160
  seen: Set<string>;
694
1161
  sourceTransform?: ClientRouteSourceTransform | undefined;
695
1162
  }): Promise<ClientRouteModuleInferenceResult> {
696
1163
  const analysis = await clientRouteModuleAnalysisForSource(options);
697
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;
698
1186
 
699
- if (isServerOnlyClientRouteSource(analysis)) {
700
- return emptyClientRouteModuleInferenceResult({
701
- navigationLinkExportNames: detectLinkComponentExportNames(analysis),
702
- serverOnly: true,
703
- serverOnlyClientRuntime: analysis.clientRuntime,
704
- usesNavigationLink: usesNavigationLinkLocal,
705
- });
706
- }
707
-
708
- if (isExplicitClientRouteSource(analysis, options.filename)) {
709
- return emptyClientRouteModuleInferenceResult({
710
- client: true,
711
- clientBoundaryModule: true,
712
- });
1187
+ if (forcedInference !== undefined && options.componentCollector === undefined) {
1188
+ return forcedInference;
713
1189
  }
714
1190
 
715
1191
  if (options.seen.has(options.filename)) {
716
- return emptyClientRouteModuleInferenceResult();
1192
+ return (
1193
+ forcedInference ??
1194
+ emptyClientRouteModuleInferenceResult({
1195
+ availableExportNames: analysis.topLevelExportRenderInfo.map((info) => info.name),
1196
+ })
1197
+ );
717
1198
  }
718
1199
 
719
1200
  options.seen.add(options.filename);
@@ -725,11 +1206,14 @@ async function inferClientRouteModuleSource(options: {
725
1206
  const nestedClientExportNames = new Set<string>();
726
1207
  const clientReferenceSourceFiles: string[] = [];
727
1208
  const diagnostics: ClientRouteInferenceDiagnostic[] = [];
1209
+ const availableExportNames = new Set(
1210
+ analysis.topLevelExportRenderInfo.map((info) => info.name),
1211
+ );
728
1212
  let boundaryGraphFallbackRequired = false;
729
1213
  let clientProxy = false;
730
1214
  let nestedClient = false;
731
1215
  let usesNavigationLink = usesNavigationLinkLocal;
732
- const navigationLinkExportNames = new Set<string>(detectLinkComponentExportNames(analysis));
1216
+ const navigationLinkExportNames = new Set(detectLinkComponentExportNames(analysis));
733
1217
  const exportInfo = analysis.topLevelExportRenderInfo;
734
1218
  const implicitModuleClient = exportInfo.length === 0 && analysis.clientRuntime;
735
1219
  for (const info of exportInfo) {
@@ -739,9 +1223,12 @@ async function inferClientRouteModuleSource(options: {
739
1223
  }
740
1224
  if (
741
1225
  hasServerOnlyImports(analysis) &&
742
- (implicitModuleClient || clientBoundaryExportNames.size > 0)
1226
+ (implicitModuleClient || clientBoundaryExportNames.size > 0) &&
1227
+ options.componentCollector === undefined
743
1228
  ) {
744
1229
  return emptyClientRouteModuleInferenceResult({
1230
+ availableExportNames: Array.from(availableExportNames),
1231
+ navigationLinkExportNames: detectLinkComponentExportNames(analysis),
745
1232
  serverOnly: true,
746
1233
  serverOnlyClientRuntime: true,
747
1234
  });
@@ -774,17 +1261,67 @@ async function inferClientRouteModuleSource(options: {
774
1261
  continue;
775
1262
  }
776
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
+ );
777
1273
  const resolved = await resolveAppLocalModule({
778
1274
  allowExplicitNonSource: options.sourceTransform !== undefined,
779
1275
  cache: options.cache,
780
1276
  importer: options.filename,
781
1277
  specifier: reference.source,
1278
+ tolerateUnresolved: options.componentCollector !== undefined,
782
1279
  });
783
1280
 
784
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
+ }
785
1298
  continue;
786
1299
  }
787
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
+
788
1325
  const source = await readClientRouteSource({
789
1326
  cache: options.cache,
790
1327
  filename: resolved,
@@ -793,6 +1330,7 @@ async function inferClientRouteModuleSource(options: {
793
1330
  const imported = await inferClientRouteModuleSource({
794
1331
  cache: options.cache,
795
1332
  code: source,
1333
+ componentCollector: options.componentCollector,
796
1334
  filename: resolved,
797
1335
  moduleContext: await compilerModuleContextForSource({
798
1336
  cache: options.cache,
@@ -800,6 +1338,7 @@ async function inferClientRouteModuleSource(options: {
800
1338
  filename: resolved,
801
1339
  }),
802
1340
  root: false,
1341
+ routeEntry: false,
803
1342
  seen: options.seen,
804
1343
  sourceTransform: options.sourceTransform,
805
1344
  });
@@ -843,8 +1382,8 @@ async function inferClientRouteModuleSource(options: {
843
1382
  }
844
1383
 
845
1384
  if (rendered) {
846
- const importedExportNames = renderedImportedExportNames(reference, renderedComponentRoots);
847
- const renderedExportNames = renderedLocalExportNames(reference, exportInfo);
1385
+ const importedExportNames = renderedImportedNames;
1386
+ const renderedExportNames = renderingExportNames;
848
1387
  const importedBoundary =
849
1388
  imported.clientBoundaryModule ||
850
1389
  matchesInferredExportNames(importedExportNames, imported.clientBoundaryExportNames);
@@ -907,12 +1446,33 @@ async function inferClientRouteModuleSource(options: {
907
1446
  cache: options.cache,
908
1447
  importer: options.filename,
909
1448
  specifier: reference.source,
1449
+ tolerateUnresolved: options.componentCollector !== undefined,
910
1450
  });
911
1451
 
912
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
+ }
913
1461
  continue;
914
1462
  }
915
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
+
916
1476
  const source = await readClientRouteSource({
917
1477
  cache: options.cache,
918
1478
  filename: resolved,
@@ -921,6 +1481,7 @@ async function inferClientRouteModuleSource(options: {
921
1481
  const exported = await inferClientRouteModuleSource({
922
1482
  cache: options.cache,
923
1483
  code: source,
1484
+ componentCollector: options.componentCollector,
924
1485
  filename: resolved,
925
1486
  moduleContext: await compilerModuleContextForSource({
926
1487
  cache: options.cache,
@@ -928,10 +1489,23 @@ async function inferClientRouteModuleSource(options: {
928
1489
  filename: resolved,
929
1490
  }),
930
1491
  root: false,
1492
+ routeEntry: false,
931
1493
  seen: options.seen,
932
1494
  sourceTransform: options.sourceTransform,
933
1495
  });
934
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
+ }
935
1509
  // A re-export renders nothing itself, so it does not set the module's
936
1510
  // own `usesNavigationLink`; it only forwards per-export `Link` usage so
937
1511
  // an importer that renders this name can decide precisely. Map the
@@ -973,7 +1547,8 @@ async function inferClientRouteModuleSource(options: {
973
1547
  }
974
1548
  }
975
1549
 
976
- return {
1550
+ const inferred = {
1551
+ availableExportNames: Array.from(availableExportNames),
977
1552
  boundaryGraphFallbackCandidate:
978
1553
  analysis.staticExports.length > 0 || boundaryGraphFallbackRequired,
979
1554
  boundaryGraphFallbackRequired,
@@ -995,6 +1570,15 @@ async function inferClientRouteModuleSource(options: {
995
1570
  serverOnlyClientRuntime: false,
996
1571
  usesNavigationLink,
997
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
+ };
998
1582
  } finally {
999
1583
  options.seen.delete(options.filename);
1000
1584
  }
@@ -1004,6 +1588,7 @@ function emptyClientRouteModuleInferenceResult(
1004
1588
  overrides: Partial<ClientRouteModuleInferenceResult> = {},
1005
1589
  ): ClientRouteModuleInferenceResult {
1006
1590
  return {
1591
+ availableExportNames: [],
1007
1592
  boundaryGraphFallbackCandidate: false,
1008
1593
  boundaryGraphFallbackRequired: false,
1009
1594
  client: false,
@@ -1425,7 +2010,7 @@ function removeSafeCallbackHandlerAttributes(
1425
2010
 
1426
2011
  function matchingBraceEnd(source: string, openBraceIndex: number): number | undefined {
1427
2012
  let depth = 0;
1428
- let quote: "\"" | "'" | "`" | undefined;
2013
+ let quote: '"' | "'" | "`" | undefined;
1429
2014
  let escaped = false;
1430
2015
 
1431
2016
  for (let index = openBraceIndex; index < source.length; index += 1) {
@@ -1442,7 +2027,7 @@ function matchingBraceEnd(source: string, openBraceIndex: number): number | unde
1442
2027
  continue;
1443
2028
  }
1444
2029
 
1445
- if (char === "\"" || char === "'" || char === "`") {
2030
+ if (char === '"' || char === "'" || char === "`") {
1446
2031
  quote = char;
1447
2032
  continue;
1448
2033
  }
@@ -1491,9 +2076,7 @@ function destructuredPropsCallbackNames(source: string): Set<string> {
1491
2076
  addDestructuredCallbackNames(names, match[1] ?? "");
1492
2077
  }
1493
2078
 
1494
- for (const match of source.matchAll(
1495
- /\{[^{}]*:\s*\{([^{}]+)\}\s*(?:=\s*\{\})?[^{}]*\}/gu,
1496
- )) {
2079
+ for (const match of source.matchAll(/\{[^{}]*:\s*\{([^{}]+)\}\s*(?:=\s*\{\})?[^{}]*\}/gu)) {
1497
2080
  addDestructuredCallbackNames(names, match[1] ?? "");
1498
2081
  }
1499
2082
 
@@ -1531,10 +2114,7 @@ function addDestructuredCallbackNames(names: Set<string>, destructured: string):
1531
2114
  const alias = destructuredBindingName(rawAlias);
1532
2115
  const name = alias ?? property;
1533
2116
 
1534
- if (
1535
- name !== undefined &&
1536
- (isCallbackPropName(property) || isCallbackPropName(name))
1537
- ) {
2117
+ if (name !== undefined && (isCallbackPropName(property) || isCallbackPropName(name))) {
1538
2118
  names.add(name);
1539
2119
  }
1540
2120
  }
@@ -1623,6 +2203,28 @@ function serverOnlyClientImportReferenceDiagnostic(options: {
1623
2203
  };
1624
2204
  }
1625
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
+
1626
2228
  function functionCallInteractiveImportDiagnostic(options: {
1627
2229
  filename: string;
1628
2230
  reference: StaticImportReference;
@@ -1718,6 +2320,7 @@ async function resolveAppLocalModule(options: {
1718
2320
  cache: ClientRouteInferenceCache;
1719
2321
  importer: string;
1720
2322
  specifier: string;
2323
+ tolerateUnresolved?: boolean | undefined;
1721
2324
  }): Promise<string | undefined> {
1722
2325
  if (!options.specifier.startsWith(".")) {
1723
2326
  return undefined;
@@ -1725,7 +2328,7 @@ async function resolveAppLocalModule(options: {
1725
2328
 
1726
2329
  const cacheKey = `${options.importer}\0${options.specifier}\0${
1727
2330
  options.allowExplicitNonSource === true ? "explicit" : "source"
1728
- }`;
2331
+ }\0${options.tolerateUnresolved === true ? "tolerant" : "strict"}`;
1729
2332
  const cached = options.cache.resolvedByImport.get(cacheKey);
1730
2333
 
1731
2334
  if (cached !== undefined) {
@@ -1736,6 +2339,7 @@ async function resolveAppLocalModule(options: {
1736
2339
  allowExplicitNonSource: options.allowExplicitNonSource === true,
1737
2340
  importer: options.importer,
1738
2341
  specifier: options.specifier,
2342
+ tolerateUnresolved: options.tolerateUnresolved === true,
1739
2343
  });
1740
2344
  options.cache.resolvedByImport.set(cacheKey, resolved);
1741
2345
  return resolved;
@@ -1745,6 +2349,7 @@ async function resolveAppLocalModuleUncached(options: {
1745
2349
  allowExplicitNonSource: boolean;
1746
2350
  importer: string;
1747
2351
  specifier: string;
2352
+ tolerateUnresolved: boolean;
1748
2353
  }): Promise<string | undefined> {
1749
2354
  const { importer, specifier } = options;
1750
2355
  const base = join(dirname(importer), specifier);
@@ -1764,6 +2369,10 @@ async function resolveAppLocalModuleUncached(options: {
1764
2369
  }
1765
2370
  }
1766
2371
 
2372
+ if (options.tolerateUnresolved) {
2373
+ return undefined;
2374
+ }
2375
+
1767
2376
  throw new Error(`${importer}: could not resolve app-local import ${JSON.stringify(specifier)}.`);
1768
2377
  }
1769
2378
 
@@ -2104,6 +2713,13 @@ export async function buildClientRouteOutput(
2104
2713
  const entry = await buildClientRouteEntrySource(options);
2105
2714
  const dropConsoleFunctions =
2106
2715
  options.dropConsoleFunctions ?? resolveClientConsolePureFunctions(options.dropClientConsole);
2716
+ const sourceRegionModulePaths =
2717
+ options.debugLabels === true ? undefined : new Set([options.filename]);
2718
+ const runtimePlugin = workspaceRuntimePlugin({
2719
+ debugLabels: options.debugLabels === true,
2720
+ routeFiles: [options.filename],
2721
+ sourceRegionModulePaths,
2722
+ });
2107
2723
  const bundled = await bundleRouterModule({
2108
2724
  code: entry.code,
2109
2725
  cacheDir: options.cacheDir,
@@ -2115,7 +2731,8 @@ export async function buildClientRouteOutput(
2115
2731
  minify: options.minify === true,
2116
2732
  platform: "browser",
2117
2733
  preserveExports: true,
2118
- plugins: [workspaceRuntimePlugin({ routeFiles: [options.filename] })],
2734
+ sourceRegionModulePaths,
2735
+ plugins: [runtimePlugin],
2119
2736
  sourceMap: options.sourceMap,
2120
2737
  vitePlugins: options.vitePlugins,
2121
2738
  });
@@ -2149,6 +2766,15 @@ export async function buildClientRouteBatchOutput(options: {
2149
2766
  }),
2150
2767
  })),
2151
2768
  );
2769
+ const debugLabels = options.routes.some((route) => route.debugLabels === true);
2770
+ const sourceRegionModulePaths = debugLabels
2771
+ ? undefined
2772
+ : new Set(entries.map((entry) => entry.filename));
2773
+ const runtimePlugin = workspaceRuntimePlugin({
2774
+ debugLabels,
2775
+ routeFiles: entries.map((entry) => entry.filename),
2776
+ sourceRegionModulePaths,
2777
+ });
2152
2778
  const bundled = await bundleRouterModules({
2153
2779
  base: options.assetBaseUrl ?? "/_mreact/client/",
2154
2780
  cacheDir: options.cacheDir,
@@ -2162,7 +2788,8 @@ export async function buildClientRouteBatchOutput(options: {
2162
2788
  })),
2163
2789
  minify: options.minify === true,
2164
2790
  platform: "browser",
2165
- plugins: [workspaceRuntimePlugin({ routeFiles: entries.map((entry) => entry.filename) })],
2791
+ sourceRegionModulePaths,
2792
+ plugins: [runtimePlugin],
2166
2793
  root: options.projectRoot,
2167
2794
  sourceMap: options.sourceMap,
2168
2795
  dropConsoleFunctions: options.dropConsoleFunctions,
@@ -2199,13 +2826,22 @@ export async function buildClientRouteEntrySource(
2199
2826
  filename: options.filename,
2200
2827
  });
2201
2828
  const routeSourceAnalysis = collectClientRouteModuleAnalysisFromContext(moduleContext);
2829
+ const compilerFilename =
2830
+ options.debugLabels === true ? basename(options.filename) : options.filename;
2831
+ const compilerModuleContext =
2832
+ compilerFilename === options.filename
2833
+ ? moduleContext
2834
+ : createCompilerModuleContext({
2835
+ code: options.code,
2836
+ filename: compilerFilename,
2837
+ });
2202
2838
  const compiled = transformCompilerModuleContext({
2203
2839
  code: options.code,
2204
2840
  clientBoundaryImports: options.clientBoundaryImports ?? [],
2205
- filename: options.filename,
2206
- moduleContext,
2841
+ filename: compilerFilename,
2842
+ moduleContext: compilerModuleContext,
2207
2843
  target: "client",
2208
- dev: options.minify !== true,
2844
+ dev: options.debugLabels === true,
2209
2845
  });
2210
2846
 
2211
2847
  if (compiled.diagnostics.length > 0) {
@@ -2238,7 +2874,11 @@ export async function buildClientRouteEntrySource(
2238
2874
  const routeId = routeIdForPath(options.routePath);
2239
2875
  const routeUsesCells = detectRouteCellStateHint(compiled.code);
2240
2876
  const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
2241
- const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect;
2877
+ const routeUsesDomRefs = compiled.metadata.imports.some(
2878
+ (entry) =>
2879
+ entry.source === "@reckona/mreact-reactive-dom" && entry.specifiers.includes("bindDomRef"),
2880
+ );
2881
+ const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect || routeUsesDomRefs;
2242
2882
  const routeExplicitlyRequiresHydration = isExplicitClientRouteSource(
2243
2883
  routeSourceAnalysis,
2244
2884
  options.filename,
@@ -2254,6 +2894,7 @@ export async function buildClientRouteEntrySource(
2254
2894
  routeExplicitlyRequiresHydration ||
2255
2895
  routeUsesCells ||
2256
2896
  routeUsesReactiveEffect ||
2897
+ routeUsesDomRefs ||
2257
2898
  routeHasEventBindings;
2258
2899
  const routeUsesOnlyClientReferenceBoundaries =
2259
2900
  !routeRequiresFullHydration &&
@@ -2276,7 +2917,7 @@ export async function buildClientRouteEntrySource(
2276
2917
  ? `import { bindCapturedEvent as __mreactBindCapturedEvent } from "@reckona/mreact-reactive-dom/internal";\n`
2277
2918
  : "";
2278
2919
  const routeReactiveDomMetadataImport = !routeUsesOnlyClientReferenceBoundaries
2279
- ? `${routeCapturedEventImport}import { withEventBindingMetadata as __mreactWithEventBindingMetadata, withPropBindingMetadata as __mreactWithPropBindingMetadata } from "@reckona/mreact-reactive-dom";\n`
2920
+ ? `${routeCapturedEventImport}import { ${routeUsesDomRefs ? "getDomRefBindings as __mreactGetDomRefBindings, " : ""}withEventBindingMetadata as __mreactWithEventBindingMetadata, withPropBindingMetadata as __mreactWithPropBindingMetadata } from "@reckona/mreact-reactive-dom";\n`
2280
2921
  : "";
2281
2922
  const navigationStateDeclaration = inlineClientNavigation
2282
2923
  ? `const __mreactNavigationState = __mreactGlobal.__mreactNavigationState ??= {
@@ -2552,9 +3193,10 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
2552
3193
  __mreactActiveCellIndex = 0;
2553
3194
  }
2554
3195
  return () => {
2555
- for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
2556
- __mreactDispose();
2557
- }
3196
+ __mreactRunLifecycleTasks(
3197
+ Array.from(__mreactRouteEffectDisposers),
3198
+ (__mreactDispose) => __mreactDispose(),
3199
+ );
2558
3200
  __mreactRouteEffectDisposers.clear();
2559
3201
  };
2560
3202
  });
@@ -2566,13 +3208,33 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
2566
3208
  ? ` __mreactDisposeRoute(__mreactRouteId);
2567
3209
  const __mreactRouteEffectDisposers = new Set();
2568
3210
  __mreactRouteDisposers.set(__mreactRouteId, () => {
2569
- for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
2570
- __mreactDispose();
2571
- }
3211
+ __mreactRunLifecycleTasks(
3212
+ Array.from(__mreactRouteEffectDisposers),
3213
+ (__mreactDispose) => __mreactDispose(),
3214
+ );
2572
3215
  __mreactRouteEffectDisposers.clear();
2573
3216
  });
2574
3217
  `
2575
3218
  : "";
3219
+ const routeLifecycleFunctions = `
3220
+ function __mreactRunLifecycleTasks(values, run) {
3221
+ let firstError;
3222
+
3223
+ for (const value of values) {
3224
+ try {
3225
+ run(value);
3226
+ } catch (error) {
3227
+ firstError ??= error;
3228
+ }
3229
+ }
3230
+
3231
+ if (firstError !== undefined) {
3232
+ queueMicrotask(() => {
3233
+ throw firstError;
3234
+ });
3235
+ }
3236
+ }
3237
+ `;
2576
3238
  const routeCellDropFunction = routeUsesCells
2577
3239
  ? `
2578
3240
  function __mreactDropMismatchedRouteState(previousState, nextState) {
@@ -2598,12 +3260,18 @@ function __mreactDisposeRoute(routeId) {
2598
3260
  }
2599
3261
  `
2600
3262
  : "";
2601
- const routeCleanupNavigationDispose = routeUsesCleanupScope
2602
- ? ` if (currentRouteId !== nextRouteId) {
2603
- __mreactDisposeRoute(currentRouteId);
3263
+ const routeCleanupNavigationDispose = ` if (currentRouteId !== nextRouteId) {
3264
+ const __mreactRegisteredRouteDisposers = __mreactGlobal.__mreactRouteDisposers;
3265
+ const __mreactRegisteredRouteDispose = __mreactRegisteredRouteDisposers?.get(currentRouteId);
3266
+ if (__mreactRegisteredRouteDispose !== undefined) {
3267
+ __mreactRegisteredRouteDisposers.delete(currentRouteId);
3268
+ __mreactRunLifecycleTasks(
3269
+ [__mreactRegisteredRouteDispose],
3270
+ (__mreactDispose) => __mreactDispose(),
3271
+ );
3272
+ }
2604
3273
  }
2605
- `
2606
- : "";
3274
+ `;
2607
3275
  const routeNodeResolver = routeUsesCells
2608
3276
  ? `
2609
3277
  function __mreactResolveRouteNode(value) {
@@ -2642,9 +3310,7 @@ function __mreactResolveRouteNode(value) {
2642
3310
  const previousDisposers = current.__mreactEventDisposers;
2643
3311
 
2644
3312
  if (Array.isArray(previousDisposers)) {
2645
- for (const dispose of previousDisposers) {
2646
- dispose();
2647
- }
3313
+ __mreactRunLifecycleTasks(previousDisposers, (dispose) => dispose());
2648
3314
  }
2649
3315
 
2650
3316
  const rawBindings = next.__mreactEventBindings;
@@ -2673,14 +3339,26 @@ function __mreactResolveRouteNode(value) {
2673
3339
  const previousDisposers = current.__mreactEventDisposers;
2674
3340
 
2675
3341
  if (Array.isArray(previousDisposers)) {
2676
- for (const dispose of previousDisposers) {
2677
- dispose();
2678
- }
3342
+ __mreactRunLifecycleTasks(previousDisposers, (dispose) => dispose());
2679
3343
  }
2680
3344
 
2681
3345
  current.__mreactEventDisposers = [];
2682
3346
  current.__mreactHasEvents = false;
2683
3347
  }
3348
+ `;
3349
+ const routeDomRefBindingSyncFunction = routeUsesDomRefs
3350
+ ? `function __mreactSyncDomRefBindings(current, next) {
3351
+ __mreactRunLifecycleTasks(
3352
+ Array.from(__mreactGetDomRefBindings(current)),
3353
+ (binding) => binding.dispose(),
3354
+ );
3355
+ __mreactRunLifecycleTasks(
3356
+ Array.from(__mreactGetDomRefBindings(next)),
3357
+ (binding) => binding.retarget(current),
3358
+ );
3359
+ }
3360
+ `
3361
+ : `function __mreactSyncDomRefBindings() {}
2684
3362
  `;
2685
3363
  const boundaryOnlyHydrationBlock = routeRequiresFullHydration
2686
3364
  ? ""
@@ -2740,6 +3418,7 @@ ${routeCellHydrationIndent}__mreactMarker.setAttribute(__mreactRouteHydratedAttr
2740
3418
  ${routeCellHydrationIndent}__mreactMarkRouteHydrated();
2741
3419
  ${routeCellHydrationEnd}}
2742
3420
  ${routeCellDropFunction}
3421
+ ${routeLifecycleFunctions}
2743
3422
  ${routeCleanupFunction}
2744
3423
 
2745
3424
  function __mreactMarkRouteHydrated() {
@@ -3327,13 +4006,14 @@ function __mreactApplyNavigationHtml(html, url) {
3327
4006
  const currentRouteId = currentMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
3328
4007
  const nextRouteId = nextMarker.getAttribute("${routeHydrationContract.routeMarkerAttribute}");
3329
4008
 
4009
+ ${routeCleanupNavigationDispose}
3330
4010
  __mreactMarkRouteHydrating();
3331
4011
  __mreactSyncHeadMetadata(template.content, html);
3332
4012
  if (!__mreactApplyNavigationShellHtml(currentMarker, nextMarker)) {
3333
4013
  __mreactUnmountCompatBoundaries(currentMarker);
3334
4014
  __mreactResumeNode(currentMarker, nextMarker);
3335
4015
  }
3336
- ${routeCleanupNavigationDispose} __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
4016
+ __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
3337
4017
 
3338
4018
  const script = template.content.querySelector('script[type="module"][src]')?.getAttribute("src");
3339
4019
  if (script !== null && script !== undefined) {
@@ -4491,6 +5171,7 @@ function __mreactResumeNode(current, next) {
4491
5171
  }
4492
5172
 
4493
5173
  __mreactSyncEventBindings(current, next);
5174
+ __mreactSyncDomRefBindings(current, next);
4494
5175
  __mreactSyncAttributes(current, next);
4495
5176
  __mreactSyncPropBindings(current, next);
4496
5177
  __mreactResumeChildren(current, next);
@@ -4513,6 +5194,7 @@ function __mreactShouldReplaceNode(current, next) {
4513
5194
  }
4514
5195
 
4515
5196
  ${routeEventBindingSyncFunction}
5197
+ ${routeDomRefBindingSyncFunction}
4516
5198
 
4517
5199
  function __mreactSyncAttributes(current, next) {
4518
5200
  for (const attribute of Array.from(current.attributes)) {
@@ -4532,9 +5214,7 @@ function __mreactSyncPropBindings(current, next) {
4532
5214
  const previousBindings = current.__mreactPropBindings;
4533
5215
 
4534
5216
  if (Array.isArray(previousBindings)) {
4535
- for (const binding of previousBindings) {
4536
- binding.dispose?.();
4537
- }
5217
+ __mreactRunLifecycleTasks(previousBindings, (binding) => binding.dispose?.());
4538
5218
  }
4539
5219
 
4540
5220
  const bindings = next.__mreactPropBindings;
@@ -4550,9 +5230,7 @@ function __mreactSyncPropBindings(current, next) {
4550
5230
  next.__mreactPropBindings = [];
4551
5231
  next.__mreactHasReactiveProps = false;
4552
5232
 
4553
- for (const binding of bindings) {
4554
- binding.retarget?.(current);
4555
- }
5233
+ __mreactRunLifecycleTasks(bindings, (binding) => binding.retarget?.(current));
4556
5234
  }
4557
5235
 
4558
5236
  function __mreactResumeChildren(current, next) {
@@ -4605,7 +5283,11 @@ function __mreactResumeChildren(current, next) {
4605
5283
  };
4606
5284
  }
4607
5285
 
4608
- function workspaceRuntimePlugin(options: { routeFiles: readonly string[] }) {
5286
+ function workspaceRuntimePlugin(options: {
5287
+ debugLabels: boolean;
5288
+ routeFiles: readonly string[];
5289
+ sourceRegionModulePaths?: Set<string> | undefined;
5290
+ }) {
4609
5291
  const routeFiles = new Set(options.routeFiles);
4610
5292
  const packageFile = (monorepoDir: string, packageName: string, entry: string): string =>
4611
5293
  workspacePackageFile({
@@ -4733,15 +5415,31 @@ export function invalidateReactiveDevtoolsCache() {}
4733
5415
  export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`,
4734
5416
  loader: "ts",
4735
5417
  }));
5418
+ if (options.sourceRegionModulePaths !== undefined) {
5419
+ buildApi.onLoad({ filter: /.*/ }, (args) => {
5420
+ if (
5421
+ isAbsolute(args.path) &&
5422
+ isRouteClientDependencySourcePath(args.path, routeFiles) &&
5423
+ !runtimePackageDirs.some((runtimePackageDir) =>
5424
+ args.path.startsWith(`${runtimePackageDir}${sep}`),
5425
+ )
5426
+ ) {
5427
+ options.sourceRegionModulePaths?.add(args.path);
5428
+ }
5429
+
5430
+ return undefined;
5431
+ });
5432
+ }
4736
5433
  buildApi.onLoad({ filter: /\.(?:mreact\.)?[cm]?[jt]sx$/ }, async (args) => {
4737
5434
  if (!isRouteClientDependencySourcePath(args.path, routeFiles)) {
4738
5435
  return undefined;
4739
5436
  }
4740
5437
 
4741
5438
  const source = await readFile(args.path, "utf8");
5439
+ const compilerFilename = options.debugLabels ? basename(args.path) : args.path;
4742
5440
  const moduleContext = createCompilerModuleContext({
4743
5441
  code: source,
4744
- filename: args.path,
5442
+ filename: compilerFilename,
4745
5443
  });
4746
5444
 
4747
5445
  if (!hasJsxSyntax(moduleContext.program)) {
@@ -4750,8 +5448,8 @@ export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`,
4750
5448
 
4751
5449
  const output = transformCompilerModuleContext({
4752
5450
  code: source,
4753
- dev: true,
4754
- filename: args.path,
5451
+ dev: options.debugLabels,
5452
+ filename: compilerFilename,
4755
5453
  mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
4756
5454
  moduleContext,
4757
5455
  target: "client",