@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.9 → 3.9.3-ultramodern.10
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 +13 -2
- package/dist/cjs/cli/routeMetadata.js +131 -0
- package/dist/cjs/cli/rscRouteLayer.js +86 -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 +28 -63
- package/dist/cjs/runtime/lifecycle.js +0 -3
- package/dist/cjs/runtime/loaderBridge.js +10 -4
- package/dist/cjs/runtime/navigation.js +102 -0
- package/dist/cjs/runtime/plugin.js +10 -1
- package/dist/cjs/runtime/plugin.node.js +89 -64
- package/dist/cjs/runtime/routeTree/construction.js +19 -61
- package/dist/cjs/runtime/routeTree/loaders.js +1 -1
- package/dist/cjs/runtime/routeTree/paths.js +0 -6
- package/dist/cjs/runtime/ssrManagedTags.js +2 -12
- package/dist/cjs/runtime/utils.js +1 -0
- package/dist/cjs/shared/routeDescriptor.js +88 -0
- package/dist/esm/cli/artifacts.mjs +13 -2
- package/dist/esm/cli/index.mjs +13 -11
- package/dist/esm/cli/routeMetadata.mjs +71 -0
- package/dist/esm/cli/rscRouteLayer.mjs +35 -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 +29 -64
- 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 +10 -1
- package/dist/esm/runtime/plugin.node.mjs +91 -66
- package/dist/esm/runtime/routeTree/construction.mjs +19 -61
- package/dist/esm/runtime/routeTree/loaders.mjs +2 -2
- package/dist/esm/runtime/routeTree/paths.mjs +1 -4
- package/dist/esm/runtime/ssrManagedTags.mjs +1 -8
- package/dist/esm/runtime/utils.mjs +1 -0
- package/dist/esm/shared/routeDescriptor.mjs +50 -0
- package/dist/esm-node/cli/artifacts.mjs +13 -2
- package/dist/esm-node/cli/index.mjs +13 -11
- package/dist/esm-node/cli/routeMetadata.mjs +72 -0
- package/dist/esm-node/cli/rscRouteLayer.mjs +36 -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 +29 -64
- 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 +10 -1
- package/dist/esm-node/runtime/plugin.node.mjs +91 -66
- package/dist/esm-node/runtime/routeTree/construction.mjs +19 -61
- 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/ssrManagedTags.mjs +1 -8
- package/dist/esm-node/runtime/utils.mjs +1 -0
- package/dist/esm-node/shared/routeDescriptor.mjs +51 -0
- package/dist/types/cli/artifacts.d.ts +16 -7
- package/dist/types/cli/index.d.ts +1 -1
- package/dist/types/cli/routeMetadata.d.ts +18 -0
- package/dist/types/cli/rscRouteLayer.d.ts +11 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +0 -2
- package/dist/types/cli/tanstackTypes/source.d.ts +3 -2
- package/dist/types/runtime/lifecycle.d.ts +1 -1
- package/dist/types/runtime/loaderBridge.d.ts +1 -0
- package/dist/types/runtime/navigation.d.ts +4 -0
- package/dist/types/runtime/pluginShared.d.ts +5 -0
- package/dist/types/runtime/routeTree/paths.d.ts +0 -2
- package/dist/types/runtime/ssrManagedTags.d.ts +0 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -7
- package/dist/types/shared/routeDescriptor.d.ts +26 -0
- package/package.json +34 -28
- package/src/cli/artifacts.ts +59 -8
- package/src/cli/index.ts +17 -4
- package/src/cli/routeMetadata.ts +121 -0
- package/src/cli/rscRouteLayer.ts +93 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +17 -12
- package/src/cli/tanstackTypes/shared.ts +0 -14
- package/src/cli/tanstackTypes/source.ts +48 -120
- package/src/runtime/lifecycle.ts +0 -1
- package/src/runtime/loaderBridge.ts +9 -9
- package/src/runtime/navigation.ts +78 -0
- package/src/runtime/plugin.node.tsx +121 -91
- package/src/runtime/plugin.tsx +10 -0
- package/src/runtime/pluginShared.ts +12 -0
- package/src/runtime/routeTree/construction.ts +32 -112
- package/src/runtime/routeTree/loaders.ts +2 -4
- package/src/runtime/routeTree/paths.ts +0 -6
- package/src/runtime/ssrManagedTags.ts +0 -17
- package/src/runtime/ssrTypes.ts +0 -7
- package/src/runtime/utils.tsx +1 -0
- package/src/shared/routeDescriptor.ts +102 -0
- package/dist/esm/rslib-runtime~1.mjs +0 -18
- package/dist/esm-node/rslib-runtime~0.mjs +0 -19
- package/rslib.config.mts +0 -4
- package/rstest.config.mts +0 -53
- package/tests/router/cli.test.ts +0 -420
- package/tests/router/clientHydration.test.tsx +0 -200
- package/tests/router/dataMutation.test.tsx +0 -428
- package/tests/router/fixtures/router-gen.oxlint.json +0 -5
- package/tests/router/flightSerialization.roundtrip.test.tsx +0 -140
- package/tests/router/generateRouteArtifacts.test.ts +0 -146
- package/tests/router/loaderBridge.test.ts +0 -207
- package/tests/router/localisedIdentity.test.tsx +0 -95
- package/tests/router/prefetchLink.test.tsx +0 -122
- package/tests/router/prefetchLinkPreload.test.tsx +0 -65
- package/tests/router/routeHooks.test.ts +0 -49
- package/tests/router/routeTree.test.ts +0 -240
- package/tests/router/rsc.test.tsx +0 -480
- package/tests/router/serverPlugin.test.ts +0 -157
- package/tests/router/slotUsageSanitizer.test.ts +0 -23
- package/tests/router/ssrPreload.test.ts +0 -30
- package/tests/router/tanstackTypes.test.ts +0 -384
- package/tsconfig.json +0 -12
- package/tsconfig.tsgo.json +0 -6
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import { isValidElement } from 'react';
|
|
3
|
-
import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
|
|
4
|
-
import {
|
|
5
|
-
reviveTanstackRscFlightValues,
|
|
6
|
-
serializeTanstackRscFlightValues,
|
|
7
|
-
} from '../../src/runtime/rsc/flightSerialization';
|
|
8
|
-
import {
|
|
9
|
-
RENDERABLE_RSC,
|
|
10
|
-
SERVER_COMPONENT_STREAM,
|
|
11
|
-
type ServerComponentStream,
|
|
12
|
-
} from '../../src/runtime/rsc/symbols';
|
|
13
|
-
|
|
14
|
-
type RoundTripFixture = {
|
|
15
|
-
bigintValue: bigint;
|
|
16
|
-
booleanValue: boolean;
|
|
17
|
-
circular: Record<string, unknown>;
|
|
18
|
-
dateValue: Date;
|
|
19
|
-
falseValue: boolean;
|
|
20
|
-
mapValue: Map<unknown, unknown>;
|
|
21
|
-
nested: {
|
|
22
|
-
array: unknown[];
|
|
23
|
-
object: {
|
|
24
|
-
child: unknown;
|
|
25
|
-
};
|
|
26
|
-
};
|
|
27
|
-
nullValue: null;
|
|
28
|
-
numberValue: number;
|
|
29
|
-
rscProxy: React.ReactElement & Record<PropertyKey, unknown>;
|
|
30
|
-
setValue: Set<unknown>;
|
|
31
|
-
sharedA: Record<string, unknown>;
|
|
32
|
-
sharedB: Record<string, unknown>;
|
|
33
|
-
stringValue: string;
|
|
34
|
-
undefinedValue: undefined;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
function createStream(): ServerComponentStream {
|
|
38
|
-
return {
|
|
39
|
-
createReplayStream: () => new ReadableStream<Uint8Array>(),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
test('round-trips TanStack RSC flight values across supported value types', async () => {
|
|
44
|
-
const shared = { marker: 'shared' };
|
|
45
|
-
const circular: Record<string, unknown> = { marker: 'circular' };
|
|
46
|
-
circular.self = circular;
|
|
47
|
-
|
|
48
|
-
const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
|
|
49
|
-
renderable: true,
|
|
50
|
-
stream: createStream(),
|
|
51
|
-
}) as React.ReactElement & Record<PropertyKey, unknown>;
|
|
52
|
-
|
|
53
|
-
const mapValue = new Map<unknown, unknown>([
|
|
54
|
-
['primitive', 1],
|
|
55
|
-
['shared', shared],
|
|
56
|
-
[shared, circular],
|
|
57
|
-
['rscProxy', rscProxy],
|
|
58
|
-
]);
|
|
59
|
-
mapValue.set('self', mapValue);
|
|
60
|
-
|
|
61
|
-
const setValue = new Set<unknown>(['set-item', shared, circular, rscProxy]);
|
|
62
|
-
setValue.add(setValue);
|
|
63
|
-
|
|
64
|
-
const fixture: RoundTripFixture = {
|
|
65
|
-
bigintValue: 9007199254740993n,
|
|
66
|
-
booleanValue: true,
|
|
67
|
-
circular,
|
|
68
|
-
dateValue: new Date('2026-07-08T12:34:56.789Z'),
|
|
69
|
-
falseValue: false,
|
|
70
|
-
mapValue,
|
|
71
|
-
nested: {
|
|
72
|
-
array: [shared, [null, undefined, 7n]],
|
|
73
|
-
object: {
|
|
74
|
-
child: shared,
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
nullValue: null,
|
|
78
|
-
numberValue: 42,
|
|
79
|
-
rscProxy,
|
|
80
|
-
setValue,
|
|
81
|
-
sharedA: shared,
|
|
82
|
-
sharedB: shared,
|
|
83
|
-
stringValue: 'flight',
|
|
84
|
-
undefinedValue: undefined,
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const serialized = serializeTanstackRscFlightValues(
|
|
88
|
-
fixture,
|
|
89
|
-
) as RoundTripFixture;
|
|
90
|
-
const revived = (await reviveTanstackRscFlightValues(
|
|
91
|
-
serialized,
|
|
92
|
-
)) as RoundTripFixture;
|
|
93
|
-
|
|
94
|
-
expect(revived).not.toBe(fixture);
|
|
95
|
-
expect(revived.stringValue).toBe('flight');
|
|
96
|
-
expect(revived.numberValue).toBe(42);
|
|
97
|
-
expect(revived.booleanValue).toBe(true);
|
|
98
|
-
expect(revived.falseValue).toBe(false);
|
|
99
|
-
expect(revived.nullValue).toBeNull();
|
|
100
|
-
expect(Object.hasOwn(revived, 'undefinedValue')).toBe(true);
|
|
101
|
-
expect(revived.undefinedValue).toBeUndefined();
|
|
102
|
-
expect(revived.bigintValue).toBe(9007199254740993n);
|
|
103
|
-
expect(revived.dateValue).toBeInstanceOf(Date);
|
|
104
|
-
expect(revived.dateValue.toISOString()).toBe('2026-07-08T12:34:56.789Z');
|
|
105
|
-
|
|
106
|
-
expect(revived.nested.array[0]).toBe(revived.sharedA);
|
|
107
|
-
expect(revived.nested.object.child).toBe(revived.sharedA);
|
|
108
|
-
expect(revived.sharedB).toBe(revived.sharedA);
|
|
109
|
-
|
|
110
|
-
expect(revived.circular).not.toBe(circular);
|
|
111
|
-
expect(revived.circular.self).toBe(revived.circular);
|
|
112
|
-
|
|
113
|
-
expect(revived.mapValue).toBeInstanceOf(Map);
|
|
114
|
-
expect(revived.mapValue).not.toBe(mapValue);
|
|
115
|
-
expect(revived.mapValue.get('primitive')).toBe(1);
|
|
116
|
-
expect(revived.mapValue.get('shared')).toBe(revived.sharedA);
|
|
117
|
-
expect(revived.mapValue.get(revived.sharedA)).toBe(revived.circular);
|
|
118
|
-
expect(revived.mapValue.get('self')).toBe(revived.mapValue);
|
|
119
|
-
expect(isValidElement(revived.mapValue.get('rscProxy'))).toBe(true);
|
|
120
|
-
|
|
121
|
-
expect(revived.setValue).toBeInstanceOf(Set);
|
|
122
|
-
expect(revived.setValue).not.toBe(setValue);
|
|
123
|
-
expect(revived.setValue.has('set-item')).toBe(true);
|
|
124
|
-
expect(revived.setValue.has(revived.sharedA)).toBe(true);
|
|
125
|
-
expect(revived.setValue.has(revived.circular)).toBe(true);
|
|
126
|
-
expect([...revived.setValue].some(value => isValidElement(value))).toBe(true);
|
|
127
|
-
expect(revived.setValue.has(revived.setValue)).toBe(true);
|
|
128
|
-
|
|
129
|
-
expect(serialized.rscProxy).not.toBe(rscProxy);
|
|
130
|
-
expect(serialized.rscProxy).toMatchObject({
|
|
131
|
-
__modernTanstackRsc: true,
|
|
132
|
-
kind: 'renderable',
|
|
133
|
-
});
|
|
134
|
-
expect(serialized.rscProxy.stream).toBeInstanceOf(ReadableStream);
|
|
135
|
-
|
|
136
|
-
expect(revived.rscProxy).not.toBe(rscProxy);
|
|
137
|
-
expect(isValidElement(revived.rscProxy)).toBe(true);
|
|
138
|
-
expect(revived.rscProxy[RENDERABLE_RSC]).toBe(true);
|
|
139
|
-
expect(revived.rscProxy[SERVER_COMPONENT_STREAM]).toBeDefined();
|
|
140
|
-
});
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
// The headless entry must drive the real TanStack artifact writer. Checking
|
|
2
|
-
// only the createRunOptions/cli.init choreography would allow a no-op CLI to
|
|
3
|
-
// pass while leaving router.gen.ts stale or missing.
|
|
4
|
-
const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
|
|
5
|
-
mockRunOptions: true,
|
|
6
|
-
received: options,
|
|
7
|
-
}));
|
|
8
|
-
const cliInitMock = rstest.fn(async () => ({ appContext: {} }));
|
|
9
|
-
|
|
10
|
-
rstest.mock('@modern-js/app-tools/cli/run', () => ({
|
|
11
|
-
createRunOptions: createRunOptionsMock,
|
|
12
|
-
}));
|
|
13
|
-
rstest.mock('@modern-js/plugin/cli', () => ({
|
|
14
|
-
cli: { init: cliInitMock },
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import os from 'node:os';
|
|
19
|
-
import path from 'node:path';
|
|
20
|
-
import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
|
|
21
|
-
import {
|
|
22
|
-
generateTanstackRouteArtifacts,
|
|
23
|
-
writeTanstackRouterTypesForEntries,
|
|
24
|
-
} from '../../src/cli';
|
|
25
|
-
|
|
26
|
-
function makeAppDir(withConfig: boolean): string {
|
|
27
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
|
|
28
|
-
if (withConfig) {
|
|
29
|
-
fs.writeFileSync(
|
|
30
|
-
path.join(dir, 'modern.config.ts'),
|
|
31
|
-
'export default {};\n',
|
|
32
|
-
'utf-8',
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return dir;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
createRunOptionsMock.mockClear();
|
|
41
|
-
cliInitMock.mockClear();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('writes a real route artifact through the prepare-only CLI drive', async () => {
|
|
45
|
-
const previousArgv = process.env.MODERN_ARGV;
|
|
46
|
-
const appDirectory = makeAppDir(true);
|
|
47
|
-
const srcDirectory = path.join(appDirectory, 'src');
|
|
48
|
-
const componentPath = path.join(srcDirectory, 'routes', 'page.tsx');
|
|
49
|
-
fs.mkdirSync(path.dirname(componentPath), { recursive: true });
|
|
50
|
-
fs.writeFileSync(
|
|
51
|
-
componentPath,
|
|
52
|
-
'export default function Page() { return null; }\n',
|
|
53
|
-
'utf-8',
|
|
54
|
-
);
|
|
55
|
-
|
|
56
|
-
const runOptions = {
|
|
57
|
-
cwd: appDirectory,
|
|
58
|
-
version: '9.9.9-test',
|
|
59
|
-
internalPlugins: INTERNAL_RUNTIME_PLUGINS,
|
|
60
|
-
configFile: path.resolve(appDirectory, 'modern.config'),
|
|
61
|
-
};
|
|
62
|
-
createRunOptionsMock.mockResolvedValue({
|
|
63
|
-
mockRunOptions: true,
|
|
64
|
-
received: runOptions,
|
|
65
|
-
});
|
|
66
|
-
cliInitMock.mockImplementation(async () => {
|
|
67
|
-
await writeTanstackRouterTypesForEntries({
|
|
68
|
-
appContext: {
|
|
69
|
-
srcDirectory,
|
|
70
|
-
internalSrcAlias: '@/_',
|
|
71
|
-
entrypoints: [{ entryName: 'main', isMainEntry: true }],
|
|
72
|
-
} as any,
|
|
73
|
-
routesByEntry: {
|
|
74
|
-
main: [
|
|
75
|
-
{
|
|
76
|
-
type: 'nested',
|
|
77
|
-
id: 'layout',
|
|
78
|
-
isRoot: true,
|
|
79
|
-
children: [
|
|
80
|
-
{
|
|
81
|
-
type: 'nested',
|
|
82
|
-
id: 'page',
|
|
83
|
-
index: true,
|
|
84
|
-
_component: '@/_/routes/page',
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
] as any,
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
return { appContext: {} };
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
await generateTanstackRouteArtifacts({
|
|
96
|
-
appDirectory,
|
|
97
|
-
version: '9.9.9-test',
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
expect(createRunOptionsMock).toHaveBeenCalledTimes(1);
|
|
101
|
-
const runOptionsArg = createRunOptionsMock.mock.calls[0][0] as Record<
|
|
102
|
-
string,
|
|
103
|
-
unknown
|
|
104
|
-
>;
|
|
105
|
-
// The headless entry must resolve cwd/configFile from the target app
|
|
106
|
-
// directory, not the process's own cwd — that was the actual bug.
|
|
107
|
-
expect(runOptionsArg.cwd).toBe(appDirectory);
|
|
108
|
-
expect(runOptionsArg.configFile).toBe(
|
|
109
|
-
path.resolve(appDirectory, 'modern.config'),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
113
|
-
|
|
114
|
-
expect(
|
|
115
|
-
fs.existsSync(
|
|
116
|
-
path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
|
|
117
|
-
),
|
|
118
|
-
).toBe(true);
|
|
119
|
-
const registerArtifact = fs.readFileSync(
|
|
120
|
-
path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
|
|
121
|
-
'utf-8',
|
|
122
|
-
);
|
|
123
|
-
expect(registerArtifact).toContain('./main/router.gen');
|
|
124
|
-
} finally {
|
|
125
|
-
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
126
|
-
if (previousArgv === undefined) {
|
|
127
|
-
delete process.env.MODERN_ARGV;
|
|
128
|
-
} else {
|
|
129
|
-
process.env.MODERN_ARGV = previousArgv;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('throws a clear error when no config file exists', async () => {
|
|
135
|
-
const appDirectory = makeAppDir(false);
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
await expect(
|
|
139
|
-
generateTanstackRouteArtifacts({ appDirectory }),
|
|
140
|
-
).rejects.toThrow(/Unable to locate a Modern\.js config file/u);
|
|
141
|
-
expect(createRunOptionsMock).not.toHaveBeenCalled();
|
|
142
|
-
} finally {
|
|
143
|
-
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
});
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { isNotFound, isRedirect } from '@tanstack/react-router';
|
|
2
|
-
import {
|
|
3
|
-
mapSplatParamsForModernLoader,
|
|
4
|
-
modernLoaderToTanstack,
|
|
5
|
-
throwTanstackRedirect,
|
|
6
|
-
} from '../../src/runtime/loaderBridge';
|
|
7
|
-
|
|
8
|
-
type RedirectLike = {
|
|
9
|
-
options?: {
|
|
10
|
-
href?: string;
|
|
11
|
-
to?: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function catchThrown(fn: () => unknown): unknown {
|
|
16
|
-
try {
|
|
17
|
-
fn();
|
|
18
|
-
} catch (err) {
|
|
19
|
-
return err;
|
|
20
|
-
}
|
|
21
|
-
throw new Error('expected the function to throw');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('throwTanstackRedirect', () => {
|
|
25
|
-
test('absolute URLs redirect via href (external), not via to', () => {
|
|
26
|
-
// The old inline codegen handler threw `redirect({ href })` INSIDE a
|
|
27
|
-
// try block whose catch replaced it with `redirect({ to: absoluteUrl })`,
|
|
28
|
-
// making TanStack treat the absolute URL as an internal path.
|
|
29
|
-
const thrown = catchThrown(() =>
|
|
30
|
-
throwTanstackRedirect('https://example.com/external'),
|
|
31
|
-
) as RedirectLike;
|
|
32
|
-
|
|
33
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
34
|
-
expect(thrown.options?.href).toBe('https://example.com/external');
|
|
35
|
-
expect(thrown.options?.to).toBeUndefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('relative paths redirect via to so the basepath rewrite applies', () => {
|
|
39
|
-
const thrown = catchThrown(() =>
|
|
40
|
-
throwTanstackRedirect('/dashboard'),
|
|
41
|
-
) as RedirectLike;
|
|
42
|
-
|
|
43
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
44
|
-
expect(thrown.options?.to).toBe('/dashboard');
|
|
45
|
-
expect(thrown.options?.href).toBeUndefined();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('empty location falls back to /', () => {
|
|
49
|
-
const thrown = catchThrown(() => throwTanstackRedirect('')) as RedirectLike;
|
|
50
|
-
expect(thrown.options?.to).toBe('/');
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('mapSplatParamsForModernLoader', () => {
|
|
55
|
-
test('maps TanStack _splat to React Router * only for splat routes', () => {
|
|
56
|
-
expect(
|
|
57
|
-
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, true),
|
|
58
|
-
).toEqual({ '*': 'a/b', id: '1' });
|
|
59
|
-
expect(
|
|
60
|
-
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, false),
|
|
61
|
-
).toEqual({ _splat: 'a/b', id: '1' });
|
|
62
|
-
expect(mapSplatParamsForModernLoader({ id: '1' }, true)).toEqual({
|
|
63
|
-
id: '1',
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe('modernLoaderToTanstack', () => {
|
|
69
|
-
const baseCtx = {
|
|
70
|
-
location: { href: 'http://localhost/products/1' },
|
|
71
|
-
params: { id: '1' },
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
test('passes request/params/context through to the modern loader', async () => {
|
|
75
|
-
const seen: { request?: Request; params?: unknown; context?: unknown } = {};
|
|
76
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, (args: any) => {
|
|
77
|
-
seen.request = args.request;
|
|
78
|
-
seen.params = args.params;
|
|
79
|
-
seen.context = args.context;
|
|
80
|
-
return { ok: true };
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
await expect(
|
|
84
|
-
loader({
|
|
85
|
-
...baseCtx,
|
|
86
|
-
context: { requestContext: { user: 'u1' } },
|
|
87
|
-
}),
|
|
88
|
-
).resolves.toEqual({ ok: true });
|
|
89
|
-
expect(seen.request).toBeInstanceOf(Request);
|
|
90
|
-
expect(seen.request?.url).toBe('http://localhost/products/1');
|
|
91
|
-
expect(seen.params).toEqual({ id: '1' });
|
|
92
|
-
expect(seen.context).toEqual({ user: 'u1' });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('uses current location when context carries a previous request', async () => {
|
|
96
|
-
const seen: { request?: Request } = {};
|
|
97
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, (args: any) => {
|
|
98
|
-
seen.request = args.request;
|
|
99
|
-
return { ok: true };
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
await expect(
|
|
103
|
-
loader({
|
|
104
|
-
...baseCtx,
|
|
105
|
-
location: { href: 'http://localhost/products/2?sort=price' },
|
|
106
|
-
context: {
|
|
107
|
-
request: new Request('http://localhost/products/1?sort=name'),
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
).resolves.toEqual({ ok: true });
|
|
111
|
-
|
|
112
|
-
expect(seen.request).toBeInstanceOf(Request);
|
|
113
|
-
expect(seen.request?.url).toBe('http://localhost/products/2?sort=price');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
test('translates an absolute-URL redirect Response into redirect({ href })', async () => {
|
|
117
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, () =>
|
|
118
|
-
Response.redirect('https://example.com/away', 302),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const thrown = (await loader(baseCtx).then(
|
|
122
|
-
() => {
|
|
123
|
-
throw new Error('expected redirect');
|
|
124
|
-
},
|
|
125
|
-
(err: unknown) => err,
|
|
126
|
-
)) as RedirectLike;
|
|
127
|
-
|
|
128
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
129
|
-
expect(thrown.options?.href).toBe('https://example.com/away');
|
|
130
|
-
expect(thrown.options?.to).toBeUndefined();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('translates a relative redirect Response into redirect({ to })', async () => {
|
|
134
|
-
const loader = modernLoaderToTanstack(
|
|
135
|
-
{ hasSplat: false },
|
|
136
|
-
() =>
|
|
137
|
-
new Response(null, { status: 302, headers: { Location: '/login' } }),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
const thrown = (await loader(baseCtx).then(
|
|
141
|
-
() => {
|
|
142
|
-
throw new Error('expected redirect');
|
|
143
|
-
},
|
|
144
|
-
(err: unknown) => err,
|
|
145
|
-
)) as RedirectLike;
|
|
146
|
-
|
|
147
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
148
|
-
expect(thrown.options?.to).toBe('/login');
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('translates a 404 Response into notFound()', async () => {
|
|
152
|
-
const loader = modernLoaderToTanstack(
|
|
153
|
-
{ hasSplat: false },
|
|
154
|
-
() => new Response(null, { status: 404 }),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
const thrown = await loader(baseCtx).then(
|
|
158
|
-
() => {
|
|
159
|
-
throw new Error('expected notFound');
|
|
160
|
-
},
|
|
161
|
-
(err: unknown) => err,
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
expect(isNotFound(thrown)).toBe(true);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('preserves returned non-404 error Responses as loader results', async () => {
|
|
168
|
-
const response = new Response('loader exploded', { status: 500 });
|
|
169
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, () => response);
|
|
170
|
-
|
|
171
|
-
await expect(loader(baseCtx)).resolves.toBe(response);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('translates redirect Responses thrown synchronously by the loader', () => {
|
|
175
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, () => {
|
|
176
|
-
throw new Response(null, {
|
|
177
|
-
status: 301,
|
|
178
|
-
headers: { Location: 'https://example.com/moved' },
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// A synchronous loader throw surfaces synchronously (TanStack handles
|
|
183
|
-
// thrown redirects from the loader call itself).
|
|
184
|
-
const thrown = catchThrown(() => loader(baseCtx)) as RedirectLike;
|
|
185
|
-
|
|
186
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
187
|
-
expect(thrown.options?.href).toBe('https://example.com/moved');
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('re-throws TanStack redirects thrown by the loader untouched', async () => {
|
|
191
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, async () => {
|
|
192
|
-
throwTanstackRedirect('/inner');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const thrown = (await loader(baseCtx).then(
|
|
196
|
-
() => {
|
|
197
|
-
throw new Error('expected redirect');
|
|
198
|
-
},
|
|
199
|
-
(err: unknown) => err,
|
|
200
|
-
)) as RedirectLike;
|
|
201
|
-
|
|
202
|
-
// The bridge must not re-translate its own redirect (a Response without
|
|
203
|
-
// a Location header) — that used to collapse internal targets to '/'.
|
|
204
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
205
|
-
expect(thrown.options?.to).toBe('/inner');
|
|
206
|
-
});
|
|
207
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { applyLocalisedUrlsToRoutes } from '@modern-js/i18n-runtime-extensions';
|
|
2
|
-
import {
|
|
3
|
-
createMemoryHistory,
|
|
4
|
-
createRouter,
|
|
5
|
-
Outlet,
|
|
6
|
-
RouterProvider,
|
|
7
|
-
useLoaderData,
|
|
8
|
-
useParams,
|
|
9
|
-
} from '@tanstack/react-router';
|
|
10
|
-
import { createElement } from 'react';
|
|
11
|
-
import { renderToString } from 'react-dom/server';
|
|
12
|
-
import { createModernBasepathRewrite } from '../../src/runtime/basepathRewrite';
|
|
13
|
-
import { createRouteTreeFromRouteObjects } from '../../src/runtime/routeTree';
|
|
14
|
-
import { createTanstackRouteObjectsFromConfig } from '../../src/runtime/utils';
|
|
15
|
-
|
|
16
|
-
function SearchPage() {
|
|
17
|
-
const data = useLoaderData({ from: '/$lang/search' }) as { language: string };
|
|
18
|
-
return createElement('p', null, `search:${data.language}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function ResourcePage() {
|
|
22
|
-
const params = useParams({ from: '/$lang/resources/$id' }) as { id: string };
|
|
23
|
-
return createElement('p', null, `resource:${params.id}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test('localized URLs preserve native loader and params route identity in SSR and navigation', async () => {
|
|
27
|
-
const routes = applyLocalisedUrlsToRoutes(
|
|
28
|
-
[
|
|
29
|
-
{
|
|
30
|
-
type: 'nested',
|
|
31
|
-
id: 'lang',
|
|
32
|
-
path: ':lang',
|
|
33
|
-
component: Outlet,
|
|
34
|
-
children: [
|
|
35
|
-
{
|
|
36
|
-
type: 'nested',
|
|
37
|
-
id: 'search',
|
|
38
|
-
path: 'search',
|
|
39
|
-
component: SearchPage,
|
|
40
|
-
loader: ({ params }: { params: { lang: string } }) => ({
|
|
41
|
-
language: params.lang,
|
|
42
|
-
}),
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
type: 'nested',
|
|
46
|
-
id: 'resource',
|
|
47
|
-
path: 'resources/:id',
|
|
48
|
-
component: ResourcePage,
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
['en', 'cs'],
|
|
54
|
-
{
|
|
55
|
-
'/search': { en: '/find', cs: '/hledat' },
|
|
56
|
-
'/resources/:id': { en: '/resources/:id', cs: '/zdroje/:id' },
|
|
57
|
-
},
|
|
58
|
-
);
|
|
59
|
-
const routeObjects = createTanstackRouteObjectsFromConfig({
|
|
60
|
-
routesConfig: { routes },
|
|
61
|
-
})!;
|
|
62
|
-
const history = createMemoryHistory({
|
|
63
|
-
initialEntries: ['/base/cs/hledat?q=tractor#results'],
|
|
64
|
-
});
|
|
65
|
-
const router = createRouter({
|
|
66
|
-
context: {
|
|
67
|
-
request: new Request(
|
|
68
|
-
'https://example.test/base/cs/hledat?q=tractor#results',
|
|
69
|
-
),
|
|
70
|
-
},
|
|
71
|
-
routeTree: createRouteTreeFromRouteObjects(routeObjects),
|
|
72
|
-
history,
|
|
73
|
-
rewrite: createModernBasepathRewrite('/base', false, routeObjects),
|
|
74
|
-
});
|
|
75
|
-
await router.load();
|
|
76
|
-
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
|
|
77
|
-
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
78
|
-
'search:cs',
|
|
79
|
-
);
|
|
80
|
-
expect(history.location.href).toBe('/base/cs/hledat?q=tractor#results');
|
|
81
|
-
|
|
82
|
-
await router.navigate({ to: '/en/search', search: { q: 'tractor' } });
|
|
83
|
-
expect(history.location.href).toBe('/base/en/find?q=tractor');
|
|
84
|
-
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/search');
|
|
85
|
-
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
86
|
-
'search:en',
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
await router.navigate({ to: '/cs/zdroje/a%2Fb' });
|
|
90
|
-
expect(history.location.href).toBe('/base/cs/zdroje/a%2Fb');
|
|
91
|
-
expect(router.state.matches.at(-1)?.routeId).toBe('/$lang/resources/$id');
|
|
92
|
-
expect(renderToString(createElement(RouterProvider, { router }))).toContain(
|
|
93
|
-
'resource:a/b',
|
|
94
|
-
);
|
|
95
|
-
});
|