@reckona/mreact-router 0.0.54 → 0.0.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { readFile, stat } from "node:fs/promises";
3
3
  import { builtinModules } from "node:module";
4
- import { dirname, join, relative, sep } from "node:path";
4
+ import { dirname, extname, join, relative, sep } from "node:path";
5
5
  import { collectClientRouteModuleAnalysis, formatDiagnostic, } from "@reckona/mreact-compiler";
6
6
  import { collectClientRouteModuleAnalysisFromContext, createCompilerModuleContext, transformCompilerModuleContext, } from "@reckona/mreact-compiler/internal";
7
7
  import { assetPath } from "./assets.js";
@@ -40,6 +40,7 @@ export function createClientRouteInferenceCache() {
40
40
  moduleContextByFile: new Map(),
41
41
  resolvedByImport: new Map(),
42
42
  sourceByFile: new Map(),
43
+ transformedSourceByFile: new Map(),
43
44
  };
44
45
  }
45
46
  export async function isClientRouteModule(options) {
@@ -47,14 +48,21 @@ export async function isClientRouteModule(options) {
47
48
  }
48
49
  export async function inferClientRouteModule(options) {
49
50
  const cache = options.cache ?? createClientRouteInferenceCache();
51
+ const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
52
+ const code = await transformClientRouteSource({
53
+ code: options.code,
54
+ filename: options.filename,
55
+ sourceTransform,
56
+ });
50
57
  try {
51
58
  const routeInference = await inferClientRouteModuleSource({
52
59
  cache,
53
- code: options.code,
60
+ code,
54
61
  filename: options.filename,
55
- moduleContext: options.moduleContext,
62
+ ...(sourceTransform === undefined ? { moduleContext: options.moduleContext } : {}),
56
63
  root: true,
57
64
  seen: new Set(),
65
+ sourceTransform,
58
66
  });
59
67
  if (options.appDir === undefined) {
60
68
  return routeInference;
@@ -63,6 +71,7 @@ export async function inferClientRouteModule(options) {
63
71
  appDir: options.appDir,
64
72
  cache,
65
73
  filename: options.filename,
74
+ sourceTransform,
66
75
  });
67
76
  return {
68
77
  client: routeInference.client || shellInferences.some((inference) => inference.client),
@@ -79,18 +88,25 @@ export async function inferClientRouteModule(options) {
79
88
  }
80
89
  export async function collectClientRouteReferences(options) {
81
90
  const cache = options.cache ?? createClientRouteInferenceCache();
91
+ const sourceTransform = clientRouteSourceTransformForVitePlugins(options.vitePlugins);
92
+ const code = await transformClientRouteSource({
93
+ code: options.code,
94
+ filename: options.filename,
95
+ sourceTransform,
96
+ });
82
97
  const routeModuleContext = await compilerModuleContextForSource({
83
98
  cache,
84
- code: options.code,
99
+ code,
85
100
  filename: options.filename,
86
101
  });
87
102
  const routeInference = await inferClientRouteModuleSource({
88
103
  cache,
89
- code: options.code,
104
+ code,
90
105
  filename: options.filename,
91
106
  moduleContext: routeModuleContext,
92
107
  root: true,
93
108
  seen: new Set(),
109
+ sourceTransform,
94
110
  });
95
111
  const sources = [];
96
112
  const seenSourceFiles = new Set();
@@ -113,6 +129,7 @@ export async function collectClientRouteReferences(options) {
113
129
  moduleContext,
114
130
  root: true,
115
131
  seen: new Set(),
132
+ sourceTransform,
116
133
  }));
117
134
  sources.push({
118
135
  code: sourceOptions.code,
@@ -121,19 +138,19 @@ export async function collectClientRouteReferences(options) {
121
138
  moduleContext,
122
139
  });
123
140
  for (const referenceFile of inference.clientReferenceSourceFiles) {
124
- const code = stripRouteClientOnlyExports(await readCachedFile(cache, referenceFile));
141
+ const code = stripRouteClientOnlyExports(await readClientRouteSource({ cache, filename: referenceFile, sourceTransform }));
125
142
  await addSource({ code, filename: referenceFile });
126
143
  }
127
144
  };
128
145
  await addSource({
129
- code: options.code,
146
+ code,
130
147
  filename: options.filename,
131
148
  inference: routeInference,
132
149
  moduleContext: routeModuleContext,
133
150
  });
134
151
  if (options.appDir !== undefined) {
135
152
  for (const shell of await clientShellFilesForPage(options.appDir, options.filename)) {
136
- const code = stripRouteClientOnlyExports(await readCachedFile(cache, shell));
153
+ const code = stripRouteClientOnlyExports(await readClientRouteSource({ cache, filename: shell, sourceTransform }));
137
154
  const moduleContext = await compilerModuleContextForSource({
138
155
  cache,
139
156
  code,
@@ -149,6 +166,7 @@ export async function collectClientRouteReferences(options) {
149
166
  moduleContext,
150
167
  root: true,
151
168
  seen: new Set(),
169
+ sourceTransform,
152
170
  }),
153
171
  moduleContext,
154
172
  });
@@ -202,13 +220,18 @@ export async function collectClientRouteReferences(options) {
202
220
  }
203
221
  async function inferClientRouteShellModules(options) {
204
222
  return Promise.all((await clientShellFilesForPage(options.appDir, options.filename)).map(async (shell) => {
205
- const code = stripRouteClientOnlyExports(await readCachedFile(options.cache, shell));
223
+ const code = stripRouteClientOnlyExports(await readClientRouteSource({
224
+ cache: options.cache,
225
+ filename: shell,
226
+ sourceTransform: options.sourceTransform,
227
+ }));
206
228
  return await inferClientRouteModuleSource({
207
229
  cache: options.cache,
208
230
  code,
209
231
  filename: shell,
210
232
  root: true,
211
233
  seen: new Set(),
234
+ sourceTransform: options.sourceTransform,
212
235
  });
213
236
  }));
214
237
  }
@@ -232,7 +255,7 @@ function isExplicitClientRouteSource(analysis, filename) {
232
255
  return analysis.hasUseClientDirective || isClientBoundaryFilename(filename);
233
256
  }
234
257
  function isClientBoundaryFilename(filename) {
235
- return /\.client(?:\.mreact)?\.[cm]?[jt]sx?$/.test(filename);
258
+ return /\.(?:client|compat)(?:\.mreact)?\.[cm]?[jt]sx?$/.test(filename);
236
259
  }
237
260
  function isServerOnlyClientRouteSource(analysis) {
238
261
  return analysis.hasUseServerDirective;
@@ -297,6 +320,7 @@ async function inferClientRouteModuleSource(options) {
297
320
  continue;
298
321
  }
299
322
  const resolved = await resolveAppLocalModule({
323
+ allowExplicitNonSource: options.sourceTransform !== undefined,
300
324
  cache: options.cache,
301
325
  importer: options.filename,
302
326
  specifier: reference.source,
@@ -304,7 +328,11 @@ async function inferClientRouteModuleSource(options) {
304
328
  if (resolved === undefined) {
305
329
  continue;
306
330
  }
307
- const source = await readCachedFile(options.cache, resolved);
331
+ const source = await readClientRouteSource({
332
+ cache: options.cache,
333
+ filename: resolved,
334
+ sourceTransform: options.sourceTransform,
335
+ });
308
336
  const imported = await inferClientRouteModuleSource({
309
337
  cache: options.cache,
310
338
  code: source,
@@ -316,6 +344,7 @@ async function inferClientRouteModuleSource(options) {
316
344
  }),
317
345
  root: false,
318
346
  seen: options.seen,
347
+ sourceTransform: options.sourceTransform,
319
348
  });
320
349
  diagnostics.push(...imported.diagnostics);
321
350
  if (!imported.client) {
@@ -364,6 +393,7 @@ async function inferClientRouteModuleSource(options) {
364
393
  if (!options.root) {
365
394
  for (const reference of analysis.staticExports) {
366
395
  const resolved = await resolveAppLocalModule({
396
+ allowExplicitNonSource: options.sourceTransform !== undefined,
367
397
  cache: options.cache,
368
398
  importer: options.filename,
369
399
  specifier: reference.source,
@@ -371,7 +401,11 @@ async function inferClientRouteModuleSource(options) {
371
401
  if (resolved === undefined) {
372
402
  continue;
373
403
  }
374
- const source = await readCachedFile(options.cache, resolved);
404
+ const source = await readClientRouteSource({
405
+ cache: options.cache,
406
+ filename: resolved,
407
+ sourceTransform: options.sourceTransform,
408
+ });
375
409
  const exported = await inferClientRouteModuleSource({
376
410
  cache: options.cache,
377
411
  code: source,
@@ -383,6 +417,7 @@ async function inferClientRouteModuleSource(options) {
383
417
  }),
384
418
  root: false,
385
419
  seen: options.seen,
420
+ sourceTransform: options.sourceTransform,
386
421
  });
387
422
  diagnostics.push(...exported.diagnostics);
388
423
  if (exported.clientBoundaryModule) {
@@ -562,19 +597,27 @@ async function resolveAppLocalModule(options) {
562
597
  if (!options.specifier.startsWith(".")) {
563
598
  return undefined;
564
599
  }
565
- const cacheKey = `${options.importer}\0${options.specifier}`;
600
+ const cacheKey = `${options.importer}\0${options.specifier}\0${options.allowExplicitNonSource === true ? "explicit" : "source"}`;
566
601
  const cached = options.cache.resolvedByImport.get(cacheKey);
567
602
  if (cached !== undefined) {
568
603
  return cached;
569
604
  }
570
- const resolved = resolveAppLocalModuleUncached(options.importer, options.specifier);
605
+ const resolved = resolveAppLocalModuleUncached({
606
+ allowExplicitNonSource: options.allowExplicitNonSource === true,
607
+ importer: options.importer,
608
+ specifier: options.specifier,
609
+ });
571
610
  options.cache.resolvedByImport.set(cacheKey, resolved);
572
611
  return resolved;
573
612
  }
574
- async function resolveAppLocalModuleUncached(importer, specifier) {
613
+ async function resolveAppLocalModuleUncached(options) {
614
+ const { importer, specifier } = options;
575
615
  const base = join(dirname(importer), specifier);
576
616
  const candidates = sourceModuleCandidates(base);
577
617
  if (candidates.length === 0) {
618
+ if (options.allowExplicitNonSource && extname(base) !== "" && (await isFile(base))) {
619
+ return base;
620
+ }
578
621
  return undefined;
579
622
  }
580
623
  for (const candidate of candidates) {
@@ -608,6 +651,126 @@ async function readCachedFile(cache, filename) {
608
651
  cache.sourceByFile.set(filename, source);
609
652
  return (await source).source;
610
653
  }
654
+ async function readClientRouteSource(options) {
655
+ if (options.sourceTransform === undefined) {
656
+ return readCachedFile(options.cache, options.filename);
657
+ }
658
+ const signature = await sourceFileSignature(options.filename);
659
+ const cacheKey = `${options.filename}\0${options.sourceTransform.cacheKey}`;
660
+ const cached = options.cache.transformedSourceByFile.get(cacheKey);
661
+ if (cached !== undefined) {
662
+ const source = await cached;
663
+ if (source.signature === signature) {
664
+ return source.source;
665
+ }
666
+ }
667
+ const source = readCachedFile(options.cache, options.filename).then(async (code) => ({
668
+ signature,
669
+ source: await options.sourceTransform.transform(options.filename, code),
670
+ }));
671
+ options.cache.transformedSourceByFile.set(cacheKey, source);
672
+ return (await source).source;
673
+ }
674
+ async function transformClientRouteSource(options) {
675
+ return options.sourceTransform === undefined
676
+ ? options.code
677
+ : await options.sourceTransform.transform(options.filename, options.code);
678
+ }
679
+ function clientRouteSourceTransformForVitePlugins(pluginOptions) {
680
+ const plugins = orderVitePlugins(flattenVitePlugins(pluginOptions)).filter((plugin) => hasViteTransformHook(plugin));
681
+ if (plugins.length === 0) {
682
+ return undefined;
683
+ }
684
+ return {
685
+ cacheKey: plugins.map((plugin, index) => `${index}:${plugin.name}`).join("\0"),
686
+ async transform(filename, code) {
687
+ let nextCode = code;
688
+ for (const plugin of plugins) {
689
+ const handler = viteTransformHookHandler(plugin);
690
+ if (handler === undefined) {
691
+ continue;
692
+ }
693
+ const result = await handler.call(createClientRouteViteTransformContext(plugin.name), nextCode, filename, { ssr: false });
694
+ if (typeof result === "string") {
695
+ nextCode = result;
696
+ }
697
+ else if (result !== null && result !== undefined && typeof result === "object") {
698
+ const codeResult = result.code;
699
+ if (typeof codeResult === "string") {
700
+ nextCode = codeResult;
701
+ }
702
+ }
703
+ }
704
+ return nextCode;
705
+ },
706
+ };
707
+ }
708
+ function flattenVitePlugins(pluginOptions) {
709
+ const plugins = [];
710
+ const visit = (option) => {
711
+ if (option === false || option === null || option === undefined) {
712
+ return;
713
+ }
714
+ if (Array.isArray(option)) {
715
+ for (const child of option) {
716
+ visit(child);
717
+ }
718
+ return;
719
+ }
720
+ if (typeof option === "object" && "then" in option) {
721
+ return;
722
+ }
723
+ plugins.push(option);
724
+ };
725
+ for (const option of pluginOptions ?? []) {
726
+ visit(option);
727
+ }
728
+ return plugins;
729
+ }
730
+ function orderVitePlugins(plugins) {
731
+ return [
732
+ ...plugins.filter((plugin) => plugin.enforce === "pre"),
733
+ ...plugins.filter((plugin) => plugin.enforce !== "pre" && plugin.enforce !== "post"),
734
+ ...plugins.filter((plugin) => plugin.enforce === "post"),
735
+ ];
736
+ }
737
+ function hasViteTransformHook(plugin) {
738
+ return viteTransformHookHandler(plugin) !== undefined;
739
+ }
740
+ function viteTransformHookHandler(plugin) {
741
+ const transform = plugin.transform;
742
+ if (typeof transform === "function") {
743
+ return transform;
744
+ }
745
+ if (transform !== null && typeof transform === "object") {
746
+ const handler = transform.handler;
747
+ if (typeof handler === "function") {
748
+ return handler;
749
+ }
750
+ }
751
+ return undefined;
752
+ }
753
+ function createClientRouteViteTransformContext(pluginName) {
754
+ return {
755
+ addWatchFile() { },
756
+ async resolve() {
757
+ return null;
758
+ },
759
+ error(error) {
760
+ if (error instanceof Error) {
761
+ throw error;
762
+ }
763
+ throw new Error(String(error));
764
+ },
765
+ warn() { },
766
+ getCombinedSourcemap() {
767
+ return null;
768
+ },
769
+ parse() {
770
+ throw new Error(`${pluginName}: client route import analysis cannot provide Rollup parser context.`);
771
+ },
772
+ };
773
+ }
611
774
  async function sourceFileSignature(filename) {
612
775
  const stats = await stat(filename);
613
776
  return `${stats.mtimeMs}\0${stats.size}`;
@@ -685,6 +848,7 @@ export async function buildClientRouteOutput(options) {
685
848
  });
686
849
  const compiled = transformCompilerModuleContext({
687
850
  code: options.code,
851
+ clientBoundaryImports: options.clientBoundaryImports ?? [],
688
852
  filename: options.filename,
689
853
  moduleContext,
690
854
  target: "client",
@@ -697,13 +861,16 @@ export async function buildClientRouteOutput(options) {
697
861
  }
698
862
  const clientNavigation = options.clientNavigation ?? detectClientNavigationHint(options.code);
699
863
  const clientReferenceManifest = options.clientReferenceManifest ?? (await inferClientReferenceManifestForBundle(options));
864
+ const compatClientReferenceNames = compatClientReferenceComponentNames(clientReferenceManifest);
700
865
  const clientReferenceImportBlock = emitClientReferenceImportBlock(options.clientReferenceImports ?? []);
701
- const clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest, options.clientReferenceImports ?? []);
866
+ const clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest, options.clientReferenceImports ?? [], compatClientReferenceNames);
702
867
  const routeComponentExpression = routeComponentExpressionForComponents(compiled.metadata.components);
703
868
  const routeId = routeIdForPath(options.routePath);
704
869
  const routeUsesCells = detectRouteCellStateHint(compiled.code);
870
+ const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
871
+ const routeUsesCleanupScope = routeUsesCells || routeUsesReactiveEffect;
705
872
  const routeHasEventBindings = (compiled.metadata.eventHydrationManifest?.events.length ?? 0) > 0;
706
- const routeRequiresFullHydration = routeUsesCells || routeHasEventBindings;
873
+ const routeRequiresFullHydration = routeUsesCells || routeUsesReactiveEffect || routeHasEventBindings;
707
874
  const routeUsesOnlyClientReferenceBoundaries = !routeRequiresFullHydration &&
708
875
  clientReferenceManifest.length > 0 &&
709
876
  (options.clientReferenceImports?.length ?? 0) > 0;
@@ -715,6 +882,9 @@ export async function buildClientRouteOutput(options) {
715
882
  const routeCellEffectImport = routeUsesCells
716
883
  ? `import { effect as __mreactRouteEffect } from "@reckona/mreact-reactive-core";\n`
717
884
  : "";
885
+ const routeCleanupScopeImport = routeUsesCleanupScope
886
+ ? `import { withCleanupScope as __mreactWithCleanupScope } from "@reckona/mreact-reactive-core/internal";\n`
887
+ : "";
718
888
  const navigationStateDeclaration = clientNavigation
719
889
  ? `const __mreactNavigationState = __mreactGlobal.__mreactNavigationState ??= {
720
890
  cache: new Map(),
@@ -737,6 +907,9 @@ export async function buildClientRouteOutput(options) {
737
907
  let __mreactActiveCellRecords = undefined;
738
908
  let __mreactActiveCellIndex = 0;`
739
909
  : "";
910
+ const routeCleanupStateDeclaration = routeUsesCleanupScope
911
+ ? `const __mreactRouteDisposers = __mreactGlobal.__mreactRouteDisposers ??= new Map();`
912
+ : "";
740
913
  const routeCellHook = routeUsesCells
741
914
  ? `
742
915
  __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
@@ -778,8 +951,10 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
778
951
  __mreactState.dispose?.();
779
952
 
780
953
  __mreactState.dispose = __mreactRouteEffect(() => {
954
+ const __mreactRouteEffectDisposers = new Set();
781
955
  __mreactActiveCellRecords = __mreactState.cells;
782
956
  __mreactActiveCellIndex = 0;
957
+ __mreactRouteDisposers.set(__mreactRouteId, () => __mreactState.dispose?.());
783
958
 
784
959
  try {
785
960
  `
@@ -789,10 +964,27 @@ __mreactGlobal.__mreactRouteCell = (nativeCell, initial) => {
789
964
  __mreactActiveCellRecords = undefined;
790
965
  __mreactActiveCellIndex = 0;
791
966
  }
967
+ return () => {
968
+ for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
969
+ __mreactDispose();
970
+ }
971
+ __mreactRouteEffectDisposers.clear();
972
+ };
792
973
  });
793
974
  `
794
975
  : "";
795
976
  const routeCellHydrationIndent = routeUsesCells ? " " : " ";
977
+ const routeCleanupHydrationStart = routeUsesCleanupScope && !routeUsesCells
978
+ ? ` __mreactDisposeRoute(__mreactRouteId);
979
+ const __mreactRouteEffectDisposers = new Set();
980
+ __mreactRouteDisposers.set(__mreactRouteId, () => {
981
+ for (const __mreactDispose of Array.from(__mreactRouteEffectDisposers)) {
982
+ __mreactDispose();
983
+ }
984
+ __mreactRouteEffectDisposers.clear();
985
+ });
986
+ `
987
+ : "";
796
988
  const routeCellDropFunction = routeUsesCells
797
989
  ? `
798
990
  function __mreactDropMismatchedRouteState(previousState, nextState) {
@@ -804,6 +996,24 @@ function __mreactDropMismatchedRouteState(previousState, nextState) {
804
996
  console.warn("mreact: dropping stale route state after route cell signature changed");
805
997
  }
806
998
  }
999
+ `
1000
+ : "";
1001
+ const routeCleanupFunction = routeUsesCleanupScope
1002
+ ? `
1003
+ function __mreactDisposeRoute(routeId) {
1004
+ const __mreactDispose = __mreactRouteDisposers.get(routeId);
1005
+ if (__mreactDispose === undefined) {
1006
+ return;
1007
+ }
1008
+ __mreactRouteDisposers.delete(routeId);
1009
+ __mreactDispose();
1010
+ }
1011
+ `
1012
+ : "";
1013
+ const routeCleanupNavigationDispose = routeUsesCleanupScope
1014
+ ? ` if (currentRouteId !== nextRouteId) {
1015
+ __mreactDisposeRoute(currentRouteId);
1016
+ }
807
1017
  `
808
1018
  : "";
809
1019
  const routeNodeResolver = routeUsesCells
@@ -830,9 +1040,12 @@ function __mreactResolveRouteNode(value) {
830
1040
  }
831
1041
  `
832
1042
  : "";
833
- const routeNodeExpression = routeUsesCells
834
- ? "__mreactResolveRouteNode(__mreactComponent(__mreactProps))"
1043
+ const routeComponentCallExpression = routeUsesCleanupScope
1044
+ ? "__mreactWithCleanupScope((__mreactDispose) => __mreactRouteEffectDisposers.add(__mreactDispose), () => __mreactComponent(__mreactProps))"
835
1045
  : "__mreactComponent(__mreactProps)";
1046
+ const routeNodeExpression = routeUsesCells
1047
+ ? `__mreactResolveRouteNode(${routeComponentCallExpression})`
1048
+ : routeComponentCallExpression;
836
1049
  const boundaryOnlyHydrationBlock = routeRequiresFullHydration
837
1050
  ? ""
838
1051
  : `${routeCellHydrationIndent}if (!__mreactHasNonSerializableClientBoundaries(__mreactMarker) && __mreactHydrateClientBoundaries(document, __mreactClientReferences, __mreactClientReferenceComponents)) {
@@ -844,13 +1057,14 @@ ${routeCellHydrationIndent}}
844
1057
  ${routeCellHydrationIndent} return;
845
1058
  ${routeCellHydrationIndent}}
846
1059
  `;
847
- const entry = `${routeCellEffectImport}${clientReferenceImportBlock}${routeHydrationCode}
1060
+ const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${emitCompatClientReferenceImportBlock(compatClientReferenceNames)}${clientReferenceImportBlock}${routeHydrationCode}
848
1061
 
849
1062
  const __mreactRouteId = ${JSON.stringify(routeId)};
850
1063
  const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
851
1064
  const __mreactGlobal = globalThis;
852
1065
  ${navigationStateDeclaration}
853
1066
  ${routeCellStateDeclaration}
1067
+ ${routeCleanupStateDeclaration}
854
1068
  ${routeCellHook}
855
1069
  ${clientReferenceRegistry}
856
1070
  ${routeNodeResolver}
@@ -873,11 +1087,13 @@ export function __mreactHydrateRoute() {
873
1087
  if (__mreactMarker === null) {
874
1088
  return;
875
1089
  }
876
- ${routeCellHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
1090
+ ${routeCellHydrationStart}${routeCleanupHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
877
1091
  ${routeCellHydrationIndent}__mreactResumeRoute(__mreactMarker, __mreactNode);
1092
+ ${routeCellHydrationIndent}__mreactHydrateClientBoundaries(__mreactMarker, __mreactClientReferences, __mreactClientReferenceComponents);
878
1093
  ${routeCellHydrationIndent}__mreactMarker.setAttribute("data-mreact-hydrated", "true");
879
1094
  ${routeCellHydrationEnd}}
880
1095
  ${routeCellDropFunction}
1096
+ ${routeCleanupFunction}
881
1097
 
882
1098
  __mreactHydrateRoute();
883
1099
  ${clientNavigation ? "__mreactInstallNavigation();" : ""}
@@ -1272,7 +1488,7 @@ function __mreactApplyNavigationHtml(html, url) {
1272
1488
 
1273
1489
  __mreactSyncHeadMetadata(template.content, html);
1274
1490
  __mreactResumeNode(currentMarker, nextMarker);
1275
- __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
1491
+ ${routeCleanupNavigationDispose} __mreactSyncRouteDataScripts(template.content, currentRouteId, nextRouteId);
1276
1492
 
1277
1493
  const script = template.content.querySelector('script[type="module"][src]')?.getAttribute("src");
1278
1494
  if (script !== null && script !== undefined) {
@@ -1748,7 +1964,9 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1748
1964
 
1749
1965
  for (const placeholder of placeholders) {
1750
1966
  const name = placeholder.getAttribute("data-mreact-client-boundary");
1751
- const component = name === null ? undefined : components.get(name);
1967
+ const entry = name === null ? undefined : components.get(name);
1968
+ const component = typeof entry === "function" ? entry : entry?.component;
1969
+ const compat = entry?.compat === true;
1752
1970
 
1753
1971
  if (typeof component !== "function") {
1754
1972
  return false;
@@ -1758,6 +1976,17 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1758
1976
  const props = propsElement?.textContent === undefined || propsElement.textContent === ""
1759
1977
  ? {}
1760
1978
  : JSON.parse(propsElement.textContent);
1979
+
1980
+ if (compat) {
1981
+ const container = document.createElement("span");
1982
+ container.setAttribute("data-mreact-compat-boundary", name ?? "");
1983
+ container.style.display = "contents";
1984
+ placeholder.replaceWith(container);
1985
+ __mreactCompatCreateRoot(container).render(__mreactCompatCreateElement(component, props));
1986
+ propsElement?.remove();
1987
+ continue;
1988
+ }
1989
+
1761
1990
  const node = component(props);
1762
1991
 
1763
1992
  placeholder.replaceWith(node);
@@ -2180,6 +2409,7 @@ function __mreactResumeChildren(current, next) {
2180
2409
  preserveExports: true,
2181
2410
  plugins: [workspaceRuntimePlugin({ routeFile: options.filename })],
2182
2411
  sourceMap: options.sourceMap,
2412
+ vitePlugins: options.vitePlugins,
2183
2413
  });
2184
2414
  return {
2185
2415
  code: bundled.code,
@@ -2196,6 +2426,10 @@ function workspaceRuntimePlugin(options) {
2196
2426
  const reactiveCorePath = packageFile("reactive-core", "@reckona/mreact-reactive-core", "index");
2197
2427
  const reactiveCoreDir = dirname(reactiveCorePath);
2198
2428
  const runtimePaths = new Map([
2429
+ [
2430
+ "@reckona/mreact-reactive-core/internal",
2431
+ packageFile("reactive-core", "@reckona/mreact-reactive-core", "internal"),
2432
+ ],
2199
2433
  ["@reckona/mreact-compat", packageFile("react-compat", "@reckona/mreact-compat", "index")],
2200
2434
  [
2201
2435
  "@reckona/mreact-compat/event-priority",
@@ -2237,7 +2471,7 @@ function workspaceRuntimePlugin(options) {
2237
2471
  path: "reactive-core",
2238
2472
  }));
2239
2473
  buildApi.onResolve({
2240
- filter: /^@reckona\/mreact-(?:compat|reactive-dom)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler))?$/,
2474
+ filter: /^@reckona\/mreact-(?:compat|reactive-core|reactive-dom)(?:\/(?:event-priority|flight|internal|jsx-dev-runtime|jsx-runtime|scheduler))?$/,
2241
2475
  }, (args) => {
2242
2476
  const path = runtimePaths.get(args.path);
2243
2477
  return path === undefined ? undefined : { path };
@@ -2271,6 +2505,7 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2271
2505
  code: source,
2272
2506
  dev: true,
2273
2507
  filename: args.path,
2508
+ mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
2274
2509
  moduleContext,
2275
2510
  target: "client",
2276
2511
  });
@@ -2291,6 +2526,9 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2291
2526
  function isRouteClientDependencySourcePath(path, routeFile) {
2292
2527
  return path !== routeFile && !path.includes(`${sep}node_modules${sep}`);
2293
2528
  }
2529
+ function isCompatSourcePath(path) {
2530
+ return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(path);
2531
+ }
2294
2532
  /**
2295
2533
  * Detects the `export const clientNavigation = false` hint in a page module
2296
2534
  * source. Returns the hinted value, or `true` when no hint is present (i.e.,
@@ -2316,6 +2554,10 @@ function detectRouteCellStateHint(code) {
2316
2554
  ? /\bcell\d*\s*\(/.test(code)
2317
2555
  : new RegExp(`(?:${callExpression})\\s*\\(`).test(code);
2318
2556
  }
2557
+ function detectRouteReactiveEffectHint(code) {
2558
+ return (/from\s+["']@reckona\/mreact-reactive-core["']/.test(code) &&
2559
+ /\beffect\s*\(/.test(code));
2560
+ }
2319
2561
  async function inferClientReferenceManifestForBundle(options) {
2320
2562
  const cache = createClientRouteInferenceCache();
2321
2563
  const moduleContext = await compilerModuleContextForSource({
@@ -2356,7 +2598,7 @@ function emitClientReferenceImportBlock(imports) {
2356
2598
  })
2357
2599
  .join("\n")}\n`;
2358
2600
  }
2359
- function emitClientReferenceRegistry(manifest, imports) {
2601
+ function emitClientReferenceRegistry(manifest, imports, compatNames) {
2360
2602
  const importedExpressions = new Map(imports.map((reference, index) => [
2361
2603
  reference.name,
2362
2604
  isIdentifierName(reference.exportName)
@@ -2367,10 +2609,24 @@ function emitClientReferenceRegistry(manifest, imports) {
2367
2609
  const expression = importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
2368
2610
  return expression === undefined
2369
2611
  ? []
2370
- : [` [${JSON.stringify(reference.name)}, ${expression}],`];
2612
+ : [
2613
+ compatNames.has(reference.name)
2614
+ ? ` [${JSON.stringify(reference.name)}, { component: ${expression}, compat: true }],`
2615
+ : ` [${JSON.stringify(reference.name)}, ${expression}],`,
2616
+ ];
2371
2617
  });
2372
2618
  return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
2373
2619
  }
2620
+ function compatClientReferenceComponentNames(manifest) {
2621
+ return new Set(manifest
2622
+ .filter((reference) => isCompatSourcePath(reference.moduleId))
2623
+ .map((reference) => reference.name));
2624
+ }
2625
+ function emitCompatClientReferenceImportBlock(compatNames) {
2626
+ return compatNames.size === 0
2627
+ ? ""
2628
+ : 'import { createElement as __mreactCompatCreateElement, createRoot as __mreactCompatCreateRoot } from "@reckona/mreact-compat";\n';
2629
+ }
2374
2630
  function clientReferenceLocalName(index) {
2375
2631
  return `__mreactClientReference${index}`;
2376
2632
  }