Skip to content

Commit 3677c01

Browse files
authored
Support nonce option to be passed to inline scripts (#22593)
1 parent 34e4c97 commit 3677c01

File tree

5 files changed

+70
-12
lines changed

5 files changed

+70
-12
lines changed

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ let PropTypes;
2323
let textCache;
2424
let document;
2525
let writable;
26+
let CSPnonce = null;
2627
let container;
2728
let buffer = '';
2829
let hasErrored = false;
@@ -91,7 +92,10 @@ describe('ReactDOMFizzServer', () => {
9192
fakeBody.innerHTML = bufferedContent;
9293
while (fakeBody.firstChild) {
9394
const node = fakeBody.firstChild;
94-
if (node.nodeName === 'SCRIPT') {
95+
if (
96+
node.nodeName === 'SCRIPT' &&
97+
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
98+
) {
9599
const script = document.createElement('script');
96100
script.textContent = node.textContent;
97101
fakeBody.removeChild(node);
@@ -281,6 +285,38 @@ describe('ReactDOMFizzServer', () => {
281285
);
282286
});
283287

288+
// @gate experimental
289+
it('should support nonce scripts', async () => {
290+
CSPnonce = 'R4nd0m';
291+
try {
292+
let resolve;
293+
const Lazy = React.lazy(() => {
294+
return new Promise(r => {
295+
resolve = r;
296+
});
297+
});
298+
299+
await act(async () => {
300+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
301+
<div>
302+
<Suspense fallback={<Text text="Loading..." />}>
303+
<Lazy text="Hello" />
304+
</Suspense>
305+
</div>,
306+
{nonce: 'R4nd0m'},
307+
);
308+
pipe(writable);
309+
});
310+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
311+
await act(async () => {
312+
resolve({default: Text});
313+
});
314+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
315+
} finally {
316+
CSPnonce = null;
317+
}
318+
});
319+
284320
// @gate experimental
285321
it('should client render a boundary if a lazy component rejects', async () => {
286322
let rejectComponent;

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
type Options = {|
2727
identifierPrefix?: string,
2828
namespaceURI?: string,
29+
nonce?: string,
2930
progressiveChunkSize?: number,
3031
signal?: AbortSignal,
3132
onCompleteShell?: () => void,
@@ -39,7 +40,10 @@ function renderToReadableStream(
3940
): ReadableStream {
4041
const request = createRequest(
4142
children,
42-
createResponseState(options ? options.identifierPrefix : undefined),
43+
createResponseState(
44+
options ? options.identifierPrefix : undefined,
45+
options ? options.nonce : undefined,
46+
),
4347
createRootFormatContext(options ? options.namespaceURI : undefined),
4448
options ? options.progressiveChunkSize : undefined,
4549
options ? options.onError : undefined,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function createDrainHandler(destination, request) {
3131
type Options = {|
3232
identifierPrefix?: string,
3333
namespaceURI?: string,
34+
nonce?: string,
3435
progressiveChunkSize?: number,
3536
onCompleteShell?: () => void,
3637
onCompleteAll?: () => void,
@@ -47,7 +48,10 @@ type Controls = {|
4748
function createRequestImpl(children: ReactNodeList, options: void | Options) {
4849
return createRequest(
4950
children,
50-
createResponseState(options ? options.identifierPrefix : undefined),
51+
createResponseState(
52+
options ? options.identifierPrefix : undefined,
53+
options ? options.nonce : undefined,
54+
),
5155
createRootFormatContext(options ? options.namespaceURI : undefined),
5256
options ? options.progressiveChunkSize : undefined,
5357
options ? options.onError : undefined,

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export const isPrimaryRenderer = true;
5959

6060
// Per response, global state that is not contextual to the rendering subtree.
6161
export type ResponseState = {
62+
startInlineScript: PrecomputedChunk,
6263
placeholderPrefix: PrecomputedChunk,
6364
segmentPrefix: PrecomputedChunk,
6465
boundaryPrefix: string,
@@ -71,12 +72,22 @@ export type ResponseState = {
7172
...
7273
};
7374

75+
const startInlineScript = stringToPrecomputedChunk('<script>');
76+
7477
// Allows us to keep track of what we've already written so we can refer back to it.
7578
export function createResponseState(
7679
identifierPrefix: string | void,
80+
nonce: string | void,
7781
): ResponseState {
7882
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
83+
const inlineScriptWithNonce =
84+
nonce === undefined
85+
? startInlineScript
86+
: stringToPrecomputedChunk(
87+
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
88+
);
7989
return {
90+
startInlineScript: inlineScriptWithNonce,
8091
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
8192
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
8293
boundaryPrefix: idPrefix + 'B:',
@@ -1689,9 +1700,9 @@ const clientRenderFunction =
16891700
'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';
16901701

16911702
const completeSegmentScript1Full = stringToPrecomputedChunk(
1692-
'<script>' + completeSegmentFunction + ';$RS("',
1703+
completeSegmentFunction + ';$RS("',
16931704
);
1694-
const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
1705+
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
16951706
const completeSegmentScript2 = stringToPrecomputedChunk('","');
16961707
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');
16971708

@@ -1700,6 +1711,7 @@ export function writeCompletedSegmentInstruction(
17001711
responseState: ResponseState,
17011712
contentSegmentID: number,
17021713
): boolean {
1714+
writeChunk(destination, responseState.startInlineScript);
17031715
if (!responseState.sentCompleteSegmentFunction) {
17041716
// The first time we write this, we'll need to include the full implementation.
17051717
responseState.sentCompleteSegmentFunction = true;
@@ -1718,11 +1730,9 @@ export function writeCompletedSegmentInstruction(
17181730
}
17191731

17201732
const completeBoundaryScript1Full = stringToPrecomputedChunk(
1721-
'<script>' + completeBoundaryFunction + ';$RC("',
1722-
);
1723-
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
1724-
'<script>$RC("',
1733+
completeBoundaryFunction + ';$RC("',
17251734
);
1735+
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
17261736
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
17271737
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');
17281738

@@ -1732,6 +1742,7 @@ export function writeCompletedBoundaryInstruction(
17321742
boundaryID: SuspenseBoundaryID,
17331743
contentSegmentID: number,
17341744
): boolean {
1745+
writeChunk(destination, responseState.startInlineScript);
17351746
if (!responseState.sentCompleteBoundaryFunction) {
17361747
// The first time we write this, we'll need to include the full implementation.
17371748
responseState.sentCompleteBoundaryFunction = true;
@@ -1756,16 +1767,17 @@ export function writeCompletedBoundaryInstruction(
17561767
}
17571768

17581769
const clientRenderScript1Full = stringToPrecomputedChunk(
1759-
'<script>' + clientRenderFunction + ';$RX("',
1770+
clientRenderFunction + ';$RX("',
17601771
);
1761-
const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
1772+
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
17621773
const clientRenderScript2 = stringToPrecomputedChunk('")</script>');
17631774

17641775
export function writeClientRenderBoundaryInstruction(
17651776
destination: Destination,
17661777
responseState: ResponseState,
17671778
boundaryID: SuspenseBoundaryID,
17681779
): boolean {
1780+
writeChunk(destination, responseState.startInlineScript);
17691781
if (!responseState.sentClientRenderFunction) {
17701782
// The first time we write this, we'll need to include the full implementation.
17711783
responseState.sentClientRenderFunction = true;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const isPrimaryRenderer = false;
2929

3030
export type ResponseState = {
3131
// Keep this in sync with ReactDOMServerFormatConfig
32+
startInlineScript: PrecomputedChunk,
3233
placeholderPrefix: PrecomputedChunk,
3334
segmentPrefix: PrecomputedChunk,
3435
boundaryPrefix: string,
@@ -46,9 +47,10 @@ export function createResponseState(
4647
generateStaticMarkup: boolean,
4748
identifierPrefix: string | void,
4849
): ResponseState {
49-
const responseState = createResponseStateImpl(identifierPrefix);
50+
const responseState = createResponseStateImpl(identifierPrefix, undefined);
5051
return {
5152
// Keep this in sync with ReactDOMServerFormatConfig
53+
startInlineScript: responseState.startInlineScript,
5254
placeholderPrefix: responseState.placeholderPrefix,
5355
segmentPrefix: responseState.segmentPrefix,
5456
boundaryPrefix: responseState.boundaryPrefix,

0 commit comments

Comments
 (0)