@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.98 → 3.8.1-ultramodern.1
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/index.js +10 -2
- package/dist/cjs/cli/tanstackTypes/source.js +9 -20
- package/dist/cjs/runtime/plugin.node.js +12 -5
- package/dist/cjs/runtime/pluginShared.js +3 -3
- package/dist/cjs/runtime/rsc/payloadErrors.js +2 -2
- package/dist/cjs/runtime/rsc/shared.js +1 -1
- package/dist/cjs/runtime/rsc/treeGetter.js +5 -9
- package/dist/cjs/runtime/submitAction.js +4 -5
- package/dist/esm/cli/index.mjs +10 -2
- package/dist/esm/cli/tanstackTypes/source.mjs +9 -10
- package/dist/esm/runtime/plugin.node.mjs +12 -5
- package/dist/esm/runtime/pluginShared.mjs +3 -3
- package/dist/esm/runtime/rsc/payloadErrors.mjs +1 -1
- package/dist/esm/runtime/rsc/shared.mjs +1 -1
- package/dist/esm/runtime/rsc/treeGetter.mjs +5 -9
- package/dist/esm/runtime/submitAction.mjs +4 -5
- package/dist/esm-node/cli/index.mjs +10 -2
- package/dist/esm-node/cli/tanstackTypes/source.mjs +9 -10
- package/dist/esm-node/runtime/plugin.node.mjs +12 -5
- package/dist/esm-node/runtime/pluginShared.mjs +3 -3
- package/dist/esm-node/runtime/rsc/payloadErrors.mjs +1 -1
- package/dist/esm-node/runtime/rsc/shared.mjs +1 -1
- package/dist/esm-node/runtime/rsc/treeGetter.mjs +5 -9
- package/dist/esm-node/runtime/submitAction.mjs +4 -5
- package/dist/types/runtime/clientHydration.d.ts +1 -1
- package/dist/types/runtime/ssrTypes.d.ts +0 -2
- package/package.json +27 -20
- package/rstest.config.mts +2 -0
- package/src/cli/index.ts +12 -0
- package/src/cli/tanstackTypes/canonicalRoutes.ts +0 -1
- package/src/cli/tanstackTypes/shared.ts +0 -1
- package/src/cli/tanstackTypes/source.ts +3 -4
- package/src/runtime/plugin.node.tsx +17 -11
- package/src/runtime/plugin.tsx +0 -2
- package/src/runtime/pluginShared.ts +8 -7
- package/src/runtime/rsc/payloadErrors.ts +1 -1
- package/src/runtime/rsc/shared.ts +1 -1
- package/src/runtime/rsc/treeGetter.tsx +9 -8
- package/src/runtime/ssrManagedTags.ts +0 -2
- package/src/runtime/ssrTypes.ts +0 -4
- package/src/runtime/submitAction.ts +6 -11
- package/src/types/react-server-dom-rspack.d.ts +4 -52
- package/tests/router/cli.test.ts +204 -96
- package/tests/router/clientHydration.test.tsx +140 -1
- package/tests/router/dataMutation.test.tsx +50 -9
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +162 -0
- package/tests/router/preloadRedirect.test.ts +3 -8
- package/tests/router/routeTree.test.ts +73 -19
- package/tests/router/routerCompatibility.test.ts +0 -6
- package/tests/router/rscDecoderSecurity.test.ts +113 -0
- package/tests/router/serverPlugin.test.ts +115 -0
- package/tests/router/tanstackTypes.test.ts +668 -356
- package/src/runtime/ssr-shim.d.ts +0 -10
- package/tests/castRatchet.test.ts +0 -85
- package/tests/router/fixtures/router-gen.golden.txt +0 -71
package/tests/router/cli.test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
4
6
|
import { mergeConfig } from '@modern-js/plugin/cli';
|
|
5
7
|
import type { Entrypoint } from '@modern-js/types';
|
|
6
8
|
import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
|
|
@@ -12,6 +14,119 @@ import {
|
|
|
12
14
|
writeTanstackRouterTypesForEntries,
|
|
13
15
|
} from '../../src/cli';
|
|
14
16
|
|
|
17
|
+
const execFileAsync = promisify(execFile);
|
|
18
|
+
const strictestTsconfigPath = path.resolve(
|
|
19
|
+
__dirname,
|
|
20
|
+
'../../node_modules/@tsconfig/strictest/tsconfig.json',
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
async function typecheckGeneratedRegistration(options: {
|
|
24
|
+
entries: string[];
|
|
25
|
+
generatedDirName: string;
|
|
26
|
+
srcDirectory: string;
|
|
27
|
+
runtimeModule?: string;
|
|
28
|
+
i18nRuntimeModule?: string;
|
|
29
|
+
canonicalTypeChecks?: string[];
|
|
30
|
+
}) {
|
|
31
|
+
const {
|
|
32
|
+
entries,
|
|
33
|
+
generatedDirName,
|
|
34
|
+
srcDirectory,
|
|
35
|
+
runtimeModule = '@modern-js/plugin-tanstack/runtime',
|
|
36
|
+
i18nRuntimeModule,
|
|
37
|
+
canonicalTypeChecks = [],
|
|
38
|
+
} = options;
|
|
39
|
+
const projectDirectory = path.dirname(srcDirectory);
|
|
40
|
+
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const routerPath = path.join(
|
|
43
|
+
srcDirectory,
|
|
44
|
+
generatedDirName,
|
|
45
|
+
entry,
|
|
46
|
+
'router.gen.ts',
|
|
47
|
+
);
|
|
48
|
+
if (!(await fs.pathExists(routerPath))) {
|
|
49
|
+
await fs.outputFile(routerPath, 'export const router = { context: {} };');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const runtimeDeclaration = [
|
|
54
|
+
`declare module '${runtimeModule}' {`,
|
|
55
|
+
' export interface Register {}',
|
|
56
|
+
' export type ModernRouterContext = { request?: Request; requestContext?: unknown };',
|
|
57
|
+
' type RouteOptions = { getParentRoute?: (...args: never[]) => unknown; id?: string; loader?: (...args: never[]) => unknown; path?: string; staticData?: unknown };',
|
|
58
|
+
' type Route<TOptions extends RouteOptions = RouteOptions> = { options: TOptions; addChildren<const TChildren extends readonly unknown[]>(children: TChildren): Route<TOptions> & { children: TChildren } };',
|
|
59
|
+
' export function createMemoryHistory<TOptions>(options: TOptions): TOptions;',
|
|
60
|
+
' export function createRootRouteWithContext<TContext extends ModernRouterContext>(): <const TOptions extends RouteOptions>(options: TOptions) => Route<TOptions>;',
|
|
61
|
+
' export function createRoute<const TOptions extends RouteOptions>(options: TOptions): Route<TOptions>;',
|
|
62
|
+
' export function createRouter<const TOptions extends { context: ModernRouterContext; routeTree: unknown }>(options: TOptions): TOptions;',
|
|
63
|
+
' export function createRouteStaticData<const TData extends Record<string, unknown>>(data: TData): TData;',
|
|
64
|
+
' export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, loader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
|
|
65
|
+
' export const modernTanstackRouterFastDefaults: Record<string, unknown>;',
|
|
66
|
+
'}',
|
|
67
|
+
];
|
|
68
|
+
if (i18nRuntimeModule) {
|
|
69
|
+
runtimeDeclaration.push(
|
|
70
|
+
`declare module '${i18nRuntimeModule}' { export interface UltramodernCanonicalRoutes {} }`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
await fs.outputFile(
|
|
74
|
+
path.join(srcDirectory, 'generated-runtime-shim.d.ts'),
|
|
75
|
+
runtimeDeclaration.join('\n'),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const contractLines = [
|
|
79
|
+
`import type { Register } from '${runtimeModule}';`,
|
|
80
|
+
...entries.map(
|
|
81
|
+
(entry, index) =>
|
|
82
|
+
`import { router as router${index} } from './${generatedDirName}/${entry}/router.gen';`,
|
|
83
|
+
),
|
|
84
|
+
...entries.map(
|
|
85
|
+
(_, index) =>
|
|
86
|
+
`const registeredRouter${index}: Register['router'] = router${index};`,
|
|
87
|
+
),
|
|
88
|
+
...entries.map((_, index) => `void registeredRouter${index};`),
|
|
89
|
+
...canonicalTypeChecks,
|
|
90
|
+
];
|
|
91
|
+
await fs.outputFile(
|
|
92
|
+
path.join(srcDirectory, 'registration-contract.ts'),
|
|
93
|
+
contractLines.join('\n'),
|
|
94
|
+
);
|
|
95
|
+
await fs.outputJSON(path.join(projectDirectory, 'tsconfig.json'), {
|
|
96
|
+
extends: strictestTsconfigPath,
|
|
97
|
+
compilerOptions: {
|
|
98
|
+
lib: ['ESNext', 'DOM'],
|
|
99
|
+
module: 'Preserve',
|
|
100
|
+
moduleResolution: 'Bundler',
|
|
101
|
+
noEmit: true,
|
|
102
|
+
target: 'ESNext',
|
|
103
|
+
types: [],
|
|
104
|
+
},
|
|
105
|
+
include: ['src/**/*.ts', 'src/**/*.d.ts'],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await execFileAsync(
|
|
110
|
+
process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
|
|
111
|
+
['-p', 'tsconfig.json'],
|
|
112
|
+
{
|
|
113
|
+
cwd: projectDirectory,
|
|
114
|
+
shell: process.platform === 'win32',
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (
|
|
119
|
+
error &&
|
|
120
|
+
typeof error === 'object' &&
|
|
121
|
+
'stdout' in error &&
|
|
122
|
+
typeof error.stdout === 'string'
|
|
123
|
+
) {
|
|
124
|
+
throw new Error(error.stdout, { cause: error });
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
15
130
|
const runtimeCliMocks = {
|
|
16
131
|
handleFileChange: rstest.fn(),
|
|
17
132
|
handleGeneratorEntryCode: rstest.fn(),
|
|
@@ -86,7 +201,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
86
201
|
}
|
|
87
202
|
});
|
|
88
203
|
|
|
89
|
-
test('
|
|
204
|
+
test('typechecks plugin-owned routers and register metadata under strictest', async () => {
|
|
90
205
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
91
206
|
const srcDirectory = path.join(tempDir, 'src');
|
|
92
207
|
await mkdir(srcDirectory, { recursive: true });
|
|
@@ -102,33 +217,33 @@ describe('tanstack router cli plugin', () => {
|
|
|
102
217
|
} as any,
|
|
103
218
|
generatedDirName: 'generated-router',
|
|
104
219
|
routesByEntry: {
|
|
105
|
-
dashboard: [
|
|
106
|
-
|
|
220
|
+
dashboard: [
|
|
221
|
+
{
|
|
222
|
+
type: 'nested',
|
|
223
|
+
id: 'layout',
|
|
224
|
+
isRoot: true,
|
|
225
|
+
children: [{ type: 'nested', id: 'page', index: true }],
|
|
226
|
+
},
|
|
227
|
+
],
|
|
228
|
+
main: [
|
|
229
|
+
{
|
|
230
|
+
type: 'nested',
|
|
231
|
+
id: 'layout',
|
|
232
|
+
isRoot: true,
|
|
233
|
+
children: [{ type: 'nested', id: 'page', index: true }],
|
|
234
|
+
},
|
|
235
|
+
],
|
|
107
236
|
},
|
|
108
237
|
});
|
|
109
238
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
"} from '@modern-js/plugin-tanstack/runtime';",
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const register = await readFile(
|
|
119
|
-
path.join(srcDirectory, 'generated-router', 'register.gen.d.ts'),
|
|
120
|
-
'utf-8',
|
|
121
|
-
);
|
|
122
|
-
expect(register).toContain("from './main/router.gen'");
|
|
123
|
-
expect(register.indexOf("from './main/router.gen'")).toBeLessThan(
|
|
124
|
-
register.indexOf("from './dashboard/router.gen'"),
|
|
125
|
-
);
|
|
126
|
-
expect(register).toContain(
|
|
127
|
-
"declare module '@modern-js/plugin-tanstack/runtime'",
|
|
128
|
-
);
|
|
239
|
+
await typecheckGeneratedRegistration({
|
|
240
|
+
entries: ['main', 'dashboard'],
|
|
241
|
+
generatedDirName: 'generated-router',
|
|
242
|
+
srcDirectory,
|
|
243
|
+
});
|
|
129
244
|
});
|
|
130
245
|
|
|
131
|
-
test('
|
|
246
|
+
test('typechecks register metadata for custom entry lists without routes', async () => {
|
|
132
247
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
133
248
|
const srcDirectory = path.join(tempDir, 'src');
|
|
134
249
|
|
|
@@ -138,17 +253,14 @@ describe('tanstack router cli plugin', () => {
|
|
|
138
253
|
srcDirectory,
|
|
139
254
|
});
|
|
140
255
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
'
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
expect(register).toContain(
|
|
147
|
-
"declare module '@modern-js/plugin-tanstack/runtime'",
|
|
148
|
-
);
|
|
256
|
+
await typecheckGeneratedRegistration({
|
|
257
|
+
entries: ['main'],
|
|
258
|
+
generatedDirName: 'tanstack',
|
|
259
|
+
srcDirectory,
|
|
260
|
+
});
|
|
149
261
|
});
|
|
150
262
|
|
|
151
|
-
test('
|
|
263
|
+
test('exposes canonical route params through plugin-i18n types', async () => {
|
|
152
264
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
153
265
|
const srcDirectory = path.join(tempDir, 'src');
|
|
154
266
|
|
|
@@ -162,20 +274,22 @@ describe('tanstack router cli plugin', () => {
|
|
|
162
274
|
},
|
|
163
275
|
});
|
|
164
276
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
'
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
277
|
+
await typecheckGeneratedRegistration({
|
|
278
|
+
entries: ['main'],
|
|
279
|
+
generatedDirName: 'tanstack',
|
|
280
|
+
srcDirectory,
|
|
281
|
+
i18nRuntimeModule: '@modern-js/plugin-i18n/runtime',
|
|
282
|
+
canonicalTypeChecks: [
|
|
283
|
+
"import type { UltramodernCanonicalRoutes } from '@modern-js/plugin-i18n/runtime';",
|
|
284
|
+
"const rootParams: UltramodernCanonicalRoutes['/'] = {};",
|
|
285
|
+
"const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
|
|
286
|
+
'void rootParams;',
|
|
287
|
+
'void productParams;',
|
|
288
|
+
],
|
|
289
|
+
});
|
|
176
290
|
});
|
|
177
291
|
|
|
178
|
-
test('
|
|
292
|
+
test('typechecks a plain TanStack register without plugin-i18n installed', async () => {
|
|
179
293
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
180
294
|
const srcDirectory = path.join(tempDir, 'src');
|
|
181
295
|
|
|
@@ -183,23 +297,16 @@ describe('tanstack router cli plugin', () => {
|
|
|
183
297
|
entries: ['main'],
|
|
184
298
|
generatedDirName: 'tanstack',
|
|
185
299
|
srcDirectory,
|
|
186
|
-
// No canonicalRoutes provided at all — plain TanStack app
|
|
187
300
|
});
|
|
188
301
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
'
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
expect(register).not.toContain('plugin-i18n');
|
|
195
|
-
expect(register).not.toContain('UltramodernCanonicalRoutes');
|
|
196
|
-
// But the standard TanStack runtime augmentation must still be present.
|
|
197
|
-
expect(register).toContain(
|
|
198
|
-
"declare module '@modern-js/plugin-tanstack/runtime'",
|
|
199
|
-
);
|
|
302
|
+
await typecheckGeneratedRegistration({
|
|
303
|
+
entries: ['main'],
|
|
304
|
+
generatedDirName: 'tanstack',
|
|
305
|
+
srcDirectory,
|
|
306
|
+
});
|
|
200
307
|
});
|
|
201
308
|
|
|
202
|
-
test('
|
|
309
|
+
test('exposes canonical route params through a custom i18n runtime module', async () => {
|
|
203
310
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
204
311
|
const srcDirectory = path.join(tempDir, 'src');
|
|
205
312
|
|
|
@@ -213,15 +320,17 @@ describe('tanstack router cli plugin', () => {
|
|
|
213
320
|
i18nRuntimeModule: '@my-org/i18n/runtime',
|
|
214
321
|
});
|
|
215
322
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
'
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
323
|
+
await typecheckGeneratedRegistration({
|
|
324
|
+
entries: ['main'],
|
|
325
|
+
generatedDirName: 'tanstack',
|
|
326
|
+
srcDirectory,
|
|
327
|
+
i18nRuntimeModule: '@my-org/i18n/runtime',
|
|
328
|
+
canonicalTypeChecks: [
|
|
329
|
+
"import type { UltramodernCanonicalRoutes } from '@my-org/i18n/runtime';",
|
|
330
|
+
"const talksParams: UltramodernCanonicalRoutes['/talks'] = {};",
|
|
331
|
+
'void talksParams;',
|
|
332
|
+
],
|
|
333
|
+
});
|
|
225
334
|
});
|
|
226
335
|
|
|
227
336
|
test('claims custom routes, injects runtime plugin, and merges route specs', async () => {
|
|
@@ -576,32 +685,36 @@ describe('tanstack router cli plugin', () => {
|
|
|
576
685
|
tanstackRouterPlugin().setup!(api as any);
|
|
577
686
|
await taps.generateEntryCode({ entrypoints: [entrypoint] });
|
|
578
687
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
'utf-8',
|
|
688
|
+
const hasI18n = registeredPlugins.some(
|
|
689
|
+
plugin => plugin.name === '@modern-js/plugin-i18n',
|
|
582
690
|
);
|
|
691
|
+
await typecheckGeneratedRegistration({
|
|
692
|
+
entries: ['main'],
|
|
693
|
+
generatedDirName: 'modern-tanstack',
|
|
694
|
+
srcDirectory,
|
|
695
|
+
...(hasI18n && {
|
|
696
|
+
i18nRuntimeModule: '@modern-js/plugin-i18n/runtime',
|
|
697
|
+
}),
|
|
698
|
+
canonicalTypeChecks: hasI18n
|
|
699
|
+
? [
|
|
700
|
+
"import type { UltramodernCanonicalRoutes } from '@modern-js/plugin-i18n/runtime';",
|
|
701
|
+
"const aboutParams: UltramodernCanonicalRoutes['/about'] = {};",
|
|
702
|
+
'void aboutParams;',
|
|
703
|
+
]
|
|
704
|
+
: [],
|
|
705
|
+
});
|
|
583
706
|
await rm(dir, { recursive: true, force: true });
|
|
584
|
-
return register;
|
|
585
707
|
};
|
|
586
708
|
|
|
587
709
|
// A hand-rolled `/:lang/` app WITHOUT plugin-i18n must not get the
|
|
588
710
|
// augmentation — it would reference an unresolvable module (TS2664).
|
|
589
|
-
|
|
590
|
-
expect(withoutI18n).not.toContain('plugin-i18n');
|
|
591
|
-
expect(withoutI18n).not.toContain('UltramodernCanonicalRoutes');
|
|
592
|
-
expect(withoutI18n).toContain(
|
|
593
|
-
"declare module '@modern-js/plugin-tanstack/runtime'",
|
|
594
|
-
);
|
|
711
|
+
await runGenerate([{ name: '@modern-js/app-tools' }]);
|
|
595
712
|
|
|
596
713
|
// With plugin-i18n registered the canonical route map is emitted.
|
|
597
|
-
|
|
714
|
+
await runGenerate([
|
|
598
715
|
{ name: '@modern-js/app-tools' },
|
|
599
716
|
{ name: '@modern-js/plugin-i18n' },
|
|
600
717
|
]);
|
|
601
|
-
expect(withI18n).toContain(
|
|
602
|
-
"declare module '@modern-js/plugin-i18n/runtime'",
|
|
603
|
-
);
|
|
604
|
-
expect(withI18n).toContain("'/about': Record<string, never>;");
|
|
605
718
|
});
|
|
606
719
|
|
|
607
720
|
test('generates plugin-owned TanStack route files through core route generation', async () => {
|
|
@@ -689,23 +802,18 @@ describe('tanstack router cli plugin', () => {
|
|
|
689
802
|
[entrypoint],
|
|
690
803
|
{
|
|
691
804
|
entrypointsKey: '@modern-js/plugin-tanstack',
|
|
805
|
+
hydrateRscClientRoutes: true,
|
|
806
|
+
includeRouteServerLoadersInSsrEntry: false,
|
|
807
|
+
isolateRouteDataInRscLayer: true,
|
|
808
|
+
serverRoutesFileName: 'tanstack-routes.server.js',
|
|
692
809
|
},
|
|
693
810
|
);
|
|
694
811
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
'
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
expect(routerGen).toContain('modernRouteAction: action_0');
|
|
701
|
-
|
|
702
|
-
const register = await readFile(
|
|
703
|
-
path.join(srcDirectory, 'tanstack-generated', 'register.gen.d.ts'),
|
|
704
|
-
'utf-8',
|
|
705
|
-
);
|
|
706
|
-
expect(register).toContain(
|
|
707
|
-
"declare module '@modern-js/plugin-tanstack/runtime'",
|
|
708
|
-
);
|
|
812
|
+
await typecheckGeneratedRegistration({
|
|
813
|
+
entries: ['main'],
|
|
814
|
+
generatedDirName: 'tanstack-generated',
|
|
815
|
+
srcDirectory,
|
|
816
|
+
});
|
|
709
817
|
});
|
|
710
818
|
|
|
711
819
|
test('regenerates plugin-owned TanStack files for scoped file changes', async () => {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { AnyRouter } from '@tanstack/react-router';
|
|
2
|
-
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import React, { Suspense } from 'react';
|
|
4
4
|
import { ModernRouterClient } from '../../src/runtime/clientHydration';
|
|
5
|
+
import { createTreeGetterFromFlightStream } from '../../src/runtime/rsc/treeGetter';
|
|
5
6
|
|
|
6
7
|
let hydrateImpl: () => Promise<unknown>;
|
|
8
|
+
let createFromReadableStreamImpl: () => Promise<unknown>;
|
|
9
|
+
let registeredCallServer: unknown;
|
|
10
|
+
let decoderCallServer: unknown;
|
|
7
11
|
|
|
8
12
|
rstest.mock('@tanstack/react-router', () => ({
|
|
9
13
|
RouterProvider: ({ router }: { router: { label: string } }) => (
|
|
@@ -15,6 +19,24 @@ rstest.mock('@tanstack/react-router/ssr/client', () => ({
|
|
|
15
19
|
hydrate: () => hydrateImpl(),
|
|
16
20
|
}));
|
|
17
21
|
|
|
22
|
+
rstest.mock('@modern-js/runtime/rsc/client', () => {
|
|
23
|
+
const callServer = () => undefined;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
callServer,
|
|
27
|
+
createFromReadableStream: (
|
|
28
|
+
_stream: ReadableStream<Uint8Array>,
|
|
29
|
+
options: { callServer: unknown },
|
|
30
|
+
) => {
|
|
31
|
+
decoderCallServer = options.callServer;
|
|
32
|
+
return createFromReadableStreamImpl();
|
|
33
|
+
},
|
|
34
|
+
setServerCallback: (callback: unknown) => {
|
|
35
|
+
registeredCallServer = callback;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
18
40
|
function createDeferred<T>() {
|
|
19
41
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
20
42
|
let reject!: (reason?: unknown) => void;
|
|
@@ -37,6 +59,41 @@ function createRouter(label: string): AnyRouter {
|
|
|
37
59
|
} as unknown as AnyRouter;
|
|
38
60
|
}
|
|
39
61
|
|
|
62
|
+
function FlightTree({ getTree }: { getTree: () => unknown }) {
|
|
63
|
+
const tree = getTree();
|
|
64
|
+
if (
|
|
65
|
+
typeof tree !== 'object' ||
|
|
66
|
+
tree === null ||
|
|
67
|
+
!('label' in tree) ||
|
|
68
|
+
typeof tree.label !== 'string'
|
|
69
|
+
) {
|
|
70
|
+
throw new Error('Expected the decoded Flight tree to contain a label.');
|
|
71
|
+
}
|
|
72
|
+
return <div data-testid="flight-tree">{tree.label}</div>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class FlightErrorBoundary extends React.Component<
|
|
76
|
+
{ children: React.ReactNode; onError: (error: unknown) => void },
|
|
77
|
+
{ error: unknown }
|
|
78
|
+
> {
|
|
79
|
+
state = { error: undefined };
|
|
80
|
+
|
|
81
|
+
static getDerivedStateFromError(error: unknown) {
|
|
82
|
+
return { error };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
componentDidCatch(error: unknown) {
|
|
86
|
+
this.props.onError(error);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
render() {
|
|
90
|
+
if (this.state.error !== undefined) {
|
|
91
|
+
return <div data-testid="flight-error">failed</div>;
|
|
92
|
+
}
|
|
93
|
+
return this.props.children;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
40
97
|
describe('TanStack SSR client hydration', () => {
|
|
41
98
|
it('keeps the router behind Suspense until late SSR hydration completes', async () => {
|
|
42
99
|
const hydration = createDeferred<void>();
|
|
@@ -58,4 +115,86 @@ describe('TanStack SSR client hydration', () => {
|
|
|
58
115
|
).toContain('inventory');
|
|
59
116
|
expect(screen.queryByTestId('hydrating')).toBeNull();
|
|
60
117
|
});
|
|
118
|
+
|
|
119
|
+
it('reuses one Flight decode while suspended and returns the resolved tree synchronously', async () => {
|
|
120
|
+
const flight = createDeferred<unknown>();
|
|
121
|
+
const decodeFlight = rstest.fn(() => flight.promise);
|
|
122
|
+
createFromReadableStreamImpl = decodeFlight;
|
|
123
|
+
registeredCallServer = undefined;
|
|
124
|
+
decoderCallServer = undefined;
|
|
125
|
+
|
|
126
|
+
const getTree = createTreeGetterFromFlightStream(
|
|
127
|
+
new ReadableStream<Uint8Array>(),
|
|
128
|
+
);
|
|
129
|
+
const view = (
|
|
130
|
+
<Suspense fallback={<div data-testid="flight-loading">loading</div>}>
|
|
131
|
+
<FlightTree getTree={getTree} />
|
|
132
|
+
</Suspense>
|
|
133
|
+
);
|
|
134
|
+
const { rerender } = render(view);
|
|
135
|
+
|
|
136
|
+
expect(screen.getByTestId('flight-loading')).toBeTruthy();
|
|
137
|
+
await waitFor(() => expect(decodeFlight).toHaveBeenCalledTimes(1));
|
|
138
|
+
expect(registeredCallServer).toBe(decoderCallServer);
|
|
139
|
+
|
|
140
|
+
await act(() => rerender(view));
|
|
141
|
+
expect(screen.getByTestId('flight-loading')).toBeTruthy();
|
|
142
|
+
expect(decodeFlight).toHaveBeenCalledTimes(1);
|
|
143
|
+
|
|
144
|
+
const tree = { label: 'decoded inventory' };
|
|
145
|
+
await act(async () => {
|
|
146
|
+
flight.resolve(tree);
|
|
147
|
+
await flight.promise;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect((await screen.findByTestId('flight-tree')).textContent).toBe(
|
|
151
|
+
tree.label,
|
|
152
|
+
);
|
|
153
|
+
expect(decodeFlight).toHaveBeenCalledTimes(1);
|
|
154
|
+
expect(getTree()).toBe(tree);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('preserves the original Flight decode rejection for the error boundary', async () => {
|
|
158
|
+
const flight = createDeferred<unknown>();
|
|
159
|
+
const failure = new Error('Flight decode failed');
|
|
160
|
+
const decodeFlight = rstest.fn(() => flight.promise);
|
|
161
|
+
createFromReadableStreamImpl = decodeFlight;
|
|
162
|
+
let caughtError: unknown;
|
|
163
|
+
const errorLog = rstest
|
|
164
|
+
.spyOn(console, 'error')
|
|
165
|
+
.mockImplementation(() => undefined);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const getTree = createTreeGetterFromFlightStream(
|
|
169
|
+
new ReadableStream<Uint8Array>(),
|
|
170
|
+
);
|
|
171
|
+
await act(() =>
|
|
172
|
+
render(
|
|
173
|
+
<FlightErrorBoundary
|
|
174
|
+
onError={error => {
|
|
175
|
+
caughtError = error;
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
<Suspense
|
|
179
|
+
fallback={<div data-testid="flight-loading">loading</div>}
|
|
180
|
+
>
|
|
181
|
+
<FlightTree getTree={getTree} />
|
|
182
|
+
</Suspense>
|
|
183
|
+
</FlightErrorBoundary>,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(screen.getByTestId('flight-loading')).toBeTruthy();
|
|
188
|
+
await waitFor(() => expect(decodeFlight).toHaveBeenCalledTimes(1));
|
|
189
|
+
await act(async () => {
|
|
190
|
+
flight.reject(failure);
|
|
191
|
+
await flight.promise.catch(() => undefined);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(await screen.findByTestId('flight-error')).toBeTruthy();
|
|
195
|
+
expect(caughtError).toBe(failure);
|
|
196
|
+
} finally {
|
|
197
|
+
errorLog.mockRestore();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
61
200
|
});
|
|
@@ -34,15 +34,21 @@ function createRouter(handler: {
|
|
|
34
34
|
action?: RouteHandler;
|
|
35
35
|
loader?: RouteHandler;
|
|
36
36
|
invalidate?: () => Promise<void>;
|
|
37
|
+
params?: Record<string, string>;
|
|
37
38
|
}) {
|
|
38
39
|
return {
|
|
39
|
-
buildLocation: ({ to }: { to?: string }) =>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
buildLocation: ({ to }: { to?: string }) => {
|
|
41
|
+
const href = typeof to === 'string' ? to : '/';
|
|
42
|
+
const url = new URL(href, 'http://localhost');
|
|
43
|
+
return {
|
|
44
|
+
href: `${url.pathname}${url.search}${url.hash}`,
|
|
45
|
+
pathname: url.pathname,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
getMatchedRoutes: () => [
|
|
49
|
+
[],
|
|
50
|
+
handler.params || {},
|
|
51
|
+
{
|
|
46
52
|
options: {
|
|
47
53
|
staticData: {
|
|
48
54
|
modernRouteAction: handler.action,
|
|
@@ -50,8 +56,7 @@ function createRouter(handler: {
|
|
|
50
56
|
},
|
|
51
57
|
},
|
|
52
58
|
},
|
|
53
|
-
|
|
54
|
-
}),
|
|
59
|
+
],
|
|
55
60
|
navigate: rstest.fn(async () => undefined),
|
|
56
61
|
invalidate: rstest.fn(handler.invalidate || (async () => undefined)),
|
|
57
62
|
};
|
|
@@ -199,6 +204,42 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
199
204
|
expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
|
|
200
205
|
});
|
|
201
206
|
|
|
207
|
+
test('passes built href and matched route params to mutation actions', async () => {
|
|
208
|
+
const action = rstest.fn(
|
|
209
|
+
async ({ request, params }: Parameters<RouteHandler>[0]) => ({
|
|
210
|
+
params,
|
|
211
|
+
requestUrl: request.url,
|
|
212
|
+
}),
|
|
213
|
+
);
|
|
214
|
+
currentRouter = createRouter({
|
|
215
|
+
action,
|
|
216
|
+
params: { userId: '42' },
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
render(<FetcherHarness />);
|
|
220
|
+
|
|
221
|
+
await act(async () => {
|
|
222
|
+
await latestFetcher!.submit(
|
|
223
|
+
{ amount: 2 },
|
|
224
|
+
{
|
|
225
|
+
method: 'post',
|
|
226
|
+
action: '/users/42?tab=edit#details',
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(action).toHaveBeenCalledTimes(1);
|
|
232
|
+
const actionArgs = action.mock.calls[0][0];
|
|
233
|
+
const expectedRequestUrl = `${window.location.origin}/users/42?tab=edit#details`;
|
|
234
|
+
expect(actionArgs.params).toEqual({ userId: '42' });
|
|
235
|
+
expect(actionArgs.request.url).toBe(expectedRequestUrl);
|
|
236
|
+
expect(latestFetcher?.data).toEqual({
|
|
237
|
+
params: { userId: '42' },
|
|
238
|
+
requestUrl: expectedRequestUrl,
|
|
239
|
+
});
|
|
240
|
+
expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
|
|
241
|
+
});
|
|
242
|
+
|
|
202
243
|
test('surfaces non-2xx action responses as fetcher errors', async () => {
|
|
203
244
|
const actionResult = createDeferred<Response>();
|
|
204
245
|
currentRouter = createRouter({
|