@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.10
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/{reactContext.js → localisedUrls/config.js} +14 -10
- package/dist/cjs/localisedUrls/index.js +95 -0
- package/dist/cjs/localisedUrls/normalise.js +87 -0
- package/dist/cjs/localisedUrls/pathname.js +68 -0
- package/dist/cjs/localisedUrls/patterns.js +214 -0
- package/dist/cjs/localisedUrls/redirect.js +104 -0
- package/dist/cjs/localisedUrls/resolve.js +78 -0
- package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
- package/dist/cjs/localisedUrls/routes.js +217 -0
- package/dist/cjs/localisedUrls/types.js +18 -0
- package/dist/cjs/rendererHead.js +18 -5
- package/dist/cjs/request-policy/policyCore.js +1 -1
- package/dist/cjs/request-policy/transport.js +2 -2
- package/dist/cjs/routerState.js +85 -77
- package/dist/cjs/routerStatePlugin.js +63 -9
- package/dist/cjs/server-config/serverSource.js +18 -0
- package/dist/esm/localisedUrls/config.mjs +11 -0
- package/dist/esm/localisedUrls/index.mjs +8 -0
- package/dist/esm/localisedUrls/normalise.mjs +37 -0
- package/dist/esm/localisedUrls/pathname.mjs +24 -0
- package/dist/esm/localisedUrls/patterns.mjs +152 -0
- package/dist/esm/localisedUrls/redirect.mjs +57 -0
- package/dist/esm/localisedUrls/resolve.mjs +37 -0
- package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
- package/dist/esm/localisedUrls/routes.mjs +176 -0
- package/dist/esm/localisedUrls/types.mjs +0 -0
- package/dist/esm/rendererHead.mjs +14 -5
- package/dist/esm/request-policy/policyCore.mjs +1 -1
- package/dist/esm/request-policy/transport.mjs +2 -2
- package/dist/esm/routerState.mjs +81 -73
- package/dist/esm/routerStatePlugin.mjs +62 -11
- package/dist/esm/server-config/serverSource.mjs +0 -0
- package/dist/esm-node/localisedUrls/config.mjs +12 -0
- package/dist/esm-node/localisedUrls/index.mjs +9 -0
- package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
- package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
- package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
- package/dist/esm-node/localisedUrls/redirect.mjs +58 -0
- package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
- package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
- package/dist/esm-node/localisedUrls/routes.mjs +177 -0
- package/dist/esm-node/localisedUrls/types.mjs +1 -0
- package/dist/esm-node/rendererHead.mjs +14 -5
- package/dist/esm-node/request-policy/policyCore.mjs +1 -1
- package/dist/esm-node/request-policy/transport.mjs +2 -2
- package/dist/esm-node/routerState.mjs +81 -73
- package/dist/esm-node/routerStatePlugin.mjs +62 -11
- package/dist/esm-node/server-config/serverSource.mjs +1 -0
- package/dist/types/helmetCompat.d.ts +1 -1
- package/dist/types/localisedUrls/config.d.ts +9 -0
- package/dist/types/localisedUrls/index.d.ts +10 -0
- package/dist/types/localisedUrls/normalise.d.ts +10 -0
- package/dist/types/localisedUrls/pathname.d.ts +4 -0
- package/dist/types/localisedUrls/patterns.d.ts +11 -0
- package/dist/types/localisedUrls/redirect.d.ts +19 -0
- package/dist/types/localisedUrls/resolve.d.ts +7 -0
- package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
- package/dist/types/localisedUrls/routes.d.ts +3 -0
- package/dist/types/localisedUrls/types.d.ts +14 -0
- package/dist/types/rendererHead.d.ts +5 -3
- package/dist/types/request-policy/browser.d.ts +5 -1
- package/dist/types/request-policy/node.d.ts +5 -1
- package/dist/types/request-policy/types.d.ts +1 -1
- package/dist/types/routeHydration.d.ts +2 -2
- package/dist/types/routerProvider.d.ts +1 -3
- package/dist/types/routerState.d.ts +11 -6
- package/dist/types/routerStatePlugin.d.ts +12 -0
- package/dist/types/routerStateTypes.d.ts +47 -17
- package/dist/types/server-config/index.d.ts +1 -0
- package/dist/types/server-config/serverSource.d.ts +16 -0
- package/package.json +43 -45
- package/src/helmetCompat.ts +1 -1
- package/src/localisedUrls/config.ts +18 -0
- package/src/localisedUrls/index.ts +25 -0
- package/src/localisedUrls/normalise.ts +62 -0
- package/src/localisedUrls/pathname.ts +60 -0
- package/src/localisedUrls/patterns.ts +283 -0
- package/src/localisedUrls/redirect.ts +110 -0
- package/src/localisedUrls/resolve.ts +106 -0
- package/src/localisedUrls/routeIdentity.ts +83 -0
- package/src/localisedUrls/routes.ts +451 -0
- package/src/localisedUrls/types.ts +18 -0
- package/src/rendererHead.ts +25 -8
- package/src/request-policy/policyCore.ts +1 -1
- package/src/request-policy/transport.ts +2 -2
- package/src/request-policy/types.ts +1 -1
- package/src/routeHydration.ts +2 -2
- package/src/routerProvider.ts +1 -1
- package/src/routerState.ts +127 -115
- package/src/routerStatePlugin.ts +106 -10
- package/src/routerStateTypes.ts +47 -17
- package/src/server-config/index.ts +1 -0
- package/src/server-config/serverSource.ts +23 -0
- package/dist/cjs/buildIdentity.js +0 -73
- package/dist/esm/buildIdentity.mjs +0 -35
- package/dist/esm/reactContext.mjs +0 -7
- package/dist/esm-node/buildIdentity.mjs +0 -36
- package/dist/esm-node/reactContext.mjs +0 -8
- package/dist/types/buildIdentity.d.ts +0 -37
- package/dist/types/reactContext.d.ts +0 -6
- package/rslib.config.mts +0 -19
- package/rstest.config.mts +0 -38
- package/src/buildIdentity.ts +0 -61
- package/src/reactContext.ts +0 -16
- package/tests/buildIdentity.test.ts +0 -51
- package/tests/contextExtensions.test.ts +0 -27
- package/tests/headRuntime.test.tsx +0 -229
- package/tests/rendererHead.test.ts +0 -187
- package/tests/request-policy/browser-policy.test.ts +0 -108
- package/tests/request-policy/client-isolation.test.ts +0 -109
- package/tests/request-policy/node-policy.test.ts +0 -447
- package/tests/request-policy/policyCore.test.ts +0 -129
- package/tests/request-policy/requestContext.test.ts +0 -66
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
- package/tests/request-policy/traceparent.test.ts +0 -52
- package/tests/request-policy/transport-retry.test.ts +0 -367
- package/tests/routeHydration.test.ts +0 -180
- package/tests/routerPrefetchPolicy.test.ts +0 -102
- package/tests/routerProvider.test.ts +0 -29
- package/tests/routerState.test.ts +0 -78
- package/tests/routerStatePlugin.test.ts +0 -63
- package/tests/runtimeContextProjection.test.ts +0 -83
- package/tests/safeFailure.test.ts +0 -136
- package/tests/shellStream.test.ts +0 -122
- package/tests/workspaceEvents.test.ts +0 -221
- package/tsconfig.json +0 -10
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createRouteHydrationScriptTags,
|
|
3
|
-
getMatchedRouteChunks,
|
|
4
|
-
injectBeforeHydrationEntryScript,
|
|
5
|
-
orderHydrationScriptChunks,
|
|
6
|
-
replaceChunkJsPlaceholder,
|
|
7
|
-
} from '../src/routeHydration';
|
|
8
|
-
|
|
9
|
-
const manifest = {
|
|
10
|
-
routeAssets: {
|
|
11
|
-
layout: { assets: ['/shared.js', '/layout.css'] },
|
|
12
|
-
page: { assets: ['/page.js', '/shared.js'] },
|
|
13
|
-
empty: {},
|
|
14
|
-
missing: undefined,
|
|
15
|
-
'async-main': { assets: ['/shared.js', '/async-main.hash.js'] },
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const tag = (src: string) => `<script src="${src}"></script>`;
|
|
20
|
-
|
|
21
|
-
describe('route hydration policy', () => {
|
|
22
|
-
it('maps matched assets in route order without deduplicating or filtering', () => {
|
|
23
|
-
const mapped = getMatchedRouteChunks(
|
|
24
|
-
manifest,
|
|
25
|
-
['layout', 'unknown', 'empty', 'missing', 'page'],
|
|
26
|
-
asset => ({ url: asset, marker: 'preserved' }),
|
|
27
|
-
);
|
|
28
|
-
expect(mapped).toEqual(
|
|
29
|
-
['/shared.js', '/layout.css', '/page.js', '/shared.js'].map(url => ({
|
|
30
|
-
url,
|
|
31
|
-
marker: 'preserved',
|
|
32
|
-
})),
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns no chunks for an explicit missing manifest or empty matches', () => {
|
|
37
|
-
const toChunk = rstest.fn((url: string) => ({ url }));
|
|
38
|
-
expect(getMatchedRouteChunks(undefined, ['page'], toChunk)).toEqual([]);
|
|
39
|
-
expect(getMatchedRouteChunks({}, ['page'], toChunk)).toEqual([]);
|
|
40
|
-
expect(getMatchedRouteChunks(manifest, [], toChunk)).toEqual([]);
|
|
41
|
-
expect(toChunk).not.toHaveBeenCalled();
|
|
42
|
-
expect(createRouteHydrationScriptTags(undefined, ['page'], 'main')).toBe(
|
|
43
|
-
'',
|
|
44
|
-
);
|
|
45
|
-
expect(createRouteHydrationScriptTags({}, ['page'], 'main')).toBe('');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('keeps dependencies, collected chunks and route chunks before async entry', () => {
|
|
49
|
-
const dependency = { url: '/dependency.js', marker: 'dependency' };
|
|
50
|
-
const collected = { url: '/collected.js', marker: 'collected' };
|
|
51
|
-
const route = { url: '/route.js', marker: 'route' };
|
|
52
|
-
const entry = { url: '/async-main.hash.js', marker: 'entry' };
|
|
53
|
-
const asyncEntryChunks = [entry, dependency];
|
|
54
|
-
const result = orderHydrationScriptChunks({
|
|
55
|
-
asyncEntryChunks,
|
|
56
|
-
collectedChunks: [collected, { ...dependency, marker: 'duplicate' }],
|
|
57
|
-
matchedRouteChunks: [route, { url: '' }, {}, { ...collected }],
|
|
58
|
-
entryName: 'main',
|
|
59
|
-
});
|
|
60
|
-
expect(result).toEqual([dependency, collected, route, entry]);
|
|
61
|
-
expect(result[0]).toBe(dependency);
|
|
62
|
-
expect(result[1]).toBe(collected);
|
|
63
|
-
expect(asyncEntryChunks).toEqual([entry, dependency]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('classifies async entries using filename while retaining the JS URL rule', () => {
|
|
67
|
-
const entry = { url: '/opaque.js', filename: 'nested/async-main-hash.js' };
|
|
68
|
-
const queried = { url: '/async-main.js?hash=1' };
|
|
69
|
-
const otherEntry = { url: '/async-other.js' };
|
|
70
|
-
const asset = { url: '/async-main.css' };
|
|
71
|
-
const route = { url: '/route.js' };
|
|
72
|
-
expect(
|
|
73
|
-
orderHydrationScriptChunks({
|
|
74
|
-
asyncEntryChunks: [entry, queried, otherEntry, asset],
|
|
75
|
-
collectedChunks: [],
|
|
76
|
-
matchedRouteChunks: [route],
|
|
77
|
-
entryName: 'main',
|
|
78
|
-
}),
|
|
79
|
-
).toEqual([queried, otherEntry, asset, route, entry]);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('serializes unique JS route assets then async assets and matches exact src', () => {
|
|
83
|
-
expect(
|
|
84
|
-
createRouteHydrationScriptTags(manifest, ['layout', 'page'], 'main', {
|
|
85
|
-
template: `${tag('/page.js.map')}${tag('/shared.js')}`,
|
|
86
|
-
}),
|
|
87
|
-
).toBe(`${tag('/page.js')} ${tag('/async-main.hash.js')}`);
|
|
88
|
-
expect(createRouteHydrationScriptTags(manifest, [], 'main')).toBe(
|
|
89
|
-
`${tag('/shared.js')} ${tag('/async-main.hash.js')}`,
|
|
90
|
-
);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('escapes asset and nonce attributes and omits empty nonce', () => {
|
|
94
|
-
const unsafeManifest = {
|
|
95
|
-
routeAssets: {
|
|
96
|
-
page: { assets: ['/route" onload="alert(1)&x=<tag>.js'] },
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
expect(
|
|
100
|
-
createRouteHydrationScriptTags(unsafeManifest, ['page'], 'main', {
|
|
101
|
-
nonce: 'nonce"&<value>',
|
|
102
|
-
}),
|
|
103
|
-
).toBe(
|
|
104
|
-
'<script src="/route" onload="alert(1)&x=<tag>.js" nonce="nonce"&<value>"></script>',
|
|
105
|
-
);
|
|
106
|
-
expect(
|
|
107
|
-
createRouteHydrationScriptTags(manifest, ['page'], 'other', {
|
|
108
|
-
nonce: '',
|
|
109
|
-
}),
|
|
110
|
-
).toBe(`${tag('/page.js')} ${tag('/shared.js')}`);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('prefers the main entry even when an async entry appears earlier', () => {
|
|
114
|
-
const asyncEntry = tag('/async-main.hash.js');
|
|
115
|
-
const entry =
|
|
116
|
-
"<script defer src='/nested/main-hash.js?build=1#hash'></script>";
|
|
117
|
-
const template = `${asyncEntry}${entry}`;
|
|
118
|
-
expect(injectBeforeHydrationEntryScript(template, 'ROUTE', 'main')).toBe(
|
|
119
|
-
`${asyncEntry}ROUTE${entry}`,
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('falls back to async entry and preserves templates with no entry', () => {
|
|
124
|
-
const asyncEntry = tag('/nested/async-main.js');
|
|
125
|
-
expect(injectBeforeHydrationEntryScript(asyncEntry, 'ROUTE', 'main')).toBe(
|
|
126
|
-
`ROUTE${asyncEntry}`,
|
|
127
|
-
);
|
|
128
|
-
expect(injectBeforeHydrationEntryScript(tag('/index.js'), 'ROUTE')).toBe(
|
|
129
|
-
`ROUTE${tag('/index.js')}`,
|
|
130
|
-
);
|
|
131
|
-
const other = tag('/other-main.js');
|
|
132
|
-
expect(injectBeforeHydrationEntryScript(other, 'ROUTE', 'main')).toBe(
|
|
133
|
-
other,
|
|
134
|
-
);
|
|
135
|
-
expect(injectBeforeHydrationEntryScript(asyncEntry, '', 'main')).toBe(
|
|
136
|
-
asyncEntry,
|
|
137
|
-
);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('moves placeholder scripts before hydration and replaces literal text safely', () => {
|
|
141
|
-
const placeholder = '<!-- chunks -->';
|
|
142
|
-
const scripts = '<script src="/$&-$`-$\'.js"></script>';
|
|
143
|
-
const entry = tag('/main.js');
|
|
144
|
-
expect(
|
|
145
|
-
replaceChunkJsPlaceholder(
|
|
146
|
-
`${entry}${placeholder}`,
|
|
147
|
-
scripts,
|
|
148
|
-
'main',
|
|
149
|
-
placeholder,
|
|
150
|
-
),
|
|
151
|
-
).toBe(`${scripts}${entry}`);
|
|
152
|
-
expect(
|
|
153
|
-
replaceChunkJsPlaceholder(
|
|
154
|
-
`before${placeholder}after`,
|
|
155
|
-
scripts,
|
|
156
|
-
'main',
|
|
157
|
-
placeholder,
|
|
158
|
-
),
|
|
159
|
-
).toBe(`before${scripts}after`);
|
|
160
|
-
expect(replaceChunkJsPlaceholder(entry, scripts, 'main', placeholder)).toBe(
|
|
161
|
-
entry,
|
|
162
|
-
);
|
|
163
|
-
expect(
|
|
164
|
-
replaceChunkJsPlaceholder(
|
|
165
|
-
`${entry}${placeholder}`,
|
|
166
|
-
'',
|
|
167
|
-
'main',
|
|
168
|
-
placeholder,
|
|
169
|
-
),
|
|
170
|
-
).toBe(entry);
|
|
171
|
-
expect(
|
|
172
|
-
replaceChunkJsPlaceholder(
|
|
173
|
-
`${placeholder}${placeholder}`,
|
|
174
|
-
'',
|
|
175
|
-
undefined,
|
|
176
|
-
placeholder,
|
|
177
|
-
),
|
|
178
|
-
).toBe(placeholder);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { createRouterPrefetchPolicy } from '../src/routerPrefetchPolicy';
|
|
2
|
-
|
|
3
|
-
const settle = async () => {
|
|
4
|
-
for (let turn = 0; turn < 10; turn += 1) await Promise.resolve();
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
afterEach(() => rstest.useRealTimers());
|
|
8
|
-
|
|
9
|
-
test('implements render defaults and cancels delayed intent before dispatch', async () => {
|
|
10
|
-
rstest.useFakeTimers();
|
|
11
|
-
const policy = createRouterPrefetchPolicy();
|
|
12
|
-
const notify = rstest.fn();
|
|
13
|
-
const render = policy.observe({ element: null, notify });
|
|
14
|
-
expect(notify).toHaveBeenLastCalledWith({ code: true, data: true });
|
|
15
|
-
render.dispose();
|
|
16
|
-
const intent = policy.observe({
|
|
17
|
-
element: null,
|
|
18
|
-
prefetch: 'none',
|
|
19
|
-
preload: 'intent',
|
|
20
|
-
notify,
|
|
21
|
-
});
|
|
22
|
-
intent.onIntent();
|
|
23
|
-
intent.onCancel();
|
|
24
|
-
await rstest.advanceTimersByTimeAsync(100);
|
|
25
|
-
expect(notify).toHaveBeenLastCalledWith({ code: false, data: false });
|
|
26
|
-
intent.onIntent();
|
|
27
|
-
await rstest.advanceTimersByTimeAsync(100);
|
|
28
|
-
expect(notify).toHaveBeenLastCalledWith({ code: true, data: false });
|
|
29
|
-
intent.dispose();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('limits concurrent loads, removes cancelled queued keys, and retries rejected loads', async () => {
|
|
33
|
-
const policy = createRouterPrefetchPolicy();
|
|
34
|
-
const context = {};
|
|
35
|
-
const loader = {};
|
|
36
|
-
const started: number[] = [];
|
|
37
|
-
const releases: (() => void)[] = [];
|
|
38
|
-
const schedule = (id: number) =>
|
|
39
|
-
policy.schedule({
|
|
40
|
-
runtimeContext: context,
|
|
41
|
-
chunkLoader: loader,
|
|
42
|
-
publicPath: '/',
|
|
43
|
-
key: String(id),
|
|
44
|
-
run: () => {
|
|
45
|
-
started.push(id);
|
|
46
|
-
return new Promise<void>(resolve => releases.push(resolve));
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
const cancellations = Array.from({ length: 6 }, (_, id) => schedule(id));
|
|
50
|
-
await settle();
|
|
51
|
-
expect(started).toEqual([0, 1, 2, 3]);
|
|
52
|
-
cancellations[4]();
|
|
53
|
-
releases[0]();
|
|
54
|
-
await settle();
|
|
55
|
-
expect(started).toEqual([0, 1, 2, 3, 5]);
|
|
56
|
-
schedule(4);
|
|
57
|
-
releases[1]();
|
|
58
|
-
await settle();
|
|
59
|
-
expect(started).toEqual([0, 1, 2, 3, 5, 4]);
|
|
60
|
-
for (const release of releases) release();
|
|
61
|
-
await settle();
|
|
62
|
-
const fail = rstest.fn(() => Promise.reject(new Error('retry')));
|
|
63
|
-
const options = {
|
|
64
|
-
runtimeContext: context,
|
|
65
|
-
chunkLoader: loader,
|
|
66
|
-
publicPath: '/',
|
|
67
|
-
key: 'failed',
|
|
68
|
-
run: fail,
|
|
69
|
-
};
|
|
70
|
-
policy.schedule(options);
|
|
71
|
-
await settle();
|
|
72
|
-
policy.schedule(options);
|
|
73
|
-
await settle();
|
|
74
|
-
expect(fail).toHaveBeenCalledTimes(2);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('deduplicates only within matching context, loader, public path and live TTL', async () => {
|
|
78
|
-
let now = 0;
|
|
79
|
-
rstest.spyOn(performance, 'now').mockImplementation(() => now);
|
|
80
|
-
const policy = createRouterPrefetchPolicy();
|
|
81
|
-
const run = rstest.fn(() => Promise.resolve());
|
|
82
|
-
const options = {
|
|
83
|
-
runtimeContext: {},
|
|
84
|
-
chunkLoader: {},
|
|
85
|
-
publicPath: '/',
|
|
86
|
-
key: 'route',
|
|
87
|
-
run,
|
|
88
|
-
};
|
|
89
|
-
policy.schedule(options);
|
|
90
|
-
policy.schedule(options);
|
|
91
|
-
await settle();
|
|
92
|
-
expect(run).toHaveBeenCalledTimes(1);
|
|
93
|
-
policy.schedule({ ...options, runtimeContext: {} });
|
|
94
|
-
policy.schedule({ ...options, chunkLoader: {} });
|
|
95
|
-
policy.schedule({ ...options, publicPath: '/other/' });
|
|
96
|
-
await settle();
|
|
97
|
-
expect(run).toHaveBeenCalledTimes(4);
|
|
98
|
-
now = 30_001;
|
|
99
|
-
policy.schedule(options);
|
|
100
|
-
await settle();
|
|
101
|
-
expect(run).toHaveBeenCalledTimes(5);
|
|
102
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { createRouterPlugin } from '../src/routerProvider';
|
|
2
|
-
|
|
3
|
-
test('selects the injected app-local factory from runtime config and preserves native hook objects', () => {
|
|
4
|
-
const registryHooks = { afterCreate: { call() {} } };
|
|
5
|
-
const defaultFactory = rstest.fn(() => ({ setup: rstest.fn() }));
|
|
6
|
-
const setup = rstest.fn();
|
|
7
|
-
const localFactory = rstest.fn(() => ({ registryHooks, setup }));
|
|
8
|
-
const plugin = createRouterPlugin({
|
|
9
|
-
defaultProvider: { name: 'native', factory: defaultFactory },
|
|
10
|
-
registryHooks,
|
|
11
|
-
localProviders: [{ name: 'custom', factory: localFactory }],
|
|
12
|
-
})();
|
|
13
|
-
const api = { getRuntimeConfig: () => ({ router: { framework: 'custom' } }) };
|
|
14
|
-
expect(plugin.registryHooks).toBe(registryHooks);
|
|
15
|
-
plugin.setup(api);
|
|
16
|
-
expect(localFactory).toHaveBeenCalledWith({});
|
|
17
|
-
expect(setup).toHaveBeenCalledWith(api);
|
|
18
|
-
expect(defaultFactory).not.toHaveBeenCalled();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('rejects missing local providers', () => {
|
|
22
|
-
const factory = createRouterPlugin({
|
|
23
|
-
defaultProvider: { name: 'native', factory: () => ({}) },
|
|
24
|
-
registryHooks: {},
|
|
25
|
-
});
|
|
26
|
-
expect(() =>
|
|
27
|
-
factory({ framework: 'foreign' }).setup({ getRuntimeConfig: () => ({}) }),
|
|
28
|
-
).toThrow(/app-owned router provider realm/);
|
|
29
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
applyRouterRuntimeState,
|
|
3
|
-
applyRouterServerPrepareResult,
|
|
4
|
-
cleanupRouterRuntimeState,
|
|
5
|
-
getRouterHydrationScripts,
|
|
6
|
-
getRouterMatchedRouteIds,
|
|
7
|
-
getRouterRuntimeState,
|
|
8
|
-
getRouterServerSnapshot,
|
|
9
|
-
} from '../src/routerState';
|
|
10
|
-
|
|
11
|
-
describe('router state primitives', () => {
|
|
12
|
-
it('preserves snapshot precedence and a captured snapshot across client state updates', () => {
|
|
13
|
-
const context = { requestId: 'one' };
|
|
14
|
-
expect(
|
|
15
|
-
applyRouterRuntimeState(context, {
|
|
16
|
-
framework: 'custom',
|
|
17
|
-
basename: '/client',
|
|
18
|
-
hydrationScripts: ['client'],
|
|
19
|
-
matchedRouteIds: ['client-route'],
|
|
20
|
-
serverSnapshot: {
|
|
21
|
-
basename: '/server',
|
|
22
|
-
hydrationScripts: ['server'],
|
|
23
|
-
matchedRouteIds: ['server-route'],
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
).toBe(context);
|
|
27
|
-
const snapshot = getRouterServerSnapshot(context);
|
|
28
|
-
expect(snapshot).toMatchObject({
|
|
29
|
-
framework: 'custom',
|
|
30
|
-
basename: '/server',
|
|
31
|
-
hydrationScripts: ['server'],
|
|
32
|
-
matchedRouteIds: ['server-route'],
|
|
33
|
-
});
|
|
34
|
-
expect(getRouterHydrationScripts(context)).toEqual(['server']);
|
|
35
|
-
expect(getRouterMatchedRouteIds(context)).toEqual(['server-route']);
|
|
36
|
-
|
|
37
|
-
applyRouterRuntimeState(context, {
|
|
38
|
-
framework: 'custom',
|
|
39
|
-
instance: { client: true },
|
|
40
|
-
});
|
|
41
|
-
expect(getRouterRuntimeState(context)?.serverSnapshot).toBeUndefined();
|
|
42
|
-
expect(getRouterServerSnapshot(context)).toBe(snapshot);
|
|
43
|
-
expect(getRouterHydrationScripts(context)).toEqual(['server']);
|
|
44
|
-
expect(getRouterMatchedRouteIds(context)).toEqual(['server-route']);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('applies server prepare overrides and awaits cleanup without propagating failures', async () => {
|
|
48
|
-
const context = {};
|
|
49
|
-
const stateCleanup = rstest.fn();
|
|
50
|
-
const cleanup = rstest.fn(async () => {
|
|
51
|
-
await Promise.resolve();
|
|
52
|
-
});
|
|
53
|
-
expect(
|
|
54
|
-
applyRouterServerPrepareResult(context, {
|
|
55
|
-
state: {
|
|
56
|
-
framework: 'custom',
|
|
57
|
-
cleanup: stateCleanup,
|
|
58
|
-
serverSnapshot: { statusCode: 200 },
|
|
59
|
-
},
|
|
60
|
-
snapshot: { statusCode: 299 },
|
|
61
|
-
cleanup,
|
|
62
|
-
}),
|
|
63
|
-
).toBe(context);
|
|
64
|
-
expect(getRouterServerSnapshot(context)?.statusCode).toBe(299);
|
|
65
|
-
await cleanupRouterRuntimeState(context);
|
|
66
|
-
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
67
|
-
expect(stateCleanup).not.toHaveBeenCalled();
|
|
68
|
-
|
|
69
|
-
applyRouterRuntimeState(context, {
|
|
70
|
-
framework: 'custom',
|
|
71
|
-
cleanup: async () => {
|
|
72
|
-
throw new Error('cleanup failed');
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
await expect(cleanupRouterRuntimeState(context)).resolves.toBeUndefined();
|
|
76
|
-
await expect(cleanupRouterRuntimeState({})).resolves.toBeUndefined();
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getRouterRuntimeState,
|
|
3
|
-
getRouterServerSnapshot,
|
|
4
|
-
} from '../src/routerState';
|
|
5
|
-
import { createRouterStatePlugin } from '../src/routerStatePlugin';
|
|
6
|
-
|
|
7
|
-
function install() {
|
|
8
|
-
const registryHooks = { onAfterCreateRouter: { call() {} } };
|
|
9
|
-
const plugin = createRouterStatePlugin({ registryHooks });
|
|
10
|
-
let beforeRender!: (context: object) => void;
|
|
11
|
-
let afterCreate!: Parameters<
|
|
12
|
-
Parameters<typeof plugin.setup>[0]['onAfterCreateRouter']
|
|
13
|
-
>[0];
|
|
14
|
-
plugin.setup({
|
|
15
|
-
onBeforeRender: callback => {
|
|
16
|
-
beforeRender = callback;
|
|
17
|
-
},
|
|
18
|
-
onAfterCreateRouter: callback => {
|
|
19
|
-
afterCreate = callback;
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
return { plugin, registryHooks, beforeRender, afterCreate };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('keeps injected hooks and per-context policies stable without sharing app values', () => {
|
|
26
|
-
const { beforeRender } = install();
|
|
27
|
-
const first: any = {};
|
|
28
|
-
const second: any = {};
|
|
29
|
-
beforeRender(first);
|
|
30
|
-
const policy = first.linkPrefetchPolicy;
|
|
31
|
-
beforeRender(first);
|
|
32
|
-
beforeRender(second);
|
|
33
|
-
expect(first.linkPrefetchPolicy).toBe(policy);
|
|
34
|
-
expect(second.linkPrefetchPolicy).not.toBe(policy);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test('projects loader data, errors and valid route ids from injected SSR input', () => {
|
|
38
|
-
const { afterCreate } = install();
|
|
39
|
-
const errors = { route: new Error('route error') };
|
|
40
|
-
const context = {
|
|
41
|
-
routerContext: {
|
|
42
|
-
statusCode: 503,
|
|
43
|
-
errors,
|
|
44
|
-
loaderData: { root: { ok: true } },
|
|
45
|
-
matches: [{ route: { id: 'root' } }, { route: {} }],
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
afterCreate({
|
|
49
|
-
framework: 'react-router',
|
|
50
|
-
phase: 'ssr-prepare',
|
|
51
|
-
runtimeContext: context,
|
|
52
|
-
router: context.routerContext,
|
|
53
|
-
basename: '/nested',
|
|
54
|
-
});
|
|
55
|
-
expect(getRouterRuntimeState(context)?.instance).toBe(context.routerContext);
|
|
56
|
-
expect(getRouterServerSnapshot(context)).toMatchObject({
|
|
57
|
-
framework: 'react-router',
|
|
58
|
-
basename: '/nested',
|
|
59
|
-
statusCode: 503,
|
|
60
|
-
matchedRouteIds: ['root'],
|
|
61
|
-
routerData: { loaderData: context.routerContext.loaderData, errors },
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { createRuntimeContextExtension } from '../src/contextExtensions';
|
|
2
|
-
import { projectRuntimeContext } from '../src/runtimeContextProjection';
|
|
3
|
-
|
|
4
|
-
describe('fork runtime context projection', () => {
|
|
5
|
-
it('removes copied enumerable extension state only from the public projection', () => {
|
|
6
|
-
const context = { isBrowser: false, requestId: 'original' };
|
|
7
|
-
const extension = createRuntimeContextExtension<object>('test:projection');
|
|
8
|
-
const state = { router: 'private' };
|
|
9
|
-
extension.set(context, state);
|
|
10
|
-
const publicInput = { ...context };
|
|
11
|
-
|
|
12
|
-
const result = projectRuntimeContext(
|
|
13
|
-
{ internalContext: context, publicContext: publicInput },
|
|
14
|
-
{ context, isRsc: false },
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
expect(result.internalContext).toBe(context);
|
|
18
|
-
expect(result.publicContext).not.toBe(publicInput);
|
|
19
|
-
expect(extension.get(result.internalContext)).toBe(state);
|
|
20
|
-
expect(extension.get(publicInput)).toBe(state);
|
|
21
|
-
expect(extension.get(result.publicContext)).toBeUndefined();
|
|
22
|
-
expect(Object.getOwnPropertySymbols(result.publicContext)).not.toContain(
|
|
23
|
-
Symbol.for('@modern-js/runtime:context-extensions'),
|
|
24
|
-
);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('projects RSC request fields and locals without mutating the original response', () => {
|
|
28
|
-
const response = {
|
|
29
|
-
setHeader: rstest.fn(),
|
|
30
|
-
status: rstest.fn(),
|
|
31
|
-
locals: { tenant: 'tractor-store' },
|
|
32
|
-
};
|
|
33
|
-
const request = {
|
|
34
|
-
params: { category: 'compact' },
|
|
35
|
-
pathname: '/tractors',
|
|
36
|
-
query: { sort: 'price' },
|
|
37
|
-
headers: { accept: 'text/html' },
|
|
38
|
-
host: 'example.test',
|
|
39
|
-
url: 'https://example.test/tractors?sort=price',
|
|
40
|
-
raw: new Request('https://example.test/tractors'),
|
|
41
|
-
};
|
|
42
|
-
const context = { isBrowser: false, ssrContext: { request, response } };
|
|
43
|
-
const extension = createRuntimeContextExtension<object>('test:rsc');
|
|
44
|
-
const state = { original: true };
|
|
45
|
-
extension.set(context, state);
|
|
46
|
-
const publicContext = { isBrowser: false };
|
|
47
|
-
const result = projectRuntimeContext(
|
|
48
|
-
{ internalContext: context, publicContext },
|
|
49
|
-
{ context, isRsc: true },
|
|
50
|
-
);
|
|
51
|
-
const internal = result.internalContext as typeof result.internalContext & {
|
|
52
|
-
context: { request: object; response: typeof response };
|
|
53
|
-
requestContext: { request: object; response: typeof response };
|
|
54
|
-
};
|
|
55
|
-
const publicValue = result.publicContext as typeof internal;
|
|
56
|
-
|
|
57
|
-
expect(internal).not.toBe(context);
|
|
58
|
-
expect(internal.ssrContext).toBeUndefined();
|
|
59
|
-
expect(internal.requestContext).toBe(publicValue.requestContext);
|
|
60
|
-
expect(internal.context).toBe(internal.requestContext);
|
|
61
|
-
expect(publicValue.context).toBe(internal.requestContext);
|
|
62
|
-
expect(internal.requestContext.response.locals).toBe(response.locals);
|
|
63
|
-
expect(context.ssrContext.response).toBe(response);
|
|
64
|
-
expect(extension.get(context)).toBe(state);
|
|
65
|
-
// Internal RSC copy behavior is retained pending separate Flight proof.
|
|
66
|
-
expect(extension.get(internal)).toBe(state);
|
|
67
|
-
expect(extension.get(publicValue)).toBeUndefined();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('does not apply server request projection to an RSC browser context', () => {
|
|
71
|
-
const context = {
|
|
72
|
-
isBrowser: true,
|
|
73
|
-
ssrContext: { request: {}, response: { locals: { browser: true } } },
|
|
74
|
-
};
|
|
75
|
-
const result = projectRuntimeContext(
|
|
76
|
-
{ internalContext: context, publicContext: { ...context } },
|
|
77
|
-
{ context, isRsc: true },
|
|
78
|
-
);
|
|
79
|
-
expect(result.internalContext).toBe(context);
|
|
80
|
-
expect(result.internalContext.ssrContext).toBe(context.ssrContext);
|
|
81
|
-
expect(result.publicContext).not.toHaveProperty('requestContext');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createSafeFailureHttpResult,
|
|
3
|
-
createSafeFailureResponse,
|
|
4
|
-
createSafeJsonFailureResponse,
|
|
5
|
-
getSafeFailureStatus,
|
|
6
|
-
} from '../src/safeFailure';
|
|
7
|
-
|
|
8
|
-
const readBody = async (response: Response) =>
|
|
9
|
-
(await response.json()) as Record<string, unknown>;
|
|
10
|
-
|
|
11
|
-
describe('safe failure responses', () => {
|
|
12
|
-
it.each([
|
|
13
|
-
[500, 'INTERNAL_SERVER_ERROR', 'Internal Server Error'],
|
|
14
|
-
])('redacts a %i failure through every response surface', async (status, code, message) => {
|
|
15
|
-
const error = Object.assign(new Error('database-password'), {
|
|
16
|
-
status,
|
|
17
|
-
code: 'SECRET_CODE',
|
|
18
|
-
cause: 'root-cause',
|
|
19
|
-
details: 'internal-details',
|
|
20
|
-
stack: 'hidden-stack',
|
|
21
|
-
});
|
|
22
|
-
const expectedBody = {
|
|
23
|
-
success: false,
|
|
24
|
-
error: { code, message, status },
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
expect(createSafeFailureHttpResult(error)).toMatchObject({
|
|
28
|
-
status,
|
|
29
|
-
body: expectedBody,
|
|
30
|
-
});
|
|
31
|
-
for (const createResponse of [
|
|
32
|
-
createSafeFailureResponse,
|
|
33
|
-
createSafeJsonFailureResponse,
|
|
34
|
-
]) {
|
|
35
|
-
const response = createResponse(error);
|
|
36
|
-
expect(response.status).toBe(status);
|
|
37
|
-
expect(response.headers.get('content-type')).toBe(
|
|
38
|
-
'application/json; charset=utf-8',
|
|
39
|
-
);
|
|
40
|
-
const body = await readBody(response);
|
|
41
|
-
expect(body).toEqual(expectedBody);
|
|
42
|
-
expect(JSON.stringify(body)).not.toMatch(
|
|
43
|
-
/database-password|SECRET_CODE|root-cause|internal-details|hidden-stack/u,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Response construction must not destroy the raw error retained for
|
|
48
|
-
// internal structured diagnostics.
|
|
49
|
-
expect(error.cause).toBe('root-cause');
|
|
50
|
-
expect(error.details).toBe('internal-details');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it.each([
|
|
54
|
-
['delay seconds text', ' 120 ', '120'],
|
|
55
|
-
[
|
|
56
|
-
'valid Date object',
|
|
57
|
-
new Date(Date.UTC(2026, 7, 30, 20, 0, 0)),
|
|
58
|
-
'Sun, 30 Aug 2026 20:00:00 GMT',
|
|
59
|
-
],
|
|
60
|
-
])('canonicalizes %s for Retry-After', (_, retryAfter, expected) => {
|
|
61
|
-
const result = createSafeFailureHttpResult({ status: 503, retryAfter });
|
|
62
|
-
expect(result.headers['Retry-After']).toBe(expected);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('derives Retry-After from a retryAfterSeconds error property', () => {
|
|
66
|
-
const error = Object.assign(new Error('maintenance'), {
|
|
67
|
-
status: 503,
|
|
68
|
-
retryAfterSeconds: 120,
|
|
69
|
-
});
|
|
70
|
-
expect(createSafeFailureHttpResult(error).headers['Retry-After']).toBe(
|
|
71
|
-
'120',
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('converts Retry-After milliseconds to canonical delay seconds', () => {
|
|
76
|
-
expect(
|
|
77
|
-
createSafeFailureHttpResult({ status: 503, retryAfterMs: 2500 }).headers[
|
|
78
|
-
'Retry-After'
|
|
79
|
-
],
|
|
80
|
-
).toBe('3');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it.each([
|
|
84
|
-
['a header injection payload', '120\r\nX-Injected: true'],
|
|
85
|
-
['an invalid Date object', new Date(Number.NaN)],
|
|
86
|
-
])('rejects %s as Retry-After', (_, retryAfter) => {
|
|
87
|
-
const result = createSafeFailureHttpResult({ status: 503, retryAfter });
|
|
88
|
-
expect(result.headers['Retry-After']).toBeUndefined();
|
|
89
|
-
expect(() =>
|
|
90
|
-
createSafeFailureResponse({ status: 503, retryAfter }),
|
|
91
|
-
).not.toThrow();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('never emits Retry-After for a non-503 response', () => {
|
|
95
|
-
expect(
|
|
96
|
-
createSafeFailureHttpResult({ status: 500, retryAfter: '120' }).headers[
|
|
97
|
-
'Retry-After'
|
|
98
|
-
],
|
|
99
|
-
).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('fails closed without throwing when error property access is hostile', () => {
|
|
103
|
-
const hostile = new Proxy(
|
|
104
|
-
{},
|
|
105
|
-
{
|
|
106
|
-
has() {
|
|
107
|
-
throw new Error('proxy trap secret');
|
|
108
|
-
},
|
|
109
|
-
get() {
|
|
110
|
-
throw new Error('getter secret');
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
expect(getSafeFailureStatus(hostile)).toBe(500);
|
|
116
|
-
expect(createSafeFailureHttpResult(hostile)).toEqual({
|
|
117
|
-
status: 500,
|
|
118
|
-
body: {
|
|
119
|
-
success: false,
|
|
120
|
-
error: {
|
|
121
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
122
|
-
message: 'Internal Server Error',
|
|
123
|
-
status: 500,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
headers: {
|
|
127
|
-
'content-type': 'application/json; charset=utf-8',
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('uses statusCode only when it is a valid failure status', () => {
|
|
133
|
-
expect(getSafeFailureStatus({ status: 200, statusCode: 503 })).toBe(503);
|
|
134
|
-
expect(getSafeFailureStatus({ status: 503.5, statusCode: 799 })).toBe(500);
|
|
135
|
-
});
|
|
136
|
-
});
|