@reckona/mreact-router 0.0.55 → 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,8 +861,9 @@ 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);
@@ -892,7 +1057,7 @@ ${routeCellHydrationIndent}}
892
1057
  ${routeCellHydrationIndent} return;
893
1058
  ${routeCellHydrationIndent}}
894
1059
  `;
895
- const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${clientReferenceImportBlock}${routeHydrationCode}
1060
+ const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${emitCompatClientReferenceImportBlock(compatClientReferenceNames)}${clientReferenceImportBlock}${routeHydrationCode}
896
1061
 
897
1062
  const __mreactRouteId = ${JSON.stringify(routeId)};
898
1063
  const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
@@ -924,6 +1089,7 @@ export function __mreactHydrateRoute() {
924
1089
  }
925
1090
  ${routeCellHydrationStart}${routeCleanupHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
926
1091
  ${routeCellHydrationIndent}__mreactResumeRoute(__mreactMarker, __mreactNode);
1092
+ ${routeCellHydrationIndent}__mreactHydrateClientBoundaries(__mreactMarker, __mreactClientReferences, __mreactClientReferenceComponents);
927
1093
  ${routeCellHydrationIndent}__mreactMarker.setAttribute("data-mreact-hydrated", "true");
928
1094
  ${routeCellHydrationEnd}}
929
1095
  ${routeCellDropFunction}
@@ -1798,7 +1964,9 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1798
1964
 
1799
1965
  for (const placeholder of placeholders) {
1800
1966
  const name = placeholder.getAttribute("data-mreact-client-boundary");
1801
- 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;
1802
1970
 
1803
1971
  if (typeof component !== "function") {
1804
1972
  return false;
@@ -1808,6 +1976,17 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1808
1976
  const props = propsElement?.textContent === undefined || propsElement.textContent === ""
1809
1977
  ? {}
1810
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
+
1811
1990
  const node = component(props);
1812
1991
 
1813
1992
  placeholder.replaceWith(node);
@@ -2230,6 +2409,7 @@ function __mreactResumeChildren(current, next) {
2230
2409
  preserveExports: true,
2231
2410
  plugins: [workspaceRuntimePlugin({ routeFile: options.filename })],
2232
2411
  sourceMap: options.sourceMap,
2412
+ vitePlugins: options.vitePlugins,
2233
2413
  });
2234
2414
  return {
2235
2415
  code: bundled.code,
@@ -2325,6 +2505,7 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2325
2505
  code: source,
2326
2506
  dev: true,
2327
2507
  filename: args.path,
2508
+ mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
2328
2509
  moduleContext,
2329
2510
  target: "client",
2330
2511
  });
@@ -2345,6 +2526,9 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2345
2526
  function isRouteClientDependencySourcePath(path, routeFile) {
2346
2527
  return path !== routeFile && !path.includes(`${sep}node_modules${sep}`);
2347
2528
  }
2529
+ function isCompatSourcePath(path) {
2530
+ return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(path);
2531
+ }
2348
2532
  /**
2349
2533
  * Detects the `export const clientNavigation = false` hint in a page module
2350
2534
  * source. Returns the hinted value, or `true` when no hint is present (i.e.,
@@ -2414,7 +2598,7 @@ function emitClientReferenceImportBlock(imports) {
2414
2598
  })
2415
2599
  .join("\n")}\n`;
2416
2600
  }
2417
- function emitClientReferenceRegistry(manifest, imports) {
2601
+ function emitClientReferenceRegistry(manifest, imports, compatNames) {
2418
2602
  const importedExpressions = new Map(imports.map((reference, index) => [
2419
2603
  reference.name,
2420
2604
  isIdentifierName(reference.exportName)
@@ -2425,10 +2609,24 @@ function emitClientReferenceRegistry(manifest, imports) {
2425
2609
  const expression = importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
2426
2610
  return expression === undefined
2427
2611
  ? []
2428
- : [` [${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
+ ];
2429
2617
  });
2430
2618
  return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
2431
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
+ }
2432
2630
  function clientReferenceLocalName(index) {
2433
2631
  return `__mreactClientReference${index}`;
2434
2632
  }