@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.5 → 3.9.0-ultramodern.7
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/register.js +0 -2
- package/dist/esm/runtime/register.mjs +0 -2
- package/dist/esm-node/runtime/register.mjs +0 -2
- package/dist/types/runtime/register.d.ts +0 -8
- package/package.json +12 -12
- package/rstest.config.mts +0 -18
- package/src/runtime/register.ts +0 -11
- package/tests/router/cli.test.ts +85 -860
- package/tests/router/dataMutation.test.tsx +36 -0
- package/tests/router/generateRouteArtifacts.test.ts +11 -11
- package/tests/router/loaderBridge.test.ts +52 -0
- package/tests/router/prefetchLink.test.tsx +0 -16
- package/tests/router/prefetchLinkPreload.test.tsx +12 -25
- package/tests/router/routeTree.test.ts +144 -624
- package/tests/router/rsc.test.tsx +14 -31
- package/tests/router/tanstackTypes.test.ts +92 -616
- package/tests/realm-isolation.test.ts +0 -120
- package/tests/router/fixtures/legacyRuntimeRouter.ts +0 -10
- package/tests/router/fixtures/rscDecoderSecurityChild.cjs +0 -162
- package/tests/router/hooks.test.ts +0 -23
- package/tests/router/register.test.ts +0 -8
- package/tests/router/routerCompatibility.test.ts +0 -262
- package/tests/router/rscDecoderSecurity.test.ts +0 -84
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { RouterProviderFactory } from '@modern-js/runtime-extensions/router-provider';
|
|
2
|
-
import { rstest } from '@rstest/core';
|
|
3
|
-
import type { RouterExtendsHooks } from '../src/runtime/hooks';
|
|
4
|
-
|
|
5
|
-
const REGISTRY_SLOT: unique symbol = Symbol.for(
|
|
6
|
-
'@modern-js/runtime:router-providers:v3',
|
|
7
|
-
);
|
|
8
|
-
const host = globalThis as { [REGISTRY_SLOT]?: unknown };
|
|
9
|
-
|
|
10
|
-
type RuntimeContextModule = typeof import('@modern-js/runtime/context');
|
|
11
|
-
type RouterModule = typeof import('../src/runtime/router');
|
|
12
|
-
|
|
13
|
-
type Vertical = {
|
|
14
|
-
context: RuntimeContextModule;
|
|
15
|
-
providers: typeof import('@modern-js/runtime-extensions/router-provider');
|
|
16
|
-
factory: RouterProviderFactory;
|
|
17
|
-
hooks: RouterExtendsHooks;
|
|
18
|
-
Link: unknown;
|
|
19
|
-
routerPlugin: RouterModule['routerPlugin'];
|
|
20
|
-
useMatches: unknown;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
async function loadVertical(): Promise<Vertical> {
|
|
24
|
-
rstest.resetModules();
|
|
25
|
-
const routerModule = (await import('../src/runtime/router')) as RouterModule;
|
|
26
|
-
const context = (await import(
|
|
27
|
-
'@modern-js/runtime/context'
|
|
28
|
-
)) as RuntimeContextModule;
|
|
29
|
-
const { tanstackRouterProviderFactory } = await import(
|
|
30
|
-
'../src/runtime/register'
|
|
31
|
-
);
|
|
32
|
-
const { routerProviderRegistryHooks } = await import('../src/runtime/hooks');
|
|
33
|
-
const { Link } = await import('../src/runtime/prefetchLink');
|
|
34
|
-
const { useMatches } = await import('../src/runtime/routeHooks');
|
|
35
|
-
|
|
36
|
-
const providers = await import(
|
|
37
|
-
'@modern-js/runtime-extensions/router-provider'
|
|
38
|
-
);
|
|
39
|
-
return {
|
|
40
|
-
context,
|
|
41
|
-
providers,
|
|
42
|
-
factory: tanstackRouterProviderFactory,
|
|
43
|
-
hooks: routerProviderRegistryHooks,
|
|
44
|
-
Link,
|
|
45
|
-
routerPlugin: routerModule.routerPlugin,
|
|
46
|
-
useMatches,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function setupRouterWrapper(vertical: Vertical) {
|
|
51
|
-
let onBeforeRender:
|
|
52
|
-
| ((context: any, interrupt: (value?: unknown) => unknown) => unknown)
|
|
53
|
-
| undefined;
|
|
54
|
-
|
|
55
|
-
const plugin = vertical.routerPlugin({ framework: 'tanstack' });
|
|
56
|
-
plugin.setup?.({
|
|
57
|
-
getHooks: () => vertical.hooks,
|
|
58
|
-
onAfterCreateRouter: () => undefined,
|
|
59
|
-
getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
|
|
60
|
-
onBeforeRender: listener => {
|
|
61
|
-
onBeforeRender = listener;
|
|
62
|
-
},
|
|
63
|
-
wrapRoot: () => undefined,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(typeof onBeforeRender).toBe('function');
|
|
67
|
-
const runtimeContext: any = {};
|
|
68
|
-
onBeforeRender?.(runtimeContext, () => undefined);
|
|
69
|
-
return runtimeContext.router as { Link: unknown; useMatches: unknown };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
describe('TanStack provider runtime-realm isolation', () => {
|
|
73
|
-
beforeEach(() => {
|
|
74
|
-
delete host[REGISTRY_SLOT];
|
|
75
|
-
rstest.resetModules();
|
|
76
|
-
(
|
|
77
|
-
globalThis as typeof globalThis & {
|
|
78
|
-
__webpack_require__?: { u: (chunkId: unknown) => string };
|
|
79
|
-
}
|
|
80
|
-
).__webpack_require__ = { u: chunkId => String(chunkId) };
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
afterEach(() => {
|
|
84
|
-
delete host[REGISTRY_SLOT];
|
|
85
|
-
rstest.resetModules();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('binds each production router wrapper to its own real provider graph', async () => {
|
|
89
|
-
const warnSpy = rstest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
90
|
-
try {
|
|
91
|
-
const verticalA = await loadVertical();
|
|
92
|
-
const routerA = setupRouterWrapper(verticalA);
|
|
93
|
-
expect(routerA.Link).toBe(verticalA.Link);
|
|
94
|
-
expect(routerA.useMatches).toBe(verticalA.useMatches);
|
|
95
|
-
|
|
96
|
-
const verticalB = await loadVertical();
|
|
97
|
-
expect(verticalB.context).not.toBe(verticalA.context);
|
|
98
|
-
expect(verticalB.factory).not.toBe(verticalA.factory);
|
|
99
|
-
expect(verticalB.Link).not.toBe(verticalA.Link);
|
|
100
|
-
expect(verticalB.useMatches).not.toBe(verticalA.useMatches);
|
|
101
|
-
|
|
102
|
-
const routerB = setupRouterWrapper(verticalB);
|
|
103
|
-
expect(routerB.Link).toBe(verticalB.Link);
|
|
104
|
-
expect(routerB.useMatches).toBe(verticalB.useMatches);
|
|
105
|
-
expect(routerB.Link).not.toBe(verticalA.Link);
|
|
106
|
-
expect(routerB.useMatches).not.toBe(verticalA.useMatches);
|
|
107
|
-
|
|
108
|
-
// Mixed-version callers that do not supply a realm still see the shared
|
|
109
|
-
// keep-first fallback, but production wrappers no longer consume it.
|
|
110
|
-
expect(verticalB.providers.resolveRouterProvider('tanstack')).toBe(
|
|
111
|
-
verticalA.factory,
|
|
112
|
-
);
|
|
113
|
-
expect(warnSpy).toHaveBeenCalledWith(
|
|
114
|
-
expect.stringMatching(/keeping the first registration/),
|
|
115
|
-
);
|
|
116
|
-
} finally {
|
|
117
|
-
warnSpy.mockRestore();
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
let invocationCount = 0;
|
|
2
|
-
|
|
3
|
-
export const routerPlugin = (
|
|
4
|
-
_userConfig: Record<string, unknown> = {},
|
|
5
|
-
): { name?: string; setup?: (...args: unknown[]) => unknown } => {
|
|
6
|
-
invocationCount += 1;
|
|
7
|
-
throw new Error('The legacy global router wrapper must not be invoked.');
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export const getLegacyRouterPluginInvocationCount = () => invocationCount;
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
const path = require('node:path');
|
|
2
|
-
|
|
3
|
-
const scenario = process.argv[2];
|
|
4
|
-
const serverPath = process.argv[3];
|
|
5
|
-
const clientPath = process.argv[4];
|
|
6
|
-
let moduleLoads = 0;
|
|
7
|
-
|
|
8
|
-
global.__webpack_chunk_load__ = async () => {};
|
|
9
|
-
global.__webpack_require__ = () => {
|
|
10
|
-
moduleLoads += 1;
|
|
11
|
-
return {
|
|
12
|
-
action(formData) {
|
|
13
|
-
return formData;
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
};
|
|
17
|
-
global.__rspack_rsc_hot_reloader__ = { on: () => () => {} };
|
|
18
|
-
global.__rspack_rsc_manifest__ = {
|
|
19
|
-
clientManifest: {},
|
|
20
|
-
entryCssFiles: {},
|
|
21
|
-
entryJsFiles: [],
|
|
22
|
-
moduleLoading: { prefix: '', crossOrigin: '' },
|
|
23
|
-
serverConsumerModuleMap: {},
|
|
24
|
-
serverManifest: Object.fromEntries(
|
|
25
|
-
Array.from({ length: 8 }, (_, index) => [
|
|
26
|
-
`action-${index}`,
|
|
27
|
-
{ id: 'server-module', chunks: [], name: 'action' },
|
|
28
|
-
]),
|
|
29
|
-
),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
let mapArrayConstructions = 0;
|
|
33
|
-
if (scenario === 'cycle') {
|
|
34
|
-
const NativeMap = Map;
|
|
35
|
-
global.Map = class CountedMap extends NativeMap {
|
|
36
|
-
constructor(iterable) {
|
|
37
|
-
if (Array.isArray(iterable)) {
|
|
38
|
-
mapArrayConstructions += 1;
|
|
39
|
-
}
|
|
40
|
-
super(iterable);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const server = require(path.resolve(serverPath));
|
|
46
|
-
|
|
47
|
-
function output(value) {
|
|
48
|
-
process.stdout.write(JSON.stringify(value), () => process.exit(0));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function main() {
|
|
52
|
-
switch (scenario) {
|
|
53
|
-
case 'cycle': {
|
|
54
|
-
const body = new FormData();
|
|
55
|
-
body.set(
|
|
56
|
-
'0',
|
|
57
|
-
JSON.stringify([
|
|
58
|
-
...Array.from({ length: 8 }, (_, index) => [`key-${index}`, index]),
|
|
59
|
-
...Array.from({ length: 8 }, () => '$Q0'),
|
|
60
|
-
]),
|
|
61
|
-
);
|
|
62
|
-
let error;
|
|
63
|
-
try {
|
|
64
|
-
await server.decodeReply(body);
|
|
65
|
-
} catch (caught) {
|
|
66
|
-
error = caught;
|
|
67
|
-
}
|
|
68
|
-
output({ mapArrayConstructions, rejected: error instanceof Error });
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
case 'referenced-form-data': {
|
|
72
|
-
const body = new FormData();
|
|
73
|
-
body.append('_1_item', 'first');
|
|
74
|
-
body.append('_2_item', 'second');
|
|
75
|
-
body.append('0', JSON.stringify(['$K1', '$K2']));
|
|
76
|
-
let keyIterations = 0;
|
|
77
|
-
const originalKeys = body.keys.bind(body);
|
|
78
|
-
body.keys = () => {
|
|
79
|
-
keyIterations += 1;
|
|
80
|
-
return originalKeys();
|
|
81
|
-
};
|
|
82
|
-
const decoded = await server.decodeReply(body);
|
|
83
|
-
output({
|
|
84
|
-
keyIterations,
|
|
85
|
-
values: decoded.map(value => value.get('item')),
|
|
86
|
-
});
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
case 'blob-type': {
|
|
90
|
-
const body = new FormData();
|
|
91
|
-
body.set('1', 'not-a-blob');
|
|
92
|
-
body.set('0', JSON.stringify('$B1'));
|
|
93
|
-
let error;
|
|
94
|
-
try {
|
|
95
|
-
await server.decodeReply(body);
|
|
96
|
-
} catch (caught) {
|
|
97
|
-
error = caught;
|
|
98
|
-
}
|
|
99
|
-
output({
|
|
100
|
-
rejected: error instanceof Error,
|
|
101
|
-
returnedString: error === undefined,
|
|
102
|
-
});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
case 'async-iterator': {
|
|
106
|
-
let throwCalls = 0;
|
|
107
|
-
const reason = new Error('iterator failed');
|
|
108
|
-
const iterator = {
|
|
109
|
-
next: () => Promise.reject(reason),
|
|
110
|
-
throw: () => {
|
|
111
|
-
throwCalls += 1;
|
|
112
|
-
return throwCalls < 6
|
|
113
|
-
? Promise.reject(reason)
|
|
114
|
-
: {
|
|
115
|
-
// biome-ignore lint/suspicious/noThenProperty: The regression requires a deliberately pending thenable to bound the vulnerable recursion.
|
|
116
|
-
then: () => {},
|
|
117
|
-
};
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
const iterable = { [Symbol.asyncIterator]: () => iterator };
|
|
121
|
-
try {
|
|
122
|
-
await server.decodeReplyFromAsyncIterable(iterable);
|
|
123
|
-
} catch {}
|
|
124
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
125
|
-
output({ throwCalls });
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
case 'action-load': {
|
|
129
|
-
const body = new FormData();
|
|
130
|
-
for (let index = 0; index < 8; index += 1) {
|
|
131
|
-
body.append(`$ACTION_ID_action-${index}`, '');
|
|
132
|
-
}
|
|
133
|
-
const action = await server.decodeAction(body);
|
|
134
|
-
if (action === null) {
|
|
135
|
-
throw new Error('Expected an action');
|
|
136
|
-
}
|
|
137
|
-
const submitted = await action();
|
|
138
|
-
output({
|
|
139
|
-
moduleLoads,
|
|
140
|
-
submittedEntries: Array.from(submitted.entries()),
|
|
141
|
-
});
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
case 'form-data-roundtrip': {
|
|
145
|
-
const client = require(path.resolve(clientPath));
|
|
146
|
-
const nested = new FormData();
|
|
147
|
-
nested.append('item', 'first');
|
|
148
|
-
nested.append('item', 'second');
|
|
149
|
-
const encoded = await client.encodeReply({ nested });
|
|
150
|
-
const decoded = await server.decodeReply(encoded);
|
|
151
|
-
output({ values: decoded.nested.getAll('item') });
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
default:
|
|
155
|
-
throw new Error(`Unknown scenario: ${scenario}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
main().catch(error => {
|
|
160
|
-
console.error(error);
|
|
161
|
-
process.exitCode = 1;
|
|
162
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
modifyRoutes as canonicalModifyRoutes,
|
|
3
|
-
routerProviderRegistryHooks,
|
|
4
|
-
} from '@modern-js/runtime/context';
|
|
5
|
-
import * as tanstackHooks from '../../src/runtime/hooks';
|
|
6
|
-
import { tanstackRouterPlugin as browserPlugin } from '../../src/runtime/plugin';
|
|
7
|
-
import { tanstackRouterPlugin as nodePlugin } from '../../src/runtime/plugin.node';
|
|
8
|
-
|
|
9
|
-
describe('tanstack router hooks single declaration source', () => {
|
|
10
|
-
test('re-exports and registers the canonical hook instances', () => {
|
|
11
|
-
// Identity matters: separate instances would split the hook registry
|
|
12
|
-
// between the built-in router wrapper and this provider.
|
|
13
|
-
expect(tanstackHooks.modifyRoutes).toBe(canonicalModifyRoutes);
|
|
14
|
-
expect(tanstackHooks.routerProviderRegistryHooks).toBe(
|
|
15
|
-
routerProviderRegistryHooks,
|
|
16
|
-
);
|
|
17
|
-
expect(tanstackHooks.modifyRoutes).toBe(
|
|
18
|
-
routerProviderRegistryHooks.modifyRoutes,
|
|
19
|
-
);
|
|
20
|
-
expect(browserPlugin().registryHooks).toBe(routerProviderRegistryHooks);
|
|
21
|
-
expect(nodePlugin().registryHooks).toBe(routerProviderRegistryHooks);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { resolveRouterProvider } from '@modern-js/runtime-extensions/router-provider';
|
|
2
|
-
import '../../src/runtime/register';
|
|
3
|
-
|
|
4
|
-
describe("'@modern-js/plugin-tanstack/runtime' import side effects", () => {
|
|
5
|
-
it('registers the tanstack router provider', () => {
|
|
6
|
-
expect(typeof resolveRouterProvider('tanstack')).toBe('function');
|
|
7
|
-
});
|
|
8
|
-
});
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import {
|
|
3
|
-
mkdirSync,
|
|
4
|
-
mkdtempSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
rmSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from 'node:fs';
|
|
9
|
-
import { tmpdir } from 'node:os';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
|
|
12
|
-
const REGISTRY_SLOT: unique symbol = Symbol.for(
|
|
13
|
-
'@modern-js/runtime:router-providers:v3',
|
|
14
|
-
);
|
|
15
|
-
const registryHost = globalThis as { [REGISTRY_SLOT]?: unknown };
|
|
16
|
-
const webpackHost = globalThis as typeof globalThis & {
|
|
17
|
-
__webpack_require__?: { u: (chunkId: unknown) => string };
|
|
18
|
-
};
|
|
19
|
-
const installWebpackRequire = () => {
|
|
20
|
-
webpackHost.__webpack_require__ = { u: chunkId => String(chunkId) };
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type LegacyRuntimeRouterModule = {
|
|
24
|
-
getLegacyRouterPluginInvocationCount: () => number;
|
|
25
|
-
routerPlugin: (...args: unknown[]) => unknown;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type CompatibilityGraph = {
|
|
29
|
-
factory: (...args: any[]) => any;
|
|
30
|
-
Link: unknown;
|
|
31
|
-
hooks: Record<string, unknown>;
|
|
32
|
-
legacyRuntimeRouter: LegacyRuntimeRouterModule;
|
|
33
|
-
resolveRouterProvider: (
|
|
34
|
-
framework?: string,
|
|
35
|
-
) => (...args: unknown[]) => unknown;
|
|
36
|
-
routerPlugin: (...args: any[]) => any;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
async function loadCompatibilityGraph(): Promise<CompatibilityGraph> {
|
|
40
|
-
rstest.resetModules();
|
|
41
|
-
installWebpackRequire();
|
|
42
|
-
|
|
43
|
-
const legacyRuntimeRouter = (await import(
|
|
44
|
-
'@modern-js/runtime/router/internal'
|
|
45
|
-
)) as unknown as LegacyRuntimeRouterModule;
|
|
46
|
-
const { routerPlugin } = await import('../../src/runtime/router');
|
|
47
|
-
const { tanstackRouterProviderFactory } = await import(
|
|
48
|
-
'../../src/runtime/register'
|
|
49
|
-
);
|
|
50
|
-
const { resolveRouterProvider } = await import(
|
|
51
|
-
'@modern-js/runtime-extensions/router-provider'
|
|
52
|
-
);
|
|
53
|
-
const { routerProviderRegistryHooks } = await import(
|
|
54
|
-
'../../src/runtime/hooks'
|
|
55
|
-
);
|
|
56
|
-
const { Link } = await import('../../src/runtime/prefetchLink');
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
factory: tanstackRouterProviderFactory,
|
|
60
|
-
Link,
|
|
61
|
-
hooks: routerProviderRegistryHooks,
|
|
62
|
-
legacyRuntimeRouter,
|
|
63
|
-
resolveRouterProvider,
|
|
64
|
-
routerPlugin,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function initializeRouter(graph: CompatibilityGraph) {
|
|
69
|
-
const beforeRender: ((context: any, interrupt: () => void) => unknown)[] = [];
|
|
70
|
-
const plugin = graph.routerPlugin({ framework: 'tanstack' });
|
|
71
|
-
plugin.setup({
|
|
72
|
-
getHooks: () => graph.hooks,
|
|
73
|
-
getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
|
|
74
|
-
onAfterCreateRouter: () => undefined,
|
|
75
|
-
onBeforeRender: (
|
|
76
|
-
callback: (context: any, interrupt: () => void) => unknown,
|
|
77
|
-
) => beforeRender.push(callback),
|
|
78
|
-
wrapRoot: () => undefined,
|
|
79
|
-
});
|
|
80
|
-
const context: any = {};
|
|
81
|
-
for (const callback of beforeRender) callback(context, () => {});
|
|
82
|
-
return context.router as { Link: unknown };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function resolveTsgoBin(): string {
|
|
86
|
-
const packageJsonPath = require.resolve(
|
|
87
|
-
'@typescript/native-preview/package.json',
|
|
88
|
-
);
|
|
89
|
-
const packageDirectory = path.dirname(packageJsonPath);
|
|
90
|
-
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
|
91
|
-
bin?: string | { tsgo?: string };
|
|
92
|
-
};
|
|
93
|
-
const binEntry =
|
|
94
|
-
typeof packageJson.bin === 'string'
|
|
95
|
-
? packageJson.bin
|
|
96
|
-
: packageJson.bin?.tsgo;
|
|
97
|
-
|
|
98
|
-
return path.resolve(packageDirectory, binEntry ?? 'bin/tsgo.js');
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function runTsgo(tsgoBin: string, configPath: string): void {
|
|
102
|
-
try {
|
|
103
|
-
execFileSync(process.execPath, [tsgoBin, '-p', configPath], {
|
|
104
|
-
stdio: 'pipe',
|
|
105
|
-
});
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const result = error as { stderr?: Buffer; stdout?: Buffer };
|
|
108
|
-
throw new Error(
|
|
109
|
-
`TS-Go compatibility check failed:\n${String(result.stdout ?? '')}${String(
|
|
110
|
-
result.stderr ?? '',
|
|
111
|
-
)}`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
installWebpackRequire();
|
|
117
|
-
|
|
118
|
-
describe('runtime router compatibility', () => {
|
|
119
|
-
beforeEach(() => {
|
|
120
|
-
delete registryHost[REGISTRY_SLOT];
|
|
121
|
-
rstest.resetModules();
|
|
122
|
-
installWebpackRequire();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
afterEach(() => {
|
|
126
|
-
delete registryHost[REGISTRY_SLOT];
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('uses the app-local factory instead of the legacy global wrapper', async () => {
|
|
130
|
-
const graph = await loadCompatibilityGraph();
|
|
131
|
-
|
|
132
|
-
expect('createRouterPlugin' in graph.legacyRuntimeRouter).toBe(false);
|
|
133
|
-
expect(initializeRouter(graph).Link).toBe(graph.Link);
|
|
134
|
-
expect(
|
|
135
|
-
graph.legacyRuntimeRouter.getLegacyRouterPluginInvocationCount(),
|
|
136
|
-
).toBe(0);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('keeps independently evaluated legacy module graphs app-local', async () => {
|
|
140
|
-
const graphA = await loadCompatibilityGraph();
|
|
141
|
-
const graphB = await loadCompatibilityGraph();
|
|
142
|
-
|
|
143
|
-
expect(graphB.factory).not.toBe(graphA.factory);
|
|
144
|
-
expect(graphB.routerPlugin).not.toBe(graphA.routerPlugin);
|
|
145
|
-
expect(graphB.resolveRouterProvider('tanstack')).toBe(graphA.factory);
|
|
146
|
-
expect(initializeRouter(graphA).Link).toBe(graphA.Link);
|
|
147
|
-
expect(initializeRouter(graphB).Link).toBe(graphB.Link);
|
|
148
|
-
expect(
|
|
149
|
-
graphA.legacyRuntimeRouter.getLegacyRouterPluginInvocationCount(),
|
|
150
|
-
).toBe(0);
|
|
151
|
-
expect(
|
|
152
|
-
graphB.legacyRuntimeRouter.getLegacyRouterPluginInvocationCount(),
|
|
153
|
-
).toBe(0);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('emits declarations compatible with the oldest runtime router shape', () => {
|
|
157
|
-
const packageRoot = path.resolve(__dirname, '../..');
|
|
158
|
-
const sourcePath = path.join(packageRoot, 'src/runtime/router.ts');
|
|
159
|
-
const legacyFixturePath = path.join(
|
|
160
|
-
__dirname,
|
|
161
|
-
'fixtures/legacyRuntimeRouter.ts',
|
|
162
|
-
);
|
|
163
|
-
const tempRoot = mkdtempSync(
|
|
164
|
-
path.join(tmpdir(), 'plugin-tanstack-router-compatibility-'),
|
|
165
|
-
);
|
|
166
|
-
const sourceRoot = path.join(tempRoot, 'src');
|
|
167
|
-
const emittedRoot = path.join(tempRoot, 'types');
|
|
168
|
-
const tsgoBin = resolveTsgoBin();
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
mkdirSync(sourceRoot, { recursive: true });
|
|
172
|
-
writeFileSync(
|
|
173
|
-
path.join(sourceRoot, 'router.ts'),
|
|
174
|
-
readFileSync(sourcePath, 'utf8'),
|
|
175
|
-
);
|
|
176
|
-
writeFileSync(
|
|
177
|
-
path.join(sourceRoot, 'register.ts'),
|
|
178
|
-
[
|
|
179
|
-
'export declare const tanstackRouterProviderFactory:',
|
|
180
|
-
' (userConfig?: Record<string, unknown>) => { name?: string };',
|
|
181
|
-
].join('\n'),
|
|
182
|
-
);
|
|
183
|
-
writeFileSync(
|
|
184
|
-
path.join(tempRoot, 'currentRuntimeRouter.d.ts'),
|
|
185
|
-
[
|
|
186
|
-
'export type RouterPluginFactory =',
|
|
187
|
-
' (userConfig?: Record<string, unknown>) => { name?: string };',
|
|
188
|
-
'export declare const routerPlugin: RouterPluginFactory;',
|
|
189
|
-
'export declare const createRouterPlugin:',
|
|
190
|
-
' (providers: readonly unknown[]) => RouterPluginFactory;',
|
|
191
|
-
].join('\n'),
|
|
192
|
-
);
|
|
193
|
-
writeFileSync(
|
|
194
|
-
path.join(tempRoot, 'emit.json'),
|
|
195
|
-
JSON.stringify(
|
|
196
|
-
{
|
|
197
|
-
compilerOptions: {
|
|
198
|
-
declaration: true,
|
|
199
|
-
emitDeclarationOnly: true,
|
|
200
|
-
module: 'Preserve',
|
|
201
|
-
moduleResolution: 'Bundler',
|
|
202
|
-
noCheck: true,
|
|
203
|
-
outDir: emittedRoot,
|
|
204
|
-
paths: {
|
|
205
|
-
'@modern-js/runtime/router/internal': [
|
|
206
|
-
'./currentRuntimeRouter.d.ts',
|
|
207
|
-
],
|
|
208
|
-
},
|
|
209
|
-
rootDir: sourceRoot,
|
|
210
|
-
skipLibCheck: false,
|
|
211
|
-
strict: true,
|
|
212
|
-
target: 'ESNext',
|
|
213
|
-
types: [],
|
|
214
|
-
},
|
|
215
|
-
files: [path.join(sourceRoot, 'router.ts')],
|
|
216
|
-
},
|
|
217
|
-
null,
|
|
218
|
-
2,
|
|
219
|
-
),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
runTsgo(tsgoBin, path.join(tempRoot, 'emit.json'));
|
|
223
|
-
|
|
224
|
-
const declarationPath = path.join(emittedRoot, 'router.d.ts');
|
|
225
|
-
writeFileSync(
|
|
226
|
-
path.join(tempRoot, 'legacyRuntimeRouter.ts'),
|
|
227
|
-
readFileSync(legacyFixturePath, 'utf8'),
|
|
228
|
-
);
|
|
229
|
-
writeFileSync(
|
|
230
|
-
path.join(tempRoot, 'compatibility.json'),
|
|
231
|
-
JSON.stringify(
|
|
232
|
-
{
|
|
233
|
-
compilerOptions: {
|
|
234
|
-
module: 'Preserve',
|
|
235
|
-
moduleResolution: 'Bundler',
|
|
236
|
-
noEmit: true,
|
|
237
|
-
paths: {
|
|
238
|
-
'@modern-js/runtime/router/internal': [
|
|
239
|
-
'./legacyRuntimeRouter.ts',
|
|
240
|
-
],
|
|
241
|
-
},
|
|
242
|
-
skipLibCheck: false,
|
|
243
|
-
strict: true,
|
|
244
|
-
target: 'ESNext',
|
|
245
|
-
types: [],
|
|
246
|
-
},
|
|
247
|
-
files: [
|
|
248
|
-
declarationPath,
|
|
249
|
-
path.join(tempRoot, 'legacyRuntimeRouter.ts'),
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
null,
|
|
253
|
-
2,
|
|
254
|
-
),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
runTsgo(tsgoBin, path.join(tempRoot, 'compatibility.json'));
|
|
258
|
-
} finally {
|
|
259
|
-
rmSync(tempRoot, { force: true, recursive: true });
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
|
|
5
|
-
type DecoderScenario =
|
|
6
|
-
| 'async-iterator'
|
|
7
|
-
| 'blob-type'
|
|
8
|
-
| 'cycle'
|
|
9
|
-
| 'form-data-roundtrip';
|
|
10
|
-
|
|
11
|
-
const resolvePackage = createRequire(
|
|
12
|
-
path.join(__dirname, 'rsc-decoder-security-resolver.cjs'),
|
|
13
|
-
).resolve;
|
|
14
|
-
const childPath = path.join(
|
|
15
|
-
__dirname,
|
|
16
|
-
'fixtures',
|
|
17
|
-
'rscDecoderSecurityChild.cjs',
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
function runDecoderScenario(options: {
|
|
21
|
-
entry: 'edge' | 'node';
|
|
22
|
-
scenario: DecoderScenario;
|
|
23
|
-
}): Record<string, unknown> {
|
|
24
|
-
const result = spawnSync(
|
|
25
|
-
process.execPath,
|
|
26
|
-
[
|
|
27
|
-
'--conditions=react-server',
|
|
28
|
-
childPath,
|
|
29
|
-
options.scenario,
|
|
30
|
-
resolvePackage(`react-server-dom-rspack/server.${options.entry}`),
|
|
31
|
-
resolvePackage(`react-server-dom-rspack/client.${options.entry}`),
|
|
32
|
-
],
|
|
33
|
-
{
|
|
34
|
-
encoding: 'utf8',
|
|
35
|
-
env: { ...process.env, NODE_ENV: 'production' },
|
|
36
|
-
timeout: 5_000,
|
|
37
|
-
},
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
if (result.error || result.status !== 0) {
|
|
41
|
-
throw new Error(
|
|
42
|
-
[
|
|
43
|
-
`RSC decoder child failed for ${options.entry}/production/${options.scenario}`,
|
|
44
|
-
result.stdout,
|
|
45
|
-
result.stderr,
|
|
46
|
-
]
|
|
47
|
-
.filter(Boolean)
|
|
48
|
-
.join('\n'),
|
|
49
|
-
{ cause: result.error },
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return JSON.parse(result.stdout) as Record<string, unknown>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
for (const entry of ['node', 'edge'] as const) {
|
|
57
|
-
describe(`${entry} production Flight decoder security`, () => {
|
|
58
|
-
test('bounds cyclic Map reconstruction before invoking constructors', () => {
|
|
59
|
-
const result = runDecoderScenario({ entry, scenario: 'cycle' });
|
|
60
|
-
|
|
61
|
-
expect(result.rejected).toBe(true);
|
|
62
|
-
expect(result.mapArrayConstructions).toBeLessThanOrEqual(2);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('round-trips referenced FormData through the public client and server APIs', () => {
|
|
66
|
-
expect(
|
|
67
|
-
runDecoderScenario({ entry, scenario: 'form-data-roundtrip' }),
|
|
68
|
-
).toEqual({ values: ['first', 'second'] });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('rejects a Blob reference backed by a string', () => {
|
|
72
|
-
expect(runDecoderScenario({ entry, scenario: 'blob-type' })).toEqual({
|
|
73
|
-
rejected: true,
|
|
74
|
-
returnedString: false,
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('does not recursively throw into a rejecting async iterator', () => {
|
|
79
|
-
expect(runDecoderScenario({ entry, scenario: 'async-iterator' })).toEqual(
|
|
80
|
-
{ throwCalls: 1 },
|
|
81
|
-
);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
}
|