Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ let PropTypes;
let textCache;
let document;
let writable;
let CSPnonce = null;
let container;
let buffer = '';
let hasErrored = false;
Expand Down Expand Up @@ -91,7 +92,10 @@ describe('ReactDOMFizzServer', () => {
fakeBody.innerHTML = bufferedContent;
while (fakeBody.firstChild) {
const node = fakeBody.firstChild;
if (node.nodeName === 'SCRIPT') {
if (
node.nodeName === 'SCRIPT' &&
(CSPnonce === null || node.getAttribute('nonce') === CSPnonce)
) {
const script = document.createElement('script');
script.textContent = node.textContent;
fakeBody.removeChild(node);
Expand Down Expand Up @@ -281,6 +285,38 @@ describe('ReactDOMFizzServer', () => {
);
});

// @gate experimental
it('should support nonce scripts', async () => {
CSPnonce = 'R4nd0m';
try {
let resolve;
const Lazy = React.lazy(() => {
return new Promise(r => {
resolve = r;
});
});

await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<div>
<Suspense fallback={<Text text="Loading..." />}>
<Lazy text="Hello" />
</Suspense>
</div>,
{nonce: 'R4nd0m'},
);
pipe(writable);
});
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
await act(async () => {
resolve({default: Text});
});
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
} finally {
CSPnonce = null;
}
});

// @gate experimental
it('should client render a boundary if a lazy component rejects', async () => {
let rejectComponent;
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
progressiveChunkSize?: number,
signal?: AbortSignal,
onCompleteShell?: () => void,
Expand All @@ -39,7 +40,10 @@ function renderToReadableStream(
): ReadableStream {
const request = createRequest(
children,
createResponseState(options ? options.identifierPrefix : undefined),
createResponseState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
Expand Down
6 changes: 5 additions & 1 deletion packages/react-dom/src/server/ReactDOMFizzServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function createDrainHandler(destination, request) {
type Options = {|
identifierPrefix?: string,
namespaceURI?: string,
nonce?: string,
progressiveChunkSize?: number,
onCompleteShell?: () => void,
onCompleteAll?: () => void,
Expand All @@ -47,7 +48,10 @@ type Controls = {|
function createRequestImpl(children: ReactNodeList, options: void | Options) {
return createRequest(
children,
createResponseState(options ? options.identifierPrefix : undefined),
createResponseState(
options ? options.identifierPrefix : undefined,
options ? options.nonce : undefined,
),
createRootFormatContext(options ? options.namespaceURI : undefined),
options ? options.progressiveChunkSize : undefined,
options ? options.onError : undefined,
Expand Down
28 changes: 20 additions & 8 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const isPrimaryRenderer = true;

// Per response, global state that is not contextual to the rendering subtree.
export type ResponseState = {
startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand All @@ -71,12 +72,22 @@ export type ResponseState = {
...
};

const startInlineScript = stringToPrecomputedChunk('<script>');

// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(
identifierPrefix: string | void,
nonce: string | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
nonce === undefined
? startInlineScript
: stringToPrecomputedChunk(
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
);
return {
startInlineScript: inlineScriptWithNonce,
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
boundaryPrefix: idPrefix + 'B:',
Expand Down Expand Up @@ -1689,9 +1700,9 @@ const clientRenderFunction =
'function $RX(a){if(a=document.getElementById(a))a=a.previousSibling,a.data="$!",a._reactRetry&&a._reactRetry()}';

const completeSegmentScript1Full = stringToPrecomputedChunk(
'<script>' + completeSegmentFunction + ';$RS("',
completeSegmentFunction + ';$RS("',
);
const completeSegmentScript1Partial = stringToPrecomputedChunk('<script>$RS("');
const completeSegmentScript1Partial = stringToPrecomputedChunk('$RS("');
const completeSegmentScript2 = stringToPrecomputedChunk('","');
const completeSegmentScript3 = stringToPrecomputedChunk('")</script>');

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

const completeBoundaryScript1Full = stringToPrecomputedChunk(
'<script>' + completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk(
'<script>$RC("',
completeBoundaryFunction + ';$RC("',
);
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
const completeBoundaryScript2 = stringToPrecomputedChunk('","');
const completeBoundaryScript3 = stringToPrecomputedChunk('")</script>');

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

const clientRenderScript1Full = stringToPrecomputedChunk(
'<script>' + clientRenderFunction + ';$RX("',
clientRenderFunction + ';$RX("',
);
const clientRenderScript1Partial = stringToPrecomputedChunk('<script>$RX("');
const clientRenderScript1Partial = stringToPrecomputedChunk('$RX("');
const clientRenderScript2 = stringToPrecomputedChunk('")</script>');

export function writeClientRenderBoundaryInstruction(
destination: Destination,
responseState: ResponseState,
boundaryID: SuspenseBoundaryID,
): boolean {
writeChunk(destination, responseState.startInlineScript);
if (!responseState.sentClientRenderFunction) {
// The first time we write this, we'll need to include the full implementation.
responseState.sentClientRenderFunction = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const isPrimaryRenderer = false;

export type ResponseState = {
// Keep this in sync with ReactDOMServerFormatConfig
startInlineScript: PrecomputedChunk,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand All @@ -46,9 +47,10 @@ export function createResponseState(
generateStaticMarkup: boolean,
identifierPrefix: string | void,
): ResponseState {
const responseState = createResponseStateImpl(identifierPrefix);
const responseState = createResponseStateImpl(identifierPrefix, undefined);
return {
// Keep this in sync with ReactDOMServerFormatConfig
startInlineScript: responseState.startInlineScript,
placeholderPrefix: responseState.placeholderPrefix,
segmentPrefix: responseState.segmentPrefix,
boundaryPrefix: responseState.boundaryPrefix,
Expand Down