@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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.
Files changed (112) hide show
  1. package/dist/cjs/cli/artifacts.js +15 -1
  2. package/dist/cjs/cli/index.js +13 -2
  3. package/dist/cjs/cli/routeMetadata.js +131 -0
  4. package/dist/cjs/cli/rscRouteLayer.js +86 -0
  5. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  6. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  7. package/dist/cjs/cli/tanstackTypes/source.js +28 -63
  8. package/dist/cjs/runtime/lifecycle.js +0 -3
  9. package/dist/cjs/runtime/loaderBridge.js +10 -4
  10. package/dist/cjs/runtime/navigation.js +102 -0
  11. package/dist/cjs/runtime/plugin.js +10 -1
  12. package/dist/cjs/runtime/plugin.node.js +89 -64
  13. package/dist/cjs/runtime/routeTree/construction.js +19 -61
  14. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  15. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  16. package/dist/cjs/runtime/ssrManagedTags.js +2 -12
  17. package/dist/cjs/runtime/utils.js +1 -0
  18. package/dist/cjs/shared/routeDescriptor.js +88 -0
  19. package/dist/esm/cli/artifacts.mjs +13 -2
  20. package/dist/esm/cli/index.mjs +13 -11
  21. package/dist/esm/cli/routeMetadata.mjs +71 -0
  22. package/dist/esm/cli/rscRouteLayer.mjs +35 -0
  23. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  24. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  25. package/dist/esm/cli/tanstackTypes/source.mjs +29 -64
  26. package/dist/esm/runtime/lifecycle.mjs +1 -1
  27. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  28. package/dist/esm/runtime/navigation.mjs +64 -0
  29. package/dist/esm/runtime/plugin.mjs +10 -1
  30. package/dist/esm/runtime/plugin.node.mjs +91 -66
  31. package/dist/esm/runtime/routeTree/construction.mjs +19 -61
  32. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  33. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  34. package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
  35. package/dist/esm/runtime/utils.mjs +1 -0
  36. package/dist/esm/shared/routeDescriptor.mjs +50 -0
  37. package/dist/esm-node/cli/artifacts.mjs +13 -2
  38. package/dist/esm-node/cli/index.mjs +13 -11
  39. package/dist/esm-node/cli/routeMetadata.mjs +72 -0
  40. package/dist/esm-node/cli/rscRouteLayer.mjs +36 -0
  41. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  42. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  43. package/dist/esm-node/cli/tanstackTypes/source.mjs +29 -64
  44. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  45. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  46. package/dist/esm-node/runtime/navigation.mjs +65 -0
  47. package/dist/esm-node/runtime/plugin.mjs +10 -1
  48. package/dist/esm-node/runtime/plugin.node.mjs +91 -66
  49. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
  50. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  51. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  52. package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
  53. package/dist/esm-node/runtime/utils.mjs +1 -0
  54. package/dist/esm-node/shared/routeDescriptor.mjs +51 -0
  55. package/dist/types/cli/artifacts.d.ts +16 -7
  56. package/dist/types/cli/index.d.ts +1 -1
  57. package/dist/types/cli/routeMetadata.d.ts +18 -0
  58. package/dist/types/cli/rscRouteLayer.d.ts +11 -0
  59. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  60. package/dist/types/cli/tanstackTypes/source.d.ts +3 -2
  61. package/dist/types/runtime/lifecycle.d.ts +1 -1
  62. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  63. package/dist/types/runtime/navigation.d.ts +4 -0
  64. package/dist/types/runtime/pluginShared.d.ts +5 -0
  65. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  66. package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
  67. package/dist/types/runtime/ssrTypes.d.ts +0 -7
  68. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  69. package/package.json +32 -26
  70. package/src/cli/artifacts.ts +59 -8
  71. package/src/cli/index.ts +17 -4
  72. package/src/cli/routeMetadata.ts +121 -0
  73. package/src/cli/rscRouteLayer.ts +93 -0
  74. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  75. package/src/cli/tanstackTypes/shared.ts +0 -14
  76. package/src/cli/tanstackTypes/source.ts +48 -120
  77. package/src/runtime/lifecycle.ts +0 -1
  78. package/src/runtime/loaderBridge.ts +9 -9
  79. package/src/runtime/navigation.ts +78 -0
  80. package/src/runtime/plugin.node.tsx +121 -91
  81. package/src/runtime/plugin.tsx +10 -0
  82. package/src/runtime/pluginShared.ts +12 -0
  83. package/src/runtime/routeTree/construction.ts +32 -112
  84. package/src/runtime/routeTree/loaders.ts +2 -4
  85. package/src/runtime/routeTree/paths.ts +0 -6
  86. package/src/runtime/ssrManagedTags.ts +0 -17
  87. package/src/runtime/ssrTypes.ts +0 -7
  88. package/src/runtime/utils.tsx +1 -0
  89. package/src/shared/routeDescriptor.ts +102 -0
  90. package/dist/esm/rslib-runtime~1.mjs +0 -18
  91. package/dist/esm-node/rslib-runtime~0.mjs +0 -19
  92. package/rslib.config.mts +0 -4
  93. package/rstest.config.mts +0 -53
  94. package/tests/router/cli.test.ts +0 -420
  95. package/tests/router/clientHydration.test.tsx +0 -200
  96. package/tests/router/dataMutation.test.tsx +0 -428
  97. package/tests/router/fixtures/router-gen.oxlint.json +0 -5
  98. package/tests/router/flightSerialization.roundtrip.test.tsx +0 -140
  99. package/tests/router/generateRouteArtifacts.test.ts +0 -146
  100. package/tests/router/loaderBridge.test.ts +0 -207
  101. package/tests/router/localisedIdentity.test.tsx +0 -95
  102. package/tests/router/prefetchLink.test.tsx +0 -122
  103. package/tests/router/prefetchLinkPreload.test.tsx +0 -65
  104. package/tests/router/routeHooks.test.ts +0 -49
  105. package/tests/router/routeTree.test.ts +0 -240
  106. package/tests/router/rsc.test.tsx +0 -480
  107. package/tests/router/serverPlugin.test.ts +0 -157
  108. package/tests/router/slotUsageSanitizer.test.ts +0 -23
  109. package/tests/router/ssrPreload.test.ts +0 -30
  110. package/tests/router/tanstackTypes.test.ts +0 -384
  111. package/tsconfig.json +0 -12
  112. package/tsconfig.tsgo.json +0 -6
