@bleedingdev/modern-js-plugin-tanstack 3.2.0-ultramodern.120 → 3.2.0-ultramodern.122
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 +47 -27
- package/dist/cjs/cli/routeSplitting.js +0 -32
- package/dist/cjs/cli/tanstackTypes.js +34 -199
- package/dist/cjs/runtime/hooks.js +11 -14
- package/dist/cjs/runtime/index.js +107 -319
- package/dist/cjs/runtime/lifecycle.js +12 -86
- package/dist/cjs/runtime/loaderBridge.js +173 -0
- package/dist/cjs/runtime/plugin.js +6 -30
- package/dist/cjs/runtime/plugin.node.js +7 -29
- package/dist/cjs/runtime/pluginCore.js +55 -0
- package/dist/cjs/runtime/register.js +56 -0
- package/dist/cjs/runtime/routeTree.js +10 -207
- package/dist/cjs/runtime/{DefaultNotFound.js → router.js} +5 -15
- package/dist/cjs/runtime/rsc/payloadRouter.js +35 -1
- package/dist/cjs/runtime/state.js +45 -0
- package/dist/cjs/runtime/utils.js +0 -5
- package/dist/esm/cli/index.mjs +52 -26
- package/dist/esm/cli/routeSplitting.mjs +1 -30
- package/dist/esm/cli/tanstackTypes.mjs +32 -194
- package/dist/esm/runtime/hooks.mjs +1 -8
- package/dist/esm/runtime/index.mjs +4 -2
- package/dist/esm/runtime/lifecycle.mjs +1 -82
- package/dist/esm/runtime/loaderBridge.mjs +114 -0
- package/dist/esm/runtime/plugin.mjs +8 -32
- package/dist/esm/runtime/plugin.node.mjs +10 -32
- package/dist/esm/runtime/pluginCore.mjs +14 -0
- package/dist/esm/runtime/register.mjs +18 -0
- package/dist/esm/runtime/routeTree.mjs +4 -198
- package/dist/esm/runtime/router.mjs +2 -0
- package/dist/esm/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm/runtime/state.mjs +7 -0
- package/dist/esm/runtime/utils.mjs +0 -5
- package/dist/esm-node/cli/index.mjs +52 -26
- package/dist/esm-node/cli/routeSplitting.mjs +1 -30
- package/dist/esm-node/cli/tanstackTypes.mjs +32 -194
- package/dist/esm-node/runtime/hooks.mjs +1 -8
- package/dist/esm-node/runtime/index.mjs +4 -2
- package/dist/esm-node/runtime/lifecycle.mjs +1 -82
- package/dist/esm-node/runtime/loaderBridge.mjs +115 -0
- package/dist/esm-node/runtime/plugin.mjs +8 -32
- package/dist/esm-node/runtime/plugin.node.mjs +10 -32
- package/dist/esm-node/runtime/pluginCore.mjs +15 -0
- package/dist/esm-node/runtime/register.mjs +19 -0
- package/dist/esm-node/runtime/routeTree.mjs +4 -198
- package/dist/esm-node/runtime/router.mjs +3 -0
- package/dist/esm-node/runtime/rsc/payloadRouter.mjs +35 -1
- package/dist/esm-node/runtime/state.mjs +8 -0
- package/dist/esm-node/runtime/utils.mjs +0 -5
- package/dist/types/cli/index.d.ts +9 -2
- package/dist/types/cli/routeSplitting.d.ts +6 -15
- package/dist/types/cli/tanstackTypes.d.ts +13 -2
- package/dist/types/runtime/hooks.d.ts +8 -18
- package/dist/types/runtime/index.d.ts +6 -4
- package/dist/types/runtime/lifecycle.d.ts +7 -22
- package/dist/types/runtime/loaderBridge.d.ts +48 -0
- package/dist/types/runtime/plugin.d.ts +1 -14
- package/dist/types/runtime/plugin.node.d.ts +1 -14
- package/dist/types/runtime/pluginCore.d.ts +21 -0
- package/dist/types/runtime/register.d.ts +9 -0
- package/dist/types/runtime/routeTree.d.ts +0 -2
- package/dist/types/runtime/router.d.ts +14 -0
- package/dist/types/runtime/state.d.ts +16 -0
- package/dist/types/runtime/types.d.ts +7 -53
- package/package.json +31 -29
- package/rstest.config.mts +6 -0
- package/src/cli/index.ts +111 -29
- package/src/cli/routeSplitting.ts +6 -44
- package/src/cli/tanstackTypes.ts +78 -214
- package/src/runtime/hooks.ts +10 -27
- package/src/runtime/index.tsx +12 -107
- package/src/runtime/lifecycle.ts +16 -151
- package/src/runtime/loaderBridge.ts +257 -0
- package/src/runtime/plugin.node.tsx +14 -77
- package/src/runtime/plugin.tsx +12 -72
- package/src/runtime/pluginCore.ts +48 -0
- package/src/runtime/register.ts +58 -0
- package/src/runtime/routeTree.ts +8 -370
- package/src/runtime/router.ts +15 -0
- package/src/runtime/rsc/payloadRouter.ts +45 -2
- package/src/runtime/state.ts +29 -0
- package/src/runtime/types.ts +20 -67
- package/src/runtime/utils.tsx +3 -6
- package/tests/router/cli.test.ts +297 -31
- package/tests/router/hooks.test.ts +26 -0
- package/tests/router/loaderBridge.test.ts +211 -0
- package/tests/router/packageSurface.test.ts +24 -0
- package/tests/router/register.test.ts +46 -0
- package/tests/router/routeTree.test.ts +65 -180
- package/tests/router/rsc.test.tsx +70 -0
- package/tests/router/tanstackTypes.test.ts +164 -6
- package/dist/esm/runtime/DefaultNotFound.mjs +0 -13
- package/dist/esm-node/runtime/DefaultNotFound.mjs +0 -14
- package/dist/types/runtime/DefaultNotFound.d.ts +0 -2
- package/src/runtime/DefaultNotFound.tsx +0 -15
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { isNotFound, isRedirect } from '@tanstack/react-router';
|
|
2
|
+
import {
|
|
3
|
+
createRouteStaticData,
|
|
4
|
+
isAbsoluteUrl,
|
|
5
|
+
mapSplatParamsForModernLoader,
|
|
6
|
+
modernLoaderToTanstack,
|
|
7
|
+
throwTanstackRedirect,
|
|
8
|
+
} from '../../src/runtime/loaderBridge';
|
|
9
|
+
|
|
10
|
+
type RedirectLike = {
|
|
11
|
+
options?: {
|
|
12
|
+
href?: string;
|
|
13
|
+
to?: string;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function catchThrown(fn: () => unknown): unknown {
|
|
18
|
+
try {
|
|
19
|
+
fn();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return err;
|
|
22
|
+
}
|
|
23
|
+
throw new Error('expected the function to throw');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('throwTanstackRedirect', () => {
|
|
27
|
+
test('absolute URLs redirect via href (external), not via to', () => {
|
|
28
|
+
// The old inline codegen handler threw `redirect({ href })` INSIDE a
|
|
29
|
+
// try block whose catch replaced it with `redirect({ to: absoluteUrl })`,
|
|
30
|
+
// making TanStack treat the absolute URL as an internal path.
|
|
31
|
+
const thrown = catchThrown(() =>
|
|
32
|
+
throwTanstackRedirect('https://example.com/external'),
|
|
33
|
+
) as RedirectLike;
|
|
34
|
+
|
|
35
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
36
|
+
expect(thrown.options?.href).toBe('https://example.com/external');
|
|
37
|
+
expect(thrown.options?.to).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('relative paths redirect via to so the basepath rewrite applies', () => {
|
|
41
|
+
const thrown = catchThrown(() =>
|
|
42
|
+
throwTanstackRedirect('/dashboard'),
|
|
43
|
+
) as RedirectLike;
|
|
44
|
+
|
|
45
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
46
|
+
expect(thrown.options?.to).toBe('/dashboard');
|
|
47
|
+
expect(thrown.options?.href).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('empty location falls back to /', () => {
|
|
51
|
+
const thrown = catchThrown(() => throwTanstackRedirect('')) as RedirectLike;
|
|
52
|
+
expect(thrown.options?.to).toBe('/');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('isAbsoluteUrl', () => {
|
|
57
|
+
test('detects absolute and relative URLs', () => {
|
|
58
|
+
expect(isAbsoluteUrl('https://example.com/a')).toBe(true);
|
|
59
|
+
expect(isAbsoluteUrl('mailto:x@example.com')).toBe(true);
|
|
60
|
+
expect(isAbsoluteUrl('/internal/path')).toBe(false);
|
|
61
|
+
expect(isAbsoluteUrl('relative')).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('mapSplatParamsForModernLoader', () => {
|
|
66
|
+
test('maps TanStack _splat to React Router * only for splat routes', () => {
|
|
67
|
+
expect(
|
|
68
|
+
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, true),
|
|
69
|
+
).toEqual({ '*': 'a/b', id: '1' });
|
|
70
|
+
expect(
|
|
71
|
+
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, false),
|
|
72
|
+
).toEqual({ _splat: 'a/b', id: '1' });
|
|
73
|
+
expect(mapSplatParamsForModernLoader({ id: '1' }, true)).toEqual({
|
|
74
|
+
id: '1',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('createRouteStaticData', () => {
|
|
80
|
+
test('drops empty fields', () => {
|
|
81
|
+
const loader = () => null;
|
|
82
|
+
expect(createRouteStaticData({})).toEqual({});
|
|
83
|
+
expect(createRouteStaticData({ modernRouteId: '' })).toEqual({});
|
|
84
|
+
expect(
|
|
85
|
+
createRouteStaticData({
|
|
86
|
+
modernRouteId: 'page',
|
|
87
|
+
modernRouteLoader: loader,
|
|
88
|
+
}),
|
|
89
|
+
).toEqual({ modernRouteId: 'page', modernRouteLoader: loader });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('modernLoaderToTanstack', () => {
|
|
94
|
+
const baseCtx = {
|
|
95
|
+
location: { href: 'http://localhost/products/1' },
|
|
96
|
+
params: { id: '1' },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
test('passes request/params/context through to the modern loader', async () => {
|
|
100
|
+
const seen: { request?: Request; params?: unknown; context?: unknown } = {};
|
|
101
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, (args: any) => {
|
|
102
|
+
seen.request = args.request;
|
|
103
|
+
seen.params = args.params;
|
|
104
|
+
seen.context = args.context;
|
|
105
|
+
return { ok: true };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await expect(
|
|
109
|
+
loader({
|
|
110
|
+
...baseCtx,
|
|
111
|
+
context: { requestContext: { user: 'u1' } },
|
|
112
|
+
}),
|
|
113
|
+
).resolves.toEqual({ ok: true });
|
|
114
|
+
expect(seen.request).toBeInstanceOf(Request);
|
|
115
|
+
expect(seen.request?.url).toBe('http://localhost/products/1');
|
|
116
|
+
expect(seen.params).toEqual({ id: '1' });
|
|
117
|
+
expect(seen.context).toEqual({ user: 'u1' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('translates an absolute-URL redirect Response into redirect({ href })', async () => {
|
|
121
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, () =>
|
|
122
|
+
Response.redirect('https://example.com/away', 302),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const thrown = (await loader(baseCtx).then(
|
|
126
|
+
() => {
|
|
127
|
+
throw new Error('expected redirect');
|
|
128
|
+
},
|
|
129
|
+
(err: unknown) => err,
|
|
130
|
+
)) as RedirectLike;
|
|
131
|
+
|
|
132
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
133
|
+
expect(thrown.options?.href).toBe('https://example.com/away');
|
|
134
|
+
expect(thrown.options?.to).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('translates a relative redirect Response into redirect({ to })', async () => {
|
|
138
|
+
const loader = modernLoaderToTanstack(
|
|
139
|
+
{ hasSplat: false },
|
|
140
|
+
() =>
|
|
141
|
+
new Response(null, { status: 302, headers: { Location: '/login' } }),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const thrown = (await loader(baseCtx).then(
|
|
145
|
+
() => {
|
|
146
|
+
throw new Error('expected redirect');
|
|
147
|
+
},
|
|
148
|
+
(err: unknown) => err,
|
|
149
|
+
)) as RedirectLike;
|
|
150
|
+
|
|
151
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
152
|
+
expect(thrown.options?.to).toBe('/login');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('translates a 404 Response into notFound()', async () => {
|
|
156
|
+
const loader = modernLoaderToTanstack(
|
|
157
|
+
{ hasSplat: false },
|
|
158
|
+
() => new Response(null, { status: 404 }),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const thrown = await loader(baseCtx).then(
|
|
162
|
+
() => {
|
|
163
|
+
throw new Error('expected notFound');
|
|
164
|
+
},
|
|
165
|
+
(err: unknown) => err,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
expect(isNotFound(thrown)).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('preserves returned non-404 error Responses as loader results', async () => {
|
|
172
|
+
const response = new Response('loader exploded', { status: 500 });
|
|
173
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, () => response);
|
|
174
|
+
|
|
175
|
+
await expect(loader(baseCtx)).resolves.toBe(response);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('translates redirect Responses thrown synchronously by the loader', () => {
|
|
179
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, () => {
|
|
180
|
+
throw new Response(null, {
|
|
181
|
+
status: 301,
|
|
182
|
+
headers: { Location: 'https://example.com/moved' },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// A synchronous loader throw surfaces synchronously (TanStack handles
|
|
187
|
+
// thrown redirects from the loader call itself).
|
|
188
|
+
const thrown = catchThrown(() => loader(baseCtx)) as RedirectLike;
|
|
189
|
+
|
|
190
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
191
|
+
expect(thrown.options?.href).toBe('https://example.com/moved');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('re-throws TanStack redirects thrown by the loader untouched', async () => {
|
|
195
|
+
const loader = modernLoaderToTanstack({ hasSplat: false }, async () => {
|
|
196
|
+
throwTanstackRedirect('/inner');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const thrown = (await loader(baseCtx).then(
|
|
200
|
+
() => {
|
|
201
|
+
throw new Error('expected redirect');
|
|
202
|
+
},
|
|
203
|
+
(err: unknown) => err,
|
|
204
|
+
)) as RedirectLike;
|
|
205
|
+
|
|
206
|
+
// The bridge must not re-translate its own redirect (a Response without
|
|
207
|
+
// a Location header) — that used to collapse internal targets to '/'.
|
|
208
|
+
expect(isRedirect(thrown)).toBe(true);
|
|
209
|
+
expect(thrown.options?.to).toBe('/inner');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
describe('tanstack package public surface', () => {
|
|
5
|
+
test('package manifest exposes the runtime subpath used by app fixtures', () => {
|
|
6
|
+
const packageJson = JSON.parse(
|
|
7
|
+
readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'),
|
|
8
|
+
) as {
|
|
9
|
+
exports: Record<string, unknown>;
|
|
10
|
+
typesVersions?: Record<string, Record<string, string[]>>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(packageJson.exports['./runtime']).toEqual({
|
|
14
|
+
types: './dist/types/runtime/index.d.ts',
|
|
15
|
+
node: {
|
|
16
|
+
module: './dist/esm/runtime/index.mjs',
|
|
17
|
+
},
|
|
18
|
+
default: './dist/esm/runtime/index.mjs',
|
|
19
|
+
});
|
|
20
|
+
expect(packageJson.typesVersions?.['*']?.runtime).toEqual([
|
|
21
|
+
'./dist/types/runtime/index.d.ts',
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveRouterProvider } from '@modern-js/runtime/context';
|
|
2
|
+
import {
|
|
3
|
+
Form,
|
|
4
|
+
Link,
|
|
5
|
+
NavLink,
|
|
6
|
+
Outlet,
|
|
7
|
+
RouteActionResponseError,
|
|
8
|
+
useFetcher,
|
|
9
|
+
} from '../../src/runtime';
|
|
10
|
+
import { tanstackRouterCompatBindings } from '../../src/runtime/register';
|
|
11
|
+
|
|
12
|
+
const COMPAT_BINDINGS_SLOT = Symbol.for(
|
|
13
|
+
'@modern-js/plugin-tanstack:runtime-compat-bindings',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
describe("'@modern-js/plugin-tanstack/runtime' import side effects", () => {
|
|
17
|
+
it('registers the tanstack router provider', () => {
|
|
18
|
+
expect(typeof resolveRouterProvider('tanstack')).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("publishes the compat bindings consumed by '@modern-js/runtime/tanstack-router'", () => {
|
|
22
|
+
const bindings = (globalThis as Record<symbol, unknown>)[
|
|
23
|
+
COMPAT_BINDINGS_SLOT
|
|
24
|
+
] as typeof tanstackRouterCompatBindings;
|
|
25
|
+
|
|
26
|
+
expect(bindings).toBeDefined();
|
|
27
|
+
expect(bindings).toBe(tanstackRouterCompatBindings);
|
|
28
|
+
expect(bindings.Form).toBe(Form);
|
|
29
|
+
expect(bindings.Link).toBe(Link);
|
|
30
|
+
expect(bindings.NavLink).toBe(NavLink);
|
|
31
|
+
expect(bindings.Outlet).toBe(Outlet);
|
|
32
|
+
expect(bindings.RouteActionResponseError).toBe(RouteActionResponseError);
|
|
33
|
+
expect(bindings.useFetcher).toBe(useFetcher);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('keeps the first published bindings when a duplicate module copy evaluates', () => {
|
|
37
|
+
// Simulates a Module Federation remote evaluating its own copy of
|
|
38
|
+
// register.ts: `??=` must keep the established bindings.
|
|
39
|
+
const host = globalThis as Record<symbol, unknown>;
|
|
40
|
+
const established = host[COMPAT_BINDINGS_SLOT];
|
|
41
|
+
expect(established).toBeDefined();
|
|
42
|
+
|
|
43
|
+
host[COMPAT_BINDINGS_SLOT] ??= { duplicate: true };
|
|
44
|
+
expect(host[COMPAT_BINDINGS_SLOT]).toBe(established);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
2
4
|
import type { NestedRoute } from '@modern-js/types';
|
|
3
5
|
import { createMemoryHistory } from '@tanstack/history';
|
|
@@ -5,10 +7,10 @@ import { createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
|
|
|
5
7
|
import type { ComponentType } from 'react';
|
|
6
8
|
import { createElement, lazy } from 'react';
|
|
7
9
|
import { renderToStaticMarkup, renderToString } from 'react-dom/server';
|
|
10
|
+
import * as TanstackRuntime from '../../src/runtime';
|
|
8
11
|
import { Outlet as PublicOutlet } from '../../src/runtime';
|
|
9
12
|
import { Outlet as ModernOutlet } from '../../src/runtime/outlet';
|
|
10
13
|
import {
|
|
11
|
-
createRouteTreeFromModernRoutes,
|
|
12
14
|
createRouteTreeFromRouteObjects,
|
|
13
15
|
getModernRouteIdsFromMatches,
|
|
14
16
|
} from '../../src/runtime/routeTree';
|
|
@@ -79,9 +81,11 @@ type TestRouter = {
|
|
|
79
81
|
looseRoutesById: Partial<Record<string, TestRoute>>;
|
|
80
82
|
state: {
|
|
81
83
|
matches: Array<{
|
|
84
|
+
error?: unknown;
|
|
82
85
|
loaderData?: unknown;
|
|
83
86
|
routeId: string;
|
|
84
87
|
}>;
|
|
88
|
+
statusCode?: number;
|
|
85
89
|
};
|
|
86
90
|
};
|
|
87
91
|
|
|
@@ -135,6 +139,28 @@ describe('tanstack runtime public exports', () => {
|
|
|
135
139
|
test('exports the Modern Outlet implementation from the runtime entrypoint', () => {
|
|
136
140
|
expect(PublicOutlet).toBe(ModernOutlet);
|
|
137
141
|
});
|
|
142
|
+
|
|
143
|
+
test('does not expose the unowned composite RSC helper API', () => {
|
|
144
|
+
expect('CompositeComponent' in TanstackRuntime).toBe(false);
|
|
145
|
+
|
|
146
|
+
const packageJson = JSON.parse(
|
|
147
|
+
readFileSync(path.join(__dirname, '../../package.json'), 'utf-8'),
|
|
148
|
+
) as {
|
|
149
|
+
exports: Record<string, unknown>;
|
|
150
|
+
typesVersions?: Record<string, Record<string, string[]>>;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(packageJson.exports['./runtime/rsc']).toBeUndefined();
|
|
154
|
+
expect(packageJson.exports['./runtime/rsc/client']).toBeUndefined();
|
|
155
|
+
expect(packageJson.exports['./runtime/rsc/server']).toBeUndefined();
|
|
156
|
+
expect(packageJson.typesVersions?.['*']?.['runtime/rsc']).toBeUndefined();
|
|
157
|
+
expect(
|
|
158
|
+
packageJson.typesVersions?.['*']?.['runtime/rsc/client'],
|
|
159
|
+
).toBeUndefined();
|
|
160
|
+
expect(
|
|
161
|
+
packageJson.typesVersions?.['*']?.['runtime/rsc/server'],
|
|
162
|
+
).toBeUndefined();
|
|
163
|
+
});
|
|
138
164
|
});
|
|
139
165
|
|
|
140
166
|
describe('tanstack route tree from RouteObject[]', () => {
|
|
@@ -175,6 +201,28 @@ describe('tanstack route tree from RouteObject[]', () => {
|
|
|
175
201
|
expect(userMatch?.loaderData).toEqual({ id: '123' });
|
|
176
202
|
});
|
|
177
203
|
|
|
204
|
+
test('reports native TanStack unknown routes as HTTP 404', async () => {
|
|
205
|
+
const routes: RouteObject[] = [
|
|
206
|
+
{
|
|
207
|
+
id: 'root',
|
|
208
|
+
path: '/',
|
|
209
|
+
Component: () => null,
|
|
210
|
+
children: [
|
|
211
|
+
{
|
|
212
|
+
id: 'known',
|
|
213
|
+
path: 'known',
|
|
214
|
+
Component: () => null,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
221
|
+
const router = await loadRouteTree(routeTree, '/missing');
|
|
222
|
+
|
|
223
|
+
expect(router.state.statusCode).toBe(404);
|
|
224
|
+
});
|
|
225
|
+
|
|
178
226
|
test('does not force Suspense wrappers for ordinary generated routes', async () => {
|
|
179
227
|
const routes: RouteObject[] = [
|
|
180
228
|
{
|
|
@@ -682,196 +730,33 @@ describe('tanstack route tree from RouteObject[]', () => {
|
|
|
682
730
|
await expect(loaderData?.later).resolves.toBe('done');
|
|
683
731
|
});
|
|
684
732
|
|
|
685
|
-
test('
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
type: 'nested',
|
|
689
|
-
origin: 'config',
|
|
690
|
-
id: 'root',
|
|
691
|
-
isRoot: true,
|
|
692
|
-
config: {
|
|
693
|
-
handle: {
|
|
694
|
-
shell: true,
|
|
695
|
-
},
|
|
696
|
-
},
|
|
697
|
-
children: [
|
|
698
|
-
{
|
|
699
|
-
type: 'nested',
|
|
700
|
-
origin: 'config',
|
|
701
|
-
id: 'dashboard',
|
|
702
|
-
path: 'dashboard',
|
|
703
|
-
handle: {
|
|
704
|
-
section: 'analytics',
|
|
705
|
-
},
|
|
706
|
-
config: {
|
|
707
|
-
handle: {
|
|
708
|
-
role: 'admin',
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
},
|
|
712
|
-
],
|
|
713
|
-
},
|
|
714
|
-
];
|
|
715
|
-
const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
|
|
716
|
-
const router = createRouter({
|
|
717
|
-
routeTree,
|
|
718
|
-
history: createMemoryHistory({
|
|
719
|
-
initialEntries: ['/dashboard'],
|
|
720
|
-
}),
|
|
721
|
-
context: {},
|
|
722
|
-
}) as unknown as TestRouter;
|
|
723
|
-
const dashboardRoute = getLooseRoute(router, '/dashboard');
|
|
724
|
-
|
|
725
|
-
expect(routeTree.options.staticData.modernRouteHandle).toEqual({
|
|
726
|
-
shell: true,
|
|
727
|
-
});
|
|
728
|
-
expect(dashboardRoute.options.staticData.modernRouteHandle).toEqual({
|
|
729
|
-
section: 'analytics',
|
|
730
|
-
role: 'admin',
|
|
731
|
-
});
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
test('preserves TanStack search contracts from Modern generated routes', () => {
|
|
735
|
-
const rootValidateSearch = (search: unknown) => ({ root: search });
|
|
736
|
-
const rootLoaderDeps = ({ search }: { search: unknown }) => ({ search });
|
|
737
|
-
const childValidateSearch = (search: unknown) => ({ child: search });
|
|
738
|
-
const childLoaderDeps = ({ search }: { search: unknown }) => ({ search });
|
|
739
|
-
const modernRoutes: TestNestedRoute[] = [
|
|
740
|
-
{
|
|
741
|
-
type: 'nested',
|
|
742
|
-
origin: 'config',
|
|
743
|
-
id: 'root',
|
|
744
|
-
isRoot: true,
|
|
745
|
-
validateSearch: rootValidateSearch,
|
|
746
|
-
loaderDeps: rootLoaderDeps,
|
|
747
|
-
children: [
|
|
748
|
-
{
|
|
749
|
-
type: 'nested',
|
|
750
|
-
origin: 'config',
|
|
751
|
-
id: 'search',
|
|
752
|
-
path: 'search',
|
|
753
|
-
validateSearch: childValidateSearch,
|
|
754
|
-
loaderDeps: childLoaderDeps,
|
|
755
|
-
},
|
|
756
|
-
],
|
|
757
|
-
},
|
|
758
|
-
];
|
|
759
|
-
const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
|
|
760
|
-
const router = createRouter({
|
|
761
|
-
routeTree,
|
|
762
|
-
history: createMemoryHistory({
|
|
763
|
-
initialEntries: ['/search'],
|
|
764
|
-
}),
|
|
765
|
-
context: {},
|
|
766
|
-
}) as unknown as TestRouter;
|
|
767
|
-
const searchRoute = getLooseRouteByModernRouteId(router, 'search');
|
|
768
|
-
|
|
769
|
-
expect(routeTree.options.validateSearch).toBe(rootValidateSearch);
|
|
770
|
-
expect(routeTree.options.loaderDeps).toBe(rootLoaderDeps);
|
|
771
|
-
expect(searchRoute.options.validateSearch).toBe(childValidateSearch);
|
|
772
|
-
expect(searchRoute.options.loaderDeps).toBe(childLoaderDeps);
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
test('does not force Suspense wrappers for ordinary Modern routes', async () => {
|
|
776
|
-
const modernRoutes: TestNestedRoute[] = [
|
|
777
|
-
{
|
|
778
|
-
type: 'nested',
|
|
779
|
-
origin: 'config',
|
|
780
|
-
id: 'root',
|
|
781
|
-
isRoot: true,
|
|
782
|
-
component: () => createElement(Outlet),
|
|
783
|
-
children: [
|
|
784
|
-
{
|
|
785
|
-
type: 'nested',
|
|
786
|
-
origin: 'config',
|
|
787
|
-
id: 'plain',
|
|
788
|
-
path: 'plain',
|
|
789
|
-
component: () => null,
|
|
790
|
-
},
|
|
791
|
-
],
|
|
792
|
-
},
|
|
793
|
-
];
|
|
794
|
-
|
|
795
|
-
const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
|
|
796
|
-
const router = await loadRouteTree(routeTree, '/plain');
|
|
797
|
-
const plain = getLooseRouteByModernRouteId(router, 'plain');
|
|
798
|
-
|
|
799
|
-
expect(routeTree.options.wrapInSuspense).toBeUndefined();
|
|
800
|
-
expect(plain.options.wrapInSuspense).toBeUndefined();
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
test('renders Modern generated Outlet through TanStack native outlet', async () => {
|
|
804
|
-
const modernRoutes: TestNestedRoute[] = [
|
|
733
|
+
test('preserves returned non-404 Response loaders as loader data', async () => {
|
|
734
|
+
const response = new Response('route status payload', { status: 500 });
|
|
735
|
+
const routes: TestRouteObject[] = [
|
|
805
736
|
{
|
|
806
|
-
type: 'nested',
|
|
807
|
-
origin: 'config',
|
|
808
737
|
id: 'root',
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
createElement('section', null, createElement(ModernOutlet)),
|
|
738
|
+
path: '/',
|
|
739
|
+
Component: () => null,
|
|
812
740
|
children: [
|
|
813
741
|
{
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
component: () => createElement('main', null, 'Plain child route'),
|
|
742
|
+
id: 'broken',
|
|
743
|
+
path: 'broken',
|
|
744
|
+
loader: () => response,
|
|
745
|
+
Component: () => null,
|
|
819
746
|
},
|
|
820
747
|
],
|
|
821
748
|
},
|
|
822
749
|
];
|
|
823
750
|
|
|
824
|
-
const routeTree =
|
|
825
|
-
const router = await loadRouteTree(routeTree, '/
|
|
826
|
-
const
|
|
827
|
-
|
|
751
|
+
const routeTree = createRouteTreeFromRouteObjects(routes);
|
|
752
|
+
const router = await loadRouteTree(routeTree, '/broken');
|
|
753
|
+
const brokenMatch = router.state.matches.find(
|
|
754
|
+
match => match.routeId === '/broken',
|
|
828
755
|
);
|
|
829
|
-
const suspenseBoundaryCount = countCompletedSuspenseBoundaries(markup);
|
|
830
|
-
|
|
831
|
-
expect(markup).toContain('Plain child route');
|
|
832
|
-
expect(suspenseBoundaryCount).toBe(1);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
test('preserves Modern generated client route metadata', () => {
|
|
836
|
-
const modernRoutes: TestNestedRoute[] = [
|
|
837
|
-
{
|
|
838
|
-
type: 'nested',
|
|
839
|
-
origin: 'config',
|
|
840
|
-
id: 'root',
|
|
841
|
-
isRoot: true,
|
|
842
|
-
children: [
|
|
843
|
-
{
|
|
844
|
-
type: 'nested',
|
|
845
|
-
origin: 'config',
|
|
846
|
-
id: 'client',
|
|
847
|
-
path: 'client',
|
|
848
|
-
clientData: './client.data',
|
|
849
|
-
hasAction: true,
|
|
850
|
-
hasClientLoader: true,
|
|
851
|
-
hasLoader: true,
|
|
852
|
-
inValidSSRRoute: true,
|
|
853
|
-
isClientComponent: true,
|
|
854
|
-
},
|
|
855
|
-
],
|
|
856
|
-
},
|
|
857
|
-
];
|
|
858
|
-
const routeTree = createRouteTreeFromModernRoutes(modernRoutes);
|
|
859
|
-
const router = createRouter({
|
|
860
|
-
routeTree,
|
|
861
|
-
history: createMemoryHistory({
|
|
862
|
-
initialEntries: ['/client'],
|
|
863
|
-
}),
|
|
864
|
-
context: {},
|
|
865
|
-
}) as unknown as TestRouter;
|
|
866
|
-
const clientRoute = getLooseRouteByModernRouteId(router, 'client');
|
|
867
756
|
|
|
868
|
-
expect(
|
|
869
|
-
expect(
|
|
870
|
-
|
|
871
|
-
modernRouteHasClientLoader: true,
|
|
872
|
-
modernRouteHasLoader: true,
|
|
873
|
-
modernRouteIsClientComponent: true,
|
|
874
|
-
});
|
|
757
|
+
expect(router.state.statusCode).toBe(200);
|
|
758
|
+
expect(brokenMatch?.loaderData).toBe(response);
|
|
759
|
+
expect(brokenMatch?.error).toBeUndefined();
|
|
875
760
|
});
|
|
876
761
|
|
|
877
762
|
test('preserves generated client metadata through RouteObject conversion', () => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { UNSAFE_ErrorResponseImpl as ErrorResponseImpl } from '@modern-js/runtime-utils/router';
|
|
1
2
|
import type React from 'react';
|
|
2
3
|
import { isValidElement } from 'react';
|
|
3
4
|
import { createRscProxy } from '../../src/runtime/rsc/createRscProxy';
|
|
@@ -30,6 +31,16 @@ async function readAll(stream: ReadableStream<Uint8Array>) {
|
|
|
30
31
|
return chunks;
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function withNodeEnv<T>(value: string, callback: () => T): T {
|
|
35
|
+
const original = process.env.NODE_ENV;
|
|
36
|
+
process.env.NODE_ENV = value;
|
|
37
|
+
try {
|
|
38
|
+
return callback();
|
|
39
|
+
} finally {
|
|
40
|
+
process.env.NODE_ENV = original;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
describe('tanstack rsc runtime helpers', () => {
|
|
34
45
|
afterEach(() => {
|
|
35
46
|
__setTanstackRscPayloadDecoderForTests();
|
|
@@ -149,6 +160,65 @@ describe('tanstack rsc runtime helpers', () => {
|
|
|
149
160
|
).toBeUndefined();
|
|
150
161
|
});
|
|
151
162
|
|
|
163
|
+
test('redacts production TanStack RSC server payload errors', () => {
|
|
164
|
+
const routeError = new ErrorResponseImpl(
|
|
165
|
+
500,
|
|
166
|
+
'secret status text',
|
|
167
|
+
'route secret',
|
|
168
|
+
true,
|
|
169
|
+
);
|
|
170
|
+
const serverError = new Error('server secret');
|
|
171
|
+
serverError.stack = 'stack secret';
|
|
172
|
+
|
|
173
|
+
const payload = withNodeEnv('production', () =>
|
|
174
|
+
createTanstackRscServerPayload({
|
|
175
|
+
state: {
|
|
176
|
+
location: { href: '/products' },
|
|
177
|
+
matches: [
|
|
178
|
+
{
|
|
179
|
+
error: serverError,
|
|
180
|
+
params: {},
|
|
181
|
+
pathname: '/',
|
|
182
|
+
pathnameBase: '/',
|
|
183
|
+
route: { id: '__root__' },
|
|
184
|
+
routeId: '__root__',
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
error: routeError,
|
|
188
|
+
params: {},
|
|
189
|
+
pathname: '/products',
|
|
190
|
+
pathnameBase: '/products',
|
|
191
|
+
route: {
|
|
192
|
+
id: '/products',
|
|
193
|
+
parentRoute: { id: '__root__' },
|
|
194
|
+
options: { path: 'products' },
|
|
195
|
+
},
|
|
196
|
+
routeId: '/products',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(payload.errors).toMatchObject({
|
|
204
|
+
__root__: {
|
|
205
|
+
message: 'Unexpected Server Error',
|
|
206
|
+
stack: undefined,
|
|
207
|
+
__type: 'Error',
|
|
208
|
+
},
|
|
209
|
+
'/products': {
|
|
210
|
+
status: 500,
|
|
211
|
+
statusText: 'Internal Server Error',
|
|
212
|
+
data: 'Unexpected Server Error',
|
|
213
|
+
__type: 'RouteErrorResponse',
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
expect(JSON.stringify(payload.errors)).not.toContain('server secret');
|
|
217
|
+
expect(JSON.stringify(payload.errors)).not.toContain('route secret');
|
|
218
|
+
expect(JSON.stringify(payload.errors)).not.toContain('secret status text');
|
|
219
|
+
expect(JSON.stringify(payload.errors)).not.toContain('stack secret');
|
|
220
|
+
});
|
|
221
|
+
|
|
152
222
|
test('converts TanStack RSC redirects to Modern RSC navigation headers', () => {
|
|
153
223
|
const response = handleTanstackRscRedirect(
|
|
154
224
|
new Headers({ Location: '/base/login' }),
|