@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.17 → 3.9.0-ultramodern.18

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.
@@ -38,12 +38,14 @@ var __webpack_exports__ = {};
38
38
  __webpack_require__.r(__webpack_exports__);
39
39
  __webpack_require__.d(__webpack_exports__, {
40
40
  generateTanstackRouteArtifacts: ()=>generateTanstackRouteArtifacts,
41
+ writeRouteMetadataManifest: ()=>writeRouteMetadataManifest,
41
42
  writeTanstackRegisterFile: ()=>writeTanstackRegisterFile,
42
43
  writeTanstackRouterTypesForEntries: ()=>writeTanstackRouterTypesForEntries
43
44
  });
44
45
  const external_node_path_namespaceObject = require("node:path");
45
46
  var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
46
47
  const utils_namespaceObject = require("@modern-js/utils");
48
+ const external_routeMetadata_js_namespaceObject = require("./routeMetadata.js");
47
49
  const external_tanstackTypes_js_namespaceObject = require("./tanstackTypes.js");
48
50
  const DEFAULT_GENERATED_DIR_NAME = 'modern-tanstack';
49
51
  async function writeFileIfChanged(filePath, content) {
@@ -121,7 +123,16 @@ async function writeTanstackRouterTypesForEntries(opts) {
121
123
  canonicalRoutes
122
124
  });
123
125
  }
