@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.3 → 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/buildIdentity.js +73 -0
- 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/localisedLoaderIdentity.js +51 -0
- package/dist/cjs/prefetchPolicy.js +52 -0
- package/dist/cjs/reactContext.js +45 -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/rsc-disabled/rscDisabledClientBrowserRuntime.js +69 -0
- package/dist/cjs/rsc-disabled/rscDisabledClientEdgeRuntime.js +61 -0
- package/dist/cjs/rsc-disabled/rscDisabledClientNodeRuntime.js +65 -0
- package/dist/cjs/rsc-disabled/rscDisabledRuntimeError.js +42 -0
- package/dist/cjs/rsc-disabled/rscDisabledServerEdgeRuntime.js +93 -0
- package/dist/cjs/rsc-disabled/rscDisabledServerNodeRuntime.js +101 -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/buildIdentity.mjs +35 -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/localisedLoaderIdentity.mjs +13 -0
- package/dist/esm/prefetchPolicy.mjs +11 -0
- package/dist/esm/reactContext.mjs +7 -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/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +10 -0
- package/dist/esm/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +8 -0
- package/dist/esm/rsc-disabled/rscDisabledClientNodeRuntime.mjs +9 -0
- package/dist/esm/rsc-disabled/rscDisabledRuntimeError.mjs +4 -0
- package/dist/esm/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +16 -0
- package/dist/esm/rsc-disabled/rscDisabledServerNodeRuntime.mjs +18 -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/buildIdentity.mjs +36 -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/localisedLoaderIdentity.mjs +14 -0
- package/dist/esm-node/prefetchPolicy.mjs +12 -0
- package/dist/esm-node/reactContext.mjs +8 -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/rsc-disabled/rscDisabledClientBrowserRuntime.mjs +11 -0
- package/dist/esm-node/rsc-disabled/rscDisabledClientEdgeRuntime.mjs +9 -0
- package/dist/esm-node/rsc-disabled/rscDisabledClientNodeRuntime.mjs +10 -0
- package/dist/esm-node/rsc-disabled/rscDisabledRuntimeError.mjs +5 -0
- package/dist/esm-node/rsc-disabled/rscDisabledServerEdgeRuntime.mjs +17 -0
- package/dist/esm-node/rsc-disabled/rscDisabledServerNodeRuntime.mjs +19 -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/buildIdentity.d.ts +37 -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/localisedLoaderIdentity.d.ts +10 -0
- package/dist/types/prefetchPolicy.d.ts +4 -0
- package/dist/types/reactContext.d.ts +6 -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/rsc-disabled/rscDisabledClientBrowserRuntime.d.ts +9 -0
- package/dist/types/rsc-disabled/rscDisabledClientEdgeRuntime.d.ts +7 -0
- package/dist/types/rsc-disabled/rscDisabledClientNodeRuntime.d.ts +8 -0
- package/dist/types/rsc-disabled/rscDisabledRuntimeError.d.ts +1 -0
- package/dist/types/rsc-disabled/rscDisabledServerEdgeRuntime.d.ts +15 -0
- package/dist/types/rsc-disabled/rscDisabledServerNodeRuntime.d.ts +17 -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 +238 -13
- package/rslib.config.mts +17 -2
- package/rstest.config.mts +33 -2
- package/src/buildIdentity.ts +61 -0
- 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/localisedLoaderIdentity.ts +30 -0
- package/src/prefetchPolicy.ts +33 -0
- package/src/reactContext.ts +16 -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/rsc-disabled/rscDisabledClientBrowserRuntime.ts +10 -0
- package/src/rsc-disabled/rscDisabledClientEdgeRuntime.ts +8 -0
- package/src/rsc-disabled/rscDisabledClientNodeRuntime.ts +9 -0
- package/src/rsc-disabled/rscDisabledRuntimeError.ts +3 -0
- package/src/rsc-disabled/rscDisabledServerEdgeRuntime.ts +16 -0
- package/src/rsc-disabled/rscDisabledServerNodeRuntime.ts +18 -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 +70 -0
- 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,391 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { transformSync } from 'esbuild';
|
|
7
|
+
import * as contracts from '../src/workspaceEvents';
|
|
8
|
+
|
|
9
|
+
const cases = [
|
|
10
|
+
{
|
|
11
|
+
name: 'ultramodern:navigate',
|
|
12
|
+
valid: { to: '/dashboard', replace: false, state: { from: 'shell' } },
|
|
13
|
+
invalid: [
|
|
14
|
+
{},
|
|
15
|
+
{ to: '' },
|
|
16
|
+
{ to: ' ' },
|
|
17
|
+
{ to: 42 },
|
|
18
|
+
{ to: '/', replace: 'false' },
|
|
19
|
+
{ to: '/', state: [] },
|
|
20
|
+
{ to: '/', state: null },
|
|
21
|
+
],
|
|
22
|
+
validate: contracts.isUltramodernNavigatePayload,
|
|
23
|
+
dispatch: contracts.dispatchUltramodernNavigate,
|
|
24
|
+
on: contracts.onUltramodernNavigate,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'ultramodern:route-settled',
|
|
28
|
+
valid: { pathname: '/cs', locale: 'cs', title: 'Home' },
|
|
29
|
+
invalid: [
|
|
30
|
+
{},
|
|
31
|
+
{ pathname: ' ' },
|
|
32
|
+
{ pathname: '/', locale: 'de' },
|
|
33
|
+
{ pathname: '/', locale: null },
|
|
34
|
+
{ pathname: '/', title: '' },
|
|
35
|
+
{ pathname: '/', title: 42 },
|
|
36
|
+
],
|
|
37
|
+
validate: contracts.isUltramodernRouteSettledPayload,
|
|
38
|
+
dispatch: contracts.dispatchUltramodernRouteSettled,
|
|
39
|
+
on: contracts.onUltramodernRouteSettled,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'ultramodern:remote-ready',
|
|
43
|
+
valid: { appId: 'catalog', build: 'a', surface: 'ui', version: '1' },
|
|
44
|
+
invalid: [
|
|
45
|
+
{},
|
|
46
|
+
{ appId: 42 },
|
|
47
|
+
{ appId: ' ' },
|
|
48
|
+
{ appId: 'catalog', build: '' },
|
|
49
|
+
{ appId: 'catalog', surface: null },
|
|
50
|
+
{ appId: 'catalog', version: 1 },
|
|
51
|
+
],
|
|
52
|
+
validate: contracts.isUltramodernRemoteReadyPayload,
|
|
53
|
+
dispatch: contracts.dispatchUltramodernRemoteReady,
|
|
54
|
+
on: contracts.onUltramodernRemoteReady,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'ultramodern:performance-signal',
|
|
58
|
+
valid: {
|
|
59
|
+
signalId: 'bfcache',
|
|
60
|
+
status: 'pass',
|
|
61
|
+
durationMs: 0,
|
|
62
|
+
detail: { source: 'browser' },
|
|
63
|
+
},
|
|
64
|
+
invalid: [
|
|
65
|
+
{},
|
|
66
|
+
{ signalId: 'unknown', status: 'pass' },
|
|
67
|
+
{ signalId: 'bfcache', status: 'unknown' },
|
|
68
|
+
...[-1, Number.NaN, Number.POSITIVE_INFINITY, '1', null].map(
|
|
69
|
+
durationMs => ({ signalId: 'bfcache', status: 'pass', durationMs }),
|
|
70
|
+
),
|
|
71
|
+
{ signalId: 'bfcache', status: 'pass', detail: [] },
|
|
72
|
+
{ signalId: 'bfcache', status: 'pass', detail: null },
|
|
73
|
+
],
|
|
74
|
+
validate: contracts.isUltramodernPerformanceSignalPayload,
|
|
75
|
+
dispatch: contracts.dispatchUltramodernPerformanceSignal,
|
|
76
|
+
on: contracts.onUltramodernPerformanceSignal,
|
|
77
|
+
},
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
describe('workspace event contracts', () => {
|
|
81
|
+
for (const { name, valid, invalid, validate, dispatch, on } of cases) {
|
|
82
|
+
it(`${name} preserves schema validation and error data`, () => {
|
|
83
|
+
expect(validate(valid)).toBe(true);
|
|
84
|
+
expect(
|
|
85
|
+
contracts.assertUltramodernWorkspaceEventPayload(name, valid),
|
|
86
|
+
).toBe(valid);
|
|
87
|
+
for (const payload of [null, undefined, [], false, 'text', ...invalid]) {
|
|
88
|
+
expect(validate(payload)).toBe(false);
|
|
89
|
+
expect(
|
|
90
|
+
contracts.isUltramodernWorkspaceEventPayload(name, payload),
|
|
91
|
+
).toBe(false);
|
|
92
|
+
let error: unknown;
|
|
93
|
+
try {
|
|
94
|
+
contracts.assertUltramodernWorkspaceEventPayload(name, payload);
|
|
95
|
+
} catch (caught) {
|
|
96
|
+
error = caught;
|
|
97
|
+
}
|
|
98
|
+
expect(error).toBeInstanceOf(
|
|
99
|
+
contracts.UltramodernWorkspaceEventValidationError,
|
|
100
|
+
);
|
|
101
|
+
expect(error).toMatchObject({
|
|
102
|
+
name: 'UltramodernWorkspaceEventValidationError',
|
|
103
|
+
message: `Invalid payload for UltraModern workspace event "${name}"`,
|
|
104
|
+
eventName: name,
|
|
105
|
+
payload,
|
|
106
|
+
});
|
|
107
|
+
// The existing public error is a data class, not an Error subclass.
|
|
108
|
+
expect(error).not.toBeInstanceOf(Error);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it(`${name} uses native dispatch, preserves payload identity and cleans up`, () => {
|
|
113
|
+
const target = new EventTarget();
|
|
114
|
+
const received: unknown[] = [];
|
|
115
|
+
const handler = (payload: unknown, event: CustomEvent<unknown>) => {
|
|
116
|
+
expect(event).toBeInstanceOf(CustomEvent);
|
|
117
|
+
expect(event.type).toBe(name);
|
|
118
|
+
expect(event.bubbles).toBe(true);
|
|
119
|
+
expect(event.composed).toBe(true);
|
|
120
|
+
expect(event.cancelable).toBe(false);
|
|
121
|
+
expect(event.target).toBe(target);
|
|
122
|
+
expect(event.detail).toBe(payload);
|
|
123
|
+
received.push(payload);
|
|
124
|
+
};
|
|
125
|
+
const unsubscribe = on(target, handler);
|
|
126
|
+
// Each table row correlates the convenience dispatch with its own schema.
|
|
127
|
+
const dispatchPayload = dispatch as (
|
|
128
|
+
target: EventTarget,
|
|
129
|
+
payload: typeof valid,
|
|
130
|
+
) => boolean;
|
|
131
|
+
expect(dispatchPayload(target, valid)).toBe(true);
|
|
132
|
+
expect(received).toEqual([valid]);
|
|
133
|
+
expect(received[0]).toBe(valid);
|
|
134
|
+
unsubscribe();
|
|
135
|
+
unsubscribe();
|
|
136
|
+
dispatchPayload(target, valid);
|
|
137
|
+
expect(received).toEqual([valid]);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
it('retains permissive records, unknown fields and undefined optional fields', () => {
|
|
142
|
+
const payload = Object.assign(Object.create(null), {
|
|
143
|
+
to: ' /raw ',
|
|
144
|
+
state: new Date(0),
|
|
145
|
+
replace: undefined,
|
|
146
|
+
extension: 'consumer-owned',
|
|
147
|
+
});
|
|
148
|
+
expect(
|
|
149
|
+
contracts.assertUltramodernWorkspaceEventPayload(
|
|
150
|
+
'ultramodern:navigate',
|
|
151
|
+
payload,
|
|
152
|
+
),
|
|
153
|
+
).toBe(payload);
|
|
154
|
+
expect(payload.to).toBe(' /raw ');
|
|
155
|
+
for (const locale of ['en', 'cs', undefined]) {
|
|
156
|
+
expect(
|
|
157
|
+
contracts.isUltramodernRouteSettledPayload({
|
|
158
|
+
pathname: '/',
|
|
159
|
+
locale,
|
|
160
|
+
title: undefined,
|
|
161
|
+
}),
|
|
162
|
+
).toBe(true);
|
|
163
|
+
}
|
|
164
|
+
expect(
|
|
165
|
+
contracts.isUltramodernRemoteReadyPayload({
|
|
166
|
+
appId: 'catalog',
|
|
167
|
+
build: undefined,
|
|
168
|
+
surface: undefined,
|
|
169
|
+
version: undefined,
|
|
170
|
+
}),
|
|
171
|
+
).toBe(true);
|
|
172
|
+
for (const signalId of [
|
|
173
|
+
'bfcache',
|
|
174
|
+
'core-web-vitals-rum',
|
|
175
|
+
'duplicate-prefetch-warmup',
|
|
176
|
+
'cache-policy-sanity',
|
|
177
|
+
'save-data-behavior',
|
|
178
|
+
'cloudflare-ssr-cache-hints',
|
|
179
|
+
]) {
|
|
180
|
+
for (const status of ['pass', 'warn', 'fail']) {
|
|
181
|
+
expect(
|
|
182
|
+
contracts.isUltramodernPerformanceSignalPayload({
|
|
183
|
+
signalId,
|
|
184
|
+
status,
|
|
185
|
+
durationMs: undefined,
|
|
186
|
+
detail: undefined,
|
|
187
|
+
}),
|
|
188
|
+
).toBe(true);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('rejects invalid dispatch before notifying listeners', () => {
|
|
194
|
+
const target = new EventTarget();
|
|
195
|
+
const handler = rstest.fn();
|
|
196
|
+
const unsubscribe = contracts.onUltramodernNavigate(target, handler);
|
|
197
|
+
expect(() =>
|
|
198
|
+
contracts.dispatchUltramodernNavigate(target, { to: '' }),
|
|
199
|
+
).toThrow();
|
|
200
|
+
expect(handler).not.toHaveBeenCalled();
|
|
201
|
+
unsubscribe();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('validates incoming event details and removes only its own listener', () => {
|
|
205
|
+
const target = new EventTarget();
|
|
206
|
+
const add = rstest.spyOn(target, 'addEventListener');
|
|
207
|
+
const remove = rstest.spyOn(target, 'removeEventListener');
|
|
208
|
+
const handler = rstest.fn();
|
|
209
|
+
const first = contracts.onUltramodernNavigate(target, handler);
|
|
210
|
+
const listener = add.mock.calls[0]?.[1] as EventListener;
|
|
211
|
+
for (const event of [
|
|
212
|
+
new Event('ultramodern:navigate'),
|
|
213
|
+
new CustomEvent('ultramodern:navigate', { detail: { to: '' } }),
|
|
214
|
+
]) {
|
|
215
|
+
// Native dispatch reports listener errors asynchronously; invoke the
|
|
216
|
+
// registered listener directly to inspect the precise validation failure.
|
|
217
|
+
expect(() => listener(event)).toThrow();
|
|
218
|
+
}
|
|
219
|
+
expect(handler).not.toHaveBeenCalled();
|
|
220
|
+
const second = contracts.onUltramodernNavigate(target, handler);
|
|
221
|
+
first();
|
|
222
|
+
expect(remove).toHaveBeenCalledWith('ultramodern:navigate', listener);
|
|
223
|
+
contracts.dispatchUltramodernNavigate(target, { to: '/still-subscribed' });
|
|
224
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
225
|
+
second();
|
|
226
|
+
add.mockRestore();
|
|
227
|
+
remove.mockRestore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('keeps event-name payload inference and compile-time rejection', () => {
|
|
231
|
+
const temporaryRoot = mkdtempSync(
|
|
232
|
+
path.join(tmpdir(), 'workspace-events-types-'),
|
|
233
|
+
);
|
|
234
|
+
try {
|
|
235
|
+
const fixture = path.join(temporaryRoot, 'consumer.ts');
|
|
236
|
+
writeFileSync(
|
|
237
|
+
fixture,
|
|
238
|
+
`
|
|
239
|
+
import * as c from ${JSON.stringify(path.resolve(__dirname, '../src/workspaceEvents'))};
|
|
240
|
+
const target = new EventTarget();
|
|
241
|
+
const value: unknown = { to: '/' };
|
|
242
|
+
if (c.isUltramodernWorkspaceEventPayload(c.ultramodernWorkspaceEventNames.navigate, value)) {
|
|
243
|
+
const destination: string = value.to;
|
|
244
|
+
// @ts-expect-error Navigate payload has no pathname
|
|
245
|
+
value.pathname;
|
|
246
|
+
}
|
|
247
|
+
const event: CustomEvent<c.UltramodernRemoteReadyPayload> =
|
|
248
|
+
c.createUltramodernWorkspaceEvent('ultramodern:remote-ready', { appId: 'catalog' });
|
|
249
|
+
const cleanup: () => void = c.onUltramodernPerformanceSignal(target, (payload, event) => {
|
|
250
|
+
const status: c.UltramodernPerformanceReadinessSignalStatus = payload.status;
|
|
251
|
+
const signal: c.UltramodernPerformanceReadinessSignalId = event.detail.signalId;
|
|
252
|
+
});
|
|
253
|
+
const dispatched: boolean = c.dispatchUltramodernRouteSettled(target, { pathname: '/', locale: 'en' });
|
|
254
|
+
const asserted: c.UltramodernNavigatePayload = c.assertUltramodernWorkspaceEventPayload('ultramodern:navigate', value);
|
|
255
|
+
// @ts-expect-error Event names are closed
|
|
256
|
+
c.createUltramodernWorkspaceEvent('consumer:unknown', {});
|
|
257
|
+
// @ts-expect-error Wrong payload for the chosen event
|
|
258
|
+
c.dispatchUltramodernWorkspaceEvent(target, 'ultramodern:navigate', { appId: 'catalog' });
|
|
259
|
+
// @ts-expect-error Unsupported locale
|
|
260
|
+
c.dispatchUltramodernRouteSettled(target, { pathname: '/', locale: 'de' });
|
|
261
|
+
// @ts-expect-error Unsupported signal status
|
|
262
|
+
c.dispatchUltramodernPerformanceSignal(target, { signalId: 'bfcache', status: 'ok' });
|
|
263
|
+
`,
|
|
264
|
+
);
|
|
265
|
+
const requireFromPackage = createRequire(
|
|
266
|
+
path.resolve(__dirname, '../package.json'),
|
|
267
|
+
);
|
|
268
|
+
const compilerPackage = requireFromPackage.resolve(
|
|
269
|
+
'typescript/package.json',
|
|
270
|
+
);
|
|
271
|
+
const metadata = JSON.parse(readFileSync(compilerPackage, 'utf8'));
|
|
272
|
+
const compiler = path.resolve(
|
|
273
|
+
path.dirname(compilerPackage),
|
|
274
|
+
metadata.bin.tsc,
|
|
275
|
+
);
|
|
276
|
+
const config = path.join(temporaryRoot, 'tsconfig.json');
|
|
277
|
+
writeFileSync(
|
|
278
|
+
config,
|
|
279
|
+
JSON.stringify({
|
|
280
|
+
compilerOptions: {
|
|
281
|
+
noEmit: true,
|
|
282
|
+
strict: true,
|
|
283
|
+
skipLibCheck: true,
|
|
284
|
+
types: [],
|
|
285
|
+
target: 'ES2022',
|
|
286
|
+
module: 'NodeNext',
|
|
287
|
+
moduleResolution: 'NodeNext',
|
|
288
|
+
},
|
|
289
|
+
files: [fixture],
|
|
290
|
+
}),
|
|
291
|
+
);
|
|
292
|
+
const checked = spawnSync(
|
|
293
|
+
process.execPath,
|
|
294
|
+
[compiler, '--project', config, '--pretty', 'false'],
|
|
295
|
+
{
|
|
296
|
+
cwd: temporaryRoot,
|
|
297
|
+
encoding: 'utf8',
|
|
298
|
+
},
|
|
299
|
+
);
|
|
300
|
+
expect(checked.error).toBeUndefined();
|
|
301
|
+
expect({
|
|
302
|
+
status: checked.status,
|
|
303
|
+
output: checked.stdout + checked.stderr,
|
|
304
|
+
}).toEqual({ status: 0, output: '' });
|
|
305
|
+
} finally {
|
|
306
|
+
rmSync(temporaryRoot, { recursive: true, force: true });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('supports native browser targets, shadow propagation and foreign realm events', async () => {
|
|
311
|
+
const requireFromBrowserFixture = createRequire(
|
|
312
|
+
path.resolve(
|
|
313
|
+
__dirname,
|
|
314
|
+
'../../../../tests/integration/rstest/basic-app-rstest-browser/package.json',
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
const { chromium } = requireFromBrowserFixture('playwright');
|
|
318
|
+
const source = readFileSync(
|
|
319
|
+
path.resolve(__dirname, '../src/workspaceEvents.ts'),
|
|
320
|
+
'utf8',
|
|
321
|
+
);
|
|
322
|
+
const compiled = transformSync(source, {
|
|
323
|
+
loader: 'ts',
|
|
324
|
+
format: 'cjs',
|
|
325
|
+
}).code;
|
|
326
|
+
const browser = await chromium.launch({ headless: true });
|
|
327
|
+
try {
|
|
328
|
+
const page = await browser.newPage();
|
|
329
|
+
const result = await page.evaluate((compiled: string) => {
|
|
330
|
+
const module = { exports: {} };
|
|
331
|
+
new Function('module', 'exports', compiled)(module, module.exports);
|
|
332
|
+
const api = module.exports as typeof contracts;
|
|
333
|
+
const host = document.createElement('div');
|
|
334
|
+
document.body.append(host);
|
|
335
|
+
const inner = document.createElement('button');
|
|
336
|
+
host.attachShadow({ mode: 'open' }).append(inner);
|
|
337
|
+
const payload = { to: '/shadow' };
|
|
338
|
+
const seen: unknown[] = [];
|
|
339
|
+
const off = api.onUltramodernNavigate(document, (detail, event) => {
|
|
340
|
+
seen.push({
|
|
341
|
+
same: detail === payload,
|
|
342
|
+
target: event.target === host,
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
api.dispatchUltramodernNavigate(inner, payload);
|
|
346
|
+
off();
|
|
347
|
+
api.dispatchUltramodernNavigate(inner, payload);
|
|
348
|
+
const iframe = document.createElement('iframe');
|
|
349
|
+
document.body.append(iframe);
|
|
350
|
+
const foreignWindow = iframe.contentWindow as Window &
|
|
351
|
+
typeof globalThis;
|
|
352
|
+
const foreignEvent = new foreignWindow.CustomEvent(
|
|
353
|
+
'ultramodern:navigate',
|
|
354
|
+
{
|
|
355
|
+
detail: payload,
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
let foreignReceived = false;
|
|
359
|
+
const offForeign = api.onUltramodernNavigate(
|
|
360
|
+
document,
|
|
361
|
+
(detail, event) => {
|
|
362
|
+
foreignReceived = detail === payload && event === foreignEvent;
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
document.dispatchEvent(foreignEvent);
|
|
366
|
+
offForeign();
|
|
367
|
+
const foreignTarget = new foreignWindow.EventTarget();
|
|
368
|
+
let foreignTargetReceived = false;
|
|
369
|
+
const offTarget = api.onUltramodernNavigate(foreignTarget, detail => {
|
|
370
|
+
foreignTargetReceived = detail === payload;
|
|
371
|
+
});
|
|
372
|
+
api.dispatchUltramodernNavigate(foreignTarget, payload);
|
|
373
|
+
offTarget();
|
|
374
|
+
return {
|
|
375
|
+
seen,
|
|
376
|
+
foreignReceived,
|
|
377
|
+
foreignTargetReceived,
|
|
378
|
+
distinctRealm: !(foreignEvent instanceof CustomEvent),
|
|
379
|
+
};
|
|
380
|
+
}, compiled);
|
|
381
|
+
expect(result).toEqual({
|
|
382
|
+
seen: [{ same: true, target: true }],
|
|
383
|
+
foreignReceived: true,
|
|
384
|
+
foreignTargetReceived: true,
|
|
385
|
+
distinctRealm: true,
|
|
386
|
+
});
|
|
387
|
+
} finally {
|
|
388
|
+
await browser.close();
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __webpack_require__ = {};
|
|
3
|
-
(()=>{
|
|
4
|
-
__webpack_require__.d = (exports1, getters, values)=>{
|
|
5
|
-
var define = (defs, kind)=>{
|
|
6
|
-
for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
|
|
7
|
-
enumerable: true,
|
|
8
|
-
[kind]: defs[key]
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
define(getters, "get");
|
|
12
|
-
define(values, "value");
|
|
13
|
-
};
|
|
14
|
-
})();
|
|
15
|
-
(()=>{
|
|
16
|
-
__webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
|
|
17
|
-
})();
|
|
18
|
-
(()=>{
|
|
19
|
-
__webpack_require__.r = (exports1)=>{
|
|
20
|
-
if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
|
|
21
|
-
value: 'Module'
|
|
22
|
-
});
|
|
23
|
-
Object.defineProperty(exports1, '__esModule', {
|
|
24
|
-
value: true
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
})();
|
|
28
|
-
var __webpack_exports__ = {};
|
|
29
|
-
__webpack_require__.r(__webpack_exports__);
|
|
30
|
-
const encoder = new TextEncoder();
|
|
31
|
-
const closingTagsPattern = /<\/body>\s*<\/html>\s*$/i;
|
|
32
|
-
const closingTagsPrefixPattern = /(?:<\/body>\s*(?:<\/html>\s*|<\/h(?:t(?:m(?:l>?)?)?)?|<\/?|<)?|<\/b(?:o(?:d(?:y>?)?)?)?|<\/?|<[^>]*)$/i;
|
|
33
|
-
const RSC_HTML_CLOSING_TAIL_LIMIT = 8192;
|
|
34
|
-
function injectRSCPayload(rscStream, { injectClosingTags = true } = {}) {
|
|
35
|
-
const decoder = new TextDecoder();
|
|
36
|
-
let retainedTail = '';
|
|
37
|
-
let committedClosingBody = false;
|
|
38
|
-
let committedHtmlClose = false;
|
|
39
|
-
let committedProbe = '';
|
|
40
|
-
let flightPromise;
|
|
41
|
-
const startFlight = (controller)=>{
|
|
42
|
-
if (!flightPromise) {
|
|
43
|
-
flightPromise = writeRSCStream(rscStream, controller);
|
|
44
|
-
flightPromise.catch((error)=>controller.error(error));
|
|
45
|
-
}
|
|
46
|
-
return flightPromise;
|
|
47
|
-
};
|
|
48
|
-
const enqueueHtml = (value, controller)=>{
|
|
49
|
-
if (value) controller.enqueue(encoder.encode(value));
|
|
50
|
-
};
|
|
51
|
-
const processHtml = async (value, controller)=>{
|
|
52
|
-
if (!value) return;
|
|
53
|
-
if (committedClosingBody) {
|
|
54
|
-
const probe = committedProbe + value;
|
|
55
|
-
committedHtmlClose ||= /<\/html>/i.test(probe);
|
|
56
|
-
committedProbe = probe.slice(-6);
|
|
57
|
-
enqueueHtml(value, controller);
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
const html = retainedTail + value;
|
|
61
|
-
retainedTail = '';
|
|
62
|
-
const suffix = html.match(closingTagsPrefixPattern)?.[0] ?? '';
|
|
63
|
-
const prefix = suffix ? html.slice(0, -suffix.length) : html;
|
|
64
|
-
enqueueHtml(prefix, controller);
|
|
65
|
-
if (!suffix) return void startFlight(controller);
|
|
66
|
-
if (suffix.length <= RSC_HTML_CLOSING_TAIL_LIMIT) {
|
|
67
|
-
retainedTail = suffix;
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
await startFlight(controller);
|
|
71
|
-
if (/<\/body>/i.test(suffix)) {
|
|
72
|
-
committedClosingBody = true;
|
|
73
|
-
committedHtmlClose = /<\/html>/i.test(suffix);
|
|
74
|
-
committedProbe = suffix.slice(-6);
|
|
75
|
-
}
|
|
76
|
-
enqueueHtml(suffix, controller);
|
|
77
|
-
};
|
|
78
|
-
return new TransformStream({
|
|
79
|
-
async transform (chunk, controller) {
|
|
80
|
-
await processHtml(decoder.decode(chunk, {
|
|
81
|
-
stream: true
|
|
82
|
-
}), controller);
|
|
83
|
-
},
|
|
84
|
-
async flush (controller) {
|
|
85
|
-
await processHtml(decoder.decode(), controller);
|
|
86
|
-
await startFlight(controller);
|
|
87
|
-
const retainedClosingTags = closingTagsPattern.test(retainedTail);
|
|
88
|
-
enqueueHtml(retainedTail, controller);
|
|
89
|
-
if (!injectClosingTags) return;
|
|
90
|
-
if (committedClosingBody) {
|
|
91
|
-
if (!committedHtmlClose) enqueueHtml('</html>', controller);
|
|
92
|
-
} else if (!retainedClosingTags) enqueueHtml('</body></html>', controller);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
async function writeRSCStream(rscStream, controller) {
|
|
97
|
-
const decoder = new TextDecoder('utf-8', {
|
|
98
|
-
fatal: true
|
|
99
|
-
});
|
|
100
|
-
for await (const chunk of rscStream)try {
|
|
101
|
-
writeChunk(JSON.stringify(decoder.decode(chunk, {
|
|
102
|
-
stream: true
|
|
103
|
-
})), controller);
|
|
104
|
-
} catch {
|
|
105
|
-
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
106
|
-
writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller);
|
|
107
|
-
}
|
|
108
|
-
const remaining = decoder.decode();
|
|
109
|
-
if (remaining) writeChunk(JSON.stringify(remaining), controller);
|
|
110
|
-
}
|
|
111
|
-
function writeChunk(chunk, controller) {
|
|
112
|
-
const script = `(self.__FLIGHT_DATA||=[]).push(${chunk})`;
|
|
113
|
-
controller.enqueue(encoder.encode(`<script>${script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1')}</script>`));
|
|
114
|
-
}
|
|
115
|
-
__webpack_require__.d(__webpack_exports__, {
|
|
116
|
-
injectRSCPayload: ()=>injectRSCPayload
|
|
117
|
-
}, {
|
|
118
|
-
RSC_HTML_CLOSING_TAIL_LIMIT: RSC_HTML_CLOSING_TAIL_LIMIT
|
|
119
|
-
});
|
|
120
|
-
exports.RSC_HTML_CLOSING_TAIL_LIMIT = __webpack_exports__.RSC_HTML_CLOSING_TAIL_LIMIT;
|
|
121
|
-
exports.injectRSCPayload = __webpack_exports__.injectRSCPayload;
|
|
122
|
-
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
123
|
-
"RSC_HTML_CLOSING_TAIL_LIMIT",
|
|
124
|
-
"injectRSCPayload"
|
|
125
|
-
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|
|
126
|
-
Object.defineProperty(exports, '__esModule', {
|
|
127
|
-
value: true
|
|
128
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
const encoder = new TextEncoder();
|
|
2
|
-
const closingTagsPattern = /<\/body>\s*<\/html>\s*$/i;
|
|
3
|
-
const closingTagsPrefixPattern = /(?:<\/body>\s*(?:<\/html>\s*|<\/h(?:t(?:m(?:l>?)?)?)?|<\/?|<)?|<\/b(?:o(?:d(?:y>?)?)?)?|<\/?|<[^>]*)$/i;
|
|
4
|
-
const RSC_HTML_CLOSING_TAIL_LIMIT = 8192;
|
|
5
|
-
function injectRSCPayload(rscStream, { injectClosingTags = true } = {}) {
|
|
6
|
-
const decoder = new TextDecoder();
|
|
7
|
-
let retainedTail = '';
|
|
8
|
-
let committedClosingBody = false;
|
|
9
|
-
let committedHtmlClose = false;
|
|
10
|
-
let committedProbe = '';
|
|
11
|
-
let flightPromise;
|
|
12
|
-
const startFlight = (controller)=>{
|
|
13
|
-
if (!flightPromise) {
|
|
14
|
-
flightPromise = writeRSCStream(rscStream, controller);
|
|
15
|
-
flightPromise.catch((error)=>controller.error(error));
|
|
16
|
-
}
|
|
17
|
-
return flightPromise;
|
|
18
|
-
};
|
|
19
|
-
const enqueueHtml = (value, controller)=>{
|
|
20
|
-
if (value) controller.enqueue(encoder.encode(value));
|
|
21
|
-
};
|
|
22
|
-
const processHtml = async (value, controller)=>{
|
|
23
|
-
if (!value) return;
|
|
24
|
-
if (committedClosingBody) {
|
|
25
|
-
const probe = committedProbe + value;
|
|
26
|
-
committedHtmlClose ||= /<\/html>/i.test(probe);
|
|
27
|
-
committedProbe = probe.slice(-6);
|
|
28
|
-
enqueueHtml(value, controller);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
const html = retainedTail + value;
|
|
32
|
-
retainedTail = '';
|
|
33
|
-
const suffix = html.match(closingTagsPrefixPattern)?.[0] ?? '';
|
|
34
|
-
const prefix = suffix ? html.slice(0, -suffix.length) : html;
|
|
35
|
-
enqueueHtml(prefix, controller);
|
|
36
|
-
if (!suffix) return void startFlight(controller);
|
|
37
|
-
if (suffix.length <= RSC_HTML_CLOSING_TAIL_LIMIT) {
|
|
38
|
-
retainedTail = suffix;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
await startFlight(controller);
|
|
42
|
-
if (/<\/body>/i.test(suffix)) {
|
|
43
|
-
committedClosingBody = true;
|
|
44
|
-
committedHtmlClose = /<\/html>/i.test(suffix);
|
|
45
|
-
committedProbe = suffix.slice(-6);
|
|
46
|
-
}
|
|
47
|
-
enqueueHtml(suffix, controller);
|
|
48
|
-
};
|
|
49
|
-
return new TransformStream({
|
|
50
|
-
async transform (chunk, controller) {
|
|
51
|
-
await processHtml(decoder.decode(chunk, {
|
|
52
|
-
stream: true
|
|
53
|
-
}), controller);
|
|
54
|
-
},
|
|
55
|
-
async flush (controller) {
|
|
56
|
-
await processHtml(decoder.decode(), controller);
|
|
57
|
-
await startFlight(controller);
|
|
58
|
-
const retainedClosingTags = closingTagsPattern.test(retainedTail);
|
|
59
|
-
enqueueHtml(retainedTail, controller);
|
|
60
|
-
if (!injectClosingTags) return;
|
|
61
|
-
if (committedClosingBody) {
|
|
62
|
-
if (!committedHtmlClose) enqueueHtml('</html>', controller);
|
|
63
|
-
} else if (!retainedClosingTags) enqueueHtml('</body></html>', controller);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
async function writeRSCStream(rscStream, controller) {
|
|
68
|
-
const decoder = new TextDecoder('utf-8', {
|
|
69
|
-
fatal: true
|
|
70
|
-
});
|
|
71
|
-
for await (const chunk of rscStream)try {
|
|
72
|
-
writeChunk(JSON.stringify(decoder.decode(chunk, {
|
|
73
|
-
stream: true
|
|
74
|
-
})), controller);
|
|
75
|
-
} catch {
|
|
76
|
-
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
77
|
-
writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller);
|
|
78
|
-
}
|
|
79
|
-
const remaining = decoder.decode();
|
|
80
|
-
if (remaining) writeChunk(JSON.stringify(remaining), controller);
|
|
81
|
-
}
|
|
82
|
-
function writeChunk(chunk, controller) {
|
|
83
|
-
const script = `(self.__FLIGHT_DATA||=[]).push(${chunk})`;
|
|
84
|
-
controller.enqueue(encoder.encode(`<script>${script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1')}</script>`));
|
|
85
|
-
}
|
|
86
|
-
export { RSC_HTML_CLOSING_TAIL_LIMIT, injectRSCPayload };
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import "node:module";
|
|
2
|
-
const encoder = new TextEncoder();
|
|
3
|
-
const closingTagsPattern = /<\/body>\s*<\/html>\s*$/i;
|
|
4
|
-
const closingTagsPrefixPattern = /(?:<\/body>\s*(?:<\/html>\s*|<\/h(?:t(?:m(?:l>?)?)?)?|<\/?|<)?|<\/b(?:o(?:d(?:y>?)?)?)?|<\/?|<[^>]*)$/i;
|
|
5
|
-
const RSC_HTML_CLOSING_TAIL_LIMIT = 8192;
|
|
6
|
-
function injectRSCPayload(rscStream, { injectClosingTags = true } = {}) {
|
|
7
|
-
const decoder = new TextDecoder();
|
|
8
|
-
let retainedTail = '';
|
|
9
|
-
let committedClosingBody = false;
|
|
10
|
-
let committedHtmlClose = false;
|
|
11
|
-
let committedProbe = '';
|
|
12
|
-
let flightPromise;
|
|
13
|
-
const startFlight = (controller)=>{
|
|
14
|
-
if (!flightPromise) {
|
|
15
|
-
flightPromise = writeRSCStream(rscStream, controller);
|
|
16
|
-
flightPromise.catch((error)=>controller.error(error));
|
|
17
|
-
}
|
|
18
|
-
return flightPromise;
|
|
19
|
-
};
|
|
20
|
-
const enqueueHtml = (value, controller)=>{
|
|
21
|
-
if (value) controller.enqueue(encoder.encode(value));
|
|
22
|
-
};
|
|
23
|
-
const processHtml = async (value, controller)=>{
|
|
24
|
-
if (!value) return;
|
|
25
|
-
if (committedClosingBody) {
|
|
26
|
-
const probe = committedProbe + value;
|
|
27
|
-
committedHtmlClose ||= /<\/html>/i.test(probe);
|
|
28
|
-
committedProbe = probe.slice(-6);
|
|
29
|
-
enqueueHtml(value, controller);
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const html = retainedTail + value;
|
|
33
|
-
retainedTail = '';
|
|
34
|
-
const suffix = html.match(closingTagsPrefixPattern)?.[0] ?? '';
|
|
35
|
-
const prefix = suffix ? html.slice(0, -suffix.length) : html;
|
|
36
|
-
enqueueHtml(prefix, controller);
|
|
37
|
-
if (!suffix) return void startFlight(controller);
|
|
38
|
-
if (suffix.length <= RSC_HTML_CLOSING_TAIL_LIMIT) {
|
|
39
|
-
retainedTail = suffix;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await startFlight(controller);
|
|
43
|
-
if (/<\/body>/i.test(suffix)) {
|
|
44
|
-
committedClosingBody = true;
|
|
45
|
-
committedHtmlClose = /<\/html>/i.test(suffix);
|
|
46
|
-
committedProbe = suffix.slice(-6);
|
|
47
|
-
}
|
|
48
|
-
enqueueHtml(suffix, controller);
|
|
49
|
-
};
|
|
50
|
-
return new TransformStream({
|
|
51
|
-
async transform (chunk, controller) {
|
|
52
|
-
await processHtml(decoder.decode(chunk, {
|
|
53
|
-
stream: true
|
|
54
|
-
}), controller);
|
|
55
|
-
},
|
|
56
|
-
async flush (controller) {
|
|
57
|
-
await processHtml(decoder.decode(), controller);
|
|
58
|
-
await startFlight(controller);
|
|
59
|
-
const retainedClosingTags = closingTagsPattern.test(retainedTail);
|
|
60
|
-
enqueueHtml(retainedTail, controller);
|
|
61
|
-
if (!injectClosingTags) return;
|
|
62
|
-
if (committedClosingBody) {
|
|
63
|
-
if (!committedHtmlClose) enqueueHtml('</html>', controller);
|
|
64
|
-
} else if (!retainedClosingTags) enqueueHtml('</body></html>', controller);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
async function writeRSCStream(rscStream, controller) {
|
|
69
|
-
const decoder = new TextDecoder('utf-8', {
|
|
70
|
-
fatal: true
|
|
71
|
-
});
|
|
72
|
-
for await (const chunk of rscStream)try {
|
|
73
|
-
writeChunk(JSON.stringify(decoder.decode(chunk, {
|
|
74
|
-
stream: true
|
|
75
|
-
})), controller);
|
|
76
|
-
} catch {
|
|
77
|
-
const base64 = JSON.stringify(btoa(String.fromCodePoint(...chunk)));
|
|
78
|
-
writeChunk(`Uint8Array.from(atob(${base64}), m => m.codePointAt(0))`, controller);
|
|
79
|
-
}
|
|
80
|
-
const remaining = decoder.decode();
|
|
81
|
-
if (remaining) writeChunk(JSON.stringify(remaining), controller);
|
|
82
|
-
}
|
|
83
|
-
function writeChunk(chunk, controller) {
|
|
84
|
-
const script = `(self.__FLIGHT_DATA||=[]).push(${chunk})`;
|
|
85
|
-
controller.enqueue(encoder.encode(`<script>${script.replace(/<!--/g, '<\\!--').replace(/<\/(script)/gi, '</\\$1')}</script>`));
|
|
86
|
-
}
|
|
87
|
-
export { RSC_HTML_CLOSING_TAIL_LIMIT, injectRSCPayload };
|