Skip to content

Commit f4284b9

Browse files
committed
Allow nonce to be used on hoistable styles
1 parent 9b042f9 commit f4284b9

File tree

3 files changed

+156
-8
lines changed

3 files changed

+156
-8
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2712,6 +2712,7 @@ function pushStyle(
27122712
}
27132713
const precedence = props.precedence;
27142714
const href = props.href;
2715+
const nonce = props.nonce;
27152716

27162717
if (
27172718
insertionMode === SVG_MODE ||
@@ -2759,9 +2760,19 @@ function pushStyle(
27592760
rules: ([]: Array<Chunk | PrecomputedChunk>),
27602761
hrefs: [stringToChunk(escapeTextForBrowser(href))],
27612762
sheets: (new Map(): Map<string, StylesheetResource>),
2763+
nonce,
27622764
};
27632765
renderState.styles.set(precedence, styleQueue);
27642766
} else {
2767+
if (__DEV__) {
2768+
if (nonce !== styleQueue.nonce) {
2769+
console.error(
2770+
'React encountered a hoistable style tag with "%s" nonce. It doesn\'t match the previously encountered nonce "%s". They have to be the same',
2771+
nonce,
2772+
styleQueue.nonce,
2773+
);
2774+
}
2775+
}
27652776
// We have seen this precedence before and need to track this href
27662777
styleQueue.hrefs.push(stringToChunk(escapeTextForBrowser(href)));
27672778
}
@@ -4684,8 +4695,9 @@ function escapeJSObjectForInstructionScripts(input: Object): string {
46844695
const lateStyleTagResourceOpen1 = stringToPrecomputedChunk(
46854696
'<style media="not all" data-precedence="',
46864697
);
4687-
const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
4688-
const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('">');
4698+
const lateStyleTagResourceOpen2 = stringToPrecomputedChunk('" nonce="');
4699+
const lateStyleTagResourceOpen3 = stringToPrecomputedChunk('" data-href="');
4700+
const lateStyleTagResourceOpen4 = stringToPrecomputedChunk('">');
46894701
const lateStyleTagTemplateClose = stringToPrecomputedChunk('</style>');
46904702

46914703
// Tracks whether the boundary currently flushing is flushign style tags or has any
@@ -4701,6 +4713,7 @@ function flushStyleTagsLateForBoundary(
47014713
) {
47024714
const rules = styleQueue.rules;
47034715
const hrefs = styleQueue.hrefs;
4716+
const nonce = styleQueue.nonce;
47044717
if (__DEV__) {
47054718
if (rules.length > 0 && hrefs.length === 0) {
47064719
console.error(
@@ -4712,13 +4725,17 @@ function flushStyleTagsLateForBoundary(
47124725
if (hrefs.length) {
47134726
writeChunk(this, lateStyleTagResourceOpen1);
47144727
writeChunk(this, styleQueue.precedence);
4715-
writeChunk(this, lateStyleTagResourceOpen2);
4728+
if (nonce) {
4729+
writeChunk(this, lateStyleTagResourceOpen2);
4730+
writeChunk(this, nonce);
4731+
}
4732+
writeChunk(this, lateStyleTagResourceOpen3);
47164733
for (; i < hrefs.length - 1; i++) {
47174734
writeChunk(this, hrefs[i]);
47184735
writeChunk(this, spaceSeparator);
47194736
}
47204737
writeChunk(this, hrefs[i]);
4721-
writeChunk(this, lateStyleTagResourceOpen3);
4738+
writeChunk(this, lateStyleTagResourceOpen4);
47224739
for (i = 0; i < rules.length; i++) {
47234740
writeChunk(this, rules[i]);
47244741
}
@@ -4805,9 +4822,10 @@ function flushStyleInPreamble(
48054822
const styleTagResourceOpen1 = stringToPrecomputedChunk(
48064823
'<style data-precedence="',
48074824
);
4808-
const styleTagResourceOpen2 = stringToPrecomputedChunk('" data-href="');
4825+
const styleTagResourceOpen2 = stringToPrecomputedChunk('" nonce="');
4826+
const styleTagResourceOpen3 = stringToPrecomputedChunk('" data-href="');
48094827
const spaceSeparator = stringToPrecomputedChunk(' ');
4810-
const styleTagResourceOpen3 = stringToPrecomputedChunk('">');
4828+
const styleTagResourceOpen4 = stringToPrecomputedChunk('">');
48114829

48124830
const styleTagResourceClose = stringToPrecomputedChunk('</style>');
48134831

@@ -4822,22 +4840,27 @@ function flushStylesInPreamble(
48224840

48234841
const rules = styleQueue.rules;
48244842
const hrefs = styleQueue.hrefs;
4843+
const nonce = styleQueue.nonce;
48254844
// If we don't emit any stylesheets at this precedence we still need to maintain the precedence
48264845
// order so even if there are no rules for style tags at this precedence we emit an empty style
48274846
// tag with the data-precedence attribute
48284847
if (!hasStylesheets || hrefs.length) {
48294848
writeChunk(this, styleTagResourceOpen1);
48304849
writeChunk(this, styleQueue.precedence);
4850+
if (nonce) {
4851+
writeChunk(this, styleTagResourceOpen2);
4852+
writeChunk(this, nonce);
4853+
}
48314854
let i = 0;
48324855
if (hrefs.length) {
4833-
writeChunk(this, styleTagResourceOpen2);
4856+
writeChunk(this, styleTagResourceOpen3);
48344857
for (; i < hrefs.length - 1; i++) {
48354858
writeChunk(this, hrefs[i]);
48364859
writeChunk(this, spaceSeparator);
48374860
}
48384861
writeChunk(this, hrefs[i]);
48394862
}
4840-
writeChunk(this, styleTagResourceOpen3);
4863+
writeChunk(this, styleTagResourceOpen4);
48414864
for (i = 0; i < rules.length; i++) {
48424865
writeChunk(this, rules[i]);
48434866
}
@@ -5534,6 +5557,7 @@ export type StyleQueue = {
55345557
rules: Array<Chunk | PrecomputedChunk>,
55355558
hrefs: Array<Chunk | PrecomputedChunk>,
55365559
sheets: Map<string, StylesheetResource>,
5560+
nonce: ?string,
55375561
};
55385562

55395563
export function createHoistableState(): HoistableState {

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10331,4 +10331,49 @@ describe('ReactDOMFizzServer', () => {
1033110331
</html>,
1033210332
);
1033310333
});
10334+
10335+
it('can render styles with nonce', async () => {
10336+
CSPnonce = 'R4nd0m';
10337+
await act(() => {
10338+
const {pipe} = renderToPipeableStream(
10339+
<>
10340+
<style
10341+
href="foo"
10342+
precedence="default"
10343+
nonce={CSPnonce}>{`.foo { color: hotpink; }`}</style>
10344+
<style
10345+
href="bar"
10346+
precedence="default"
10347+
nonce={CSPnonce}>{`.bar { background-color: blue; }`}</style>
10348+
</>,
10349+
);
10350+
pipe(writable);
10351+
});
10352+
expect(document.querySelector('style').getAttribute('nonce')).toBe(
10353+
CSPnonce,
10354+
);
10355+
});
10356+
10357+
// @gate __DEV__
10358+
it('warns when it encounters a mismatched nonce on a style', async () => {
10359+
CSPnonce = 'R4nd0m';
10360+
await act(() => {
10361+
const {pipe} = renderToPipeableStream(
10362+
<>
10363+
<style
10364+
href="foo"
10365+
precedence="default"
10366+
nonce={CSPnonce}>{`.foo { color: hotpink; }`}</style>
10367+
<style
10368+
href="bar"
10369+
precedence="default"
10370+
nonce={`${CSPnonce}${CSPnonce}`}>{`.bar { background-color: blue; }`}</style>
10371+
</>,
10372+
);
10373+
pipe(writable);
10374+
});
10375+
assertConsoleErrorDev([
10376+
'React encountered a hoistable style tag with "R4nd0mR4nd0m" nonce. It doesn\'t match the previously encountered nonce "R4nd0m". They have to be the same',
10377+
]);
10378+
});
1033410379
});

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8435,6 +8435,85 @@ background-color: green;
84358435
: '\n in body (at **)' + '\n in html (at **)'),
84368436
]);
84378437
});
8438+
8439+
it('can emit styles with nonce', async () => {
8440+
const nonce = 'R4nD0m';
8441+
const fooCss = '.foo { color: hotpink; }';
8442+
const barCss = '.bar { background-color: blue; }';
8443+
const bazCss = '.baz { border: 1px solid black; }';
8444+
await act(() => {
8445+
renderToPipeableStream(
8446+
<html>
8447+
<body>
8448+
<Suspense>
8449+
<BlockedOn value="first">
8450+
<div>first</div>
8451+
<style href="foo" precedence="default" nonce={nonce}>
8452+
{fooCss}
8453+
</style>
8454+
<style href="bar" precedence="default" nonce={nonce}>
8455+
{barCss}
8456+
</style>
8457+
<BlockedOn value="second">
8458+
<div>second</div>
8459+
<style href="baz" precedence="default" nonce={nonce}>
8460+
{bazCss}
8461+
</style>
8462+
</BlockedOn>
8463+
</BlockedOn>
8464+
</Suspense>
8465+
</body>
8466+
</html>,
8467+
).pipe(writable);
8468+
});
8469+
8470+
expect(getMeaningfulChildren(document)).toEqual(
8471+
<html>
8472+
<head />
8473+
<body />
8474+
</html>,
8475+
);
8476+
8477+
await act(() => {
8478+
resolveText('first');
8479+
});
8480+
8481+
expect(getMeaningfulChildren(document)).toEqual(
8482+
<html>
8483+
<head />
8484+
<body>
8485+
<style
8486+
data-href="foo bar"
8487+
data-precedence="default"
8488+
media="not all"
8489+
nonce={nonce}>
8490+
{`${fooCss}${barCss}`}
8491+
</style>
8492+
</body>
8493+
</html>,
8494+
);
8495+
8496+
await act(() => {
8497+
resolveText('second');
8498+
});
8499+
8500+
expect(getMeaningfulChildren(document)).toEqual(
8501+
<html>
8502+
<head>
8503+
<style data-href="foo bar" data-precedence="default" nonce={nonce}>
8504+
{`${fooCss}${barCss}`}
8505+
</style>
8506+
<style data-href="baz" data-precedence="default" nonce={nonce}>
8507+
{bazCss}
8508+
</style>
8509+
</head>
8510+
<body>
8511+
<div>first</div>
8512+
<div>second</div>
8513+
</body>
8514+
</html>,
8515+
);
8516+
});
84388517
});
84398518

84408519
describe('Script Resources', () => {

0 commit comments

Comments
 (0)