@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.
- package/dist/cjs/cli/artifacts.js +15 -1
- package/dist/cjs/cli/index.js +12 -0
- package/dist/cjs/cli/routeMetadata.js +131 -0
- package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +7 -7
- package/dist/cjs/cli/tanstackTypes/shared.js +0 -14
- package/dist/cjs/cli/tanstackTypes/source.js +27 -60
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +6 -9
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +7 -2
- package/dist/cjs/runtime/plugin.node.js +86 -64
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +0 -2
- package/dist/cjs/runtime/routeTree/construction.js +19 -59
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +3 -0
- package/dist/cjs/shared/routeDescriptor.js +60 -0
- package/dist/esm/cli/artifacts.mjs +13 -2
- package/dist/esm/cli/index.mjs +12 -9
- package/dist/esm/cli/routeMetadata.mjs +71 -0
- package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/loaderBridge.mjs +8 -5
- package/dist/esm/runtime/navigation.mjs +64 -0
- package/dist/esm/runtime/plugin.mjs +7 -2
- package/dist/esm/runtime/plugin.node.mjs +88 -66
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm/runtime/routeTree/construction.mjs +19 -59
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +3 -0
- package/dist/esm/shared/routeDescriptor.mjs +22 -0
- package/dist/esm-node/cli/artifacts.mjs +13 -2
- package/dist/esm-node/cli/index.mjs +12 -9
- package/dist/esm-node/cli/routeMetadata.mjs +72 -0
- package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +7 -7
- package/dist/esm-node/cli/tanstackTypes/shared.mjs +1 -9
- package/dist/esm-node/cli/tanstackTypes/source.mjs +28 -61
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/loaderBridge.mjs +8 -5
- package/dist/esm-node/runtime/navigation.mjs +65 -0
- package/dist/esm-node/runtime/plugin.mjs +7 -2
- package/dist/esm-node/runtime/plugin.node.mjs +88 -66
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/routeTree/construction.mjs +19 -59
- package/dist/esm-node/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm-node/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +3 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +23 -0
- package/dist/types/cli/artifacts.d.ts +13 -5
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/routeMetadata.d.ts +18 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/register.d.ts +1 -9
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +22 -19
- package/rstest.config.mts +2 -24
- package/src/cli/artifacts.ts +53 -6
- package/src/cli/index.ts +11 -2
- package/src/cli/routeMetadata.ts +121 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +42 -116
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -4
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +120 -93
- package/src/runtime/plugin.tsx +11 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -12
- package/src/runtime/routeTree/construction.ts +32 -110
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/router.ts +25 -40
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +4 -1
- package/src/shared/routeDescriptor.ts +46 -0
- package/tests/router/cli.test.ts +148 -824
- package/tests/router/clientNavigation.ts +13 -0
- package/tests/router/dataMutation.test.tsx +17 -85
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +139 -27
- package/tests/router/loaderBridge.test.ts +0 -25
- package/tests/router/localisedIdentity.test.tsx +100 -0
- package/tests/router/prefetchLink.test.tsx +46 -21
- package/tests/router/prefetchLinkPreload.test.tsx +11 -32
- package/tests/router/routeHooks.test.ts +42 -105
- package/tests/router/routeMetadata.test.ts +233 -0
- package/tests/router/routeTree.test.ts +144 -836
- package/tests/router/rsc.test.tsx +131 -166
- package/tests/router/serverPlugin.test.ts +193 -1
- package/tests/router/tanstackTypes.test.ts +218 -963
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
- package/tests/realm-isolation.test.ts +0 -114
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -40
- package/tests/router/hydrationBoundary.test.tsx +0 -23
- package/tests/router/packageSurface.test.ts +0 -24
- package/tests/router/preloadRedirect.test.ts +0 -64
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -239
- package/tests/router/rscDecoderSecurity.test.ts +0 -113
- package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
|
-
import
|
|
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('
|
|
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=
|
|
31
|
+
<Link to="/settings" prefetch="render" preload={false}>
|
|
39
32
|
Settings
|
|
40
33
|
</Link>,
|
|
41
34
|
);
|
|
42
35
|
|
|
43
|
-
expect(capturedOptions
|
|
36
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
44
37
|
});
|
|
45
38
|
|
|
46
|
-
it('
|
|
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
|
|
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
|
|
52
|
+
expect(capturedOptions[0]?.preload).toBe(false);
|
|
64
53
|
});
|
|
65
54
|
|
|
66
|
-
it
|
|
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=
|
|
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([
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
40
|
+
await router.load();
|
|
41
|
+
expect(
|
|
42
|
+
renderToString(React.createElement(RouterProvider, { router })),
|
|
43
|
+
).toContain('__root__');
|
|
100
44
|
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
});
|