Skip to content

Commit 1e5245d

Browse files
authored
support subresource integrity for bootstrapScripts and bootstrapModules (#25104)
1 parent 6ef466c commit 1e5245d

6 files changed

+101
-14
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

+55
Original file line numberDiff line numberDiff line change
@@ -3390,6 +3390,61 @@ describe('ReactDOMFizzServer', () => {
33903390
});
33913391
});
33923392

3393+
it('accepts an integrity property for bootstrapScripts and bootstrapModules', async () => {
3394+
await actIntoEmptyDocument(() => {
3395+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
3396+
<html>
3397+
<head />
3398+
<body>
3399+
<div>hello world</div>
3400+
</body>
3401+
</html>,
3402+
{
3403+
bootstrapScripts: [
3404+
'foo',
3405+
{
3406+
src: 'bar',
3407+
},
3408+
{
3409+
src: 'baz',
3410+
integrity: 'qux',
3411+
},
3412+
],
3413+
bootstrapModules: [
3414+
'quux',
3415+
{
3416+
src: 'corge',
3417+
},
3418+
{
3419+
src: 'grault',
3420+
integrity: 'garply',
3421+
},
3422+
],
3423+
},
3424+
);
3425+
pipe(writable);
3426+
});
3427+
3428+
expect(getVisibleChildren(document)).toEqual(
3429+
<html>
3430+
<head />
3431+
<body>
3432+
<div>hello world</div>
3433+
</body>
3434+
</html>,
3435+
);
3436+
expect(
3437+
Array.from(document.getElementsByTagName('script')).map(n => n.outerHTML),
3438+
).toEqual([
3439+
'<script src="foo" async=""></script>',
3440+
'<script src="bar" async=""></script>',
3441+
'<script src="baz" integrity="qux" async=""></script>',
3442+
'<script type="module" src="quux" async=""></script>',
3443+
'<script type="module" src="corge" async=""></script>',
3444+
'<script type="module" src="grault" integrity="garply" async=""></script>',
3445+
]);
3446+
});
3447+
33933448
describe('bootstrapScriptContent escaping', () => {
33943449
it('the "S" in "</?[Ss]cript" strings are replaced with unicode escaped lowercase s or S depending on case, preserving case sensitivity of nearby characters', async () => {
33953450
window.__test_outlet = '';

packages/react-dom/src/server/ReactDOMFizzServerBrowser.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {ReactNodeList} from 'shared/ReactTypes';
11+
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
1112

1213
import ReactVersion from 'shared/ReactVersion';
1314

@@ -28,8 +29,8 @@ type Options = {|
2829
namespaceURI?: string,
2930
nonce?: string,
3031
bootstrapScriptContent?: string,
31-
bootstrapScripts?: Array<string>,
32-
bootstrapModules?: Array<string>,
32+
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
33+
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
3334
progressiveChunkSize?: number,
3435
signal?: AbortSignal,
3536
onError?: (error: mixed) => ?string,

packages/react-dom/src/server/ReactDOMFizzServerNode.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import type {ReactNodeList} from 'shared/ReactTypes';
1111
import type {Writable} from 'stream';
12+
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
1213

1314
import ReactVersion from 'shared/ReactVersion';
1415

@@ -38,8 +39,8 @@ type Options = {|
3839
namespaceURI?: string,
3940
nonce?: string,
4041
bootstrapScriptContent?: string,
41-
bootstrapScripts?: Array<string>,
42-
bootstrapModules?: Array<string>,
42+
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
43+
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
4344
progressiveChunkSize?: number,
4445
onShellReady?: () => void,
4546
onShellError?: (error: mixed) => void,

packages/react-dom/src/server/ReactDOMFizzStaticBrowser.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import type {ReactNodeList} from 'shared/ReactTypes';
11+
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
1112

1213
import ReactVersion from 'shared/ReactVersion';
1314

@@ -27,8 +28,8 @@ type Options = {|
2728
identifierPrefix?: string,
2829
namespaceURI?: string,
2930
bootstrapScriptContent?: string,
30-
bootstrapScripts?: Array<string>,
31-
bootstrapModules?: Array<string>,
31+
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
32+
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
3233
progressiveChunkSize?: number,
3334
signal?: AbortSignal,
3435
onError?: (error: mixed) => ?string,

packages/react-dom/src/server/ReactDOMFizzStaticNode.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
*/
99

1010
import type {ReactNodeList} from 'shared/ReactTypes';
11+
import type {BootstrapScriptDescriptor} from './ReactDOMServerFormatConfig';
12+
1113
import {Writable, Readable} from 'stream';
1214

1315
import ReactVersion from 'shared/ReactVersion';
@@ -28,8 +30,8 @@ type Options = {|
2830
identifierPrefix?: string,
2931
namespaceURI?: string,
3032
bootstrapScriptContent?: string,
31-
bootstrapScripts?: Array<string>,
32-
bootstrapModules?: Array<string>,
33+
bootstrapScripts?: Array<string | BootstrapScriptDescriptor>,
34+
bootstrapModules?: Array<string | BootstrapScriptDescriptor>,
3335
progressiveChunkSize?: number,
3436
signal?: AbortSignal,
3537
onError?: (error: mixed) => ?string,

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

+33-6
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const endInlineScript = stringToPrecomputedChunk('</script>');
8282

8383
const startScriptSrc = stringToPrecomputedChunk('<script src="');
8484
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
85+
const scriptIntegirty = stringToPrecomputedChunk('" integrity="');
8586
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
8687

8788
/**
@@ -104,13 +105,17 @@ const scriptRegex = /(<\/|<)(s)(cript)/gi;
104105
const scriptReplacer = (match, prefix, s, suffix) =>
105106
`${prefix}${s === 's' ? '\\u0073' : '\\u0053'}${suffix}`;
106107

108+
export type BootstrapScriptDescriptor = {
109+
src: string,
110+
integrity?: string,
111+
};
107112
// Allows us to keep track of what we've already written so we can refer back to it.
108113
export function createResponseState(
109114
identifierPrefix: string | void,
110115
nonce: string | void,
111116
bootstrapScriptContent: string | void,
112-
bootstrapScripts: Array<string> | void,
113-
bootstrapModules: Array<string> | void,
117+
bootstrapScripts: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
118+
bootstrapModules: $ReadOnlyArray<string | BootstrapScriptDescriptor> | void,
114119
): ResponseState {
115120
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
116121
const inlineScriptWithNonce =
@@ -129,20 +134,42 @@ export function createResponseState(
129134
}
130135
if (bootstrapScripts !== undefined) {
131136
for (let i = 0; i < bootstrapScripts.length; i++) {
137+
const scriptConfig = bootstrapScripts[i];
138+
const src =
139+
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
140+
const integrity =
141+
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
132142
bootstrapChunks.push(
133143
startScriptSrc,
134-
stringToChunk(escapeTextForBrowser(bootstrapScripts[i])),
135-
endAsyncScript,
144+
stringToChunk(escapeTextForBrowser(src)),
136145
);
146+
if (integrity) {
147+
bootstrapChunks.push(
148+
scriptIntegirty,
149+
stringToChunk(escapeTextForBrowser(integrity)),
150+
);
151+
}
152+
bootstrapChunks.push(endAsyncScript);
137153
}
138154
}
139155
if (bootstrapModules !== undefined) {
140156
for (let i = 0; i < bootstrapModules.length; i++) {
157+
const scriptConfig = bootstrapModules[i];
158+
const src =
159+
typeof scriptConfig === 'string' ? scriptConfig : scriptConfig.src;
160+
const integrity =
161+
typeof scriptConfig === 'string' ? undefined : scriptConfig.integrity;
141162
bootstrapChunks.push(
142163
startModuleSrc,
143-
stringToChunk(escapeTextForBrowser(bootstrapModules[i])),
144-
endAsyncScript,
164+
stringToChunk(escapeTextForBrowser(src)),
145165
);
166+
if (integrity) {
167+
bootstrapChunks.push(
168+
scriptIntegirty,
169+
stringToChunk(escapeTextForBrowser(integrity)),
170+
);
171+
}
172+
bootstrapChunks.push(endAsyncScript);
146173
}
147174
}
148175
return {

0 commit comments

Comments
 (0)