126
+ async function writeRouteMetadataManifest(opts) {
127
+ const routesDirectory = external_node_path_default().join(opts.appDirectory, 'src', 'routes');
128
+ const manifestPath = external_node_path_default().join(routesDirectory, external_routeMetadata_js_namespaceObject.ROUTE_METADATA_MANIFEST_FILE_NAME);
129
+ const manifest = (0, external_routeMetadata_js_namespaceObject.renderRouteMetadataManifest)(await (0, external_routeMetadata_js_namespaceObject.findRouteMetaFiles)(routesDirectory));
130
+ if (null !== manifest) return void await writeFileIfChanged(manifestPath, manifest);
131
+ const previous = await utils_namespaceObject.fs.pathExists(manifestPath) ? await utils_namespaceObject.fs.readFile(manifestPath, 'utf-8') : null;
132
+ if (previous?.startsWith(`${external_routeMetadata_js_namespaceObject.ROUTE_METADATA_MANIFEST_HEADER}\n`)) throw new Error(`[plugin-tanstack] ${manifestPath} is generated from route.meta.ts files, but none remain below ${routesDirectory}. Restore at least one route.meta.ts that exports routeMeta, or delete the manifest together with its imports.`);
133
+ }
124
134
  async function generateTanstackRouteArtifacts(opts) {
135
+ await writeRouteMetadataManifest(opts);
125
136
  const [{ createRunOptions }, { cli }, utils] = await Promise.all([
126
137
  import("@modern-js/app-tools/cli/run"),
127
138
  import("@modern-js/plugin/cli"),
@@ -146,10 +157,12 @@ async function generateTanstackRouteArtifacts(opts) {
146
157
  await appContext.hooks.onBeforeExit.call();
147
158
  }
148
159
  exports.generateTanstackRouteArtifacts = __webpack_exports__.generateTanstackRouteArtifacts;
160
+ exports.writeRouteMetadataManifest = __webpack_exports__.writeRouteMetadataManifest;
149
161
  exports.writeTanstackRegisterFile = __webpack_exports__.writeTanstackRegisterFile;
150
162
  exports.writeTanstackRouterTypesForEntries = __webpack_exports__.writeTanstackRouterTypesForEntries;
151
163
  for(var __rspack_i in __webpack_exports__)if (-1 === [
152
164
  "generateTanstackRouteArtifacts",
165
+ "writeRouteMetadataManifest",
153
166
  "writeTanstackRegisterFile",
154
167
  "writeTanstackRouterTypesForEntries"
155
168
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -59,6 +59,7 @@ var __webpack_exports__ = {};
59
59
  generateTanstackRouterTypesSourceForEntry: ()=>external_tanstackTypes_js_namespaceObject.generateTanstackRouterTypesSourceForEntry,
60
60
  resolveTanstackRouteCodeSplittingEnabled: ()=>external_routeSplitting_js_namespaceObject.resolveTanstackRouteCodeSplittingEnabled,
61
61
  tanstackRouterPlugin: ()=>tanstackRouterPlugin,
62
+ writeRouteMetadataManifest: ()=>external_artifacts_js_namespaceObject.writeRouteMetadataManifest,
62
63
  writeTanstackRegisterFile: ()=>external_artifacts_js_namespaceObject.writeTanstackRegisterFile,
63
64
  writeTanstackRouterTypesForEntries: ()=>external_artifacts_js_namespaceObject.writeTanstackRouterTypesForEntries
64
65
  });
@@ -248,6 +249,7 @@ exports.generateTanstackRouteArtifacts = __webpack_exports__.generateTanstackRou
248
249
  exports.generateTanstackRouterTypesSourceForEntry = __webpack_exports__.generateTanstackRouterTypesSourceForEntry;
249
250
  exports.resolveTanstackRouteCodeSplittingEnabled = __webpack_exports__.resolveTanstackRouteCodeSplittingEnabled;
250
251
  exports.tanstackRouterPlugin = __webpack_exports__.tanstackRouterPlugin;
252
+ exports.writeRouteMetadataManifest = __webpack_exports__.writeRouteMetadataManifest;
251
253
  exports.writeTanstackRegisterFile = __webpack_exports__.writeTanstackRegisterFile;
252
254
  exports.writeTanstackRouterTypesForEntries = __webpack_exports__.writeTanstackRouterTypesForEntries;
253
255
  for(var __rspack_i in __webpack_exports__)if (-1 === [
@@ -258,6 +260,7 @@ for(var __rspack_i in __webpack_exports__)if (-1 === [
258
260
  "generateTanstackRouterTypesSourceForEntry",
259
261
  "resolveTanstackRouteCodeSplittingEnabled",
260
262
  "tanstackRouterPlugin",
263
+ "writeRouteMetadataManifest",
261
264
  "writeTanstackRegisterFile",
262
265
  "writeTanstackRouterTypesForEntries"
263
266
  ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.n = (module)=>{
5
+ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module;
6
+ __webpack_require__.d(getter, {
7
+ a: getter
8
+ });
9
+ return getter;
10
+ };
11
+ })();
12
+ (()=>{
13
+ __webpack_require__.d = (exports1, getters, values)=>{
14
+ var define = (defs, kind)=>{
15
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
16
+ enumerable: true,
17
+ [kind]: defs[key]
18
+ });
19
+ };
20
+ define(getters, "get");
21
+ define(values, "value");
22
+ };
23
+ })();
24
+ (()=>{
25
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
26
+ })();
27
+ (()=>{
28
+ __webpack_require__.r = (exports1)=>{
29
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
30
+ value: 'Module'
31
+ });
32
+ Object.defineProperty(exports1, '__esModule', {
33
+ value: true
34
+ });
35
+ };
36
+ })();
37
+ var __webpack_exports__ = {};
38
+ __webpack_require__.r(__webpack_exports__);
39
+ __webpack_require__.d(__webpack_exports__, {
40
+ ROUTE_METADATA_MANIFEST_FILE_NAME: ()=>ROUTE_METADATA_MANIFEST_FILE_NAME,
41
+ ROUTE_METADATA_MANIFEST_HEADER: ()=>ROUTE_METADATA_MANIFEST_HEADER,
42
+ ROUTE_META_FILE_NAME: ()=>ROUTE_META_FILE_NAME,
43
+ findRouteMetaFiles: ()=>findRouteMetaFiles,
44
+ renderRouteMetadataManifest: ()=>renderRouteMetadataManifest
45
+ });
46
+ const promises_namespaceObject = require("node:fs/promises");
47
+ const external_node_path_namespaceObject = require("node:path");
48
+ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
49
+ const ROUTE_META_FILE_NAME = 'route.meta.ts';
50
+ const ROUTE_METADATA_MANIFEST_FILE_NAME = 'ultramodern-route-metadata.ts';
51
+ const ROUTE_METADATA_MANIFEST_HEADER = `// @generated by @modern-js/plugin-tanstack from ${ROUTE_META_FILE_NAME} files. Run routes-generate; do not edit.`;
52
+ const PRINT_WIDTH = 120;
53
+ const MANIFEST_BODY = `export const ultramodernLocalisedUrls = Object.fromEntries(
54
+ ultramodernRouteMetadata
55
+ .filter((route) => route.canonicalPath !== '/')
56
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
57
+ );
58
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
59
+ .filter((route) => route.public && route.indexable)
60
+ .map((route) => ({
61
+ canonicalPath: route.canonicalPath,
62
+ descriptionKey: route.descriptionKey,
63
+ id: route.id,
64
+ localisedPaths: route.localisedPaths,
65
+ namespace: route.namespace,
66
+ ownerAppId: route.ownerAppId,
67
+ titleKey: route.titleKey,
68
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
69
+ }));
70
+ export const ultramodernRouteConfig = {
71
+ authoring: 'colocated-route-meta',
72
+ generatedManifest: true,
73
+ localisedUrls: ultramodernLocalisedUrls,
74
+ namespace: ultramodernRouteNamespace,
75
+ publicRoutes: ultramodernPublicRoutes,
76
+ routes: ultramodernRouteMetadata,
77
+ source: 'route-owned',
78
+ } as const;
79
+ `;
80
+ const compareCodeUnits = (left, right)=>left < right ? -1 : left > right ? 1 : 0;
81
+ const quoteSpecifier = (specifier)=>specifier.includes("'") ? JSON.stringify(specifier) : `'${specifier}'`;
82
+ function renderRouteMetadataManifest(routeMetaFiles) {
83
+ const specifiers = routeMetaFiles.map((file)=>`./${file.replaceAll('\\', '/').replace(/\.ts$/u, '')}`).sort(compareCodeUnits);
84
+ if (0 === specifiers.length) return null;
85
+ const bindings = specifiers.map((_, index)=>`route${index}`);
86
+ const imports = specifiers.map((specifier, index)=>`import { routeMeta as ${bindings[index]} } from ${quoteSpecifier(specifier)};`);
87
+ const inlineRoutes = `export const ultramodernRouteMetadata = [${bindings.join(', ')}] as const;`;
88
+ const routes = inlineRoutes.length <= PRINT_WIDTH ? inlineRoutes : `export const ultramodernRouteMetadata = [\n${bindings.map((binding)=>` ${binding},\n`).join('')}] as const;`;
89
+ return `${ROUTE_METADATA_MANIFEST_HEADER}
90
+ ${imports.join('\n')}
91
+
92
+ export const ultramodernRouteNamespace = route0.namespace;
93
+ ${routes}
94
+ ${MANIFEST_BODY}`;
95
+ }
96
+ async function findRouteMetaFiles(routesDirectory) {
97
+ const files = [];
98
+ const visit = async (relativeDirectory)=>{
99
+ const entries = await (0, promises_namespaceObject.readdir)(external_node_path_default().join(routesDirectory, relativeDirectory), {
100
+ withFileTypes: true
101
+ });
102
+ for (const entry of entries){
103
+ const relativePath = external_node_path_default().join(relativeDirectory, entry.name);
104
+ if (entry.isDirectory()) await visit(relativePath);
105
+ else if (entry.isFile() && entry.name === ROUTE_META_FILE_NAME) files.push(relativePath);
106
+ }
107
+ };
108
+ try {
109
+ await visit('');
110
+ } catch (error) {
111
+ const { code, path: missingPath } = error;
112
+ if ('ENOENT' === code && missingPath === routesDirectory) return [];
113
+ throw error;
114
+ }
115
+ return files;
116
+ }
117
+ exports.ROUTE_METADATA_MANIFEST_FILE_NAME = __webpack_exports__.ROUTE_METADATA_MANIFEST_FILE_NAME;
118
+ exports.ROUTE_METADATA_MANIFEST_HEADER = __webpack_exports__.ROUTE_METADATA_MANIFEST_HEADER;
119
+ exports.ROUTE_META_FILE_NAME = __webpack_exports__.ROUTE_META_FILE_NAME;
120
+ exports.findRouteMetaFiles = __webpack_exports__.findRouteMetaFiles;
121
+ exports.renderRouteMetadataManifest = __webpack_exports__.renderRouteMetadataManifest;
122
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
123
+ "ROUTE_METADATA_MANIFEST_FILE_NAME",
124
+ "ROUTE_METADATA_MANIFEST_HEADER",
125
+ "ROUTE_META_FILE_NAME",
126
+ "findRouteMetaFiles",
127
+ "renderRouteMetadataManifest"
128
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
129
+ Object.defineProperty(exports, '__esModule', {
130
+ value: true
131
+ });
@@ -1,5 +1,6 @@
1
1
  import node_path from "node:path";
2
2
  import { fs } from "@modern-js/utils";
3
+ import { ROUTE_METADATA_MANIFEST_FILE_NAME, ROUTE_METADATA_MANIFEST_HEADER, findRouteMetaFiles, renderRouteMetadataManifest } from "./routeMetadata.mjs";
3
4
  import { collectCanonicalRoutesForEntry, generateTanstackRouterTypesSourceForEntry } from "./tanstackTypes.mjs";
4
5
  const DEFAULT_GENERATED_DIR_NAME = 'modern-tanstack';
5
6
  async function writeFileIfChanged(filePath, content) {
@@ -77,7 +78,16 @@ async function writeTanstackRouterTypesForEntries(opts) {
77
78
  canonicalRoutes
78
79
  });
79
80
  }
