@reckona/mreact-router 0.0.55 → 0.0.57

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;
@@ -296,7 +319,11 @@ async function inferClientRouteModuleSource(options) {
296
319
  (!referenced || !hasPotentialClientBoundaryReference(reference, identifierReferences))) {
297
320
  continue;
298
321
  }
322
+ if (reference.sideEffect && isStyleModuleSpecifier(reference.source)) {
323
+ continue;
324
+ }
299
325
  const resolved = await resolveAppLocalModule({
326
+ allowExplicitNonSource: options.sourceTransform !== undefined,
300
327
  cache: options.cache,
301
328
  importer: options.filename,
302
329
  specifier: reference.source,
@@ -304,7 +331,11 @@ async function inferClientRouteModuleSource(options) {
304
331
  if (resolved === undefined) {
305
332
  continue;
306
333
  }
307
- const source = await readCachedFile(options.cache, resolved);
334
+ const source = await readClientRouteSource({
335
+ cache: options.cache,
336
+ filename: resolved,
337
+ sourceTransform: options.sourceTransform,
338
+ });
308
339
  const imported = await inferClientRouteModuleSource({
309
340
  cache: options.cache,
310
341
  code: source,
@@ -316,6 +347,7 @@ async function inferClientRouteModuleSource(options) {
316
347
  }),
317
348
  root: false,
318
349
  seen: options.seen,
350
+ sourceTransform: options.sourceTransform,
319
351
  });
320
352
  diagnostics.push(...imported.diagnostics);
321
353
  if (!imported.client) {
@@ -364,6 +396,7 @@ async function inferClientRouteModuleSource(options) {
364
396
  if (!options.root) {
365
397
  for (const reference of analysis.staticExports) {
366
398
  const resolved = await resolveAppLocalModule({
399
+ allowExplicitNonSource: options.sourceTransform !== undefined,
367
400
  cache: options.cache,
368
401
  importer: options.filename,
369
402
  specifier: reference.source,
@@ -371,7 +404,11 @@ async function inferClientRouteModuleSource(options) {
371
404
  if (resolved === undefined) {
372
405
  continue;
373
406
  }
374
- const source = await readCachedFile(options.cache, resolved);
407
+ const source = await readClientRouteSource({
408
+ cache: options.cache,
409
+ filename: resolved,
410
+ sourceTransform: options.sourceTransform,
411
+ });
375
412
  const exported = await inferClientRouteModuleSource({
376
413
  cache: options.cache,
377
414
  code: source,
@@ -383,6 +420,7 @@ async function inferClientRouteModuleSource(options) {
383
420
  }),
384
421
  root: false,
385
422
  seen: options.seen,
423
+ sourceTransform: options.sourceTransform,
386
424
  });
387
425
  diagnostics.push(...exported.diagnostics);
388
426
  if (exported.clientBoundaryModule) {
@@ -481,6 +519,18 @@ function hasPotentialClientBoundaryReference(reference, identifierReferences) {
481
519
  reference.specifiers.some((specifier) => specifier.kind === "namespace" && identifierReferences.has(specifier.localName)) ||
482
520
  reference.localNames.some((localName) => identifierReferences.has(localName) && startsUppercase(localName)));
483
521
  }
522
+ function isStyleModuleSpecifier(source) {
523
+ const pathname = source.split(/[?#]/u, 1)[0] ?? source;
524
+ return styleModuleExtensions.has(extname(pathname));
525
+ }
526
+ const styleModuleExtensions = new Set([
527
+ ".css",
528
+ ".less",
529
+ ".sass",
530
+ ".scss",
531
+ ".styl",
532
+ ".stylus",
533
+ ]);
484
534
  function renderedImportedExportNames(reference, componentRoots) {
485
535
  if (reference.sideEffect) {
486
536
  return undefined;
@@ -562,19 +612,27 @@ async function resolveAppLocalModule(options) {
562
612
  if (!options.specifier.startsWith(".")) {
563
613
  return undefined;
564
614
  }
565
- const cacheKey = `${options.importer}\0${options.specifier}`;
615
+ const cacheKey = `${options.importer}\0${options.specifier}\0${options.allowExplicitNonSource === true ? "explicit" : "source"}`;
566
616
  const cached = options.cache.resolvedByImport.get(cacheKey);
567
617
  if (cached !== undefined) {
568
618
  return cached;
569
619
  }
570
- const resolved = resolveAppLocalModuleUncached(options.importer, options.specifier);
620
+ const resolved = resolveAppLocalModuleUncached({
621
+ allowExplicitNonSource: options.allowExplicitNonSource === true,
622
+ importer: options.importer,
623
+ specifier: options.specifier,
624
+ });
571
625
  options.cache.resolvedByImport.set(cacheKey, resolved);
572
626
  return resolved;
573
627
  }
574
- async function resolveAppLocalModuleUncached(importer, specifier) {
628
+ async function resolveAppLocalModuleUncached(options) {
629
+ const { importer, specifier } = options;
575
630
  const base = join(dirname(importer), specifier);
576
631
  const candidates = sourceModuleCandidates(base);
577
632
  if (candidates.length === 0) {
633
+ if (options.allowExplicitNonSource && extname(base) !== "" && (await isFile(base))) {
634
+ return base;
635
+ }
578
636
  return undefined;
579
637
  }
580
638
  for (const candidate of candidates) {
@@ -608,6 +666,126 @@ async function readCachedFile(cache, filename) {
608
666
  cache.sourceByFile.set(filename, source);
609
667
  return (await source).source;
610
668
  }
669
+ async function readClientRouteSource(options) {
670
+ if (options.sourceTransform === undefined) {
671
+ return readCachedFile(options.cache, options.filename);
672
+ }
673
+ const signature = await sourceFileSignature(options.filename);
674
+ const cacheKey = `${options.filename}\0${options.sourceTransform.cacheKey}`;
675
+ const cached = options.cache.transformedSourceByFile.get(cacheKey);
676
+ if (cached !== undefined) {
677
+ const source = await cached;
678
+ if (source.signature === signature) {
679
+ return source.source;
680
+ }
681
+ }
682
+ const source = readCachedFile(options.cache, options.filename).then(async (code) => ({
683
+ signature,
684
+ source: await options.sourceTransform.transform(options.filename, code),
685
+ }));
686
+ options.cache.transformedSourceByFile.set(cacheKey, source);
687
+ return (await source).source;
688
+ }
689
+ async function transformClientRouteSource(options) {
690
+ return options.sourceTransform === undefined
691
+ ? options.code
692
+ : await options.sourceTransform.transform(options.filename, options.code);
693
+ }
694
+ function clientRouteSourceTransformForVitePlugins(pluginOptions) {
695
+ const plugins = orderVitePlugins(flattenVitePlugins(pluginOptions)).filter((plugin) => hasViteTransformHook(plugin));
696
+ if (plugins.length === 0) {
697
+ return undefined;
698
+ }
699
+ return {
700
+ cacheKey: plugins.map((plugin, index) => `${index}:${plugin.name}`).join("\0"),
701
+ async transform(filename, code) {
702
+ let nextCode = code;
703
+ for (const plugin of plugins) {
704
+ const handler = viteTransformHookHandler(plugin);
705
+ if (handler === undefined) {
706
+ continue;
707
+ }
708
+ const result = await handler.call(createClientRouteViteTransformContext(plugin.name), nextCode, filename, { ssr: false });
709
+ if (typeof result === "string") {
710
+ nextCode = result;
711
+ }
712
+ else if (result !== null && result !== undefined && typeof result === "object") {
713
+ const codeResult = result.code;
714
+ if (typeof codeResult === "string") {
715
+ nextCode = codeResult;
716
+ }
717
+ }
718
+ }
719
+ return nextCode;
720
+ },
721
+ };
722
+ }
723
+ function flattenVitePlugins(pluginOptions) {
724
+ const plugins = [];
725
+ const visit = (option) => {
726
+ if (option === false || option === null || option === undefined) {
727
+ return;
728
+ }
729
+ if (Array.isArray(option)) {
730
+ for (const child of option) {
731
+ visit(child);
732
+ }
733
+ return;
734
+ }
735
+ if (typeof option === "object" && "then" in option) {
736
+ return;
737
+ }
738
+ plugins.push(option);
739
+ };
740
+ for (const option of pluginOptions ?? []) {
741
+ visit(option);
742
+ }
743
+ return plugins;
744
+ }
745
+ function orderVitePlugins(plugins) {
746
+ return [
747
+ ...plugins.filter((plugin) => plugin.enforce === "pre"),
748
+ ...plugins.filter((plugin) => plugin.enforce !== "pre" && plugin.enforce !== "post"),
749
+ ...plugins.filter((plugin) => plugin.enforce === "post"),
750
+ ];
751
+ }
752
+ function hasViteTransformHook(plugin) {
753
+ return viteTransformHookHandler(plugin) !== undefined;
754
+ }
755
+ function viteTransformHookHandler(plugin) {
756
+ const transform = plugin.transform;
757
+ if (typeof transform === "function") {
758
+ return transform;
759
+ }
760
+ if (transform !== null && typeof transform === "object") {
761
+ const handler = transform.handler;
762
+ if (typeof handler === "function") {
763
+ return handler;
764
+ }
765
+ }
766
+ return undefined;
767
+ }
768
+ function createClientRouteViteTransformContext(pluginName) {
769
+ return {
770
+ addWatchFile() { },
771
+ async resolve() {
772
+ return null;
773
+ },
774
+ error(error) {
775
+ if (error instanceof Error) {
776
+ throw error;
777
+ }
778
+ throw new Error(String(error));
779
+ },
780
+ warn() { },
781
+ getCombinedSourcemap() {
782
+ return null;
783
+ },
784
+ parse() {
785
+ throw new Error(`${pluginName}: client route import analysis cannot provide Rollup parser context.`);
786
+ },
787
+ };
788
+ }
611
789
  async function sourceFileSignature(filename) {
612
790
  const stats = await stat(filename);
613
791
  return `${stats.mtimeMs}\0${stats.size}`;
@@ -685,6 +863,7 @@ export async function buildClientRouteOutput(options) {
685
863
  });
686
864
  const compiled = transformCompilerModuleContext({
687
865
  code: options.code,
866
+ clientBoundaryImports: options.clientBoundaryImports ?? [],
688
867
  filename: options.filename,
689
868
  moduleContext,
690
869
  target: "client",
@@ -697,8 +876,9 @@ export async function buildClientRouteOutput(options) {
697
876
  }
698
877
  const clientNavigation = options.clientNavigation ?? detectClientNavigationHint(options.code);
699
878
  const clientReferenceManifest = options.clientReferenceManifest ?? (await inferClientReferenceManifestForBundle(options));
879
+ const compatClientReferenceNames = compatClientReferenceComponentNames(clientReferenceManifest);
700
880
  const clientReferenceImportBlock = emitClientReferenceImportBlock(options.clientReferenceImports ?? []);
701
- const clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest, options.clientReferenceImports ?? []);
881
+ const clientReferenceRegistry = emitClientReferenceRegistry(clientReferenceManifest, options.clientReferenceImports ?? [], compatClientReferenceNames);
702
882
  const routeComponentExpression = routeComponentExpressionForComponents(compiled.metadata.components);
703
883
  const routeId = routeIdForPath(options.routePath);
704
884
  const routeUsesCells = detectRouteCellStateHint(compiled.code);
@@ -892,7 +1072,7 @@ ${routeCellHydrationIndent}}
892
1072
  ${routeCellHydrationIndent} return;
893
1073
  ${routeCellHydrationIndent}}
894
1074
  `;
895
- const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${clientReferenceImportBlock}${routeHydrationCode}
1075
+ const entry = `${routeCellEffectImport}${routeCleanupScopeImport}${emitCompatClientReferenceImportBlock(compatClientReferenceNames)}${clientReferenceImportBlock}${routeHydrationCode}
896
1076
 
897
1077
  const __mreactRouteId = ${JSON.stringify(routeId)};
898
1078
  const __mreactRouteStateSignature = ${JSON.stringify(routeStateSignature)};
@@ -924,6 +1104,7 @@ export function __mreactHydrateRoute() {
924
1104
  }
925
1105
  ${routeCellHydrationStart}${routeCleanupHydrationStart}${boundaryOnlyHydrationBlock}${routeComponentGuard}${routeCellHydrationIndent}const __mreactNode = ${routeNodeExpression};
926
1106
  ${routeCellHydrationIndent}__mreactResumeRoute(__mreactMarker, __mreactNode);
1107
+ ${routeCellHydrationIndent}__mreactHydrateClientBoundaries(__mreactMarker, __mreactClientReferences, __mreactClientReferenceComponents);
927
1108
  ${routeCellHydrationIndent}__mreactMarker.setAttribute("data-mreact-hydrated", "true");
928
1109
  ${routeCellHydrationEnd}}
929
1110
  ${routeCellDropFunction}
@@ -1798,7 +1979,9 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1798
1979
 
1799
1980
  for (const placeholder of placeholders) {
1800
1981
  const name = placeholder.getAttribute("data-mreact-client-boundary");
1801
- const component = name === null ? undefined : components.get(name);
1982
+ const entry = name === null ? undefined : components.get(name);
1983
+ const component = typeof entry === "function" ? entry : entry?.component;
1984
+ const compat = entry?.compat === true;
1802
1985
 
1803
1986
  if (typeof component !== "function") {
1804
1987
  return false;
@@ -1808,6 +1991,17 @@ function __mreactHydrateClientBoundaries(marker, references, components) {
1808
1991
  const props = propsElement?.textContent === undefined || propsElement.textContent === ""
1809
1992
  ? {}
1810
1993
  : JSON.parse(propsElement.textContent);
1994
+
1995
+ if (compat) {
1996
+ const container = document.createElement("span");
1997
+ container.setAttribute("data-mreact-compat-boundary", name ?? "");
1998
+ container.style.display = "contents";
1999
+ placeholder.replaceWith(container);
2000
+ __mreactCompatCreateRoot(container).render(__mreactCompatCreateElement(component, props));
2001
+ propsElement?.remove();
2002
+ continue;
2003
+ }
2004
+
1811
2005
  const node = component(props);
1812
2006
 
1813
2007
  placeholder.replaceWith(node);
@@ -2230,6 +2424,7 @@ function __mreactResumeChildren(current, next) {
2230
2424
  preserveExports: true,
2231
2425
  plugins: [workspaceRuntimePlugin({ routeFile: options.filename })],
2232
2426
  sourceMap: options.sourceMap,
2427
+ vitePlugins: options.vitePlugins,
2233
2428
  });
2234
2429
  return {
2235
2430
  code: bundled.code,
@@ -2325,6 +2520,7 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2325
2520
  code: source,
2326
2521
  dev: true,
2327
2522
  filename: args.path,
2523
+ mode: isCompatSourcePath(args.path) ? "compat" : "reactive",
2328
2524
  moduleContext,
2329
2525
  target: "client",
2330
2526
  });
@@ -2345,6 +2541,9 @@ export function currentDevtoolsEmitter() { return undefined; }`,
2345
2541
  function isRouteClientDependencySourcePath(path, routeFile) {
2346
2542
  return path !== routeFile && !path.includes(`${sep}node_modules${sep}`);
2347
2543
  }
2544
+ function isCompatSourcePath(path) {
2545
+ return /\.compat(?:\.mreact)?(?:\.[cm]?[jt]sx?)?$/.test(path);
2546
+ }
2348
2547
  /**
2349
2548
  * Detects the `export const clientNavigation = false` hint in a page module
2350
2549
  * source. Returns the hinted value, or `true` when no hint is present (i.e.,
@@ -2414,7 +2613,7 @@ function emitClientReferenceImportBlock(imports) {
2414
2613
  })
2415
2614
  .join("\n")}\n`;
2416
2615
  }
2417
- function emitClientReferenceRegistry(manifest, imports) {
2616
+ function emitClientReferenceRegistry(manifest, imports, compatNames) {
2418
2617
  const importedExpressions = new Map(imports.map((reference, index) => [
2419
2618
  reference.name,
2420
2619
  isIdentifierName(reference.exportName)
@@ -2425,10 +2624,24 @@ function emitClientReferenceRegistry(manifest, imports) {
2425
2624
  const expression = importedExpressions.get(reference.name) ?? clientReferenceExpression(reference.name);
2426
2625
  return expression === undefined
2427
2626
  ? []
2428
- : [` [${JSON.stringify(reference.name)}, ${expression}],`];
2627
+ : [
2628
+ compatNames.has(reference.name)
2629
+ ? ` [${JSON.stringify(reference.name)}, { component: ${expression}, compat: true }],`
2630
+ : ` [${JSON.stringify(reference.name)}, ${expression}],`,
2631
+ ];
2429
2632
  });
2430
2633
  return ["const __mreactClientReferenceComponents = new Map([", ...entries, "]);"].join("\n");
2431
2634
  }
2635
+ function compatClientReferenceComponentNames(manifest) {
2636
+ return new Set(manifest
2637
+ .filter((reference) => isCompatSourcePath(reference.moduleId))
2638
+ .map((reference) => reference.name));
2639
+ }
2640
+ function emitCompatClientReferenceImportBlock(compatNames) {
2641
+ return compatNames.size === 0
2642
+ ? ""
2643
+ : 'import { createElement as __mreactCompatCreateElement, createRoot as __mreactCompatCreateRoot } from "@reckona/mreact-compat";\n';
2644
+ }
2432
2645
  function clientReferenceLocalName(index) {
2433
2646
  return `__mreactClientReference${index}`;
2434
2647
  }