@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.30 → 3.5.0-ultramodern.32
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 +157 -0
- package/dist/cjs/cli/index.js +6 -103
- package/dist/cjs/cli/tanstackTypes/canonicalRoutes.js +113 -0
- package/dist/cjs/cli/tanstackTypes/shared.js +130 -0
- package/dist/cjs/cli/tanstackTypes/source.js +251 -0
- package/dist/cjs/cli/tanstackTypes.js +4 -350
- package/dist/cjs/runtime/blockingSubscribe.js +64 -0
- package/dist/cjs/runtime/{pluginCore.js → clientBasepath.js} +12 -19
- package/dist/cjs/runtime/clientHydration.js +129 -0
- package/dist/cjs/runtime/dataMutation.js +11 -176
- package/dist/cjs/runtime/formData.js +116 -0
- package/dist/cjs/runtime/loaderBridge.js +4 -1
- package/dist/cjs/runtime/plugin.js +21 -125
- package/dist/cjs/runtime/plugin.node.js +38 -119
- package/dist/cjs/runtime/pluginShared.js +90 -0
- package/dist/cjs/runtime/routeTree/components.js +102 -0
- package/dist/cjs/runtime/routeTree/construction.js +152 -0
- package/dist/cjs/runtime/routeTree/index.js +47 -0
- package/dist/cjs/runtime/routeTree/loaders.js +158 -0
- package/dist/cjs/runtime/routeTree/paths.js +57 -0
- package/dist/cjs/runtime/routeTree/staticData.js +63 -0
- package/dist/cjs/runtime/routeTree/tanstackRoutes.js +57 -0
- package/dist/cjs/runtime/routeTree/types.js +18 -0
- package/dist/cjs/runtime/routeTree.js +7 -320
- package/dist/cjs/runtime/rsc/CompositeComponent.js +18 -13
- package/dist/cjs/runtime/rsc/RscNodeRenderer.js +5 -16
- package/dist/cjs/runtime/rsc/client.js +8 -25
- package/dist/cjs/runtime/rsc/flightSerialization.js +132 -0
- package/dist/cjs/runtime/rsc/payloadErrors.js +99 -0
- package/dist/cjs/runtime/rsc/payloadFetch.js +117 -0
- package/dist/cjs/runtime/rsc/payloadRouter.js +21 -142
- package/dist/cjs/runtime/rsc/payloadRoutes.js +106 -0
- package/dist/cjs/runtime/rsc/payloadStorage.js +58 -0
- package/dist/cjs/runtime/rsc/server.js +14 -18
- package/dist/cjs/runtime/rsc/shared.js +71 -0
- package/dist/cjs/runtime/rsc/treeGetter.js +72 -0
- package/dist/cjs/runtime/ssrManagedTags.js +103 -0
- package/dist/cjs/runtime/ssrPreload.js +89 -0
- package/dist/cjs/runtime/ssrTypes.js +18 -0
- package/dist/cjs/runtime/submitAction.js +161 -0
- package/dist/cjs/runtime/utils.js +49 -86
- package/dist/esm/cli/artifacts.mjs +103 -0
- package/dist/esm/cli/index.mjs +4 -101
- package/dist/esm/cli/tanstackTypes/canonicalRoutes.mjs +75 -0
- package/dist/esm/cli/tanstackTypes/shared.mjs +68 -0
- package/dist/esm/cli/tanstackTypes/source.mjs +203 -0
- package/dist/esm/cli/tanstackTypes.mjs +2 -339
- package/dist/esm/runtime/blockingSubscribe.mjs +19 -0
- package/dist/esm/runtime/clientBasepath.mjs +10 -0
- package/dist/esm/runtime/clientHydration.mjs +70 -0
- package/dist/esm/runtime/dataMutation.mjs +2 -167
- package/dist/esm/runtime/formData.mjs +63 -0
- package/dist/esm/runtime/loaderBridge.mjs +4 -1
- package/dist/esm/runtime/plugin.mjs +14 -118
- package/dist/esm/runtime/plugin.node.mjs +33 -114
- package/dist/esm/runtime/pluginShared.mjs +40 -0
- package/dist/esm/runtime/routeTree/components.mjs +55 -0
- package/dist/esm/runtime/routeTree/construction.mjs +111 -0
- package/dist/esm/runtime/routeTree/index.mjs +2 -0
- package/dist/esm/runtime/routeTree/loaders.mjs +117 -0
- package/dist/esm/runtime/routeTree/paths.mjs +16 -0
- package/dist/esm/runtime/routeTree/staticData.mjs +22 -0
- package/dist/esm/runtime/routeTree/tanstackRoutes.mjs +13 -0
- package/dist/esm/runtime/routeTree/types.mjs +0 -0
- package/dist/esm/runtime/routeTree.mjs +1 -318
- package/dist/esm/runtime/rsc/CompositeComponent.mjs +18 -13
- package/dist/esm/runtime/rsc/RscNodeRenderer.mjs +4 -15
- package/dist/esm/runtime/rsc/client.mjs +6 -26
- package/dist/esm/runtime/rsc/flightSerialization.mjs +88 -0
- package/dist/esm/runtime/rsc/payloadErrors.mjs +49 -0
- package/dist/esm/runtime/rsc/payloadFetch.mjs +61 -0
- package/dist/esm/runtime/rsc/payloadRouter.mjs +12 -133
- package/dist/esm/runtime/rsc/payloadRoutes.mjs +50 -0
- package/dist/esm/runtime/rsc/payloadStorage.mjs +14 -0
- package/dist/esm/runtime/rsc/server.mjs +17 -21
- package/dist/esm/runtime/rsc/shared.mjs +21 -0
- package/dist/esm/runtime/rsc/treeGetter.mjs +34 -0
- package/dist/esm/runtime/ssrManagedTags.mjs +53 -0
- package/dist/esm/runtime/ssrPreload.mjs +51 -0
- package/dist/esm/runtime/ssrTypes.mjs +0 -0
- package/dist/esm/runtime/submitAction.mjs +120 -0
- package/dist/esm/runtime/utils.mjs +47 -75
- package/dist/esm-node/cli/artifacts.mjs +104 -0
- package/dist/esm-node/cli/index.mjs +4 -101
- package/dist/esm-node/cli/tanstackTypes/canonicalRoutes.mjs +76 -0
- package/dist/esm-node/cli/tanstackTypes/shared.mjs +69 -0
- package/dist/esm-node/cli/tanstackTypes/source.mjs +204 -0
- package/dist/esm-node/cli/tanstackTypes.mjs +2 -339
- package/dist/esm-node/runtime/blockingSubscribe.mjs +20 -0
- package/dist/esm-node/runtime/clientBasepath.mjs +11 -0
- package/dist/esm-node/runtime/clientHydration.mjs +71 -0
- package/dist/esm-node/runtime/dataMutation.mjs +2 -167
- package/dist/esm-node/runtime/formData.mjs +64 -0
- package/dist/esm-node/runtime/loaderBridge.mjs +4 -1
- package/dist/esm-node/runtime/plugin.mjs +14 -118
- package/dist/esm-node/runtime/plugin.node.mjs +33 -114
- package/dist/esm-node/runtime/pluginShared.mjs +41 -0
- package/dist/esm-node/runtime/routeTree/components.mjs +56 -0
- package/dist/esm-node/runtime/routeTree/construction.mjs +112 -0
- package/dist/esm-node/runtime/routeTree/index.mjs +3 -0
- package/dist/esm-node/runtime/routeTree/loaders.mjs +118 -0
- package/dist/esm-node/runtime/routeTree/paths.mjs +17 -0
- package/dist/esm-node/runtime/routeTree/staticData.mjs +23 -0
- package/dist/esm-node/runtime/routeTree/tanstackRoutes.mjs +14 -0
- package/dist/esm-node/runtime/routeTree/types.mjs +1 -0
- package/dist/esm-node/runtime/routeTree.mjs +1 -318
- package/dist/esm-node/runtime/rsc/CompositeComponent.mjs +18 -13
- package/dist/esm-node/runtime/rsc/RscNodeRenderer.mjs +4 -15
- package/dist/esm-node/runtime/rsc/client.mjs +6 -26
- package/dist/esm-node/runtime/rsc/flightSerialization.mjs +89 -0
- package/dist/esm-node/runtime/rsc/payloadErrors.mjs +50 -0
- package/dist/esm-node/runtime/rsc/payloadFetch.mjs +62 -0
- package/dist/esm-node/runtime/rsc/payloadRouter.mjs +12 -133
- package/dist/esm-node/runtime/rsc/payloadRoutes.mjs +51 -0
- package/dist/esm-node/runtime/rsc/payloadStorage.mjs +15 -0
- package/dist/esm-node/runtime/rsc/server.mjs +17 -21
- package/dist/esm-node/runtime/rsc/shared.mjs +22 -0
- package/dist/esm-node/runtime/rsc/treeGetter.mjs +35 -0
- package/dist/esm-node/runtime/ssrManagedTags.mjs +54 -0
- package/dist/esm-node/runtime/ssrPreload.mjs +52 -0
- package/dist/esm-node/runtime/ssrTypes.mjs +1 -0
- package/dist/esm-node/runtime/submitAction.mjs +121 -0
- package/dist/esm-node/runtime/utils.mjs +47 -75
- package/dist/types/cli/artifacts.d.ts +33 -0
- package/dist/types/cli/index.d.ts +2 -33
- package/dist/types/cli/tanstackTypes/canonicalRoutes.d.ts +22 -0
- package/dist/types/cli/tanstackTypes/shared.d.ts +20 -0
- package/dist/types/cli/tanstackTypes/source.d.ts +10 -0
- package/dist/types/cli/tanstackTypes.d.ts +3 -31
- package/dist/types/runtime/blockingSubscribe.d.ts +3 -0
- package/dist/types/runtime/clientBasepath.d.ts +1 -0
- package/dist/types/runtime/clientHydration.d.ts +19 -0
- package/dist/types/runtime/dataMutation.d.ts +3 -12
- package/dist/types/runtime/formData.d.ts +23 -0
- package/dist/types/runtime/plugin.d.ts +1 -1
- package/dist/types/runtime/plugin.node.d.ts +1 -1
- package/dist/types/runtime/{pluginCore.d.ts → pluginShared.d.ts} +14 -0
- package/dist/types/runtime/routeTree/components.d.ts +7 -0
- package/dist/types/runtime/routeTree/construction.d.ts +5 -0
- package/dist/types/runtime/routeTree/index.d.ts +2 -0
- package/dist/types/runtime/routeTree/loaders.d.ts +4 -0
- package/dist/types/runtime/routeTree/paths.d.ts +3 -0
- package/dist/types/runtime/routeTree/staticData.d.ts +19 -0
- package/dist/types/runtime/routeTree/tanstackRoutes.d.ts +4 -0
- package/dist/types/runtime/routeTree/types.d.ts +75 -0
- package/dist/types/runtime/routeTree.d.ts +1 -9
- package/dist/types/runtime/rsc/CompositeComponent.d.ts +1 -2
- package/dist/types/runtime/rsc/RscNodeRenderer.d.ts +2 -3
- package/dist/types/runtime/rsc/client.d.ts +2 -0
- package/dist/types/runtime/rsc/flightSerialization.d.ts +3 -0
- package/dist/types/runtime/rsc/payloadErrors.d.ts +6 -0
- package/dist/types/runtime/rsc/payloadFetch.d.ts +10 -0
- package/dist/types/runtime/rsc/payloadRouter.d.ts +2 -10
- package/dist/types/runtime/rsc/payloadRoutes.d.ts +43 -0
- package/dist/types/runtime/rsc/payloadStorage.d.ts +5 -0
- package/dist/types/runtime/rsc/shared.d.ts +19 -0
- package/dist/types/runtime/rsc/treeGetter.d.ts +1 -0
- package/dist/types/runtime/ssrManagedTags.d.ts +7 -0
- package/dist/types/runtime/ssrPreload.d.ts +2 -0
- package/dist/types/runtime/ssrTypes.d.ts +43 -0
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/submitAction.d.ts +14 -0
- package/dist/types/runtime/types.d.ts +3 -46
- package/dist/types/runtime/utils.d.ts +1 -22
- package/package.json +30 -9
- package/rstest.config.mts +1 -0
- package/src/cli/artifacts.ts +255 -0
- package/src/cli/index.ts +6 -246
- package/src/cli/tanstackTypes/canonicalRoutes.ts +157 -0
- package/src/cli/tanstackTypes/shared.ts +119 -0
- package/src/cli/tanstackTypes/source.ts +405 -0
- package/src/cli/tanstackTypes.ts +4 -647
- package/src/runtime/blockingSubscribe.ts +48 -0
- package/src/runtime/clientBasepath.ts +12 -0
- package/src/runtime/clientHydration.ts +151 -0
- package/src/runtime/dataMutation.tsx +9 -306
- package/src/runtime/formData.ts +153 -0
- package/src/runtime/loaderBridge.ts +8 -3
- package/src/runtime/plugin.node.tsx +60 -284
- package/src/runtime/plugin.tsx +21 -257
- package/src/runtime/pluginShared.ts +112 -0
- package/src/runtime/routeTree/components.ts +130 -0
- package/src/runtime/routeTree/construction.ts +238 -0
- package/src/runtime/routeTree/index.ts +7 -0
- package/src/runtime/routeTree/loaders.ts +232 -0
- package/src/runtime/routeTree/paths.ts +36 -0
- package/src/runtime/routeTree/staticData.ts +66 -0
- package/src/runtime/routeTree/tanstackRoutes.ts +36 -0
- package/src/runtime/routeTree/types.ts +87 -0
- package/src/runtime/routeTree.ts +5 -750
- package/src/runtime/rsc/CompositeComponent.tsx +29 -20
- package/src/runtime/rsc/RscNodeRenderer.tsx +9 -27
- package/src/runtime/rsc/client.tsx +12 -40
- package/src/runtime/rsc/flightSerialization.ts +158 -0
- package/src/runtime/rsc/payloadErrors.ts +73 -0
- package/src/runtime/rsc/payloadFetch.ts +97 -0
- package/src/runtime/rsc/payloadRouter.ts +29 -234
- package/src/runtime/rsc/payloadRoutes.ts +122 -0
- package/src/runtime/rsc/payloadStorage.ts +35 -0
- package/src/runtime/rsc/server.tsx +20 -29
- package/src/runtime/rsc/shared.ts +70 -0
- package/src/runtime/rsc/treeGetter.tsx +52 -0
- package/src/runtime/ssrManagedTags.ts +107 -0
- package/src/runtime/ssrPreload.ts +125 -0
- package/src/runtime/ssrTypes.ts +51 -0
- package/src/runtime/state.ts +4 -2
- package/src/runtime/submitAction.ts +194 -0
- package/src/runtime/types.ts +14 -51
- package/src/runtime/utils.tsx +72 -108
- package/src/types/react-server-dom-rspack.d.ts +56 -0
- package/tests/castRatchet.test.ts +85 -0
- package/tests/router/dataMutation.test.tsx +59 -0
- package/tests/router/fixtures/router-gen.golden.txt +72 -0
- package/tests/router/loaderBridge.test.ts +21 -0
- package/tests/router/routeTree.test.ts +51 -11
- package/tests/router/rsc.test.tsx +189 -0
- package/tests/router/ssrPreload.test.ts +30 -0
- package/tests/router/tanstackTypes.test.ts +254 -1
- package/dist/esm/runtime/pluginCore.mjs +0 -14
- package/dist/esm-node/runtime/pluginCore.mjs +0 -15
- package/src/runtime/pluginCore.ts +0 -56
|
@@ -2,6 +2,10 @@ import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtim
|
|
|
2
2
|
import type React from 'react';
|
|
3
3
|
import { isValidElement } from 'react';
|
|
4
4
|
import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
|
|
5
|
+
import {
|
|
6
|
+
reviveTanstackRscFlightValues,
|
|
7
|
+
serializeTanstackRscFlightValues,
|
|
8
|
+
} from '../../src/runtime/rsc/flightSerialization';
|
|
5
9
|
import {
|
|
6
10
|
__setTanstackRscPayloadDecoderForTests,
|
|
7
11
|
createTanstackRscServerPayload,
|
|
@@ -65,6 +69,54 @@ describe('tanstack rsc runtime helpers', () => {
|
|
|
65
69
|
]);
|
|
66
70
|
});
|
|
67
71
|
|
|
72
|
+
test('revives shared structures that the Flight serializer preserves', async () => {
|
|
73
|
+
const shared = { label: 'shared' };
|
|
74
|
+
const serialized = serializeTanstackRscFlightValues({
|
|
75
|
+
left: shared,
|
|
76
|
+
right: shared,
|
|
77
|
+
}) as { left: unknown; right: unknown };
|
|
78
|
+
|
|
79
|
+
const revived = (await reviveTanstackRscFlightValues(serialized)) as {
|
|
80
|
+
left: unknown;
|
|
81
|
+
right: unknown;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
expect(serialized.left).toBe(serialized.right);
|
|
85
|
+
expect(revived.left).toBe(revived.right);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('serializes TanStack RSC flight values nested in map and set containers', () => {
|
|
89
|
+
const stream: ServerComponentStream = {
|
|
90
|
+
createReplayStream: () => new ReadableStream<Uint8Array>(),
|
|
91
|
+
};
|
|
92
|
+
const proxy = createRscProxy(() => null, {
|
|
93
|
+
renderable: true,
|
|
94
|
+
stream,
|
|
95
|
+
}) as React.ReactElement & Record<PropertyKey, unknown>;
|
|
96
|
+
|
|
97
|
+
const serialized = serializeTanstackRscFlightValues({
|
|
98
|
+
map: new Map([['slot', proxy]]),
|
|
99
|
+
set: new Set([proxy]),
|
|
100
|
+
}) as {
|
|
101
|
+
map: Map<string, Record<string, unknown>>;
|
|
102
|
+
set: Set<Record<string, unknown>>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const mapValue = serialized.map.get('slot');
|
|
106
|
+
const [setValue] = serialized.set;
|
|
107
|
+
|
|
108
|
+
expect(mapValue).not.toBe(proxy);
|
|
109
|
+
expect(setValue).not.toBe(proxy);
|
|
110
|
+
expect(mapValue).toMatchObject({
|
|
111
|
+
__modernTanstackRsc: true,
|
|
112
|
+
kind: 'renderable',
|
|
113
|
+
});
|
|
114
|
+
expect(setValue).toMatchObject({
|
|
115
|
+
__modernTanstackRsc: true,
|
|
116
|
+
kind: 'renderable',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
68
120
|
test('renderable RSC proxies preserve React element behavior and metadata', () => {
|
|
69
121
|
const stream: ServerComponentStream = {
|
|
70
122
|
createReplayStream: () => new ReadableStream<Uint8Array>(),
|
|
@@ -219,6 +271,36 @@ describe('tanstack rsc runtime helpers', () => {
|
|
|
219
271
|
expect(JSON.stringify(payload.errors)).not.toContain('stack secret');
|
|
220
272
|
});
|
|
221
273
|
|
|
274
|
+
test('redacts production TanStack RSC non-Error server payload errors', () => {
|
|
275
|
+
const payload = withNodeEnv('production', () =>
|
|
276
|
+
createTanstackRscServerPayload({
|
|
277
|
+
state: {
|
|
278
|
+
location: { href: '/plain' },
|
|
279
|
+
matches: [
|
|
280
|
+
{
|
|
281
|
+
error: { message: 'plain secret', token: 'token secret' },
|
|
282
|
+
params: {},
|
|
283
|
+
pathname: '/plain',
|
|
284
|
+
pathnameBase: '/plain',
|
|
285
|
+
route: { id: '/plain' },
|
|
286
|
+
routeId: '/plain',
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(payload.errors).toMatchObject({
|
|
294
|
+
'/plain': {
|
|
295
|
+
message: 'Unexpected Server Error',
|
|
296
|
+
stack: undefined,
|
|
297
|
+
__type: 'Error',
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
expect(JSON.stringify(payload.errors)).not.toContain('plain secret');
|
|
301
|
+
expect(JSON.stringify(payload.errors)).not.toContain('token secret');
|
|
302
|
+
});
|
|
303
|
+
|
|
222
304
|
test('converts TanStack RSC redirects to Modern RSC navigation headers', () => {
|
|
223
305
|
const response = handleTanstackRscRedirect(
|
|
224
306
|
new Headers({ Location: '/base/login' }),
|
|
@@ -232,6 +314,54 @@ describe('tanstack rsc runtime helpers', () => {
|
|
|
232
314
|
expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
|
|
233
315
|
});
|
|
234
316
|
|
|
317
|
+
test('preserves TanStack RSC redirect paths that do not start with the basename', () => {
|
|
318
|
+
const response = handleTanstackRscRedirect(
|
|
319
|
+
new Headers({ Location: '/shop/base/login' }),
|
|
320
|
+
'/base',
|
|
321
|
+
302,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(response.headers.get('X-Modernjs-Redirect')).toBe(
|
|
325
|
+
'/shop/base/login',
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('preserves RSC redirect response status and headers in TanStack redirects', async () => {
|
|
330
|
+
rstest.stubGlobal(
|
|
331
|
+
'fetch',
|
|
332
|
+
rstest.fn(() =>
|
|
333
|
+
Promise.resolve(
|
|
334
|
+
new Response(null, {
|
|
335
|
+
headers: {
|
|
336
|
+
'X-Modernjs-Redirect': '/login',
|
|
337
|
+
'X-Trace': 'preserved',
|
|
338
|
+
},
|
|
339
|
+
status: 308,
|
|
340
|
+
}),
|
|
341
|
+
),
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const thrown = (await loadTanstackRscRouteData({
|
|
346
|
+
loadClientData: async () => ({ fallback: true }),
|
|
347
|
+
request: new Request('http://localhost/products'),
|
|
348
|
+
routeId: '/products',
|
|
349
|
+
}).then(
|
|
350
|
+
() => {
|
|
351
|
+
throw new Error('expected redirect');
|
|
352
|
+
},
|
|
353
|
+
err => err,
|
|
354
|
+
)) as Response & {
|
|
355
|
+
options?: { statusCode?: number; to?: string };
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
expect(thrown).toBeInstanceOf(Response);
|
|
359
|
+
expect(thrown.status).toBe(308);
|
|
360
|
+
expect(thrown.headers.get('X-Trace')).toBe('preserved');
|
|
361
|
+
expect(thrown.options?.statusCode).toBe(308);
|
|
362
|
+
expect(thrown.options?.to).toBe('/login');
|
|
363
|
+
});
|
|
364
|
+
|
|
235
365
|
test('loads one RSC payload for multiple server route loaders', async () => {
|
|
236
366
|
const payload = {
|
|
237
367
|
type: 'render',
|
|
@@ -275,6 +405,65 @@ describe('tanstack rsc runtime helpers', () => {
|
|
|
275
405
|
expect(decodeMock).toHaveBeenCalledTimes(1);
|
|
276
406
|
});
|
|
277
407
|
|
|
408
|
+
test('separates RSC payload cache entries by forwarded request headers', async () => {
|
|
409
|
+
const payloadByUser: Record<string, unknown> = {
|
|
410
|
+
alice: {
|
|
411
|
+
type: 'render',
|
|
412
|
+
actionData: null,
|
|
413
|
+
errors: null,
|
|
414
|
+
loaderData: {
|
|
415
|
+
'/profile': { user: 'alice' },
|
|
416
|
+
},
|
|
417
|
+
location: { href: '/profile' },
|
|
418
|
+
routes: [{ id: '/profile', hasLoader: true }],
|
|
419
|
+
},
|
|
420
|
+
bob: {
|
|
421
|
+
type: 'render',
|
|
422
|
+
actionData: null,
|
|
423
|
+
errors: null,
|
|
424
|
+
loaderData: {
|
|
425
|
+
'/profile': { user: 'bob' },
|
|
426
|
+
},
|
|
427
|
+
location: { href: '/profile' },
|
|
428
|
+
routes: [{ id: '/profile', hasLoader: true }],
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
const fetchMock = rstest.fn(
|
|
432
|
+
(_url: string | URL | Request, init?: RequestInit) => {
|
|
433
|
+
const user =
|
|
434
|
+
init?.headers instanceof Headers ? init.headers.get('x-user') : null;
|
|
435
|
+
return Promise.resolve(
|
|
436
|
+
new Response(JSON.stringify(payloadByUser[user || ''])),
|
|
437
|
+
);
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
rstest.stubGlobal('fetch', fetchMock);
|
|
441
|
+
__setTanstackRscPayloadDecoderForTests(async stream =>
|
|
442
|
+
JSON.parse(await new Response(stream).text()),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
const [aliceData, bobData] = await Promise.all([
|
|
446
|
+
loadTanstackRscRouteData({
|
|
447
|
+
loadClientData: async () => ({ fallback: 'alice' }),
|
|
448
|
+
request: new Request('http://localhost/profile', {
|
|
449
|
+
headers: { 'x-user': 'alice' },
|
|
450
|
+
}),
|
|
451
|
+
routeId: '/profile',
|
|
452
|
+
}),
|
|
453
|
+
loadTanstackRscRouteData({
|
|
454
|
+
loadClientData: async () => ({ fallback: 'bob' }),
|
|
455
|
+
request: new Request('http://localhost/profile', {
|
|
456
|
+
headers: { 'x-user': 'bob' },
|
|
457
|
+
}),
|
|
458
|
+
routeId: '/profile',
|
|
459
|
+
}),
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
expect(aliceData).toEqual({ user: 'alice' });
|
|
463
|
+
expect(bobData).toEqual({ user: 'bob' });
|
|
464
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
278
467
|
test('uses client loader data without requesting an RSC payload for client-loader routes', async () => {
|
|
279
468
|
const fetchMock = rstest.fn();
|
|
280
469
|
const loadClientData = rstest.fn(async () => ({ client: true }));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { preloadMatchedRouteComponents } from '../../src/runtime/ssrPreload';
|
|
2
|
+
|
|
3
|
+
describe('preloadMatchedRouteComponents', () => {
|
|
4
|
+
test('preloads each component identity once per SSR pass', async () => {
|
|
5
|
+
const sharedComponent = Object.assign(() => null, {
|
|
6
|
+
preload: rstest.fn(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
await preloadMatchedRouteComponents({
|
|
10
|
+
routesById: {
|
|
11
|
+
child: {
|
|
12
|
+
options: {
|
|
13
|
+
component: sharedComponent,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
root: {
|
|
17
|
+
options: {
|
|
18
|
+
component: sharedComponent,
|
|
19
|
+
pendingComponent: sharedComponent,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
state: {
|
|
24
|
+
matches: [{ routeId: 'root' }, { routeId: 'child' }],
|
|
25
|
+
},
|
|
26
|
+
} as never);
|
|
27
|
+
|
|
28
|
+
expect(sharedComponent.preload).toHaveBeenCalledTimes(1);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -1,19 +1,163 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { isRedirect } from '@tanstack/react-router';
|
|
7
|
+
|
|
8
|
+
const resolverDelays = new Map<string, number>();
|
|
9
|
+
|
|
10
|
+
rstest.mock('../../src/cli/tanstackTypes/shared', () => {
|
|
11
|
+
const actual = rstest.requireActual(
|
|
12
|
+
'../../src/cli/tanstackTypes/shared',
|
|
13
|
+
) as typeof import('../../src/cli/tanstackTypes/shared');
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
resolveFileNoExt: async (inputNoExtPath: string) => {
|
|
18
|
+
const normalized = inputNoExtPath.replaceAll('\\', '/');
|
|
19
|
+
const delay =
|
|
20
|
+
[...resolverDelays.entries()].find(([suffix]) =>
|
|
21
|
+
normalized.endsWith(suffix),
|
|
22
|
+
)?.[1] ?? 0;
|
|
23
|
+
|
|
24
|
+
if (delay > 0) {
|
|
25
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return actual.resolveFileNoExt(inputNoExtPath);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
6
33
|
import {
|
|
7
34
|
collectCanonicalRoutesForEntry,
|
|
8
35
|
generateTanstackRouterTypesSourceForEntry,
|
|
9
36
|
} from '../../src/cli/tanstackTypes';
|
|
37
|
+
import { throwTanstackRedirect } from '../../src/runtime/loaderBridge';
|
|
10
38
|
|
|
11
39
|
const execFileAsync = promisify(execFile);
|
|
40
|
+
const routerGenGoldenPath = path.join(
|
|
41
|
+
__dirname,
|
|
42
|
+
'fixtures',
|
|
43
|
+
'router-gen.golden.txt',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
type RedirectLike = {
|
|
47
|
+
options?: {
|
|
48
|
+
href?: string;
|
|
49
|
+
to?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function catchThrown(fn: () => unknown): unknown {
|
|
54
|
+
try {
|
|
55
|
+
fn();
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return err;
|
|
58
|
+
}
|
|
59
|
+
throw new Error('expected function to throw');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractRouteGenerationOrder(source: string) {
|
|
63
|
+
return source.split('\n').filter(line => {
|
|
64
|
+
return (
|
|
65
|
+
line.startsWith('import component_') ||
|
|
66
|
+
line.startsWith('const route_') ||
|
|
67
|
+
line.includes('component: component_') ||
|
|
68
|
+
line.startsWith('export const routeTree')
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function writeGoldenRouterFixture(srcDirectory: string) {
|
|
74
|
+
const files = new Map([
|
|
75
|
+
[
|
|
76
|
+
'routes/(app)/layout.tsx',
|
|
77
|
+
'export default function AppLayout() { return null; }',
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
'routes/(app)/users/(userId)/page.tsx',
|
|
81
|
+
'export default function UserPage() { return null; }',
|
|
82
|
+
],
|
|
83
|
+
[
|
|
84
|
+
'routes/(app)/users/(userId)/page.data.ts',
|
|
85
|
+
[
|
|
86
|
+
'export const loader = () => ({ userId: "42" });',
|
|
87
|
+
'export const action = () => Response.json({ ok: true });',
|
|
88
|
+
].join('\n'),
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
'routes/(app)/docs/splat.tsx',
|
|
92
|
+
'export default function DocsSplatPage() { return null; }',
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
'routes/search.contract.ts',
|
|
96
|
+
[
|
|
97
|
+
'export const validateSearch = (search: { tab?: string }) => ({ tab: search.tab ?? "overview" });',
|
|
98
|
+
'export const loaderDeps = ({ search }: { search: { tab: string } }) => ({ tab: search.tab });',
|
|
99
|
+
].join('\n'),
|
|
100
|
+
],
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
for (const [relativePath, contents] of files) {
|
|
104
|
+
const filePath = path.join(srcDirectory, relativePath);
|
|
105
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
106
|
+
await writeFile(filePath, contents);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function generateGoldenRouterGen(srcDirectory: string) {
|
|
111
|
+
await writeGoldenRouterFixture(srcDirectory);
|
|
112
|
+
|
|
113
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
114
|
+
appContext: {
|
|
115
|
+
srcDirectory,
|
|
116
|
+
internalSrcAlias: '@/_',
|
|
117
|
+
} as any,
|
|
118
|
+
entryName: 'golden',
|
|
119
|
+
routes: [
|
|
120
|
+
{
|
|
121
|
+
type: 'nested',
|
|
122
|
+
id: 'layout',
|
|
123
|
+
isRoot: true,
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
type: 'nested',
|
|
127
|
+
id: '(app)/layout',
|
|
128
|
+
_component: '@/_/routes/(app)/layout',
|
|
129
|
+
children: [
|
|
130
|
+
{
|
|
131
|
+
type: 'nested',
|
|
132
|
+
id: '(app)/users/(userId)/page',
|
|
133
|
+
path: 'users/:userId',
|
|
134
|
+
_component: '@/_/routes/(app)/users/(userId)/page',
|
|
135
|
+
data: '@/_/routes/(app)/users/(userId)/page.data',
|
|
136
|
+
action: '@/_/routes/(app)/users/(userId)/page.data',
|
|
137
|
+
validateSearch: '@/_/routes/search.contract',
|
|
138
|
+
loaderDeps: '@/_/routes/search.contract',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: 'nested',
|
|
142
|
+
id: '(app)/docs/splat/page',
|
|
143
|
+
path: 'docs/*',
|
|
144
|
+
_component: '@/_/routes/(app)/docs/splat',
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
] as any,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return routerGenTs;
|
|
154
|
+
}
|
|
12
155
|
|
|
13
156
|
describe('tanstack router type generation', () => {
|
|
14
157
|
let tempDir: string | undefined;
|
|
15
158
|
|
|
16
159
|
afterEach(async () => {
|
|
160
|
+
resolverDelays.clear();
|
|
17
161
|
if (tempDir) {
|
|
18
162
|
await rm(tempDir, { recursive: true, force: true });
|
|
19
163
|
tempDir = undefined;
|
|
@@ -77,6 +221,15 @@ describe('tanstack router type generation', () => {
|
|
|
77
221
|
expect(routerGenTs).not.toContain('function modernLoaderToTanstack');
|
|
78
222
|
expect(routerGenTs).not.toContain('function createRouteStaticData');
|
|
79
223
|
expect(routerGenTs).not.toContain('function throwTanstackRedirect');
|
|
224
|
+
expect(routerGenTs).not.toContain('new URL(target)');
|
|
225
|
+
expect(routerGenTs).not.toContain('redirect({ to: target })');
|
|
226
|
+
|
|
227
|
+
const redirectError = catchThrown(() =>
|
|
228
|
+
throwTanstackRedirect('https://example.com/absolute'),
|
|
229
|
+
) as RedirectLike;
|
|
230
|
+
expect(isRedirect(redirectError)).toBe(true);
|
|
231
|
+
expect(redirectError.options?.href).toBe('https://example.com/absolute');
|
|
232
|
+
expect(redirectError.options?.to).toBeUndefined();
|
|
80
233
|
});
|
|
81
234
|
|
|
82
235
|
test('emits resolvable relative component imports for routes carrying _component', async () => {
|
|
@@ -167,6 +320,106 @@ describe('tanstack router type generation', () => {
|
|
|
167
320
|
expect(dataOnlyRoute).not.toContain('component:');
|
|
168
321
|
});
|
|
169
322
|
|
|
323
|
+
test('keeps generated source stable when route module resolution races', async () => {
|
|
324
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
325
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
326
|
+
|
|
327
|
+
for (const componentFile of [
|
|
328
|
+
'routes/slow-page.tsx',
|
|
329
|
+
'routes/fast-page.tsx',
|
|
330
|
+
'routes/shared-page.tsx',
|
|
331
|
+
]) {
|
|
332
|
+
const componentPath = path.join(srcDirectory, componentFile);
|
|
333
|
+
await mkdir(path.dirname(componentPath), { recursive: true });
|
|
334
|
+
await writeFile(
|
|
335
|
+
componentPath,
|
|
336
|
+
'export default function Page() { return null; }',
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const routes = [
|
|
341
|
+
{
|
|
342
|
+
type: 'nested',
|
|
343
|
+
id: 'layout',
|
|
344
|
+
isRoot: true,
|
|
345
|
+
children: [
|
|
346
|
+
{
|
|
347
|
+
type: 'nested',
|
|
348
|
+
id: 'slow/page',
|
|
349
|
+
path: 'slow',
|
|
350
|
+
_component: '@/_/routes/slow-page',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
type: 'nested',
|
|
354
|
+
id: 'fast/page',
|
|
355
|
+
path: 'fast',
|
|
356
|
+
_component: '@/_/routes/fast-page',
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
type: 'nested',
|
|
360
|
+
id: 'shared-a/page',
|
|
361
|
+
path: 'shared-a',
|
|
362
|
+
_component: '@/_/routes/shared-page',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
type: 'nested',
|
|
366
|
+
id: 'shared-b/page',
|
|
367
|
+
path: 'shared-b',
|
|
368
|
+
_component: '@/_/routes/shared-page',
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
] as any;
|
|
373
|
+
|
|
374
|
+
const generateWithResolverDelays = async (
|
|
375
|
+
delays: Record<string, number>,
|
|
376
|
+
) => {
|
|
377
|
+
resolverDelays.clear();
|
|
378
|
+
for (const [suffix, delay] of Object.entries(delays)) {
|
|
379
|
+
resolverDelays.set(suffix, delay);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const { routerGenTs } = await generateTanstackRouterTypesSourceForEntry({
|
|
383
|
+
appContext: {
|
|
384
|
+
srcDirectory,
|
|
385
|
+
internalSrcAlias: '@/_',
|
|
386
|
+
} as any,
|
|
387
|
+
entryName: 'index',
|
|
388
|
+
routes,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return routerGenTs;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const sequentialOutput = await generateWithResolverDelays({});
|
|
395
|
+
const racedOutput = await generateWithResolverDelays({
|
|
396
|
+
'/routes/slow-page': 25,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(
|
|
400
|
+
racedOutput.match(/from "\.\.\/\.\.\/routes\/shared-page";/g),
|
|
401
|
+
).toHaveLength(1);
|
|
402
|
+
expect(extractRouteGenerationOrder(racedOutput)).toEqual(
|
|
403
|
+
extractRouteGenerationOrder(sequentialOutput),
|
|
404
|
+
);
|
|
405
|
+
expect(racedOutput).toBe(sequentialOutput);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('matches checked-in router.gen golden output', async () => {
|
|
409
|
+
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
410
|
+
const srcDirectory = path.join(tempDir, 'src');
|
|
411
|
+
const routerGenTs = await generateGoldenRouterGen(srcDirectory);
|
|
412
|
+
|
|
413
|
+
if (process.env.UPDATE_TANSTACK_ROUTER_GOLDEN === '1') {
|
|
414
|
+
await mkdir(path.dirname(routerGenGoldenPath), { recursive: true });
|
|
415
|
+
await writeFile(routerGenGoldenPath, routerGenTs);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
await expect(readFile(routerGenGoldenPath, 'utf8')).resolves.toBe(
|
|
419
|
+
routerGenTs,
|
|
420
|
+
);
|
|
421
|
+
});
|
|
422
|
+
|
|
170
423
|
test('typechecks generated TanStack search contracts', async () => {
|
|
171
424
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-types-'));
|
|
172
425
|
const srcDirectory = path.join(tempDir, 'src');
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { getGlobalLayoutApp, getGlobalRoutes } from "@modern-js/runtime/context";
|
|
2
|
-
import { merge } from "@modern-js/runtime-utils/merge";
|
|
3
|
-
function getMergedRouterConfig(api, userConfig) {
|
|
4
|
-
const pluginConfig = api.getRuntimeConfig();
|
|
5
|
-
return merge(pluginConfig.router || {}, userConfig);
|
|
6
|
-
}
|
|
7
|
-
function getFinalRouteConfig(mergedConfig) {
|
|
8
|
-
return {
|
|
9
|
-
routes: getGlobalRoutes(),
|
|
10
|
-
globalApp: getGlobalLayoutApp(),
|
|
11
|
-
...mergedConfig.routesConfig
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export { getFinalRouteConfig, getMergedRouterConfig };
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "node:module";
|
|
2
|
-
import { getGlobalLayoutApp, getGlobalRoutes } from "@modern-js/runtime/context";
|
|
3
|
-
import { merge } from "@modern-js/runtime-utils/merge";
|
|
4
|
-
function getMergedRouterConfig(api, userConfig) {
|
|
5
|
-
const pluginConfig = api.getRuntimeConfig();
|
|
6
|
-
return merge(pluginConfig.router || {}, userConfig);
|
|
7
|
-
}
|
|
8
|
-
function getFinalRouteConfig(mergedConfig) {
|
|
9
|
-
return {
|
|
10
|
-
routes: getGlobalRoutes(),
|
|
11
|
-
globalApp: getGlobalLayoutApp(),
|
|
12
|
-
...mergedConfig.routesConfig
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
export { getFinalRouteConfig, getMergedRouterConfig };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getGlobalLayoutApp,
|
|
3
|
-
getGlobalRoutes,
|
|
4
|
-
type TInternalRuntimeContext,
|
|
5
|
-
} from '@modern-js/runtime/context';
|
|
6
|
-
import { merge } from '@modern-js/runtime-utils/merge';
|
|
7
|
-
import type { RouterExtendsHooks } from './hooks';
|
|
8
|
-
import type { RouterConfig } from './types';
|
|
9
|
-
|
|
10
|
-
export type TanstackRouterRuntimeConfig = {
|
|
11
|
-
plugins?: TanstackRouterRuntimePlugin[];
|
|
12
|
-
router?: Partial<RouterConfig>;
|
|
13
|
-
[key: string]: unknown;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type RuntimeInterrupt = (value?: unknown) => unknown;
|
|
17
|
-
|
|
18
|
-
export type TanstackRouterPluginAPI = {
|
|
19
|
-
getRuntimeConfig: () => TanstackRouterRuntimeConfig;
|
|
20
|
-
getHooks: () => RouterExtendsHooks;
|
|
21
|
-
onBeforeRender: (
|
|
22
|
-
listener: (
|
|
23
|
-
context: TInternalRuntimeContext,
|
|
24
|
-
interrupt: RuntimeInterrupt,
|
|
25
|
-
) => unknown,
|
|
26
|
-
) => void;
|
|
27
|
-
wrapRoot: (listener: (App: any) => any) => void;
|
|
28
|
-
[key: string]: any;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type TanstackRouterRuntimePlugin = {
|
|
32
|
-
name?: string;
|
|
33
|
-
registryHooks?: RouterExtendsHooks;
|
|
34
|
-
setup?: (api: TanstackRouterPluginAPI) => unknown;
|
|
35
|
-
[key: string]: unknown;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export function getMergedRouterConfig(
|
|
39
|
-
api: TanstackRouterPluginAPI,
|
|
40
|
-
userConfig: Partial<RouterConfig>,
|
|
41
|
-
): RouterConfig {
|
|
42
|
-
const pluginConfig = api.getRuntimeConfig() as {
|
|
43
|
-
router?: Partial<RouterConfig>;
|
|
44
|
-
};
|
|
45
|
-
return merge(pluginConfig.router || {}, userConfig) as RouterConfig;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function getFinalRouteConfig(
|
|
49
|
-
mergedConfig: RouterConfig,
|
|
50
|
-
): RouterConfig['routesConfig'] {
|
|
51
|
-
return {
|
|
52
|
-
routes: getGlobalRoutes() as RouterConfig['routesConfig']['routes'],
|
|
53
|
-
globalApp: getGlobalLayoutApp(),
|
|
54
|
-
...mergedConfig.routesConfig,
|
|
55
|
-
};
|
|
56
|
-
}
|