package/src/cli/index.ts CHANGED
@@ -3,7 +3,6 @@ import path from 'node:path';
3
3
  import type {
4
4
  AppNormalizedConfig,
5
5
  AppTools,
6
- AppToolsContext,
7
6
  CliPlugin,
8
7
  } from '@modern-js/app-tools';
9
8
  import type { CLIPluginAPI } from '@modern-js/plugin';
@@ -15,7 +14,6 @@ import type {
15
14
  } from '@modern-js/types';
16
15
  import {
17
16
  filterRoutesForServer,
18
- fs,
19
17
  NESTED_ROUTE_SPEC_FILE,
20
18
  } from '@modern-js/utils';
21
19
  import { writeTanstackRouterTypesForEntries } from './artifacts';
@@ -23,9 +21,14 @@ import {
23
21
  createTanstackRsbuildRouteSplittingProfile,
24
22
  type TanstackRouteCodeSplittingOption,
25
23
  } from './routeSplitting';
24
+ import {
25
+ createTanstackRscRouteLayerPlugin,
26
+ TANSTACK_SERVER_ROUTES_FILE,
27
+ } from './rscRouteLayer';
26
28
 
27
29
  export {
28
30
  generateTanstackRouteArtifacts,
31
+ writeRouteMetadataManifest,
29
32
  writeTanstackRegisterFile,
30
33
  writeTanstackRouterTypesForEntries,
31
34
  } from './artifacts';