81
+ async function writeRouteMetadataManifest(opts) {
82
+ const routesDirectory = node_path.join(opts.appDirectory, 'src', 'routes');
83
+ const manifestPath = node_path.join(routesDirectory, ROUTE_METADATA_MANIFEST_FILE_NAME);
84
+ const manifest = renderRouteMetadataManifest(await findRouteMetaFiles(routesDirectory));
85
+ if (null !== manifest) return void await writeFileIfChanged(manifestPath, manifest);
86
+ const previous = await fs.pathExists(manifestPath) ? await fs.readFile(manifestPath, 'utf-8') : null;
87
+ if (previous?.startsWith(`${ROUTE_METADATA_MANIFEST_HEADER}\n`)) throw new Error(`[plugin-tanstack] ${manifestPath} is generated from route.meta.ts files, but none remain below ${routesDirectory}. Restore at least one route.meta.ts that exports routeMeta, or delete the manifest together with its imports.`);
88
+ }
80
89
  async function generateTanstackRouteArtifacts(opts) {
90
+ await writeRouteMetadataManifest(opts);
81
91
  const [{ createRunOptions }, { cli }, utils] = await Promise.all([
82
92
  import("@modern-js/app-tools/cli/run"),
83
93
  import("@modern-js/plugin/cli"),
@@ -101,4 +111,4 @@ async function generateTanstackRouteArtifacts(opts) {
101
111
  });
102
112
  await appContext.hooks.onBeforeExit.call();
103
113
  }
104
- export { generateTanstackRouteArtifacts, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
114
+ export { generateTanstackRouteArtifacts, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
@@ -1,6 +1,6 @@
1
1
  import node_path from "node:path";
2
2
  import { NESTED_ROUTE_SPEC_FILE, filterRoutesForServer } from "@modern-js/utils";
3
- import { generateTanstackRouteArtifacts, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries } from "./artifacts.mjs";
3
+ import { generateTanstackRouteArtifacts, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries } from "./artifacts.mjs";
4
4
  import { createTanstackRsbuildRouteSplittingProfile, resolveTanstackRouteCodeSplittingEnabled } from "./routeSplitting.mjs";
5
5
  const DEFAULT_ROUTES_DIR = 'routes';
6
6
  const DEFAULT_GENERATED_DIR_NAME = 'modern-tanstack';
@@ -176,4 +176,4 @@ function tanstackRouterPlugin(options = {}) {
176
176
  const src_cli = tanstackRouterPlugin;
177
177
  export { collectCanonicalRoutesForEntry, generateTanstackRouterTypesSourceForEntry } from "./tanstackTypes.mjs";
178
178
  export default src_cli;
179
- export { createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
179
+ export { createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
@@ -0,0 +1,71 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import node_path from "node:path";
3
+ const ROUTE_META_FILE_NAME = 'route.meta.ts';
4
+ const ROUTE_METADATA_MANIFEST_FILE_NAME = 'ultramodern-route-metadata.ts';
5
+ const ROUTE_METADATA_MANIFEST_HEADER = `// @generated by @modern-js/plugin-tanstack from ${ROUTE_META_FILE_NAME} files. Run routes-generate; do not edit.`;
6
+ const PRINT_WIDTH = 120;
7
+ const MANIFEST_BODY = `export const ultramodernLocalisedUrls = Object.fromEntries(
8
+ ultramodernRouteMetadata
9
+ .filter((route) => route.canonicalPath !== '/')
10
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
11
+ );
12
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
13
+ .filter((route) => route.public && route.indexable)
14
+ .map((route) => ({
15
+ canonicalPath: route.canonicalPath,
16
+ descriptionKey: route.descriptionKey,
17
+ id: route.id,
18
+ localisedPaths: route.localisedPaths,
19
+ namespace: route.namespace,
20
+ ownerAppId: route.ownerAppId,
21
+ titleKey: route.titleKey,
22
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
23
+ }));
24
+ export const ultramodernRouteConfig = {
25
+ authoring: 'colocated-route-meta',
26
+ generatedManifest: true,
27
+ localisedUrls: ultramodernLocalisedUrls,
28
+ namespace: ultramodernRouteNamespace,
29
+ publicRoutes: ultramodernPublicRoutes,
30
+ routes: ultramodernRouteMetadata,
31
+ source: 'route-owned',
32
+ } as const;
33
+ `;
34
+ const compareCodeUnits = (left, right)=>left < right ? -1 : left > right ? 1 : 0;
35
+ const quoteSpecifier = (specifier)=>specifier.includes("'") ? JSON.stringify(specifier) : `'${specifier}'`;
36
+ function renderRouteMetadataManifest(routeMetaFiles) {
37
+ const specifiers = routeMetaFiles.map((file)=>`./${file.replaceAll('\\', '/').replace(/\.ts$/u, '')}`).sort(compareCodeUnits);
38
+ if (0 === specifiers.length) return null;
39
+ const bindings = specifiers.map((_, index)=>`route${index}`);
40
+ const imports = specifiers.map((specifier, index)=>`import { routeMeta as ${bindings[index]} } from ${quoteSpecifier(specifier)};`);
41
+ const inlineRoutes = `export const ultramodernRouteMetadata = [${bindings.join(', ')}] as const;`;
42
+ const routes = inlineRoutes.length <= PRINT_WIDTH ? inlineRoutes : `export const ultramodernRouteMetadata = [\n${bindings.map((binding)=>` ${binding},\n`).join('')}] as const;`;
43
+ return `${ROUTE_METADATA_MANIFEST_HEADER}
44
+ ${imports.join('\n')}
45
+
46
+ export const ultramodernRouteNamespace = route0.namespace;
47
+ ${routes}
48
+ ${MANIFEST_BODY}`;
49
+ }
50
+ async function findRouteMetaFiles(routesDirectory) {
51
+ const files = [];
52
+ const visit = async (relativeDirectory)=>{
53
+ const entries = await readdir(node_path.join(routesDirectory, relativeDirectory), {
54
+ withFileTypes: true
55
+ });
56
+ for (const entry of entries){
57
+ const relativePath = node_path.join(relativeDirectory, entry.name);
58
+ if (entry.isDirectory()) await visit(relativePath);
59
+ else if (entry.isFile() && entry.name === ROUTE_META_FILE_NAME) files.push(relativePath);
60
+ }
61
+ };
62
+ try {
63
+ await visit('');
64
+ } catch (error) {
65
+ const { code, path: missingPath } = error;
66
+ if ('ENOENT' === code && missingPath === routesDirectory) return [];
67
+ throw error;
68
+ }
69
+ return files;
70
+ }
71
+ export { ROUTE_METADATA_MANIFEST_FILE_NAME, ROUTE_METADATA_MANIFEST_HEADER, ROUTE_META_FILE_NAME, findRouteMetaFiles, renderRouteMetadataManifest };
@@ -1,6 +1,7 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { fs } from "@modern-js/utils";
4
+ import { ROUTE_METADATA_MANIFEST_FILE_NAME, ROUTE_METADATA_MANIFEST_HEADER, findRouteMetaFiles, renderRouteMetadataManifest } from "./routeMetadata.mjs";
4
5
  import { collectCanonicalRoutesForEntry, generateTanstackRouterTypesSourceForEntry } from "./tanstackTypes.mjs";
5
6
  const DEFAULT_GENERATED_DIR_NAME = 'modern-tanstack';
6
7
  async function writeFileIfChanged(filePath, content) {
@@ -78,7 +79,16 @@ async function writeTanstackRouterTypesForEntries(opts) {
78
79
  canonicalRoutes
79
80
  });
80
81
  }
82
+ async function writeRouteMetadataManifest(opts) {
83
+ const routesDirectory = node_path.join(opts.appDirectory, 'src', 'routes');
84
+ const manifestPath = node_path.join(routesDirectory, ROUTE_METADATA_MANIFEST_FILE_NAME);
85
+ const manifest = renderRouteMetadataManifest(await findRouteMetaFiles(routesDirectory));
86
+ if (null !== manifest) return void await writeFileIfChanged(manifestPath, manifest);
87
+ const previous = await fs.pathExists(manifestPath) ? await fs.readFile(manifestPath, 'utf-8') : null;
88
+ if (previous?.startsWith(`${ROUTE_METADATA_MANIFEST_HEADER}\n`)) throw new Error(`[plugin-tanstack] ${manifestPath} is generated from route.meta.ts files, but none remain below ${routesDirectory}. Restore at least one route.meta.ts that exports routeMeta, or delete the manifest together with its imports.`);
89
+ }
81
90
  async function generateTanstackRouteArtifacts(opts) {
91
+ await writeRouteMetadataManifest(opts);
82
92
  const [{ createRunOptions }, { cli }, utils] = await Promise.all([
83
93
  import("@modern-js/app-tools/cli/run"),
84
94
  import("@modern-js/plugin/cli"),
@@ -102,4 +112,4 @@ async function generateTanstackRouteArtifacts(opts) {
102
112
  });
103
113
  await appContext.hooks.onBeforeExit.call();
104
114
  }
105
- export { generateTanstackRouteArtifacts, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
115
+ export { generateTanstackRouteArtifacts, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
@@ -1,7 +1,7 @@
1
1
  import "node:module";
2
2
  import node_path from "node:path";
3
3
  import { NESTED_ROUTE_SPEC_FILE, filterRoutesForServer } from "@modern-js/utils";
4
- import { generateTanstackRouteArtifacts, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries } from "./artifacts.mjs";
4
+ import { generateTanstackRouteArtifacts, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries } from "./artifacts.mjs";
5
5
  import { createTanstackRsbuildRouteSplittingProfile, resolveTanstackRouteCodeSplittingEnabled } from "./routeSplitting.mjs";
6
6
  import { fileURLToPath as __rspack_fileURLToPath } from "node:url";
7
7
  import { dirname as __rspack_dirname } from "node:path";
@@ -182,4 +182,4 @@ function tanstackRouterPlugin(options = {}) {
182
182
  const src_cli = tanstackRouterPlugin;
183
183
  export { collectCanonicalRoutesForEntry, generateTanstackRouterTypesSourceForEntry } from "./tanstackTypes.mjs";
184
184
  export default src_cli;
185
- export { createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
185
+ export { createTanstackRsbuildRouteSplittingProfile, generateTanstackRouteArtifacts, resolveTanstackRouteCodeSplittingEnabled, tanstackRouterPlugin, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries };
@@ -0,0 +1,72 @@
1
+ import "node:module";
2
+ import { readdir } from "node:fs/promises";
3
+ import node_path from "node:path";
4
+ const ROUTE_META_FILE_NAME = 'route.meta.ts';
5
+ const ROUTE_METADATA_MANIFEST_FILE_NAME = 'ultramodern-route-metadata.ts';
6
+ const ROUTE_METADATA_MANIFEST_HEADER = `// @generated by @modern-js/plugin-tanstack from ${ROUTE_META_FILE_NAME} files. Run routes-generate; do not edit.`;
7
+ const PRINT_WIDTH = 120;
8
+ const MANIFEST_BODY = `export const ultramodernLocalisedUrls = Object.fromEntries(
9
+ ultramodernRouteMetadata
10
+ .filter((route) => route.canonicalPath !== '/')
11
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
12
+ );
13
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
14
+ .filter((route) => route.public && route.indexable)
15
+ .map((route) => ({
16
+ canonicalPath: route.canonicalPath,
17
+ descriptionKey: route.descriptionKey,
18
+ id: route.id,
19
+ localisedPaths: route.localisedPaths,
20
+ namespace: route.namespace,
21
+ ownerAppId: route.ownerAppId,
22
+ titleKey: route.titleKey,
23
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
24
+ }));
25
+ export const ultramodernRouteConfig = {
26
+ authoring: 'colocated-route-meta',
27
+ generatedManifest: true,
28
+ localisedUrls: ultramodernLocalisedUrls,
29
+ namespace: ultramodernRouteNamespace,
30
+ publicRoutes: ultramodernPublicRoutes,
31
+ routes: ultramodernRouteMetadata,
32
+ source: 'route-owned',
33
+ } as const;
34
+ `;
35
+ const compareCodeUnits = (left, right)=>left < right ? -1 : left > right ? 1 : 0;
36
+ const quoteSpecifier = (specifier)=>specifier.includes("'") ? JSON.stringify(specifier) : `'${specifier}'`;
37
+ function renderRouteMetadataManifest(routeMetaFiles) {
38
+ const specifiers = routeMetaFiles.map((file)=>`./${file.replaceAll('\\', '/').replace(/\.ts$/u, '')}`).sort(compareCodeUnits);
39
+ if (0 === specifiers.length) return null;
40
+ const bindings = specifiers.map((_, index)=>`route${index}`);
41
+ const imports = specifiers.map((specifier, index)=>`import { routeMeta as ${bindings[index]} } from ${quoteSpecifier(specifier)};`);
42
+ const inlineRoutes = `export const ultramodernRouteMetadata = [${bindings.join(', ')}] as const;`;
43
+ const routes = inlineRoutes.length <= PRINT_WIDTH ? inlineRoutes : `export const ultramodernRouteMetadata = [\n${bindings.map((binding)=>` ${binding},\n`).join('')}] as const;`;
44
+ return `${ROUTE_METADATA_MANIFEST_HEADER}
45
+ ${imports.join('\n')}
46
+
47
+ export const ultramodernRouteNamespace = route0.namespace;
48
+ ${routes}
49
+ ${MANIFEST_BODY}`;
50
+ }
51
+ async function findRouteMetaFiles(routesDirectory) {
52
+ const files = [];
53
+ const visit = async (relativeDirectory)=>{
54
+ const entries = await readdir(node_path.join(routesDirectory, relativeDirectory), {
55
+ withFileTypes: true
56
+ });
57
+ for (const entry of entries){
58
+ const relativePath = node_path.join(relativeDirectory, entry.name);
59
+ if (entry.isDirectory()) await visit(relativePath);
60
+ else if (entry.isFile() && entry.name === ROUTE_META_FILE_NAME) files.push(relativePath);
61
+ }
62
+ };
63
+ try {
64
+ await visit('');
65
+ } catch (error) {
66
+ const { code, path: missingPath } = error;
67
+ if ('ENOENT' === code && missingPath === routesDirectory) return [];
68
+ throw error;
69
+ }
70
+ return files;
71
+ }
72
+ export { ROUTE_METADATA_MANIFEST_FILE_NAME, ROUTE_METADATA_MANIFEST_HEADER, ROUTE_META_FILE_NAME, findRouteMetaFiles, renderRouteMetadataManifest };
@@ -21,11 +21,19 @@ export declare function writeTanstackRouterTypesForEntries(opts: {
21
21
  i18nPluginInstalled?: boolean;
22
22
  }): Promise<void>;
23
23
  /**
24
- * Headless regeneration of `router.gen.ts` / `register.gen.d.ts` for a generated
25
- * app, without running a full dev/build. Drives the app-tools analyze `onPrepare`
26
- * (which fires `generateEntryCode`) and returns before any bundling — the app's
27
- * own registered plugin-tanstack writes the artifacts via its `generateEntryCode`
28
- * subscriber. Reuses the exact hook dev/build use, so there is no scanning drift.
24
+ * Writes `src/routes/ultramodern-route-metadata.ts` from the `route.meta.ts`
25
+ * files below `src/routes`. Reads only the file system, never the app config.
26
+ */
27
+ export declare function writeRouteMetadataManifest(opts: {
28
+ appDirectory: string;
29
+ }): Promise<void>;
30
+ /**
31
+ * Headless regeneration of the route metadata manifest and of `router.gen.ts` /
32
+ * `register.gen.d.ts` for a generated app, without running a full dev/build.
33
+ * Drives the app-tools analyze `onPrepare` (which fires `generateEntryCode`)
34
+ * and returns before any bundling — the app's own registered plugin-tanstack
35
+ * writes the router artifacts via its `generateEntryCode` subscriber. Reuses
36
+ * the exact hook dev/build use, so there is no scanning drift.
29
37
  */
30
38
  export declare function generateTanstackRouteArtifacts(opts: {
31
39
  appDirectory: string;
@@ -1,6 +1,6 @@
1
1
  import type { AppTools, CliPlugin } from '@modern-js/app-tools';
2
2
  import { type TanstackRouteCodeSplittingOption } from './routeSplitting.js';
3
- export { generateTanstackRouteArtifacts, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries, } from './artifacts.js';
3
+ export { generateTanstackRouteArtifacts, writeRouteMetadataManifest, writeTanstackRegisterFile, writeTanstackRouterTypesForEntries, } from './artifacts.js';
4
4
  export type { TanstackRouteCodeSplittingOption, TanstackRsbuildRouteSplittingProfile, } from './routeSplitting.js';
5
5
  export { createTanstackRsbuildRouteSplittingProfile, resolveTanstackRouteCodeSplittingEnabled, } from './routeSplitting.js';
6
6
  export { type CollectCanonicalRoutesOptions, collectCanonicalRoutesForEntry, generateTanstackRouterTypesSourceForEntry, } from './tanstackTypes.js';
@@ -0,0 +1,18 @@
1
+ export declare const ROUTE_META_FILE_NAME = "route.meta.ts";
2
+ export declare const ROUTE_METADATA_MANIFEST_FILE_NAME = "ultramodern-route-metadata.ts";
3
+ export declare const ROUTE_METADATA_MANIFEST_HEADER = "// @generated by @modern-js/plugin-tanstack from route.meta.ts files. Run routes-generate; do not edit.";
4
+ /**
5
+ * Renders the route metadata manifest for the `route.meta.ts` files of one
6
+ * routes directory. Each path is relative to that directory (either separator).
7
+ * Every `route.meta.ts` must export `routeMeta`; the manifest imports it
8
+ * statically, so the metadata is never evaluated while generating. Returns
9
+ * `null` when there are no route metadata files. Output depends only on the
10
+ * set of paths, never on their order or on the file system.
11
+ */
12
+ export declare function renderRouteMetadataManifest(routeMetaFiles: readonly string[]): string | null;
13
+ /**
14
+ * Lists the `route.meta.ts` files below `routesDirectory`, relative to it.
15
+ * Symbolic links are not followed: linked metadata belongs to the tree it
16
+ * lives in. A missing directory has no route metadata.
17
+ */
18
+ export declare function findRouteMetaFiles(routesDirectory: string): Promise<string[]>;
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "modern.js",
19
19
  "tanstack-router"
20
20
  ],
21
- "version": "3.9.0-ultramodern.17",
21
+ "version": "3.9.0-ultramodern.18",
22
22
  "engines": {
23
23
  "node": ">=20"
24
24
  },
@@ -94,19 +94,19 @@
94
94
  }
95
95
  },
96
96
  "dependencies": {
97
- "@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.17",
98
- "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.17",
99
- "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.17",
100
- "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.17",
101
- "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.17",
102
- "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.17",
97
+ "@modern-js/i18n-runtime-extensions": "npm:@bleedingdev/modern-js-i18n-runtime-extensions@3.9.0-ultramodern.18",
98
+ "@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.9.0-ultramodern.18",
99
+ "@modern-js/runtime-extensions": "npm:@bleedingdev/modern-js-runtime-extensions@3.9.0-ultramodern.18",
100
+ "@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.9.0-ultramodern.18",
101
+ "@modern-js/types": "npm:@bleedingdev/modern-js-types@3.9.0-ultramodern.18",
102
+ "@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.9.0-ultramodern.18",
103
103
  "@swc/helpers": "^0.5.23",
104
104
  "@tanstack/react-router": "1.170.39",
105
105
  "@tanstack/router-core": "1.171.32"
106
106
  },
107
107
  "peerDependencies": {
108
- "@modern-js/app-tools": "3.9.0-ultramodern.17",
109
- "@modern-js/runtime": "3.9.0-ultramodern.17",
108
+ "@modern-js/app-tools": "3.9.0-ultramodern.18",
109
+ "@modern-js/runtime": "3.9.0-ultramodern.18",
110
110
  "react": "^19.2.8",
111
111
  "react-dom": "^19.2.8",
112
112
  "react-server-dom-rspack": "0.1.0"
@@ -117,9 +117,9 @@
117
117
  }
118
118
  },
119
119
  "devDependencies": {
120
- "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.17",
121
- "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.17",
122
- "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.17",
120
+ "@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.9.0-ultramodern.18",
121
+ "@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.9.0-ultramodern.18",
122
+ "@modern-js/server-utils": "npm:@bleedingdev/modern-js-server-utils@3.9.0-ultramodern.18",
123
123
  "@rslib/core": "1.0.2",
124
124
  "@scripts/rstest-config": "2.66.0",
125
125
  "@tanstack/history": "1.162.4",
@@ -153,6 +153,6 @@
153
153
  "scripts": {
154
154
  "dev": "rslib build --watch",
155
155
  "build": "rslib build",
156
- "test": "rstest --passWithNoTests"
156
+ "test": "rstest"
157
157
  }
158
158
  }
package/rstest.config.mts CHANGED
@@ -29,6 +29,7 @@ export default {
29
29
  'tests/router/loaderBridge.test.ts',
30
30
  'tests/router/localisedIdentity.test.tsx',
31
31
  'tests/router/routeHooks.test.ts',
32
+ 'tests/router/routeMetadata.test.ts',
32
33
  'tests/router/serverPlugin.test.ts',
33
34
  'tests/router/rsc.test.tsx',
34
35
  'tests/router/slotUsageSanitizer.test.ts',
@@ -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'),
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';
@@ -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
+ }
@@ -165,6 +165,44 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
165
165
  }
166
166
  });
167
167
 
168
+ it('regenerates the route metadata manifest before loading the config', async () => {
169
+ const appDirectory = makeAppDir(true);
170
+ const routesDirectory = path.join(appDirectory, 'src', 'routes');
171
+ const manifestPath = path.join(
172
+ routesDirectory,
173
+ 'ultramodern-route-metadata.ts',
174
+ );
175
+ fs.mkdirSync(path.join(routesDirectory, '[lang]'), { recursive: true });
176
+ fs.writeFileSync(
177
+ path.join(routesDirectory, '[lang]', 'route.meta.ts'),
178
+ "export const routeMeta = { namespace: 'app' } as const;\n",
179
+ );
180
+ // A manifest that still imports a deleted route.meta.ts makes the config
181
+ // (which imports the manifest) fail to load.
182
+ fs.writeFileSync(
183
+ manifestPath,
184
+ "import { routeMeta as route0 } from './[lang]/deleted/route.meta';\n",
185
+ );
186
+ const configLoadFailure = new Error(
187
+ "Cannot find module './[lang]/deleted/route.meta'",
188
+ );
189
+ cliInitMock.mockImplementationOnce(async () => {
190
+ throw configLoadFailure;
191
+ });
192
+ try {
193
+ await expect(
194
+ generateTanstackRouteArtifacts({ appDirectory }),
195
+ ).rejects.toBe(configLoadFailure);
196
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
197
+ expect(manifest).toContain(
198
+ "import { routeMeta as route0 } from './[lang]/route.meta';",
199
+ );
200
+ expect(manifest).not.toContain('deleted');
201
+ } finally {
202
+ fs.rmSync(appDirectory, { force: true, recursive: true });
203
+ }
204
+ });
205
+
168
206
  it('throws a clear error when no config file exists', async () => {
169
207
  const appDirectory = makeAppDir(false);
170
208
 
@@ -1,5 +1,4 @@
1
1
  import { render } from '@testing-library/react';
2
- import React from 'react';
3
2
  import { Link } from '../../src/runtime/prefetchLink';
4
3
 
5
4
  type CapturedOptions = {
@@ -0,0 +1,171 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { writeRouteMetadataManifest } from '../../src/cli/artifacts';
5
+ import { renderRouteMetadataManifest } from '../../src/cli/routeMetadata';
6
+
7
+ const routeMeta = (id: string) =>
8
+ `export const routeMeta = { id: '${id}', namespace: 'shop' } as const;\n`;
9
+
10
+ const EXPECTED_MANIFEST = `// @generated by @modern-js/plugin-tanstack from route.meta.ts files. Run routes-generate; do not edit.
11
+ import { routeMeta as route0 } from './[lang]/about/route.meta';
12
+ import { routeMeta as route1 } from './[lang]/route.meta';
13
+ import { routeMeta as route2 } from './[lang]/stores/[id]/route.meta';
14
+
15
+ export const ultramodernRouteNamespace = route0.namespace;
16
+ export const ultramodernRouteMetadata = [route0, route1, route2] as const;
17
+ export const ultramodernLocalisedUrls = Object.fromEntries(
18
+ ultramodernRouteMetadata
19
+ .filter((route) => route.canonicalPath !== '/')
20
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
21
+ );
22
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
23
+ .filter((route) => route.public && route.indexable)
24
+ .map((route) => ({
25
+ canonicalPath: route.canonicalPath,
26
+ descriptionKey: route.descriptionKey,
27
+ id: route.id,
28
+ localisedPaths: route.localisedPaths,
29
+ namespace: route.namespace,
30
+ ownerAppId: route.ownerAppId,
31
+ titleKey: route.titleKey,
32
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
33
+ }));
34
+ export const ultramodernRouteConfig = {
35
+ authoring: 'colocated-route-meta',
36
+ generatedManifest: true,
37
+ localisedUrls: ultramodernLocalisedUrls,
38
+ namespace: ultramodernRouteNamespace,
39
+ publicRoutes: ultramodernPublicRoutes,
40
+ routes: ultramodernRouteMetadata,
41
+ source: 'route-owned',
42
+ } as const;
43
+ `;
44
+
45
+ describe('route metadata manifest', () => {
46
+ let appDirectory: string;
47
+ let routesDirectory: string;
48
+ const write = (relativePath: string, content: string) => {
49
+ const file = path.join(routesDirectory, relativePath);
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ fs.writeFileSync(file, content);
52
+ };
53
+
54
+ beforeEach(() => {
55
+ appDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'route-metadata-'));
56
+ routesDirectory = path.join(appDirectory, 'src', 'routes');
57
+ });
58
+
59
+ afterEach(() => {
60
+ fs.rmSync(appDirectory, { recursive: true, force: true });
61
+ });
62
+
63
+ it('writes a byte-stable, formatted manifest from route.meta.ts files', async () => {
64
+ write('[lang]/stores/[id]/route.meta.ts', routeMeta('store'));
65
+ write('[lang]/route.meta.ts', routeMeta('home'));
66
+ write('[lang]/about/route.meta.ts', routeMeta('about'));
67
+ write('[lang]/about/page.tsx', 'export default () => null;\n');
68
+ // Linked metadata belongs to another tree; following it would make the
69
+ // manifest depend on where the link points.
70
+ fs.mkdirSync(path.join(routesDirectory, '[lang]/linked-file'));
71
+ fs.symlinkSync(
72
+ path.join(routesDirectory, '[lang]/route.meta.ts'),
73
+ path.join(routesDirectory, '[lang]/linked-file/route.meta.ts'),
74
+ );
75
+ fs.symlinkSync(
76
+ path.join(routesDirectory, '[lang]/about'),
77
+ path.join(routesDirectory, '[lang]/linked'),
78
+ );
79
+ const manifestPath = path.join(
80
+ routesDirectory,
81
+ 'ultramodern-route-metadata.ts',
82
+ );
83
+
84
+ await writeRouteMetadataManifest({ appDirectory });
85
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(EXPECTED_MANIFEST);
86
+
87
+ const writtenAt = fs.statSync(manifestPath).mtimeMs;
88
+ await new Promise(resolve => setTimeout(resolve, 20));
89
+ await writeRouteMetadataManifest({ appDirectory });
90
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(EXPECTED_MANIFEST);
91
+ expect(fs.statSync(manifestPath).mtimeMs).toBe(writtenAt);
92
+ });
93
+
94
+ it('drops deleted route metadata from an existing manifest', async () => {
95
+ write('[lang]/route.meta.ts', routeMeta('home'));
96
+ write('[lang]/gone/route.meta.ts', routeMeta('gone'));
97
+ await writeRouteMetadataManifest({ appDirectory });
98
+ fs.rmSync(path.join(routesDirectory, '[lang]/gone'), { recursive: true });
99
+
100
+ await writeRouteMetadataManifest({ appDirectory });
101
+
102
+ const manifest = fs.readFileSync(
103
+ path.join(routesDirectory, 'ultramodern-route-metadata.ts'),
104
+ 'utf8',
105
+ );
106
+ expect(manifest).not.toContain('gone');
107
+ expect(manifest).toContain(
108
+ "import { routeMeta as route0 } from './[lang]/route.meta';\n\n",
109
+ );
110
+ });
111
+
112
+ it('names the fix once the last route metadata is deleted', async () => {
113
+ write('[lang]/route.meta.ts', routeMeta('home'));
114
+ await writeRouteMetadataManifest({ appDirectory });
115
+ const manifestPath = path.join(
116
+ routesDirectory,
117
+ 'ultramodern-route-metadata.ts',
118
+ );
119
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
120
+ fs.rmSync(path.join(routesDirectory, '[lang]/route.meta.ts'));
121
+
122
+ await expect(writeRouteMetadataManifest({ appDirectory })).rejects.toThrow(
123
+ /none remain below .*Restore at least one route\.meta\.ts/u,
124
+ );
125
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(manifest);
126
+ });
127
+
128
+ it('keeps an authored file at the manifest path', async () => {
129
+ write('ultramodern-route-metadata.ts', 'export const authored = true;\n');
130
+
131
+ await writeRouteMetadataManifest({ appDirectory });
132
+
133
+ expect(
134
+ fs.readFileSync(
135
+ path.join(routesDirectory, 'ultramodern-route-metadata.ts'),
136
+ 'utf8',
137
+ ),
138
+ ).toBe('export const authored = true;\n');
139
+ });
140
+
141
+ it('writes nothing for an app without route metadata', async () => {
142
+ await writeRouteMetadataManifest({ appDirectory });
143
+ write('[lang]/page.tsx', 'export default () => null;\n');
144
+ await writeRouteMetadataManifest({ appDirectory });
145
+
146
+ expect(fs.readdirSync(routesDirectory)).toEqual(['[lang]']);
147
+ });
148
+
149
+ it('renders independently of input order and separators', () => {
150
+ expect(
151
+ renderRouteMetadataManifest([
152
+ '[lang]\\stores\\[id]\\route.meta.ts',
153
+ '[lang]/route.meta.ts',
154
+ '[lang]/about/route.meta.ts',
155
+ ]),
156
+ ).toBe(EXPECTED_MANIFEST);
157
+ });
158
+
159
+ it('breaks the route list one binding per line past the print width', () => {
160
+ const manifest = renderRouteMetadataManifest(
161
+ Array.from({ length: 12 }, (_, index) => `r${index}/route.meta.ts`),
162
+ );
163
+
164
+ expect(manifest).toContain(
165
+ `export const ultramodernRouteMetadata = [\n${Array.from(
166
+ { length: 12 },
167
+ (_, index) => ` route${index},\n`,
168
+ ).join('')}] as const;\n`,
169
+ );
170
+ });
171
+ });