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

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 +233 -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
@@ -1,6 +1,5 @@
1
1
  import { render } from '@testing-library/react';
2
- import React from 'react';
3
- import { Link, NavLink } from '../../src/runtime/prefetchLink';
2
+ import { Link } from '../../src/runtime/prefetchLink';
4
3
 
5
4
  type CapturedOptions = {
6
5
  preload?: unknown;
@@ -27,30 +26,20 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
27
26
  mockReturnProps = { href: '/settings' };
28
27
  });
29
28
 
30
- it('defaults TanStack preload to viewport', () => {
31
- render(<Link to="/settings">Settings</Link>);
32
-
33
- expect(capturedOptions.map(o => o.preload)).toEqual(['viewport']);
34
- });
35
-
36
- it('preserves explicit preload', () => {
29
+ it('preserves an explicit false preload override', () => {
37
30
  render(
38
- <Link to="/settings" prefetch="render" preload="intent">
31
+ <Link to="/settings" prefetch="render" preload={false}>
39
32
  Settings
40
33
  </Link>,
41
34
  );
42
35
 
43
- expect(capturedOptions.map(o => o.preload)).toEqual(['intent']);
36
+ expect(capturedOptions[0]?.preload).toBe(false);
44
37
  });
45
38
 
46
- it('preserves explicit disabled preload', () => {
47
- render(
48
- <Link to="/settings" prefetch="render" preload={false}>
49
- Settings
50
- </Link>,
51
- );
39
+ it('defaults to viewport preload when no prefetch is given', () => {
40
+ render(<Link to="/settings">Settings</Link>);
52
41
 
53
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
42
+ expect(capturedOptions[0]?.preload).toBe('viewport');
54
43
  });
55
44
 
56
45
  it('maps none prefetch to disabled TanStack preload', () => {
@@ -60,26 +49,16 @@ describe('tanstack prefetch link adapter - preload mapping', () => {
60
49
  </Link>,
61
50
  );
62
51
 
63
- expect(capturedOptions.map(o => o.preload)).toEqual([false]);
52
+ expect(capturedOptions[0]?.preload).toBe(false);
64
53
  });
65
54
 
66
- it.each([
67
- 'intent',
68
- 'render',
69
- 'viewport',
70
- ] as const)('maps %s prefetch to TanStack preload', prefetch => {
55
+ it('lets explicit preload re-enable prefetch=none', () => {
71
56
  render(
72
- <Link to="/settings" prefetch={prefetch}>
57
+ <Link to="/settings" prefetch="none" preload="render">
73
58
  Settings
74
59
  </Link>,
75
60
  );
76
61
 
77
- expect(capturedOptions.map(o => o.preload)).toEqual([prefetch]);
78
- });
79
-
80
- it('defaults NavLink preload to viewport', () => {
81
- render(<NavLink to="/settings">Settings</NavLink>);
82
-
83
- expect(capturedOptions.map(o => o.preload)).toEqual(['viewport']);
62
+ expect(capturedOptions.map(o => o.preload)).toEqual(['render']);
84
63
  });
85
64
  });
@@ -1,113 +1,50 @@
1
- const calls: Record<string, unknown[]> = {
2
- useChildMatches: [],
3
- useMatch: [],
4
- useMatches: [],
5
- useParentMatches: [],
6
- useRouterState: [],
7
- };
8
-
9
- rstest.mock('@tanstack/react-router', () => ({
10
- useChildMatches: (options: unknown) => {
11
- calls.useChildMatches.push(options);
12
- return options;
13
- },
14
- useMatch: (options: unknown) => {
15
- calls.useMatch.push(options);
16
- return options;
17
- },
18
- useMatches: (options: unknown) => {
19
- calls.useMatches.push(options);
20
- return options;
21
- },
22
- useParentMatches: (options: unknown) => {
23
- calls.useParentMatches.push(options);
24
- return options;
25
- },
26
- useRouterState: (options: unknown) => {
27
- calls.useRouterState.push(options);
28
- return options;
29
- },
30
- }));
31
-
1
+ import { createMemoryHistory } from '@tanstack/history';
32
2
  import {
33
- useChildMatches,
34
- useMatch,
35
- useMatches,
36
- useParentMatches,
37
- useRouterState,
38
- } from '../../src/runtime/routeHooks';
39
-
40
- const getLastCall = (hookName: keyof typeof calls) => {
41
- const callList = calls[hookName];
42
- return callList[callList.length - 1];
43
- };
44
-
45
- describe('tanstack route hook structural sharing guards', () => {
46
- beforeEach(() => {
47
- for (const hookName of Object.keys(calls)) {
48
- calls[hookName] = [];
49
- }
3
+ createRootRoute,
4
+ createRoute,
5
+ createRouter,
6
+ RouterProvider,
7
+ } from '@tanstack/react-router';
8
+ import React from 'react';
9
+ import { renderToString } from 'react-dom/server';
10
+ import { useMatches } from '../../src/runtime/routeHooks';
11
+ import { navigateOnClient } from './clientNavigation';
12
+
13
+ function MatchIds() {
14
+ const matches = useMatches();
15
+ return React.createElement(
16
+ 'output',
17
+ { 'data-testid': 'route-ids' },
18
+ matches.map(match => match.routeId).join(','),
19
+ );
20
+ }
21
+
22
+ test('updates whole match objects after navigation with the structural-sharing guard', async () => {
23
+ const rootRoute = createRootRoute({ component: MatchIds });
24
+ const homeRoute = createRoute({
25
+ getParentRoute: () => rootRoute,
26
+ path: '/',
27
+ component: () => null,
50
28
  });
51
-
52
- test('disables structural sharing for full internal router objects', () => {
53
- useMatch({ from: '/' } as never);
54
- useMatches();
55
- useParentMatches();
56
- useChildMatches();
57
- useRouterState();
58
-
59
- expect(getLastCall('useMatch')).toEqual({
60
- from: '/',
61
- structuralSharing: false,
62
- });
63
- expect(getLastCall('useMatches')).toEqual({
64
- structuralSharing: false,
65
- });
66
- expect(getLastCall('useParentMatches')).toEqual({
67
- structuralSharing: false,
68
- });
69
- expect(getLastCall('useChildMatches')).toEqual({
70
- structuralSharing: false,
71
- });
72
- expect(getLastCall('useRouterState')).toEqual({
73
- structuralSharing: false,
74
- });
29
+ const settingsRoute = createRoute({
30
+ getParentRoute: () => rootRoute,
31
+ path: '/settings',
32
+ component: () => null,
75
33
  });
76
-
77
- test('keeps selected data on the router default', () => {
78
- const select = (value: unknown) => value;
79
- const matchOptions = { from: '/', select };
80
- const matchesOptions = { select };
81
- const stateOptions = { select };
82
-
83
- useMatch(matchOptions as never);
84
- useMatches(matchesOptions as never);
85
- useParentMatches(matchesOptions as never);
86
- useChildMatches(matchesOptions as never);
87
- useRouterState(stateOptions as never);
88
-
89
- expect(getLastCall('useMatch')).toBe(matchOptions);
90
- expect(getLastCall('useMatches')).toBe(matchesOptions);
91
- expect(getLastCall('useParentMatches')).toBe(matchesOptions);
92
- expect(getLastCall('useChildMatches')).toBe(matchesOptions);
93
- expect(getLastCall('useRouterState')).toBe(stateOptions);
34
+ const router = createRouter({
35
+ routeTree: rootRoute.addChildren([homeRoute, settingsRoute]),
36
+ history: createMemoryHistory({ initialEntries: ['/'] }),
37
+ defaultStructuralSharing: true,
94
38
  });
95
39
 
96
- test('honors explicit structural sharing choices', () => {
97
- const enabled = { structuralSharing: true };
98
- const disabled = { structuralSharing: false };
99
- const matchEnabled = { from: '/', structuralSharing: true };
40
+ await router.load();
41
+ expect(
42
+ renderToString(React.createElement(RouterProvider, { router })),
43
+ ).toContain('__root__');
100
44
 
101
- useMatch(matchEnabled as never);
102
- useMatches(enabled as never);
103
- useParentMatches(disabled as never);
104
- useChildMatches(enabled as never);
105
- useRouterState(disabled as never);
45
+ await navigateOnClient(router, { to: '/settings' });
106
46
 
107
- expect(getLastCall('useMatch')).toBe(matchEnabled);
108
- expect(getLastCall('useMatches')).toBe(enabled);
109
- expect(getLastCall('useParentMatches')).toBe(disabled);
110
- expect(getLastCall('useChildMatches')).toBe(enabled);
111
- expect(getLastCall('useRouterState')).toBe(disabled);
112
- });
47
+ expect(
48
+ renderToString(React.createElement(RouterProvider, { router })),
49
+ ).toContain('/settings');
113
50
  });
@@ -0,0 +1,233 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { writeRouteMetadataManifest } from '../../src/cli/artifacts';
7
+ import { renderRouteMetadataManifest } from '../../src/cli/routeMetadata';
8
+
9
+ const requireCjs = createRequire(import.meta.url);
10
+
11
+ const typedRouteMeta = (canonicalPath: string) => `export const routeMeta = {
12
+ canonicalPath: '${canonicalPath}',
13
+ descriptionKey: 'contacts.description',
14
+ id: 'contacts',
15
+ indexable: false,
16
+ localisedPaths: { cs: '/kontakty', en: '${canonicalPath}' },
17
+ namespace: 'contacts',
18
+ ownerAppId: 'contacts',
19
+ public: false,
20
+ titleKey: 'contacts.title',
21
+ } as const;
22
+ `;
23
+
24
+ const typecheck = (directory: string) => {
25
+ fs.writeFileSync(
26
+ path.join(directory, 'tsconfig.json'),
27
+ JSON.stringify({
28
+ compilerOptions: {
29
+ lib: ['ESNext'],
30
+ module: 'ESNext',
31
+ moduleResolution: 'Bundler',
32
+ noEmit: true,
33
+ strict: true,
34
+ target: 'ES2024',
35
+ types: [],
36
+ },
37
+ include: ['src/routes/ultramodern-route-metadata.ts'],
38
+ }),
39
+ );
40
+ const compilerManifestPath = requireCjs.resolve(
41
+ '@typescript/native-preview/package.json',
42
+ );
43
+ const { bin } = JSON.parse(fs.readFileSync(compilerManifestPath, 'utf8')) as {
44
+ bin: { tsgo: string };
45
+ };
46
+ return spawnSync(
47
+ process.execPath,
48
+ [
49
+ path.resolve(path.dirname(compilerManifestPath), bin.tsgo),
50
+ '--project',
51
+ path.join(directory, 'tsconfig.json'),
52
+ ],
53
+ { encoding: 'utf8' },
54
+ );
55
+ };
56
+
57
+ const routeMeta = (id: string) =>
58
+ `export const routeMeta = { id: '${id}', namespace: 'shop' } as const;\n`;
59
+
60
+ const EXPECTED_MANIFEST = `// @generated by @modern-js/plugin-tanstack from route.meta.ts files. Run routes-generate; do not edit.
61
+ import { routeMeta as route0 } from './[lang]/about/route.meta';
62
+ import { routeMeta as route1 } from './[lang]/route.meta';
63
+ import { routeMeta as route2 } from './[lang]/stores/[id]/route.meta';
64
+
65
+ export const ultramodernRouteNamespace = route0.namespace;
66
+ export const ultramodernRouteMetadata = [route0, route1, route2] as const;
67
+ export const ultramodernLocalisedUrls = Object.fromEntries(
68
+ ultramodernRouteMetadata
69
+ .filter((route: { canonicalPath: string }) => route.canonicalPath !== '/')
70
+ .map((route) => [route.canonicalPath, route.localisedPaths]),
71
+ );
72
+ export const ultramodernPublicRoutes = ultramodernRouteMetadata
73
+ .filter((route) => route.public && route.indexable)
74
+ .map((route) => ({
75
+ canonicalPath: route.canonicalPath,
76
+ descriptionKey: route.descriptionKey,
77
+ id: route.id,
78
+ localisedPaths: route.localisedPaths,
79
+ namespace: route.namespace,
80
+ ownerAppId: route.ownerAppId,
81
+ titleKey: route.titleKey,
82
+ ...('jsonLd' in route ? { jsonLd: route.jsonLd } : {}),
83
+ }));
84
+ export const ultramodernRouteConfig = {
85
+ authoring: 'colocated-route-meta',
86
+ generatedManifest: true,
87
+ localisedUrls: ultramodernLocalisedUrls,
88
+ namespace: ultramodernRouteNamespace,
89
+ publicRoutes: ultramodernPublicRoutes,
90
+ routes: ultramodernRouteMetadata,
91
+ source: 'route-owned',
92
+ } as const;
93
+ `;
94
+
95
+ describe('route metadata manifest', () => {
96
+ let appDirectory: string;
97
+ let routesDirectory: string;
98
+ const write = (relativePath: string, content: string) => {
99
+ const file = path.join(routesDirectory, relativePath);
100
+ fs.mkdirSync(path.dirname(file), { recursive: true });
101
+ fs.writeFileSync(file, content);
102
+ };
103
+
104
+ beforeEach(() => {
105
+ appDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'route-metadata-'));
106
+ routesDirectory = path.join(appDirectory, 'src', 'routes');
107
+ });
108
+
109
+ afterEach(() => {
110
+ fs.rmSync(appDirectory, { recursive: true, force: true });
111
+ });
112
+
113
+ it('writes a byte-stable, formatted manifest from route.meta.ts files', async () => {
114
+ write('[lang]/stores/[id]/route.meta.ts', routeMeta('store'));
115
+ write('[lang]/route.meta.ts', routeMeta('home'));
116
+ write('[lang]/about/route.meta.ts', routeMeta('about'));
117
+ write('[lang]/about/page.tsx', 'export default () => null;\n');
118
+ // Linked metadata belongs to another tree; following it would make the
119
+ // manifest depend on where the link points.
120
+ fs.mkdirSync(path.join(routesDirectory, '[lang]/linked-file'));
121
+ fs.symlinkSync(
122
+ path.join(routesDirectory, '[lang]/route.meta.ts'),
123
+ path.join(routesDirectory, '[lang]/linked-file/route.meta.ts'),
124
+ );
125
+ fs.symlinkSync(
126
+ path.join(routesDirectory, '[lang]/about'),
127
+ path.join(routesDirectory, '[lang]/linked'),
128
+ );
129
+ const manifestPath = path.join(
130
+ routesDirectory,
131
+ 'ultramodern-route-metadata.ts',
132
+ );
133
+
134
+ await writeRouteMetadataManifest({ appDirectory });
135
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(EXPECTED_MANIFEST);
136
+
137
+ const writtenAt = fs.statSync(manifestPath).mtimeMs;
138
+ await new Promise(resolve => setTimeout(resolve, 20));
139
+ await writeRouteMetadataManifest({ appDirectory });
140
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(EXPECTED_MANIFEST);
141
+ expect(fs.statSync(manifestPath).mtimeMs).toBe(writtenAt);
142
+ });
143
+
144
+ // Every route literal is narrowed by `as const`. An app whose only route is
145
+ // not the root must still type-check the root-path filter.
146
+ it('type-checks a manifest whose only route is not the root', async () => {
147
+ write('[lang]/contacts/route.meta.ts', typedRouteMeta('/contacts'));
148
+ await writeRouteMetadataManifest({ appDirectory });
149
+
150
+ const result = typecheck(appDirectory);
151
+
152
+ expect(result.error).toBeUndefined();
153
+ expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
154
+ });
155
+
156
+ it('drops deleted route metadata from an existing manifest', async () => {
157
+ write('[lang]/route.meta.ts', routeMeta('home'));
158
+ write('[lang]/gone/route.meta.ts', routeMeta('gone'));
159
+ await writeRouteMetadataManifest({ appDirectory });
160
+ fs.rmSync(path.join(routesDirectory, '[lang]/gone'), { recursive: true });
161
+
162
+ await writeRouteMetadataManifest({ appDirectory });
163
+
164
+ const manifest = fs.readFileSync(
165
+ path.join(routesDirectory, 'ultramodern-route-metadata.ts'),
166
+ 'utf8',
167
+ );
168
+ expect(manifest).not.toContain('gone');
169
+ expect(manifest).toContain(
170
+ "import { routeMeta as route0 } from './[lang]/route.meta';\n\n",
171
+ );
172
+ });
173
+
174
+ it('names the fix once the last route metadata is deleted', async () => {
175
+ write('[lang]/route.meta.ts', routeMeta('home'));
176
+ await writeRouteMetadataManifest({ appDirectory });
177
+ const manifestPath = path.join(
178
+ routesDirectory,
179
+ 'ultramodern-route-metadata.ts',
180
+ );
181
+ const manifest = fs.readFileSync(manifestPath, 'utf8');
182
+ fs.rmSync(path.join(routesDirectory, '[lang]/route.meta.ts'));
183
+
184
+ await expect(writeRouteMetadataManifest({ appDirectory })).rejects.toThrow(
185
+ /none remain below .*Restore at least one route\.meta\.ts/u,
186
+ );
187
+ expect(fs.readFileSync(manifestPath, 'utf8')).toBe(manifest);
188
+ });
189
+
190
+ it('keeps an authored file at the manifest path', async () => {
191
+ write('ultramodern-route-metadata.ts', 'export const authored = true;\n');
192
+
193
+ await writeRouteMetadataManifest({ appDirectory });
194
+
195
+ expect(
196
+ fs.readFileSync(
197
+ path.join(routesDirectory, 'ultramodern-route-metadata.ts'),
198
+ 'utf8',
199
+ ),
200
+ ).toBe('export const authored = true;\n');
201
+ });
202
+
203
+ it('writes nothing for an app without route metadata', async () => {
204
+ await writeRouteMetadataManifest({ appDirectory });
205
+ write('[lang]/page.tsx', 'export default () => null;\n');
206
+ await writeRouteMetadataManifest({ appDirectory });
207
+
208
+ expect(fs.readdirSync(routesDirectory)).toEqual(['[lang]']);
209
+ });
210
+
211
+ it('renders independently of input order and separators', () => {
212
+ expect(
213
+ renderRouteMetadataManifest([
214
+ '[lang]\\stores\\[id]\\route.meta.ts',
215
+ '[lang]/route.meta.ts',
216
+ '[lang]/about/route.meta.ts',
217
+ ]),
218
+ ).toBe(EXPECTED_MANIFEST);
219
+ });
220
+
221
+ it('breaks the route list one binding per line past the print width', () => {
222
+ const manifest = renderRouteMetadataManifest(
223
+ Array.from({ length: 12 }, (_, index) => `r${index}/route.meta.ts`),
224
+ );
225
+
226
+ expect(manifest).toContain(
227
+ `export const ultramodernRouteMetadata = [\n${Array.from(
228
+ { length: 12 },
229
+ (_, index) => ` route${index},\n`,
230
+ ).join('')}] as const;\n`,
231
+ );
232
+ });
233
+ });