@@ -239,6 +242,16 @@ export function tanstackRouterPlugin(
239
242
 
240
243
  api.config(() => ({
241
244
  ...routeSplittingProfile.defaultConfig,
245
+ builderPlugins: [
246
+ createTanstackRscRouteLayerPlugin(() => ({
247
+ entryNames: api
248
+ .getAppContext()
249
+ .entrypoints.filter(isTanstackEntrypoint)
250
+ .map(entrypoint => entrypoint.entryName),
251
+ internalDirectory: api.getAppContext().internalDirectory,
252
+ rsc: api.getNormalizedConfig().server.rsc,
253
+ })),
254
+ ],
242
255
  source: {
243
256
  include: [
244
257
  // TanStack Router and its runtime deps ship modern syntax and
@@ -282,7 +295,7 @@ export function tanstackRouterPlugin(
282
295
  hydrateRscClientRoutes: true,
283
296
  includeRouteServerLoadersInSsrEntry: false,
284
297
  isolateRouteDataInRscLayer: true,
285
- serverRoutesFileName: 'tanstack-routes.server.js',
298
+ serverRoutesFileName: TANSTACK_SERVER_ROUTES_FILE,
286
299
  },
287
300
  );
288
301
 
@@ -312,7 +325,7 @@ export function tanstackRouterPlugin(
312
325
  hydrateRscClientRoutes: true,
313
326
  includeRouteServerLoadersInSsrEntry: false,
314
327
  isolateRouteDataInRscLayer: true,
315
- serverRoutesFileName: 'tanstack-routes.server.js',
328
+ serverRoutesFileName: TANSTACK_SERVER_ROUTES_FILE,
316
329
  },
317
330
  );
318
331
 
@@ -0,0 +1,121 @@
1
+ // @effect-diagnostics asyncFunction:off nodeBuiltinImport:off
2
+ import { readdir } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ export const ROUTE_META_FILE_NAME = 'route.meta.ts';
6
+ export const ROUTE_METADATA_MANIFEST_FILE_NAME =
7
+ 'ultramodern-route-metadata.ts';
8
+
9
+ export const ROUTE_METADATA_MANIFEST_HEADER = `// @generated by @modern-js/plugin-tanstack from ${ROUTE_META_FILE_NAME} files. Run routes-generate; do not edit.`;
10
+
11
+ // The manifest is emitted formatted (oxfmt/Prettier style: print width 120,
12
+ // single quotes, trailing commas) and is generated output, so formatters
13
+ // ignore it like router.gen.ts; no formatter pass is needed to keep it stable.
14
+ const PRINT_WIDTH = 120;
15
+
16
+ const MANIFEST_BODY = `export const ultramodernLocalisedUrls = Object.fromEntries(
17
+ ultramodernRouteMetadata
18
+ .filter((route: { canonicalPath: string }) => route.canonicalPath !== '/')
19
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
20
+ );
21
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
22
+ .filter((route) => route.public && route.indexable)
23
+ .map((route) => ({
24
+ canonicalPath: route.canonicalPath,
25
+ descriptionKey: route.descriptionKey,
26
+ id: route.id,
27
+ localisedPaths: route.localisedPaths,
28
+ namespace: route.namespace,
29
+ ownerAppId: route.ownerAppId,
30
+ titleKey: route.titleKey,
31
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
32
+ }));
33
+ export const ultramodernRouteConfig = {
34
+ authoring: 'colocated-route-meta',
35
+ generatedManifest: true,
36
+ localisedUrls: ultramodernLocalisedUrls,
37
+ namespace: ultramodernRouteNamespace,
38
+ publicRoutes: ultramodernPublicRoutes,
39
+ routes: ultramodernRouteMetadata,
40
+ source: 'route-owned',
41
+ } as const;
42
+ `;
43
+
44
+ const compareCodeUnits = (left: string, right: string) =>
45
+ left < right ? -1 : left > right ? 1 : 0;
46
+
47
+ const quoteSpecifier = (specifier: string) =>
48
+ specifier.includes("'") ? JSON.stringify(specifier) : `'${specifier}'`;
49
+
50
+ /**
51
+ * Renders the route metadata manifest for the `route.meta.ts` files of one
52
+ * routes directory. Each path is relative to that directory (either separator).
53
+ * Every `route.meta.ts` must export `routeMeta`; the manifest imports it
54
+ * statically, so the metadata is never evaluated while generating. Returns
55
+ * `null` when there are no route metadata files. Output depends only on the
56
+ * set of paths, never on their order or on the file system.
57
+ */
58
+ export function renderRouteMetadataManifest(
59
+ routeMetaFiles: readonly string[],
60
+ ): string | null {
61
+ const specifiers = routeMetaFiles
62
+ .map(file => `./${file.replaceAll('\\', '/').replace(/\.ts$/u, '')}`)
63
+ .sort(compareCodeUnits);
64
+ if (specifiers.length === 0) {
65
+ return null;
66
+ }
67
+ const bindings = specifiers.map((_, index) => `route${index}`);
68
+ const imports = specifiers.map(
69
+ (specifier, index) =>
70
+ `import { routeMeta as ${bindings[index]} } from ${quoteSpecifier(specifier)};`,
71
+ );
72
+ const inlineRoutes = `export const ultramodernRouteMetadata = [${bindings.join(', ')}] as const;`;
73
+ const routes =
74
+ inlineRoutes.length <= PRINT_WIDTH
75
+ ? inlineRoutes
76
+ : `export const ultramodernRouteMetadata = [\n${bindings
77
+ .map(binding => ` ${binding},\n`)
78
+ .join('')}] as const;`;
79
+
80
+ return `${ROUTE_METADATA_MANIFEST_HEADER}
81
+ ${imports.join('\n')}
82
+
83
+ export const ultramodernRouteNamespace = route0.namespace;
84
+ ${routes}
85
+ ${MANIFEST_BODY}`;
86
+ }
87
+
88
+ /**
89
+ * Lists the `route.meta.ts` files below `routesDirectory`, relative to it.
90
+ * Symbolic links are not followed: linked metadata belongs to the tree it
91
+ * lives in. A missing directory has no route metadata.
92
+ */
93
+ export async function findRouteMetaFiles(
94
+ routesDirectory: string,
95
+ ): Promise<string[]> {
96
+ const files: string[] = [];
97
+ const visit = async (relativeDirectory: string): Promise<void> => {
98
+ const entries = await readdir(
99
+ path.join(routesDirectory, relativeDirectory),
100
+ { withFileTypes: true },
101
+ );
102
+ for (const entry of entries) {
103
+ const relativePath = path.join(relativeDirectory, entry.name);
104
+ if (entry.isDirectory()) {
105
+ await visit(relativePath);
106
+ } else if (entry.isFile() && entry.name === ROUTE_META_FILE_NAME) {
107
+ files.push(relativePath);
108
+ }
109
+ }
110
+ };
111
+ try {
112
+ await visit('');
113
+ } catch (error) {
114
+ const { code, path: missingPath } = error as NodeJS.ErrnoException;
115
+ if (code === 'ENOENT' && missingPath === routesDirectory) {
116
+ return [];
117
+ }
118
+ throw error;
119
+ }
120
+ return files;
121
+ }
@@ -0,0 +1,93 @@
1
+ // @effect-diagnostics nodeBuiltinImport:off
2
+ // Rsbuild invokes this compiler hook synchronously with native filesystem paths.
3
+ import path from 'node:path';
4
+ import type { AppNormalizedConfig } from '@modern-js/app-tools/cli-config';
5
+
6
+ type BuilderPlugin = NonNullable<AppNormalizedConfig['builderPlugins']>[number];
7
+ type ModifyRspackConfig = Extract<
8
+ NonNullable<AppNormalizedConfig['tools']['rspack']>,
9
+ (...args: never[]) => unknown
10
+ >;
11
+
12
+ export const TANSTACK_SERVER_ROUTES_FILE = 'tanstack-routes.server.js';
13
+
14
+ type RouteLayerContext = {
15
+ entryNames: readonly string[];
16
+ internalDirectory: string;
17
+ rsc: AppNormalizedConfig['server']['rsc'];
18
+ };
19
+
20
+ function exactResource(...filenames: string[]) {
21
+ const escaped = filenames.map(filename =>
22
+ filename.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'),
23
+ );
24
+ return new RegExp(`^(?:${escaped.join('|')})(?:\\?.*)?$`, 'u');
25
+ }
26
+
27
+ /** The HTML router consumes executable components, while route data stays in Flight. */
28
+ export function createTanstackRscRouteLayerPlugin(
29
+ getContext: () => RouteLayerContext,
30
+ ): BuilderPlugin {
31
+ return {
32
+ name: 'modern:tanstack:rsc-route-layer',
33
+ setup(api) {
34
+ api.modifyRspackConfig({
35
+ order: 'post',
36
+ handler(
37
+ config: Parameters<ModifyRspackConfig>[0],
38
+ { environment, rspack, target }: Parameters<ModifyRspackConfig>[1],
39
+ ) {
40
+ const { entryNames, internalDirectory, rsc } = getContext();
41
+ if (rsc === false || rsc === undefined || entryNames.length === 0) {
42
+ return;
43
+ }
44
+ const rscServerEnvironment =
45
+ typeof rsc === 'object'
46
+ ? (rsc.environments?.server ?? 'server')
47
+ : 'server';
48
+ if (
49
+ target !== 'node' &&
50
+ config.target !== 'node' &&
51
+ environment.name !== rscServerEnvironment
52
+ ) {
53
+ return;
54
+ }
55
+
56
+ config.module ??= {};
57
+ config.module.rules ??= [];
58
+ config.plugins ??= [];
59
+ for (const entryName of entryNames) {
60
+ const routes = path.resolve(
61
+ internalDirectory,
62
+ entryName,
63
+ 'routes.js',
64
+ );
65
+ const serverRoutes = path.resolve(
66
+ internalDirectory,
67
+ entryName,
68
+ TANSTACK_SERVER_ROUTES_FILE,
69
+ );
70
+ // runtime-global-context.js imports the browser route table in both
71
+ // compilers. Select the complete, loader-isolated table for HTML.
72
+ config.plugins.push(
73
+ new rspack.NormalModuleReplacementPlugin(
74
+ exactResource(routes),
75
+ serverRoutes,
76
+ ),
77
+ );
78
+ // Match both names: native replacement can occur after Rspack has
79
+ // already selected rules and a layer for the original request.
80
+ // Avoid inheriting an RSC issuer's layer. Native use-server-entry
81
+ // processing returns client references in that layer, which ReactDOM
82
+ // must never invoke. Isolated __rsc_route_data__ retains its native
83
+ // explicit react-server-components rule and Flight serialization.
84
+ config.module.rules.push({
85
+ resource: exactResource(routes, serverRoutes),
86
+ layer: 'server-side-rendering',
87
+ });
88
+ }
89
+ },
90
+ });
91
+ },
92
+ };
93
+ }
@@ -1,7 +1,10 @@
1
1
  // @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
2
2
 
3
3
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
4
- import { MAIN_ENTRY_NAME } from '@modern-js/utils';
4
+ import {
5
+ describeRouteTree,
6
+ type RouteDescriptor,
7
+ } from '../../shared/routeDescriptor';
5
8
 
6
9
  import { toTanstackPath } from './shared';
7
10
 
@@ -16,6 +19,7 @@ const LOCALE_PARAM_SEGMENTS = new Set([
16
19
 
17
20
  type CanonicalAwareRoute = (NestedRouteForCli | PageRoute) & {
18
21
  modernCanonicalPath?: string;
22
+ modernLocalisedRoute?: { canonicalPath: string };
19
23
  index?: boolean;
20
24
  isRoot?: boolean;
21
25
  children?: CanonicalAwareRoute[];
@@ -101,12 +105,18 @@ export function collectCanonicalRoutesForEntry(
101
105
  }
102
106
  };
103
107
 
104
- const visit = (route: CanonicalAwareRoute, parentPath: string) => {
108
+ const visit = (
109
+ descriptor: RouteDescriptor<CanonicalAwareRoute>,
110
+ parentPath: string,
111
+ ) => {
112
+ const { source: route, children } = descriptor;
105
113
  let currentPath = parentPath;
106
114
 
107
- if (typeof route.modernCanonicalPath === 'string') {
115
+ const canonicalPath =
116
+ route.modernLocalisedRoute?.canonicalPath ?? route.modernCanonicalPath;
117
+ if (typeof canonicalPath === 'string') {
108
118
  hasI18nSurface = true;
109
- currentPath = normalizeJoined(route.modernCanonicalPath);
119
+ currentPath = canonicalPath ? normalizeJoined(canonicalPath) : '';
110
120
  } else if (typeof route.path === 'string' && route.path.length > 0) {
111
121
  const segments = route.path
112
122
  .replace(/\[(.+?)\]/g, ':$1')
@@ -125,7 +135,6 @@ export function collectCanonicalRoutesForEntry(
125
135
  : parentPath;
126
136
  }
127
137
 
128
- const children = route.children;
129
138
  if (children && children.length > 0) {
130
139
  for (const child of children) {
131
140
  visit(child, currentPath);
@@ -137,13 +146,9 @@ export function collectCanonicalRoutesForEntry(
137
146
  record(currentPath || '/');
138
147
  };
139
148
 
140
- const rootModern = routes.find(
141
- route => (route as CanonicalAwareRoute).isRoot,
142
- ) as CanonicalAwareRoute | undefined;
143
- const topLevel = rootModern ? (rootModern.children ?? []) : routes;
144
-
145
- for (const route of topLevel) {
146
- visit(route as CanonicalAwareRoute, '');
149
+ for (const route of describeRouteTree(routes as CanonicalAwareRoute[])
150
+ .children) {
151
+ visit(route, '');
147
152
  }
148
153
 
149
154
  if (!hasI18nSurface || canonicalParams.size === 0) {
@@ -71,20 +71,6 @@ export function pickRouteSearchContractModules(
71
71
  };
72
72
  }
73
73
 
74
- export function isPathlessLayout(route: NestedRouteForCli | PageRoute) {
75
- const extras = route as RouteExtras;
76
- return (
77
- extras.type === 'nested' &&
78
- typeof extras.index !== 'boolean' &&
79
- typeof extras.path === 'undefined'
80
- );
81
- }
82
-
83
- export function isIndexRoute(route: NestedRouteForCli | PageRoute) {
84
- const extras = route as RouteExtras;
85
- return extras.type === 'nested' && Boolean(extras.index);
86
- }
87
-
88
74
  export function createRouteStaticDataSnippet(opts: {
89
75
  modernRouteId?: string;
90
76
  loaderName?: string | null;
@@ -1,25 +1,23 @@
1
1
  // @effect-diagnostics asyncFunction:off strictBooleanExpressions:off
2
2
 
3
- import type {
4
- AppNormalizedConfig,
5
- AppTools,
6
- AppToolsContext,
7
- } from '@modern-js/app-tools';
8
- import { canonicaliseLocalisedRoutes } from '@modern-js/i18n-runtime-extensions';
3
+ import type { AppTools } from '@modern-js/app-tools/cli-config';
4
+ import type { CLIPluginAPI } from '@modern-js/plugin';
9
5
  import { makeLegalIdentifier } from '@modern-js/runtime/cli';
10
6
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
11
7
  import { upath as path } from '@modern-js/utils';
12
8
 
9
+ import {
10
+ describeRouteTree,
11
+ type RouteDescriptor,
12
+ } from '../../shared/routeDescriptor';
13
+
13
14
  import {
14
15
  createRouteStaticDataSnippet,
15
- isIndexRoute,
16
- isPathlessLayout,
17
16
  normalizeRelativeImport,
18
17
  pickModernLoaderModule,
19
18
  pickRouteSearchContractModules,
20
19
  quote,
21
20
  resolveFileNoExt,
22
- toTanstackPath,
23
21
  } from './shared';
24
22
 
25
23
  type RouteForCli = NestedRouteForCli | PageRoute;
@@ -35,7 +33,10 @@ type RouteExtras = {
35
33
  };
36
34
 
37
35
  export async function generateTanstackRouterTypesSourceForEntry(opts: {
38
- appContext: AppToolsContext;
36
+ appContext: Pick<
37
+ ReturnType<CLIPluginAPI<AppTools>['getAppContext']>,
38
+ 'srcDirectory' | 'internalSrcAlias'
39
+ >;
39
40
  entryName: string;
40
41
  generatedDirName?: string;
41
42
  routes: RouteForCli[];
@@ -46,22 +47,15 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
46
47
  appContext,
47
48
  entryName,
48
49
  generatedDirName = 'modern-tanstack',
49
- routes: rawRoutes,
50
+ routes,
50
51
  } = opts;
51
- const routes: RouteForCli[] = canonicaliseLocalisedRoutes(rawRoutes);
52
52
  const outDir = path.join(
53
53
  appContext.srcDirectory,
54
54
  generatedDirName,
55
55
  entryName,
56
56
  );
57
57
 
58
- const rootModern = routes.find(r => {
59
- const extras = r as RouteExtras;
60
- return r && extras.type === 'nested' && extras.isRoot;
61
- }) as NestedRouteForCli | undefined;
62
-
63
- const rootExtras = rootModern as RouteExtras | undefined;
64
- const topLevel = rootModern ? rootExtras?.children || [] : routes;
58
+ const { root: rootModern, children: topLevel } = describeRouteTree(routes);
65
59
 
66
60
  const imports: string[] = [];
67
61
  const statements: string[] = [];
@@ -92,15 +86,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
92
86
  // value carries the internal `@_modern_js_src` alias, which the app's
93
87
  // tsconfig does not map — the generated file must use relative
94
88
  // imports.
95
- const resolvedNoExt = await resolveRouteModuleNoExt(componentPath);
96
- if (!resolvedNoExt) {
89
+ const relImport = await resolveRouteImport(componentPath);
90
+ if (!relImport) {
97
91
  return null;
98
92
  }
99
93
 
100
- const relImport = normalizeRelativeImport(
101
- path.relative(outDir, resolvedNoExt),
102
- );
103
-
104
94
  const componentName = `component_${componentIndex++}`;
105
95
  imports.push(`import ${componentName} from ${quote(relImport)};`);
106
96
  return componentName;
@@ -111,7 +101,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
111
101
  return pendingImportName;
112
102
  };
113
103
 
114
- const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
104
+ const resolveRouteImport = async (aliasedNoExtPath: string) => {
115
105
  const prefix = `${appContext.internalSrcAlias}/`;
116
106
  let absNoExt: string;
117
107
  if (aliasedNoExtPath.startsWith(prefix)) {
@@ -124,7 +114,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
124
114
  absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
125
115
  }
126
116
 
127
- return resolveFileNoExt(absNoExt);
117
+ const resolvedNoExt = await resolveFileNoExt(absNoExt);
118
+ return resolvedNoExt
119
+ ? normalizeRelativeImport(path.relative(outDir, resolvedNoExt))
120
+ : null;
128
121
  };
129
122
 
130
123
  const getImportNamesForLoader = async (
@@ -143,15 +136,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
143
136
  };
144
137
  }
145
138
 
146
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
147
- if (!resolvedNoExt) {
139
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
140
+ if (!relImport) {
148
141
  return null;
149
142
  }
150
143
 
151
- const relImport = normalizeRelativeImport(
152
- path.relative(outDir, resolvedNoExt),
153
- );
154
-
155
144
  const importName = `loader_${loaderIndex++}`;
156
145
  const actionName = hasAction
157
146
  ? importName.replace(/^loader_/, 'action_')
@@ -182,14 +171,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
182
171
  return existing;
183
172
  }
184
173
 
185
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
186
- if (!resolvedNoExt) {
174
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
175
+ if (!relImport) {
187
176
  return null;
188
177
  }
189
-
190
- const relImport = normalizeRelativeImport(
191
- path.relative(outDir, resolvedNoExt),
192
- );
193
178
  const importName =
194
179
  exportName === 'validateSearch'
195
180
  ? `validateSearch_${validateSearchIndex++}`
@@ -217,15 +202,8 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
217
202
  return reserveRouteVarName(`route_${base}`);
218
203
  };
219
204
 
220
- const buildRoute = async (opts: {
221
- parentVar: string;
222
- route: NestedRouteForCli | PageRoute;
223
- }): Promise<string> => {
224
- const { parentVar, route } = opts;
225
-
226
- const varName = createRouteVarName(route);
205
+ const createRouteOptions = async (route: RouteForCli, hasSplat: boolean) => {
227
206
  const routeExtras = route as RouteExtras;
228
-
229
207
  const loaderInfo = pickModernLoaderModule(route);
230
208
  const routeAction = routeExtras.action;
231
209
  const loaderImports = loaderInfo
@@ -251,10 +229,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
251
229
  )
252
230
  : null;
253
231
 
254
- const rawPath = routeExtras.path;
255
- const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
256
-
257
- const routeOpts: string[] = [`getParentRoute: () => ${parentVar},`];
232
+ const routeOpts: string[] = [];
258
233
 
259
234
  const componentName = await getImportNameForComponent(
260
235
  routeExtras._component,
@@ -263,14 +238,6 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
263
238
  routeOpts.push(`component: ${componentName},`);
264
239
  }
265
240
 
266
- if (isPathlessLayout(route)) {
267
- const id = routeExtras.id;
268
- routeOpts.push(`id: ${quote(id || 'pathless')},`);
269
- } else {
270
- const p = isIndexRoute(route) ? '/' : toTanstackPath(rawPath || '');
271
- routeOpts.push(`path: ${quote(p)},`);
272
- }
273
-
274
241
  if (loaderName) {
275
242
  routeOpts.push(
276
243
  `loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
@@ -292,7 +259,22 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
292
259
  routeOpts.push(staticDataSnippet);
293
260
  }
294
261
 
295
- const children = routeExtras.children;
262
+ return routeOpts;
263
+ };
264
+
265
+ const buildRoute = async (
266
+ descriptor: RouteDescriptor<RouteForCli>,
267
+ parentVar: string,
268
+ ): Promise<string> => {
269
+ const { source: route, location, hasSplat, children } = descriptor;
270
+ const varName = createRouteVarName(route);
271
+ const routeOpts = [
272
+ `getParentRoute: () => ${parentVar},`,
273
+ ...Object.entries(location).map(
274
+ ([key, value]) => `${key}: ${quote(value)},`,
275
+ ),
276
+ ...(await createRouteOptions(route, hasSplat)),
277
+ ];
296
278
  const hasChildren = Boolean(children && children.length > 0);
297
279
  const routeCtorVarName = hasChildren
298
280
  ? reserveRouteVarName(`${varName}__base`)
@@ -305,9 +287,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
305
287
  if (children && children.length > 0) {
306
288
  const childVars: string[] = [];
307
289
  for (const child of children) {
308
- childVars.push(
309
- await buildRoute({ parentVar: routeCtorVarName, route: child }),
310
- );
290
+ childVars.push(await buildRoute(child, routeCtorVarName));
311
291
  }
312
292
  statements.push(
313
293
  `const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
@@ -317,60 +297,14 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
317
297
  return varName;
318
298
  };
319
299
 
320
- const rootLoaderInfo = rootModern ? pickModernLoaderModule(rootModern) : null;
321
- const rootAction = rootExtras?.action;
322
- const rootLoaderImports = rootLoaderInfo?.loaderPath
323
- ? await getImportNamesForLoader(
324
- rootLoaderInfo.loaderPath,
325
- rootLoaderInfo.inline,
326
- Boolean(
327
- rootLoaderInfo.inline && rootAction === rootLoaderInfo.loaderPath,
328
- ),
329
- )
330
- : null;
331
- const rootLoaderName = rootLoaderImports?.loaderName || null;
332
- const rootActionName = rootLoaderImports?.actionName || null;
333
- const rootSearchContractInfo = rootModern
334
- ? pickRouteSearchContractModules(rootModern)
335
- : null;
336
- const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
337
- ? await getImportNameForSearchContract(
338
- rootSearchContractInfo.validateSearchPath,
339
- 'validateSearch',
340
- )
341
- : null;
342
- const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
343
- ? await getImportNameForSearchContract(
344
- rootSearchContractInfo.loaderDepsPath,
345
- 'loaderDeps',
346
- )
347
- : null;
348
-
349
300
  const topLevelVars: string[] = [];
350
301
  for (const route of topLevel) {
351
- topLevelVars.push(await buildRoute({ parentVar: 'rootRoute', route }));
302
+ topLevelVars.push(await buildRoute(route, 'rootRoute'));
352
303
  }
353
304
 
354
- const rootOpts: string[] = [];
355
-
356
- const rootComponentName = await getImportNameForComponent(
357
- rootExtras?._component,
358
- );
359
- if (rootComponentName) {
360
- rootOpts.push(`component: ${rootComponentName},`);
361
- }
362
-
363
- if (rootLoaderName) {
364
- rootOpts.push(
365
- `loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
366
- );
367
- }
368
- if (rootValidateSearchName) {
369
- rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
370
- }
371
- if (rootLoaderDepsName) {
372
- rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
373
- }
305
+ const rootOpts = rootModern
306
+ ? await createRouteOptions(rootModern, false)
307
+ : [];
374
308
 
375
309
  const routerGenTs = `// This file is auto-generated by Modern.js. Do not edit manually.
376
310
 
@@ -388,13 +322,7 @@ ${imports.join('\n')}
388
322
 
389
323
  export const rootRoute = createRootRouteWithContext<ModernRouterContext>()({
390
324
  ${rootOpts.join('\n ')}
391
- ${
392
- createRouteStaticDataSnippet({
393
- modernRouteId: rootExtras?.id,
394
- loaderName: rootLoaderName,
395
- actionName: rootActionName,
396
- }) || ''
397
- }
325
+
398
326
  });
399
327
 
400
328
  ${statements.join('\n\n')}
@@ -19,7 +19,6 @@ export type RouterLifecycleContext = NativeRouterLifecycleContext & {
19
19
  export {
20
20
  applyRouterRuntimeState,
21
21
  applyRouterServerPrepareResult,
22
- createRouterRuntimeState,
23
22
  createRouterServerSnapshot,
24
23
  getRouterRuntimeState,
25
24
  getRouterServerSnapshot,