@bleedingdev/modern-js-plugin-tanstack 3.5.0-ultramodern.34 → 3.5.0-ultramodern.36
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/runtime/rsc/payloadFetch.js +1 -0
- package/dist/esm/runtime/rsc/payloadFetch.mjs +1 -0
- package/dist/esm-node/runtime/rsc/payloadFetch.mjs +1 -0
- package/package.json +9 -9
- package/rstest.config.mts +1 -0
- package/src/runtime/rsc/payloadFetch.ts +1 -0
- package/tests/router/rscPayloadRouterMatrix.test.ts +197 -0
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"modern.js",
|
|
19
19
|
"tanstack-router"
|
|
20
20
|
],
|
|
21
|
-
"version": "3.5.0-ultramodern.
|
|
21
|
+
"version": "3.5.0-ultramodern.36",
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|
|
24
24
|
},
|
|
@@ -98,14 +98,14 @@
|
|
|
98
98
|
"@tanstack/react-router": "1.170.16",
|
|
99
99
|
"@tanstack/router-core": "1.171.13",
|
|
100
100
|
"react-server-dom-rspack": "0.0.2",
|
|
101
|
-
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.
|
|
102
|
-
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.
|
|
103
|
-
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.
|
|
104
|
-
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.
|
|
101
|
+
"@modern-js/plugin": "npm:@bleedingdev/modern-js-plugin@3.5.0-ultramodern.36",
|
|
102
|
+
"@modern-js/runtime-utils": "npm:@bleedingdev/modern-js-runtime-utils@3.5.0-ultramodern.36",
|
|
103
|
+
"@modern-js/types": "npm:@bleedingdev/modern-js-types@3.5.0-ultramodern.36",
|
|
104
|
+
"@modern-js/utils": "npm:@bleedingdev/modern-js-utils@3.5.0-ultramodern.36"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
|
-
"@modern-js/app-tools": "3.5.0-ultramodern.
|
|
108
|
-
"@modern-js/runtime": "3.5.0-ultramodern.
|
|
107
|
+
"@modern-js/app-tools": "3.5.0-ultramodern.36",
|
|
108
|
+
"@modern-js/runtime": "3.5.0-ultramodern.36",
|
|
109
109
|
"react": "^19.2.7",
|
|
110
110
|
"react-dom": "^19.2.7"
|
|
111
111
|
},
|
|
@@ -120,8 +120,8 @@
|
|
|
120
120
|
"@typescript/native-preview": "7.0.0-dev.20260628.1",
|
|
121
121
|
"react": "^19.2.7",
|
|
122
122
|
"react-dom": "^19.2.7",
|
|
123
|
-
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.
|
|
124
|
-
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.
|
|
123
|
+
"@modern-js/app-tools": "npm:@bleedingdev/modern-js-app-tools@3.5.0-ultramodern.36",
|
|
124
|
+
"@modern-js/runtime": "npm:@bleedingdev/modern-js-runtime@3.5.0-ultramodern.36",
|
|
125
125
|
"@scripts/rstest-config": "2.66.0"
|
|
126
126
|
},
|
|
127
127
|
"sideEffects": [
|
package/rstest.config.mts
CHANGED
|
@@ -33,6 +33,7 @@ export default {
|
|
|
33
33
|
'tests/router/preloadRedirect.test.ts',
|
|
34
34
|
'tests/router/register.test.ts',
|
|
35
35
|
'tests/router/routeHooks.test.ts',
|
|
36
|
+
'tests/router/rscPayloadRouterMatrix.test.ts',
|
|
36
37
|
'tests/router/rsc.test.tsx',
|
|
37
38
|
'tests/router/slotUsageSanitizer.test.ts',
|
|
38
39
|
'tests/router/ssrPreload.test.ts',
|
|
@@ -31,6 +31,7 @@ export function isServerPayload(value: unknown): value is ServerPayload {
|
|
|
31
31
|
export function createPayloadFetchKey(request: Request) {
|
|
32
32
|
return JSON.stringify([
|
|
33
33
|
request.url,
|
|
34
|
+
request.method,
|
|
34
35
|
[...request.headers.entries()].sort(([left], [right]) =>
|
|
35
36
|
left.localeCompare(right),
|
|
36
37
|
),
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__setTanstackRscPayloadDecoderForTests,
|
|
3
|
+
createTanstackRscServerPayload,
|
|
4
|
+
handleTanstackRscRedirect,
|
|
5
|
+
loadTanstackRscRouteData,
|
|
6
|
+
} from '../../src/runtime/rsc/payloadRouter';
|
|
7
|
+
|
|
8
|
+
function createPayload(
|
|
9
|
+
routeId: string,
|
|
10
|
+
loaderData: unknown,
|
|
11
|
+
options: { errors?: Record<string, unknown> | null } = {},
|
|
12
|
+
) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'render',
|
|
15
|
+
actionData: null,
|
|
16
|
+
errors: options.errors ?? null,
|
|
17
|
+
loaderData: loaderData === undefined ? {} : { [routeId]: loaderData },
|
|
18
|
+
location: { href: routeId },
|
|
19
|
+
routes: [{ id: routeId, hasLoader: true }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function decodeJsonPayload(stream: ReadableStream<Uint8Array>) {
|
|
24
|
+
return JSON.parse(await new Response(stream).text());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('tanstack rsc payload router matrix', () => {
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
__setTanstackRscPayloadDecoderForTests();
|
|
30
|
+
rstest.restoreAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('normalizes redirect basenames while preserving status and non-location headers', () => {
|
|
34
|
+
const scenarios = [
|
|
35
|
+
{
|
|
36
|
+
location: '/base',
|
|
37
|
+
name: 'exact basename',
|
|
38
|
+
redirect: '/',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
location: '/base/login?from=%2Fbase',
|
|
42
|
+
name: 'leading basename',
|
|
43
|
+
redirect: '/login?from=%2Fbase',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
location: '/shop/base/login',
|
|
47
|
+
name: 'mid-path basename',
|
|
48
|
+
redirect: '/shop/base/login',
|
|
49
|
+
},
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
for (const scenario of scenarios) {
|
|
53
|
+
const response = handleTanstackRscRedirect(
|
|
54
|
+
new Headers({
|
|
55
|
+
Location: scenario.location,
|
|
56
|
+
'X-Trace': scenario.name,
|
|
57
|
+
}),
|
|
58
|
+
'/base',
|
|
59
|
+
307,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(response.status).toBe(307);
|
|
63
|
+
expect(response.headers.get('Location')).toBeNull();
|
|
64
|
+
expect(response.headers.get('X-Modernjs-BaseUrl')).toBe('/base');
|
|
65
|
+
expect(response.headers.get('X-Modernjs-Redirect')).toBe(
|
|
66
|
+
scenario.redirect,
|
|
67
|
+
);
|
|
68
|
+
expect(response.headers.get('X-Trace')).toBe(scenario.name);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('separates payload fetch cache entries by URL and request method', async () => {
|
|
73
|
+
const payloads = [
|
|
74
|
+
createPayload('/resource', { method: 'GET' }),
|
|
75
|
+
createPayload('/resource', { method: 'POST' }),
|
|
76
|
+
];
|
|
77
|
+
let payloadIndex = 0;
|
|
78
|
+
const fetchMock = rstest.fn(() =>
|
|
79
|
+
Promise.resolve(new Response(JSON.stringify(payloads[payloadIndex++]))),
|
|
80
|
+
);
|
|
81
|
+
rstest.stubGlobal('fetch', fetchMock);
|
|
82
|
+
__setTanstackRscPayloadDecoderForTests(decodeJsonPayload);
|
|
83
|
+
|
|
84
|
+
const [getData, postData] = await Promise.all([
|
|
85
|
+
loadTanstackRscRouteData({
|
|
86
|
+
loadClientData: async () => ({ fallback: 'get' }),
|
|
87
|
+
request: new Request('http://localhost/resource', { method: 'GET' }),
|
|
88
|
+
routeId: '/resource',
|
|
89
|
+
}),
|
|
90
|
+
loadTanstackRscRouteData({
|
|
91
|
+
loadClientData: async () => ({ fallback: 'post' }),
|
|
92
|
+
request: new Request('http://localhost/resource', { method: 'POST' }),
|
|
93
|
+
routeId: '/resource',
|
|
94
|
+
}),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
expect(getData).toEqual({ method: 'GET' });
|
|
98
|
+
expect(postData).toEqual({ method: 'POST' });
|
|
99
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
100
|
+
expect(fetchMock.mock.calls.map(call => call[0])).toEqual([
|
|
101
|
+
'http://localhost/resource',
|
|
102
|
+
'http://localhost/resource',
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('rethrows matched route errors from the decoded payload', async () => {
|
|
107
|
+
const routeError = new Error('route loader failed');
|
|
108
|
+
const payload = createPayload('/broken', undefined, {
|
|
109
|
+
errors: { '/broken': routeError },
|
|
110
|
+
});
|
|
111
|
+
rstest.stubGlobal(
|
|
112
|
+
'fetch',
|
|
113
|
+
rstest.fn(() => Promise.resolve(new Response('payload'))),
|
|
114
|
+
);
|
|
115
|
+
__setTanstackRscPayloadDecoderForTests(async () => payload);
|
|
116
|
+
|
|
117
|
+
await expect(
|
|
118
|
+
loadTanstackRscRouteData({
|
|
119
|
+
loadClientData: async () => ({ fallback: true }),
|
|
120
|
+
request: new Request('http://localhost/broken'),
|
|
121
|
+
routeId: '/broken',
|
|
122
|
+
}),
|
|
123
|
+
).rejects.toBe(routeError);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('returns undefined for route ids missing from the decoded payload', async () => {
|
|
127
|
+
const loadClientData = rstest.fn(async () => ({ fallback: true }));
|
|
128
|
+
const payload = createPayload('/known', { known: true });
|
|
129
|
+
rstest.stubGlobal(
|
|
130
|
+
'fetch',
|
|
131
|
+
rstest.fn(() => Promise.resolve(new Response('payload'))),
|
|
132
|
+
);
|
|
133
|
+
__setTanstackRscPayloadDecoderForTests(async () => payload);
|
|
134
|
+
|
|
135
|
+
await expect(
|
|
136
|
+
loadTanstackRscRouteData({
|
|
137
|
+
loadClientData,
|
|
138
|
+
request: new Request('http://localhost/missing'),
|
|
139
|
+
routeId: '/missing',
|
|
140
|
+
}),
|
|
141
|
+
).resolves.toBeUndefined();
|
|
142
|
+
expect(loadClientData).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('omits server payload matches with no route identity', () => {
|
|
146
|
+
const payload = createTanstackRscServerPayload({
|
|
147
|
+
state: {
|
|
148
|
+
location: { href: '/kept' },
|
|
149
|
+
matches: [
|
|
150
|
+
{
|
|
151
|
+
loaderData: { omitted: true },
|
|
152
|
+
params: {},
|
|
153
|
+
pathname: '/missing-id',
|
|
154
|
+
pathnameBase: '/missing-id',
|
|
155
|
+
route: {
|
|
156
|
+
options: {
|
|
157
|
+
path: 'missing-id',
|
|
158
|
+
staticData: { modernRouteHasLoader: true },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
loaderData: { kept: true },
|
|
164
|
+
params: {},
|
|
165
|
+
pathname: '/kept',
|
|
166
|
+
pathnameBase: '/kept',
|
|
167
|
+
route: {
|
|
168
|
+
id: '/kept',
|
|
169
|
+
options: {
|
|
170
|
+
path: 'kept',
|
|
171
|
+
staticData: { modernRouteHasLoader: true },
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(payload.loaderData).toEqual({ '/kept': { kept: true } });
|
|
180
|
+
expect(payload.routes).toEqual([
|
|
181
|
+
{
|
|
182
|
+
handle: undefined,
|
|
183
|
+
hasAction: false,
|
|
184
|
+
hasClientLoader: false,
|
|
185
|
+
hasErrorBoundary: false,
|
|
186
|
+
hasLoader: true,
|
|
187
|
+
id: '/kept',
|
|
188
|
+
index: undefined,
|
|
189
|
+
params: {},
|
|
190
|
+
parentId: undefined,
|
|
191
|
+
path: 'kept',
|
|
192
|
+
pathname: '/kept',
|
|
193
|
+
pathnameBase: '/kept',
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
});
|