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

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 (137) hide show
  1. package/dist/cjs/cli/artifacts.js +15 -1
  2. package/dist/cjs/cli/index.js +12 -0
  3. package/dist/cjs/cli/routeMetadata.js +131 -0
  4. package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
  5. package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
  6. package/dist/cjs/cli/tanstackTypes/source.js +27 -60
  7. package/dist/cjs/runtime/basepathRewrite.js +13 -6
  8. package/dist/cjs/runtime/lifecycle.js +6 -9
  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 +7 -2
  12. package/dist/cjs/runtime/plugin.node.js +86 -64
  13. package/dist/cjs/runtime/prefetchLink.js +2 -7
  14. package/dist/cjs/runtime/register.js +0 -2
  15. package/dist/cjs/runtime/routeTree/construction.js +19 -59
  16. package/dist/cjs/runtime/routeTree/loaders.js +1 -1
  17. package/dist/cjs/runtime/routeTree/paths.js +0 -6
  18. package/dist/cjs/runtime/router.js +25 -9
  19. package/dist/cjs/runtime/ssrManagedTags.js +2 -12
  20. package/dist/cjs/runtime/state.js +2 -2
  21. package/dist/cjs/runtime/utils.js +3 -0
  22. package/dist/cjs/shared/routeDescriptor.js +60 -0
  23. package/dist/esm/cli/artifacts.mjs +13 -2
  24. package/dist/esm/cli/index.mjs +12 -9
  25. package/dist/esm/cli/routeMetadata.mjs +71 -0
  26. package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  27. package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
  28. package/dist/esm/cli/tanstackTypes/source.mjs +28 -61
  29. package/dist/esm/runtime/basepathRewrite.mjs +10 -3
  30. package/dist/esm/runtime/lifecycle.mjs +1 -1
  31. package/dist/esm/runtime/loaderBridge.mjs +8 -5
  32. package/dist/esm/runtime/navigation.mjs +64 -0
  33. package/dist/esm/runtime/plugin.mjs +7 -2
  34. package/dist/esm/runtime/plugin.node.mjs +88 -66
  35. package/dist/esm/runtime/prefetchLink.mjs +1 -6
  36. package/dist/esm/runtime/register.mjs +0 -2
  37. package/dist/esm/runtime/routeTree/construction.mjs +19 -59
  38. package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
  39. package/dist/esm/runtime/routeTree/paths.mjs +1 -4
  40. package/dist/esm/runtime/router.mjs +27 -11
  41. package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
  42. package/dist/esm/runtime/state.mjs +1 -1
  43. package/dist/esm/runtime/utils.mjs +3 -0
  44. package/dist/esm/shared/routeDescriptor.mjs +22 -0
  45. package/dist/esm-node/cli/artifacts.mjs +13 -2
  46. package/dist/esm-node/cli/index.mjs +12 -9
  47. package/dist/esm-node/cli/routeMetadata.mjs +72 -0
  48. package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
  49. package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
  50. package/dist/esm-node/cli/tanstackTypes/source.mjs +28 -61
  51. package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
  52. package/dist/esm-node/runtime/lifecycle.mjs +1 -1
  53. package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
  54. package/dist/esm-node/runtime/navigation.mjs +65 -0
  55. package/dist/esm-node/runtime/plugin.mjs +7 -2
  56. package/dist/esm-node/runtime/plugin.node.mjs +88 -66
  57. package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
  58. package/dist/esm-node/runtime/register.mjs +0 -2
  59. package/dist/esm-node/runtime/routeTree/construction.mjs +19 -59
  60. package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
  61. package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
  62. package/dist/esm-node/runtime/router.mjs +27 -11
  63. package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
  64. package/dist/esm-node/runtime/state.mjs +1 -1
  65. package/dist/esm-node/runtime/utils.mjs +3 -0
  66. package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
  67. package/dist/types/cli/artifacts.d.ts +13 -5
  68. package/dist/types/cli/index.d.ts +1 -1
  69. package/dist/types/cli/routeMetadata.d.ts +18 -0
  70. package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
  71. package/dist/types/runtime/basepathRewrite.d.ts +2 -1
  72. package/dist/types/runtime/lifecycle.d.ts +9 -2
  73. package/dist/types/runtime/loaderBridge.d.ts +1 -0
  74. package/dist/types/runtime/navigation.d.ts +4 -0
  75. package/dist/types/runtime/register.d.ts +1 -9
  76. package/dist/types/runtime/routeTree/paths.d.ts +0 -2
  77. package/dist/types/runtime/router.d.ts +2 -13
  78. package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
  79. package/dist/types/runtime/ssrTypes.d.ts +0 -7
  80. package/dist/types/runtime/state.d.ts +1 -1
  81. package/dist/types/runtime/types.d.ts +2 -1
  82. package/dist/types/shared/routeDescriptor.d.ts +26 -0
  83. package/package.json +22 -19
  84. package/rstest.config.mts +2 -24
  85. package/src/cli/artifacts.ts +53 -6
  86. package/src/cli/index.ts +11 -2
  87. package/src/cli/routeMetadata.ts +121 -0
  88. package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
  89. package/src/cli/tanstackTypes/shared.ts +0 -14
  90. package/src/cli/tanstackTypes/source.ts +42 -116
  91. package/src/runtime/basepathRewrite.ts +11 -2
  92. package/src/runtime/lifecycle.ts +14 -4
  93. package/src/runtime/loaderBridge.ts +9 -9
  94. package/src/runtime/navigation.ts +78 -0
  95. package/src/runtime/plugin.node.tsx +120 -93
  96. package/src/runtime/plugin.tsx +11 -1
  97. package/src/runtime/prefetchLink.tsx +1 -23
  98. package/src/runtime/register.ts +1 -12
  99. package/src/runtime/routeTree/construction.ts +32 -110
  100. package/src/runtime/routeTree/loaders.ts +2 -4
  101. package/src/runtime/routeTree/paths.ts +0 -6
  102. package/src/runtime/router.ts +25 -40
  103. package/src/runtime/ssrManagedTags.ts +0 -17
  104. package/src/runtime/ssrTypes.ts +0 -7
  105. package/src/runtime/state.ts +1 -1
  106. package/src/runtime/types.ts +2 -2
  107. package/src/runtime/utils.tsx +4 -1
  108. package/src/shared/routeDescriptor.ts +46 -0
  109. package/tests/router/cli.test.ts +148 -824
  110. package/tests/router/clientNavigation.ts +13 -0
  111. package/tests/router/dataMutation.test.tsx +17 -85
  112. package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
  113. package/tests/router/generateRouteArtifacts.test.ts +139 -27
  114. package/tests/router/loaderBridge.test.ts +0 -25
  115. package/tests/router/localisedIdentity.test.tsx +100 -0
  116. package/tests/router/prefetchLink.test.tsx +46 -21
  117. package/tests/router/prefetchLinkPreload.test.tsx +11 -32
  118. package/tests/router/routeHooks.test.ts +42 -105
  119. package/tests/router/routeMetadata.test.ts +171 -0
  120. package/tests/router/routeTree.test.ts +144 -836
  121. package/tests/router/rsc.test.tsx +131 -166
  122. package/tests/router/serverPlugin.test.ts +193 -1
  123. package/tests/router/tanstackTypes.test.ts +218 -963
  124. package/dist/esm/rslib-runtime~1.mjs +0 -18
  125. package/dist/esm-node/rslib-runtime~0.mjs +0 -19
  126. package/tests/realm-isolation.test.ts +0 -114
  127. package/tests/router/fastDefaults.test.ts +0 -32
  128. package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
  129. package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
  130. package/tests/router/hooks.test.ts +0 -40
  131. package/tests/router/hydrationBoundary.test.tsx +0 -23
  132. package/tests/router/packageSurface.test.ts +0 -24
  133. package/tests/router/preloadRedirect.test.ts +0 -64
  134. package/tests/router/register.test.ts +0 -8
  135. package/tests/router/routerCompatibility.test.ts +0 -239
  136. package/tests/router/rscDecoderSecurity.test.ts +0 -113
  137. package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
