@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.4 → 3.9.0-ultramodern.5
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/contextExtensions.js +74 -0
- package/dist/cjs/headRuntime.js +91 -0
- package/dist/cjs/helmetContext.js +54 -0
- package/dist/cjs/index.js +18 -4
- package/dist/cjs/prefetchPolicy.js +52 -0
- package/dist/cjs/request-policy/browser.js +139 -0
- package/dist/cjs/request-policy/client.js +234 -0
- package/dist/cjs/request-policy/node.js +149 -0
- package/dist/cjs/request-policy/policyCore.js +266 -0
- package/dist/cjs/request-policy/requestContext.js +137 -0
- package/dist/cjs/request-policy/traceparent.js +56 -0
- package/dist/cjs/request-policy/transport.js +234 -0
- package/dist/cjs/request-policy/types.js +57 -0
- package/dist/cjs/routeHydration.js +127 -0
- package/dist/cjs/routerPrefetchPolicy.js +153 -0
- package/dist/cjs/routerProvider.js +170 -0
- package/dist/cjs/routerState.js +167 -0
- package/dist/cjs/routerStatePlugin.js +86 -0
- package/dist/cjs/routerStateTypes.js +18 -0
- package/dist/cjs/runtimeContextProjection.js +112 -0
- package/dist/cjs/server-config/bffRuntime.js +18 -0
- package/dist/cjs/server-config/index.js +18 -0
- package/dist/cjs/server-config/serverTelemetry.js +18 -0
- package/dist/cjs/shellStream.js +34 -2
- package/dist/cjs/workspaceEvents.js +159 -0
- package/dist/esm/contextExtensions.mjs +33 -0
- package/dist/esm/headRuntime.mjs +43 -0
- package/dist/esm/helmetContext.mjs +13 -0
- package/dist/esm/index.mjs +2 -0
- package/dist/esm/prefetchPolicy.mjs +11 -0
- package/dist/esm/request-policy/browser.mjs +11 -0
- package/dist/esm/request-policy/client.mjs +195 -0
- package/dist/esm/request-policy/policyCore.mjs +171 -0
- package/dist/esm/request-policy/requestContext.mjs +75 -0
- package/dist/esm/request-policy/traceparent.mjs +18 -0
- package/dist/esm/request-policy/transport.mjs +196 -0
- package/dist/esm/request-policy/types.mjs +10 -0
- package/dist/esm/routeHydration.mjs +77 -0
- package/dist/esm/routerPrefetchPolicy.mjs +115 -0
- package/dist/esm/routerProvider.mjs +117 -0
- package/dist/esm/routerState.mjs +105 -0
- package/dist/esm/routerStatePlugin.mjs +48 -0
- package/dist/esm/routerStateTypes.mjs +0 -0
- package/dist/esm/runtimeContextProjection.mjs +74 -0
- package/dist/esm/server-config/bffRuntime.mjs +0 -0
- package/dist/esm/server-config/index.mjs +0 -0
- package/dist/esm/server-config/serverTelemetry.mjs +0 -0
- package/dist/esm/shellStream.mjs +34 -2
- package/dist/esm/workspaceEvents.mjs +66 -0
- package/dist/esm-node/contextExtensions.mjs +34 -0
- package/dist/esm-node/headRuntime.mjs +44 -0
- package/dist/esm-node/helmetContext.mjs +14 -0
- package/dist/esm-node/index.mjs +2 -0
- package/dist/esm-node/prefetchPolicy.mjs +12 -0
- package/dist/esm-node/request-policy/browser.mjs +12 -0
- package/dist/esm-node/request-policy/client.mjs +196 -0
- package/dist/esm-node/request-policy/node.mjs +22 -0
- package/dist/esm-node/request-policy/policyCore.mjs +172 -0
- package/dist/esm-node/request-policy/requestContext.mjs +76 -0
- package/dist/esm-node/request-policy/traceparent.mjs +19 -0
- package/dist/esm-node/request-policy/transport.mjs +197 -0
- package/dist/esm-node/request-policy/types.mjs +11 -0
- package/dist/esm-node/routeHydration.mjs +78 -0
- package/dist/esm-node/routerPrefetchPolicy.mjs +116 -0
- package/dist/esm-node/routerProvider.mjs +118 -0
- package/dist/esm-node/routerState.mjs +106 -0
- package/dist/esm-node/routerStatePlugin.mjs +49 -0
- package/dist/esm-node/routerStateTypes.mjs +1 -0
- package/dist/esm-node/runtimeContextProjection.mjs +75 -0
- package/dist/esm-node/server-config/bffRuntime.mjs +1 -0
- package/dist/esm-node/server-config/index.mjs +1 -0
- package/dist/esm-node/server-config/serverTelemetry.mjs +1 -0
- package/dist/esm-node/shellStream.mjs +34 -2
- package/dist/esm-node/workspaceEvents.mjs +67 -0
- package/dist/types/contextExtensions.d.ts +34 -0
- package/dist/types/headRuntime.d.ts +12 -0
- package/dist/types/helmetContext.d.ts +10 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/prefetchPolicy.d.ts +4 -0
- package/dist/types/request-policy/browser.d.ts +10 -0
- package/dist/types/request-policy/client.d.ts +14 -0
- package/dist/types/request-policy/node.d.ts +10 -0
- package/dist/types/request-policy/policyCore.d.ts +107 -0
- package/dist/types/request-policy/requestContext.d.ts +22 -0
- package/dist/types/request-policy/traceparent.d.ts +10 -0
- package/dist/types/request-policy/transport.d.ts +12 -0
- package/dist/types/request-policy/types.d.ts +144 -0
- package/dist/types/routeHydration.d.ts +24 -0
- package/dist/types/routerPrefetchPolicy.d.ts +28 -0
- package/dist/types/routerProvider.d.ts +77 -0
- package/dist/types/routerState.d.ts +12 -0
- package/dist/types/routerStatePlugin.d.ts +19 -0
- package/dist/types/routerStateTypes.d.ts +40 -0
- package/dist/types/runtimeContextProjection.d.ts +30 -0
- package/dist/types/server-config/bffRuntime.d.ts +122 -0
- package/dist/types/server-config/index.d.ts +2 -0
- package/dist/types/server-config/serverTelemetry.d.ts +322 -0
- package/dist/types/workspaceEvents.d.ts +62 -0
- package/package.json +198 -13
- package/rslib.config.mts +17 -2
- package/rstest.config.mts +33 -2
- package/src/contextExtensions.ts +81 -0
- package/src/headRuntime.tsx +77 -0
- package/src/helmetContext.ts +31 -0
- package/src/index.ts +2 -0
- package/src/prefetchPolicy.ts +33 -0
- package/src/request-policy/browser.ts +22 -0
- package/src/request-policy/client.ts +404 -0
- package/src/request-policy/node.ts +41 -0
- package/src/request-policy/policyCore.ts +424 -0
- package/src/request-policy/requestContext.ts +149 -0
- package/src/request-policy/traceparent.ts +43 -0
- package/src/request-policy/transport.ts +349 -0
- package/src/request-policy/types.ts +229 -0
- package/src/routeHydration.ts +208 -0
- package/src/routerPrefetchPolicy.ts +195 -0
- package/src/routerProvider.ts +340 -0
- package/src/routerState.ts +208 -0
- package/src/routerStatePlugin.ts +86 -0
- package/src/routerStateTypes.ts +46 -0
- package/src/runtimeContextProjection.ts +105 -0
- package/src/server-config/bffRuntime.ts +136 -0
- package/src/server-config/index.ts +2 -0
- package/src/server-config/serverTelemetry.ts +337 -0
- package/src/shellStream.ts +55 -25
- package/src/workspaceEvents.ts +324 -0
- package/tests/buildIdentity.test.ts +2 -4
- package/tests/contextExtensions.test.ts +71 -0
- package/tests/headRuntime.test.tsx +369 -0
- package/tests/nativeSafeFailureCompatibility.test.ts +117 -0
- package/tests/prefetchPolicy.test.ts +51 -0
- package/tests/rendererHead.test.ts +1 -72
- package/tests/request-policy/browser-policy.test.ts +608 -0
- package/tests/request-policy/client-isolation.test.ts +149 -0
- package/tests/request-policy/identityBinding.test.ts +75 -0
- package/tests/request-policy/node-policy.test.ts +741 -0
- package/tests/request-policy/policyCore.test.ts +250 -0
- package/tests/request-policy/requestContext.test.ts +80 -0
- package/tests/request-policy/requestFactory.outbound-contract.test.ts +691 -0
- package/tests/request-policy/traceparent.test.ts +55 -0
- package/tests/request-policy/transport-retry.test.ts +370 -0
- package/tests/routeHydration.test.ts +180 -0
- package/tests/routerPrefetchPolicy.test.ts +102 -0
- package/tests/routerProvider.test.ts +38 -0
- package/tests/routerState.test.ts +157 -0
- package/tests/routerStatePlugin.test.ts +108 -0
- package/tests/runtimeContextProjection.test.ts +135 -0
- package/tests/shellStream.test.ts +37 -0
- package/tests/workspaceEvents.test.ts +391 -0
- package/dist/cjs/rscHtmlStream.js +0 -128
- package/dist/esm/rscHtmlStream.mjs +0 -86
- package/dist/esm-node/rscHtmlStream.mjs +0 -87
- package/dist/types/rscHtmlStream.d.ts +0 -4
- package/src/rscHtmlStream.ts +0 -142
- package/tests/rscHtmlStream.test.ts +0 -153
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRouterPlugin,
|
|
3
|
+
registerRouterProvider,
|
|
4
|
+
unsafe_resetRouterProvidersForTesting,
|
|
5
|
+
} from '../src/routerProvider';
|
|
6
|
+
|
|
7
|
+
afterEach(() => unsafe_resetRouterProvidersForTesting());
|
|
8
|
+
|
|
9
|
+
test('selects the injected app-local factory from runtime config and preserves native hook objects', () => {
|
|
10
|
+
const registryHooks = { afterCreate: { call() {} } };
|
|
11
|
+
const defaultFactory = rstest.fn(() => ({ setup: rstest.fn() }));
|
|
12
|
+
const setup = rstest.fn();
|
|
13
|
+
const localFactory = rstest.fn(() => ({ registryHooks, setup }));
|
|
14
|
+
const plugin = createRouterPlugin({
|
|
15
|
+
defaultProvider: { name: 'native', factory: defaultFactory },
|
|
16
|
+
registryHooks,
|
|
17
|
+
localProviders: [{ name: 'custom', factory: localFactory }],
|
|
18
|
+
})();
|
|
19
|
+
const api = { getRuntimeConfig: () => ({ router: { framework: 'custom' } }) };
|
|
20
|
+
expect(plugin.registryHooks).toBe(registryHooks);
|
|
21
|
+
plugin.setup(api);
|
|
22
|
+
expect(localFactory).toHaveBeenCalledWith({});
|
|
23
|
+
expect(setup).toHaveBeenCalledWith(api);
|
|
24
|
+
expect(defaultFactory).not.toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('does not resolve missing local providers through the global compatibility registry', () => {
|
|
28
|
+
const foreign = rstest.fn(() => ({}));
|
|
29
|
+
registerRouterProvider('foreign', foreign);
|
|
30
|
+
const factory = createRouterPlugin({
|
|
31
|
+
defaultProvider: { name: 'native', factory: () => ({}) },
|
|
32
|
+
registryHooks: {},
|
|
33
|
+
});
|
|
34
|
+
expect(() =>
|
|
35
|
+
factory({ framework: 'foreign' }).setup({ getRuntimeConfig: () => ({}) }),
|
|
36
|
+
).toThrow(/app-owned router provider realm/);
|
|
37
|
+
expect(foreign).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createRuntimeContextExtension } from '../src/contextExtensions';
|
|
2
|
+
import {
|
|
3
|
+
applyRouterRuntimeState,
|
|
4
|
+
applyRouterServerPrepareResult,
|
|
5
|
+
cleanupRouterRuntimeState,
|
|
6
|
+
createRouterRuntimeState,
|
|
7
|
+
createRouterServerSnapshot,
|
|
8
|
+
getRouterHydrationScripts,
|
|
9
|
+
getRouterMatchedRouteIds,
|
|
10
|
+
getRouterRuntimeState,
|
|
11
|
+
getRouterServerSnapshot,
|
|
12
|
+
type InternalRouterRuntimeState,
|
|
13
|
+
} from '../src/routerState';
|
|
14
|
+
|
|
15
|
+
describe('router state primitives', () => {
|
|
16
|
+
it('normalizes snapshots without changing explicit values or input objects', () => {
|
|
17
|
+
const matches = [
|
|
18
|
+
{ routeId: 'route-a', assetRouteId: 'asset-a' },
|
|
19
|
+
{ routeId: 'route-b' },
|
|
20
|
+
];
|
|
21
|
+
const snapshot = {
|
|
22
|
+
framework: 'custom',
|
|
23
|
+
hydrationScript: '<script>first()</script>',
|
|
24
|
+
matches,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
expect(createRouterServerSnapshot(snapshot)).toEqual({
|
|
28
|
+
...snapshot,
|
|
29
|
+
hydrationScripts: ['<script>first()</script>'],
|
|
30
|
+
matchedRouteIds: ['asset-a', 'route-b'],
|
|
31
|
+
});
|
|
32
|
+
expect(snapshot).not.toHaveProperty('hydrationScripts');
|
|
33
|
+
expect(snapshot).not.toHaveProperty('matchedRouteIds');
|
|
34
|
+
expect(
|
|
35
|
+
createRouterServerSnapshot({
|
|
36
|
+
...snapshot,
|
|
37
|
+
hydrationScripts: ['<script>explicit()</script>'],
|
|
38
|
+
matchedRouteIds: [],
|
|
39
|
+
}),
|
|
40
|
+
).toMatchObject({
|
|
41
|
+
hydrationScript: '<script>first()</script>',
|
|
42
|
+
hydrationScripts: ['<script>explicit()</script>'],
|
|
43
|
+
matchedRouteIds: [],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('preserves snapshot precedence and a captured snapshot across client state updates', () => {
|
|
48
|
+
const context = { requestId: 'one' };
|
|
49
|
+
expect(
|
|
50
|
+
applyRouterRuntimeState(context, {
|
|
51
|
+
framework: 'custom',
|
|
52
|
+
basename: '/client',
|
|
53
|
+
hydrationScripts: ['client'],
|
|
54
|
+
matchedRouteIds: ['client-route'],
|
|
55
|
+
serverSnapshot: {
|
|
56
|
+
basename: '/server',
|
|
57
|
+
hydrationScripts: ['server'],
|
|
58
|
+
matchedRouteIds: ['server-route'],
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
).toBe(context);
|
|
62
|
+
const snapshot = getRouterServerSnapshot(context);
|
|
63
|
+
expect(snapshot).toMatchObject({
|
|
64
|
+
framework: 'custom',
|
|
65
|
+
basename: '/server',
|
|
66
|
+
hydrationScripts: ['server'],
|
|
67
|
+
matchedRouteIds: ['server-route'],
|
|
68
|
+
});
|
|
69
|
+
expect(getRouterHydrationScripts(context)).toEqual(['server']);
|
|
70
|
+
expect(getRouterMatchedRouteIds(context)).toEqual(['server-route']);
|
|
71
|
+
|
|
72
|
+
applyRouterRuntimeState(context, {
|
|
73
|
+
framework: 'custom',
|
|
74
|
+
instance: { client: true },
|
|
75
|
+
});
|
|
76
|
+
expect(getRouterRuntimeState(context)?.serverSnapshot).toBeUndefined();
|
|
77
|
+
expect(getRouterServerSnapshot(context)).toBe(snapshot);
|
|
78
|
+
expect(getRouterHydrationScripts(context)).toEqual(['server']);
|
|
79
|
+
expect(getRouterMatchedRouteIds(context)).toEqual(['server-route']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('shares interned slots across accessors and context spreads without serializing state', () => {
|
|
83
|
+
const context = { requestId: 'one' };
|
|
84
|
+
const state: InternalRouterRuntimeState = {
|
|
85
|
+
framework: 'custom',
|
|
86
|
+
instance: { private: true },
|
|
87
|
+
hydrationScript: 'legacy',
|
|
88
|
+
matches: [{ routeId: 'route', assetRouteId: 'asset' }],
|
|
89
|
+
};
|
|
90
|
+
const otherAccessor =
|
|
91
|
+
createRuntimeContextExtension<InternalRouterRuntimeState>(
|
|
92
|
+
'@modern-js/runtime:router-runtime-state',
|
|
93
|
+
);
|
|
94
|
+
otherAccessor.set(context, state);
|
|
95
|
+
expect(getRouterRuntimeState({ ...context })).toBe(state);
|
|
96
|
+
expect(getRouterHydrationScripts(context)).toEqual(['legacy']);
|
|
97
|
+
expect(getRouterMatchedRouteIds(context)).toEqual(['asset']);
|
|
98
|
+
expect(Object.keys(context)).toEqual(['requestId']);
|
|
99
|
+
expect(JSON.stringify(context)).toBe('{"requestId":"one"}');
|
|
100
|
+
expect(getRouterRuntimeState({})).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('applies server prepare overrides and awaits cleanup without propagating failures', async () => {
|
|
104
|
+
const context = {};
|
|
105
|
+
const stateCleanup = rstest.fn();
|
|
106
|
+
const cleanup = rstest.fn(async () => {
|
|
107
|
+
await Promise.resolve();
|
|
108
|
+
});
|
|
109
|
+
expect(
|
|
110
|
+
applyRouterServerPrepareResult(context, {
|
|
111
|
+
state: {
|
|
112
|
+
framework: 'custom',
|
|
113
|
+
cleanup: stateCleanup,
|
|
114
|
+
serverSnapshot: { statusCode: 200 },
|
|
115
|
+
},
|
|
116
|
+
snapshot: { statusCode: 299 },
|
|
117
|
+
cleanup,
|
|
118
|
+
}),
|
|
119
|
+
).toBe(context);
|
|
120
|
+
expect(getRouterServerSnapshot(context)?.statusCode).toBe(299);
|
|
121
|
+
await cleanupRouterRuntimeState(context);
|
|
122
|
+
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
123
|
+
expect(stateCleanup).not.toHaveBeenCalled();
|
|
124
|
+
|
|
125
|
+
applyRouterRuntimeState(context, {
|
|
126
|
+
framework: 'custom',
|
|
127
|
+
cleanup: async () => {
|
|
128
|
+
throw new Error('cleanup failed');
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
await expect(cleanupRouterRuntimeState(context)).resolves.toBeUndefined();
|
|
132
|
+
await expect(cleanupRouterRuntimeState({})).resolves.toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('keeps empty state empty and derives server data only when present', () => {
|
|
136
|
+
expect(createRouterRuntimeState({ framework: 'custom' })).toEqual({
|
|
137
|
+
framework: 'custom',
|
|
138
|
+
});
|
|
139
|
+
expect(getRouterHydrationScripts({})).toEqual([]);
|
|
140
|
+
expect(getRouterMatchedRouteIds({})).toBeUndefined();
|
|
141
|
+
expect(
|
|
142
|
+
createRouterRuntimeState({
|
|
143
|
+
framework: 'custom',
|
|
144
|
+
matches: [{ routeId: 'root' }],
|
|
145
|
+
hydrationScripts: ['first', 'second'],
|
|
146
|
+
}),
|
|
147
|
+
).toMatchObject({
|
|
148
|
+
hydrationScript: 'first',
|
|
149
|
+
matchedRouteIds: ['root'],
|
|
150
|
+
serverSnapshot: {
|
|
151
|
+
framework: 'custom',
|
|
152
|
+
hydrationScripts: ['first', 'second'],
|
|
153
|
+
matchedRouteIds: ['root'],
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyRouterRuntimeState,
|
|
3
|
+
getRouterRuntimeState,
|
|
4
|
+
getRouterServerSnapshot,
|
|
5
|
+
} from '../src/routerState';
|
|
6
|
+
import { createRouterStatePlugin } from '../src/routerStatePlugin';
|
|
7
|
+
|
|
8
|
+
function install() {
|
|
9
|
+
const registryHooks = { onAfterCreateRouter: { call() {} } };
|
|
10
|
+
const plugin = createRouterStatePlugin({ registryHooks });
|
|
11
|
+
let beforeRender!: (context: object) => void;
|
|
12
|
+
let afterCreate!: Parameters<
|
|
13
|
+
Parameters<typeof plugin.setup>[0]['onAfterCreateRouter']
|
|
14
|
+
>[0];
|
|
15
|
+
plugin.setup({
|
|
16
|
+
onBeforeRender: callback => {
|
|
17
|
+
beforeRender = callback;
|
|
18
|
+
},
|
|
19
|
+
onAfterCreateRouter: callback => {
|
|
20
|
+
afterCreate = callback;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return { plugin, registryHooks, beforeRender, afterCreate };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('keeps injected hooks and per-context policies stable without sharing app values', () => {
|
|
27
|
+
const { plugin, registryHooks, beforeRender } = install();
|
|
28
|
+
expect(plugin.registryHooks).toBe(registryHooks);
|
|
29
|
+
const first: any = {};
|
|
30
|
+
const second: any = {};
|
|
31
|
+
beforeRender(first);
|
|
32
|
+
const policy = first.linkPrefetchPolicy;
|
|
33
|
+
beforeRender(first);
|
|
34
|
+
beforeRender(second);
|
|
35
|
+
expect(first.linkPrefetchPolicy).toBe(policy);
|
|
36
|
+
expect(second.linkPrefetchPolicy).not.toBe(policy);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('stores the exact native client instance and retains an earlier server snapshot', () => {
|
|
40
|
+
const { afterCreate } = install();
|
|
41
|
+
const context = {};
|
|
42
|
+
applyRouterRuntimeState(context, {
|
|
43
|
+
framework: 'react-router',
|
|
44
|
+
serverSnapshot: { statusCode: 207, matchedRouteIds: ['server'] },
|
|
45
|
+
});
|
|
46
|
+
const router = { navigate() {} };
|
|
47
|
+
afterCreate({
|
|
48
|
+
framework: 'react-router',
|
|
49
|
+
phase: 'client-create',
|
|
50
|
+
runtimeContext: context,
|
|
51
|
+
router,
|
|
52
|
+
basename: '/app',
|
|
53
|
+
});
|
|
54
|
+
expect(getRouterRuntimeState(context)?.instance).toBe(router);
|
|
55
|
+
expect(getRouterServerSnapshot(context)?.statusCode).toBe(207);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('projects loader data, errors and valid route ids from injected SSR input', () => {
|
|
59
|
+
const { afterCreate } = install();
|
|
60
|
+
const errors = { route: new Error('route error') };
|
|
61
|
+
const context = {
|
|
62
|
+
routerContext: {
|
|
63
|
+
statusCode: 503,
|
|
64
|
+
errors,
|
|
65
|
+
loaderData: { root: { ok: true } },
|
|
66
|
+
matches: [{ route: { id: 'root' } }, { route: {} }],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
afterCreate({
|
|
70
|
+
framework: 'react-router',
|
|
71
|
+
phase: 'ssr-prepare',
|
|
72
|
+
runtimeContext: context,
|
|
73
|
+
router: context.routerContext,
|
|
74
|
+
basename: '/nested',
|
|
75
|
+
});
|
|
76
|
+
expect(getRouterRuntimeState(context)?.instance).toBe(context.routerContext);
|
|
77
|
+
expect(getRouterServerSnapshot(context)).toMatchObject({
|
|
78
|
+
framework: 'react-router',
|
|
79
|
+
basename: '/nested',
|
|
80
|
+
statusCode: 503,
|
|
81
|
+
matchedRouteIds: ['root'],
|
|
82
|
+
routerData: { loaderData: context.routerContext.loaderData, errors },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('leaves other providers and incomplete native preparation untouched', () => {
|
|
87
|
+
const { afterCreate } = install();
|
|
88
|
+
const context = {};
|
|
89
|
+
applyRouterRuntimeState(context, {
|
|
90
|
+
framework: 'tanstack',
|
|
91
|
+
hydrationScripts: ['<script>hydrate()</script>'],
|
|
92
|
+
matches: [{ routeId: 'r', assetRouteId: 'asset' }],
|
|
93
|
+
});
|
|
94
|
+
const state = getRouterRuntimeState(context);
|
|
95
|
+
afterCreate({
|
|
96
|
+
framework: 'tanstack',
|
|
97
|
+
phase: 'ssr-prepare',
|
|
98
|
+
runtimeContext: context,
|
|
99
|
+
});
|
|
100
|
+
expect(getRouterRuntimeState(context)).toBe(state);
|
|
101
|
+
const incomplete = {};
|
|
102
|
+
afterCreate({
|
|
103
|
+
framework: 'react-router',
|
|
104
|
+
phase: 'ssr-prepare',
|
|
105
|
+
runtimeContext: incomplete,
|
|
106
|
+
});
|
|
107
|
+
expect(getRouterRuntimeState(incomplete)).toBeUndefined();
|
|
108
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
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.request).toEqual({
|
|
63
|
+
params: { category: 'compact' },
|
|
64
|
+
pathname: '/tractors',
|
|
65
|
+
query: { sort: 'price' },
|
|
66
|
+
headers: { accept: 'text/html' },
|
|
67
|
+
host: 'example.test',
|
|
68
|
+
url: 'https://example.test/tractors?sort=price',
|
|
69
|
+
userAgent: undefined,
|
|
70
|
+
cookie: undefined,
|
|
71
|
+
referer: undefined,
|
|
72
|
+
});
|
|
73
|
+
expect(internal.requestContext.response).toEqual({
|
|
74
|
+
locals: response.locals,
|
|
75
|
+
});
|
|
76
|
+
expect(internal.requestContext.response.locals).toBe(response.locals);
|
|
77
|
+
expect(Object.keys(internal.requestContext.response)).toEqual(['locals']);
|
|
78
|
+
expect(typeof internal.requestContext.response.setHeader).toBe('function');
|
|
79
|
+
expect(typeof internal.requestContext.response.status).toBe('function');
|
|
80
|
+
expect(context.ssrContext.response).toBe(response);
|
|
81
|
+
expect(Object.keys(response)).toEqual(['setHeader', 'status', 'locals']);
|
|
82
|
+
expect(extension.get(context)).toBe(state);
|
|
83
|
+
// Internal RSC copy behavior is retained pending separate Flight proof.
|
|
84
|
+
expect(extension.get(internal)).toBe(state);
|
|
85
|
+
expect(extension.get(publicValue)).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('provides the existing empty RSC request fallback when no SSR context exists', () => {
|
|
89
|
+
const context = { isBrowser: false };
|
|
90
|
+
const result = projectRuntimeContext(
|
|
91
|
+
{ internalContext: context, publicContext: { ...context } },
|
|
92
|
+
{ context, isRsc: true },
|
|
93
|
+
);
|
|
94
|
+
expect(result.internalContext).toMatchObject({
|
|
95
|
+
context: {
|
|
96
|
+
request: {
|
|
97
|
+
params: {},
|
|
98
|
+
pathname: '',
|
|
99
|
+
query: {},
|
|
100
|
+
headers: {},
|
|
101
|
+
host: '',
|
|
102
|
+
url: '',
|
|
103
|
+
},
|
|
104
|
+
response: { locals: {} },
|
|
105
|
+
},
|
|
106
|
+
requestContext: {
|
|
107
|
+
request: {
|
|
108
|
+
params: {},
|
|
109
|
+
pathname: '',
|
|
110
|
+
query: {},
|
|
111
|
+
headers: {},
|
|
112
|
+
host: '',
|
|
113
|
+
url: '',
|
|
114
|
+
},
|
|
115
|
+
response: { locals: {} },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
expect(result.internalContext).not.toHaveProperty('ssrContext');
|
|
119
|
+
expect(context).toEqual({ isBrowser: false });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('does not apply server request projection to an RSC browser context', () => {
|
|
123
|
+
const context = {
|
|
124
|
+
isBrowser: true,
|
|
125
|
+
ssrContext: { request: {}, response: { locals: { browser: true } } },
|
|
126
|
+
};
|
|
127
|
+
const result = projectRuntimeContext(
|
|
128
|
+
{ internalContext: context, publicContext: { ...context } },
|
|
129
|
+
{ context, isRsc: true },
|
|
130
|
+
);
|
|
131
|
+
expect(result.internalContext).toBe(context);
|
|
132
|
+
expect(result.internalContext.ssrContext).toBe(context.ssrContext);
|
|
133
|
+
expect(result.publicContext).not.toHaveProperty('requestContext');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -83,3 +83,40 @@ describe('conserving web shell stream', () => {
|
|
|
83
83
|
expect(published).toEqual(['previous']);
|
|
84
84
|
});
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
test('cancellation waits for the owned source cancellation to finish once', async () => {
|
|
88
|
+
let release!: () => void;
|
|
89
|
+
let started!: () => void;
|
|
90
|
+
const cancelling = new Promise<void>(resolve => {
|
|
91
|
+
started = resolve;
|
|
92
|
+
});
|
|
93
|
+
const gate = new Promise<void>(resolve => {
|
|
94
|
+
release = resolve;
|
|
95
|
+
});
|
|
96
|
+
const cancel = rstest.fn(async () => {
|
|
97
|
+
started();
|
|
98
|
+
await gate;
|
|
99
|
+
});
|
|
100
|
+
const source = new ReadableStream<Uint8Array>({ cancel });
|
|
101
|
+
const reader = createConservingWebShellStream(source, {}, MARKER).getReader();
|
|
102
|
+
let settled = false;
|
|
103
|
+
const cancellation = reader.cancel('stop').then(() => {
|
|
104
|
+
settled = true;
|
|
105
|
+
});
|
|
106
|
+
await cancelling;
|
|
107
|
+
expect(settled).toBe(false);
|
|
108
|
+
release();
|
|
109
|
+
await cancellation;
|
|
110
|
+
expect(cancel).toHaveBeenCalledExactlyOnceWith('stop');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('cancellation propagates an owned source cleanup failure', async () => {
|
|
114
|
+
const source = new ReadableStream<Uint8Array>({
|
|
115
|
+
cancel() {
|
|
116
|
+
throw new Error('cleanup failed');
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
await expect(
|
|
120
|
+
createConservingWebShellStream(source, {}, MARKER).cancel('stop'),
|
|
121
|
+
).rejects.toThrow('cleanup failed');
|
|
122
|
+
});
|