@@ -5,6 +5,12 @@ import type { AppToolsContext } from '@modern-js/app-tools';
5
5
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
6
6
  import { fs } from '@modern-js/utils';
7
7
 
8
+ import {
9
+ findRouteMetaFiles,
10
+ ROUTE_METADATA_MANIFEST_FILE_NAME,
11
+ ROUTE_METADATA_MANIFEST_HEADER,
12
+ renderRouteMetadataManifest,
13
+ } from './routeMetadata';
8
14
  import {
9
15
  collectCanonicalRoutesForEntry,
10
16
  generateTanstackRouterTypesSourceForEntry,
@@ -193,16 +199,52 @@ export async function writeTanstackRouterTypesForEntries(opts: {
193
199
  }
194
200
 
195
201
  /**
196
- * Headless regeneration of `router.gen.ts` / `register.gen.d.ts` for a generated
197
- * app, without running a full dev/build. Drives the app-tools analyze `onPrepare`
198
- * (which fires `generateEntryCode`) and returns before any bundling — the app's
199
- * own registered plugin-tanstack writes the artifacts via its `generateEntryCode`
200
- * subscriber. Reuses the exact hook dev/build use, so there is no scanning drift.
202
+ * Writes `src/routes/ultramodern-route-metadata.ts` from the `route.meta.ts`
203
+ * files below `src/routes`. Reads only the file system, never the app config.
204
+ */
205
+ export async function writeRouteMetadataManifest(opts: {
206
+ appDirectory: string;
207
+ }): Promise<void> {
208
+ const routesDirectory = path.join(opts.appDirectory, 'src', 'routes');
209
+ const manifestPath = path.join(
210
+ routesDirectory,
211
+ ROUTE_METADATA_MANIFEST_FILE_NAME,
212
+ );
213
+ const manifest = renderRouteMetadataManifest(
214
+ await findRouteMetaFiles(routesDirectory),
215
+ );
216
+ if (manifest !== null) {
217
+ await writeFileIfChanged(manifestPath, manifest);
218
+ return;
219
+ }
220
+ const previous = (await fs.pathExists(manifestPath))
221
+ ? await fs.readFile(manifestPath, 'utf-8')
222
+ : null;
223
+ // No valid manifest exists without routes (its namespace comes from them),
224
+ // and the app config imports it, so the app cannot load in this state.
225
+ if (previous?.startsWith(`${ROUTE_METADATA_MANIFEST_HEADER}\n`)) {
226
+ throw new Error(
227
+ `[plugin-tanstack] ${manifestPath} is generated from route.meta.ts files, but none remain below ${routesDirectory}. ` +
228
+ 'Restore at least one route.meta.ts that exports routeMeta, or delete the manifest together with its imports.',
229
+ );
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Headless regeneration of the route metadata manifest and of `router.gen.ts` /
235
+ * `register.gen.d.ts` for a generated app, without running a full dev/build.
236
+ * Drives the app-tools analyze `onPrepare` (which fires `generateEntryCode`)
237
+ * and returns before any bundling — the app's own registered plugin-tanstack
238
+ * writes the router artifacts via its `generateEntryCode` subscriber. Reuses
239
+ * the exact hook dev/build use, so there is no scanning drift.
201
240
  */
202
241
  export async function generateTanstackRouteArtifacts(opts: {
203
242
  appDirectory: string;
204
243
  version?: string;
205
244
  }): Promise<void> {
245
+ // The app config imports the manifest. Writing it first means a manifest
246
+ // that still imports a deleted route.meta.ts cannot fail the config load.
247
+ await writeRouteMetadataManifest(opts);
206
248
  const [{ createRunOptions }, { cli }, utils] = await Promise.all([
207
249
  import('@modern-js/app-tools/cli/run'),
208
250
  import('@modern-js/plugin/cli'),
@@ -251,5 +293,10 @@ export async function generateTanstackRouteArtifacts(opts: {
251
293
  internalPlugins: (utils as { INTERNAL_RUNTIME_PLUGINS: unknown })
252
294
  .INTERNAL_RUNTIME_PLUGINS as never,
253
295
  });
254
- await cli.init({ ...runOptions, command: 'build' });
296
+ const { appContext } = await cli.init({ ...runOptions, command: 'build' });
297
+ // Preparation owns the artifact writes; there is no dev server or bundler
298
+ // phase after it. Release plugin resources before the headless caller exits.
299
+ // Failed initialization propagates unchanged: it has not returned a context
300
+ // whose lifecycle this caller can close.
301
+ await appContext.hooks.onBeforeExit.call();
255
302
  }
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';
@@ -26,6 +24,7 @@ import {
26
24
 
27
25
  export {
28
26
  generateTanstackRouteArtifacts,
27
+ writeRouteMetadataManifest,
29
28
  writeTanstackRegisterFile,
30
29
  writeTanstackRouterTypesForEntries,
31
30
  } from './artifacts';
@@ -132,6 +131,16 @@ export function tanstackRouterPlugin(
132
131
  name: '@modern-js/plugin-tanstack',
133
132
  required: ['@modern-js/runtime'],
134
133
  setup: api => {
134
+ const { srcDirectory, serverCompileExcludedFiles = [] } =
135
+ api.getAppContext();
136
+ api.updateAppContext({
137
+ serverCompileExcludedFiles: [
138
+ ...new Set([
139
+ ...serverCompileExcludedFiles,
140
+ path.join(srcDirectory, generatedDirName, 'register.gen.d.ts'),
141
+ ]),
142
+ ],
143
+ });
135
144
  const nestedRoutesForServer: Record<string, unknown> = {};
136
145
 
137
146
  const isTanstackEntrypoint = (entrypoint: Entrypoint) => {
@@ -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) => 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
+ }
@@ -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,24 +1,22 @@
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';
3
+ import type { AppToolsContext } from '@modern-js/app-tools';
8
4
  import { makeLegalIdentifier } from '@modern-js/runtime/cli';
9
5
  import type { NestedRouteForCli, PageRoute } from '@modern-js/types';
10
6
  import { upath as path } from '@modern-js/utils';
11
7
 
8
+ import {
9
+ describeRouteTree,
10
+ type RouteDescriptor,
11
+ } from '../../shared/routeDescriptor';
12
+
12
13
  import {
13
14
  createRouteStaticDataSnippet,
14
- isIndexRoute,
15
- isPathlessLayout,
16
15
  normalizeRelativeImport,
17
16
  pickModernLoaderModule,
18
17
  pickRouteSearchContractModules,
19
18
  quote,
20
19
  resolveFileNoExt,
21
- toTanstackPath,
22
20
  } from './shared';
23
21
 
24
22
  type RouteForCli = NestedRouteForCli | PageRoute;
@@ -53,13 +51,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
53
51
  entryName,
54
52
  );
55
53
 
56
- const rootModern = routes.find(r => {
57
- const extras = r as RouteExtras;
58
- return r && extras.type === 'nested' && extras.isRoot;
59
- }) as NestedRouteForCli | undefined;
60
-
61
- const rootExtras = rootModern as RouteExtras | undefined;
62
- const topLevel = rootModern ? rootExtras?.children || [] : routes;
54
+ const { root: rootModern, children: topLevel } = describeRouteTree(routes);
63
55
 
64
56
  const imports: string[] = [];
65
57
  const statements: string[] = [];
@@ -90,15 +82,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
90
82
  // value carries the internal `@_modern_js_src` alias, which the app's
91
83
  // tsconfig does not map — the generated file must use relative
92
84
  // imports.
93
- const resolvedNoExt = await resolveRouteModuleNoExt(componentPath);
94
- if (!resolvedNoExt) {
85
+ const relImport = await resolveRouteImport(componentPath);
86
+ if (!relImport) {
95
87
  return null;
96
88
  }
97
89
 
98
- const relImport = normalizeRelativeImport(
99
- path.relative(outDir, resolvedNoExt),
100
- );
101
-
102
90
  const componentName = `component_${componentIndex++}`;
103
91
  imports.push(`import ${componentName} from ${quote(relImport)};`);
104
92
  return componentName;
@@ -109,7 +97,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
109
97
  return pendingImportName;
110
98
  };
111
99
 
112
- const resolveRouteModuleNoExt = async (aliasedNoExtPath: string) => {
100
+ const resolveRouteImport = async (aliasedNoExtPath: string) => {
113
101
  const prefix = `${appContext.internalSrcAlias}/`;
114
102
  let absNoExt: string;
115
103
  if (aliasedNoExtPath.startsWith(prefix)) {
@@ -122,7 +110,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
122
110
  absNoExt = path.join(appContext.srcDirectory, aliasedNoExtPath);
123
111
  }
124
112
 
125
- return resolveFileNoExt(absNoExt);
113
+ const resolvedNoExt = await resolveFileNoExt(absNoExt);
114
+ return resolvedNoExt
115
+ ? normalizeRelativeImport(path.relative(outDir, resolvedNoExt))
116
+ : null;
126
117
  };
127
118
 
128
119
  const getImportNamesForLoader = async (
@@ -141,15 +132,11 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
141
132
  };
142
133
  }
143
134
 
144
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
145
- if (!resolvedNoExt) {
135
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
136
+ if (!relImport) {
146
137
  return null;
147
138
  }
148
139
 
149
- const relImport = normalizeRelativeImport(
150
- path.relative(outDir, resolvedNoExt),
151
- );
152
-
153
140
  const importName = `loader_${loaderIndex++}`;
154
141
  const actionName = hasAction
155
142
  ? importName.replace(/^loader_/, 'action_')
@@ -180,14 +167,10 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
180
167
  return existing;
181
168
  }
182
169
 
183
- const resolvedNoExt = await resolveRouteModuleNoExt(aliasedNoExtPath);
184
- if (!resolvedNoExt) {
170
+ const relImport = await resolveRouteImport(aliasedNoExtPath);
171
+ if (!relImport) {
185
172
  return null;
186
173
  }
187
-
188
- const relImport = normalizeRelativeImport(
189
- path.relative(outDir, resolvedNoExt),
190
- );
191
174
  const importName =
192
175
  exportName === 'validateSearch'
193
176
  ? `validateSearch_${validateSearchIndex++}`
@@ -215,15 +198,8 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
215
198
  return reserveRouteVarName(`route_${base}`);
216
199
  };
217
200
 
218
- const buildRoute = async (opts: {
219
- parentVar: string;
220
- route: NestedRouteForCli | PageRoute;
221
- }): Promise<string> => {
222
- const { parentVar, route } = opts;
223
-
224
- const varName = createRouteVarName(route);
201
+ const createRouteOptions = async (route: RouteForCli, hasSplat: boolean) => {
225
202
  const routeExtras = route as RouteExtras;
226
-
227
203
  const loaderInfo = pickModernLoaderModule(route);
228
204
  const routeAction = routeExtras.action;
229
205
  const loaderImports = loaderInfo
@@ -249,10 +225,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
249
225
  )
250
226
  : null;
251
227
 
252
- const rawPath = routeExtras.path;
253
- const hasSplat = typeof rawPath === 'string' && rawPath.includes('*');
254
-
255
- const routeOpts: string[] = [`getParentRoute: () => ${parentVar},`];
228
+ const routeOpts: string[] = [];
256
229
 
257
230
  const componentName = await getImportNameForComponent(
258
231
  routeExtras._component,
@@ -261,14 +234,6 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
261
234
  routeOpts.push(`component: ${componentName},`);
262
235
  }
263
236
 
264
- if (isPathlessLayout(route)) {
265
- const id = routeExtras.id;
266
- routeOpts.push(`id: ${quote(id || 'pathless')},`);
267
- } else {
268
- const p = isIndexRoute(route) ? '/' : toTanstackPath(rawPath || '');
269
- routeOpts.push(`path: ${quote(p)},`);
270
- }
271
-
272
237
  if (loaderName) {
273
238
  routeOpts.push(
274
239
  `loader: modernLoaderToTanstack({ hasSplat: ${hasSplat} }, ${loaderName}),`,
@@ -290,7 +255,22 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
290
255
  routeOpts.push(staticDataSnippet);
291
256
  }
292
257
 
293
- const children = routeExtras.children;
258
+ return routeOpts;
259
+ };
260
+
261
+ const buildRoute = async (
262
+ descriptor: RouteDescriptor<RouteForCli>,
263
+ parentVar: string,
264
+ ): Promise<string> => {
265
+ const { source: route, location, hasSplat, children } = descriptor;
266
+ const varName = createRouteVarName(route);
267
+ const routeOpts = [
268
+ `getParentRoute: () => ${parentVar},`,
269
+ ...Object.entries(location).map(
270
+ ([key, value]) => `${key}: ${quote(value)},`,
271
+ ),
272
+ ...(await createRouteOptions(route, hasSplat)),
273
+ ];
294
274
  const hasChildren = Boolean(children && children.length > 0);
295
275
  const routeCtorVarName = hasChildren
296
276
  ? reserveRouteVarName(`${varName}__base`)
@@ -303,9 +283,7 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
303
283
  if (children && children.length > 0) {
304
284
  const childVars: string[] = [];
305
285
  for (const child of children) {
306
- childVars.push(
307
- await buildRoute({ parentVar: routeCtorVarName, route: child }),
308
- );
286
+ childVars.push(await buildRoute(child, routeCtorVarName));
309
287
  }
310
288
  statements.push(
311
289
  `const ${varName} = ${routeCtorVarName}.addChildren([${childVars.join(', ')}]);`,
@@ -315,60 +293,14 @@ export async function generateTanstackRouterTypesSourceForEntry(opts: {
315
293
  return varName;
316
294
  };
317
295
 
318
- const rootLoaderInfo = rootModern ? pickModernLoaderModule(rootModern) : null;
319
- const rootAction = rootExtras?.action;
320
- const rootLoaderImports = rootLoaderInfo?.loaderPath
321
- ? await getImportNamesForLoader(
322
- rootLoaderInfo.loaderPath,
323
- rootLoaderInfo.inline,
324
- Boolean(
325
- rootLoaderInfo.inline && rootAction === rootLoaderInfo.loaderPath,
326
- ),
327
- )
328
- : null;
329
- const rootLoaderName = rootLoaderImports?.loaderName || null;
330
- const rootActionName = rootLoaderImports?.actionName || null;
331
- const rootSearchContractInfo = rootModern
332
- ? pickRouteSearchContractModules(rootModern)
333
- : null;
334
- const rootValidateSearchName = rootSearchContractInfo?.validateSearchPath
335
- ? await getImportNameForSearchContract(
336
- rootSearchContractInfo.validateSearchPath,
337
- 'validateSearch',
338
- )
339
- : null;
340
- const rootLoaderDepsName = rootSearchContractInfo?.loaderDepsPath
341
- ? await getImportNameForSearchContract(
342
- rootSearchContractInfo.loaderDepsPath,
343
- 'loaderDeps',
344
- )
345
- : null;
346
-
347
296
  const topLevelVars: string[] = [];
348
297
  for (const route of topLevel) {
349
- topLevelVars.push(await buildRoute({ parentVar: 'rootRoute', route }));
298
+ topLevelVars.push(await buildRoute(route, 'rootRoute'));
350
299
  }
351
300
 
352
- const rootOpts: string[] = [];
353
-
354
- const rootComponentName = await getImportNameForComponent(
355
- rootExtras?._component,
356
- );
357
- if (rootComponentName) {
358
- rootOpts.push(`component: ${rootComponentName},`);
359
- }
360
-
361
- if (rootLoaderName) {
362
- rootOpts.push(
363
- `loader: modernLoaderToTanstack({ hasSplat: false }, ${rootLoaderName}),`,
364
- );
365
- }
366
- if (rootValidateSearchName) {
367
- rootOpts.push(`validateSearch: ${rootValidateSearchName},`);
368
- }
369
- if (rootLoaderDepsName) {
370
- rootOpts.push(`loaderDeps: ${rootLoaderDepsName},`);
371
- }
301
+ const rootOpts = rootModern
302
+ ? await createRouteOptions(rootModern, false)
303
+ : [];
372
304
 
373
305
  const routerGenTs = `// This file is auto-generated by Modern.js. Do not edit manually.
374
306
 
@@ -386,13 +318,7 @@ ${imports.join('\n')}
386
318
 
387
319
  export const rootRoute = createRootRouteWithContext<ModernRouterContext>()({
388
320
  ${rootOpts.join('\n ')}
389
- ${
390
- createRouteStaticDataSnippet({
391
- modernRouteId: rootExtras?.id,
392
- loaderName: rootLoaderName,
393
- actionName: rootActionName,
394
- }) || ''
395
- }
321
+
396
322
  });
397
323
 
398
324
  ${statements.join('\n\n')}
@@ -1,4 +1,8 @@
1
1
  // @effect-diagnostics strictBooleanExpressions:off
2
+ import { createLocalisedRouteRewrite } from '@modern-js/i18n-runtime-extensions';
3
+ import type { RouteObject } from '@modern-js/runtime-utils/router';
4
+ import { composeRewrites } from '@tanstack/react-router';
5
+
2
6
  function normalizeBasepath(basepath: string): string {
3
7
  if (!basepath) {
4
8
  return '/';
@@ -15,10 +19,12 @@ function normalizeBasepath(basepath: string): string {
15
19
  export function createModernBasepathRewrite(
16
20
  basepath: string,
17
21
  caseSensitive = false,
22
+ routes: RouteObject[] = [],
18
23
  ) {
24
+ const localisedRewrite = createLocalisedRouteRewrite(routes);
19
25
  const normalizedBasepath = normalizeBasepath(basepath);
20
26
  if (normalizedBasepath === '/') {
21
- return undefined;
27
+ return localisedRewrite;
22
28
  }
23
29
 
24
30
  const normalizedBasepathWithSlash = `${normalizedBasepath}/`;
@@ -29,7 +35,7 @@ export function createModernBasepathRewrite(
29
35
  ? normalizedBasepathWithSlash
30
36
  : normalizedBasepathWithSlash.toLowerCase();
31
37
 
32
- return {
38
+ const basepathRewrite = {
33
39
  input: ({ url }: { url: URL }) => {
34
40
  const pathname = caseSensitive
35
41
  ? url.pathname
@@ -57,4 +63,7 @@ export function createModernBasepathRewrite(
57
63
  return url;
58
64
  },
59
65
  };
66
+ return localisedRewrite
67
+ ? composeRewrites([basepathRewrite, localisedRewrite])
68
+ : basepathRewrite;
60
69
  }
@@ -1,16 +1,26 @@
1
1
  /**
2
- * The router runtime state helpers are owned by @modern-js/runtime (the same
2
+ * The router runtime state helpers are owned by @modern-js/runtime-extensions (the same
3
3
  * implementation backs the built-in react-router provider and the SSR
4
4
  * pipeline). This module only re-exports them so every router provider
5
5
  * writes to the exact same runtime-context extension slot.
6
6
  */
7
+
8
+ import type { RouterLifecycleContext as NativeRouterLifecycleContext } from '@modern-js/runtime/context';
9
+ import type {
10
+ InternalRouterServerSnapshot,
11
+ RouterRouteMatchSnapshot,
12
+ } from '@modern-js/runtime-extensions/router-state';
13
+
14
+ export type RouterLifecycleContext = NativeRouterLifecycleContext & {
15
+ matches?: RouterRouteMatchSnapshot[];
16
+ cleanup?: () => void | Promise<void>;
17
+ serverSnapshot?: InternalRouterServerSnapshot;
18
+ };
7
19
  export {
8
20
  applyRouterRuntimeState,
9
21
  applyRouterServerPrepareResult,
10
- createRouterRuntimeState,
11
22
  createRouterServerSnapshot,
12
23
  getRouterRuntimeState,
13
24
  getRouterServerSnapshot,
14
- type RouterLifecycleContext,
15
25
  type RouterLifecyclePhase,
16
- } from '@modern-js/runtime/context';
26
+ } from '@modern-js/runtime-extensions/router-state';