mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
[Float] handle noscript context for Resources (#25559)
stacked on https://github.com/facebook/react/pull/25569 On the client noscript already never renders children so no resources will be extracted from this context. On the server we now track if we are in a noscript context and turn off Resource semantics in this scope
This commit is contained in:
@@ -863,6 +863,10 @@ export function resourcesFromLink(props: Props): boolean {
|
||||
}
|
||||
}
|
||||
if (props.onLoad || props.onError) {
|
||||
// When a link has these props we can't treat it is a Resource but if we rendered it on the
|
||||
// server it would look like a Resource in the rendered html (the onLoad/onError aren't emitted)
|
||||
// Instead we expect the client to insert them rather than hydrate them which also guarantees
|
||||
// that the onLoad and onError won't fire before the event handlers are attached
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,15 +274,18 @@ type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export type FormatContext = {
|
||||
insertionMode: InsertionMode, // root/svg/html/mathml/table
|
||||
selectedValue: null | string | Array<string>, // the selected value(s) inside a <select>, or null outside <select>
|
||||
noscriptTagInScope: boolean,
|
||||
};
|
||||
|
||||
function createFormatContext(
|
||||
insertionMode: InsertionMode,
|
||||
selectedValue: null | string,
|
||||
noscriptTagInScope: boolean,
|
||||
): FormatContext {
|
||||
return {
|
||||
insertionMode,
|
||||
selectedValue,
|
||||
noscriptTagInScope,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,7 +296,7 @@ export function createRootFormatContext(namespaceURI?: string): FormatContext {
|
||||
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
|
||||
? MATHML_MODE
|
||||
: ROOT_HTML_MODE;
|
||||
return createFormatContext(insertionMode, null);
|
||||
return createFormatContext(insertionMode, null, false);
|
||||
}
|
||||
|
||||
export function getChildFormatContext(
|
||||
@@ -302,38 +305,77 @@ export function getChildFormatContext(
|
||||
props: Object,
|
||||
): FormatContext {
|
||||
switch (type) {
|
||||
case 'noscript':
|
||||
return createFormatContext(HTML_MODE, null, true);
|
||||
case 'select':
|
||||
return createFormatContext(
|
||||
HTML_MODE,
|
||||
props.value != null ? props.value : props.defaultValue,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'svg':
|
||||
return createFormatContext(SVG_MODE, null);
|
||||
return createFormatContext(
|
||||
SVG_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'math':
|
||||
return createFormatContext(MATHML_MODE, null);
|
||||
return createFormatContext(
|
||||
MATHML_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'foreignObject':
|
||||
return createFormatContext(HTML_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
// Table parents are special in that their children can only be created at all if they're
|
||||
// wrapped in a table parent. So we need to encode that we're entering this mode.
|
||||
case 'table':
|
||||
return createFormatContext(HTML_TABLE_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_TABLE_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'thead':
|
||||
case 'tbody':
|
||||
case 'tfoot':
|
||||
return createFormatContext(HTML_TABLE_BODY_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_TABLE_BODY_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'colgroup':
|
||||
return createFormatContext(HTML_COLGROUP_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_COLGROUP_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
case 'tr':
|
||||
return createFormatContext(HTML_TABLE_ROW_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_TABLE_ROW_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
}
|
||||
if (parentContext.insertionMode >= HTML_TABLE_MODE) {
|
||||
// Whatever tag this was, it wasn't a table parent or other special parent, so we must have
|
||||
// entered plain HTML again.
|
||||
return createFormatContext(HTML_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
}
|
||||
if (parentContext.insertionMode === ROOT_HTML_MODE) {
|
||||
// We've emitted the root and is now in plain HTML mode.
|
||||
return createFormatContext(HTML_MODE, null);
|
||||
return createFormatContext(
|
||||
HTML_MODE,
|
||||
null,
|
||||
parentContext.noscriptTagInScope,
|
||||
);
|
||||
}
|
||||
return parentContext;
|
||||
}
|
||||
@@ -1155,8 +1197,13 @@ function pushBase(
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
textEmbedded: boolean,
|
||||
noscriptTagInScope: boolean,
|
||||
): ReactNodeList {
|
||||
if (enableFloat && resourcesFromElement('base', props)) {
|
||||
if (
|
||||
enableFloat &&
|
||||
!noscriptTagInScope &&
|
||||
resourcesFromElement('base', props)
|
||||
) {
|
||||
if (textEmbedded) {
|
||||
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
|
||||
// to be safe and assume text will follow by inserting a textSeparator
|
||||
@@ -1175,8 +1222,13 @@ function pushMeta(
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
textEmbedded: boolean,
|
||||
noscriptTagInScope: boolean,
|
||||
): ReactNodeList {
|
||||
if (enableFloat && resourcesFromElement('meta', props)) {
|
||||
if (
|
||||
enableFloat &&
|
||||
!noscriptTagInScope &&
|
||||
resourcesFromElement('meta', props)
|
||||
) {
|
||||
if (textEmbedded) {
|
||||
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
|
||||
// to be safe and assume text will follow by inserting a textSeparator
|
||||
@@ -1195,8 +1247,9 @@ function pushLink(
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
textEmbedded: boolean,
|
||||
noscriptTagInScope: boolean,
|
||||
): ReactNodeList {
|
||||
if (enableFloat && resourcesFromLink(props)) {
|
||||
if (enableFloat && !noscriptTagInScope && resourcesFromLink(props)) {
|
||||
if (textEmbedded) {
|
||||
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
|
||||
// to be safe and assume text will follow by inserting a textSeparator
|
||||
@@ -1318,6 +1371,7 @@ function pushTitle(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
noscriptTagInScope: boolean,
|
||||
): ReactNodeList {
|
||||
if (__DEV__) {
|
||||
const children = props.children;
|
||||
@@ -1359,7 +1413,11 @@ function pushTitle(
|
||||
}
|
||||
}
|
||||
|
||||
if (enableFloat && resourcesFromElement('title', props)) {
|
||||
if (
|
||||
enableFloat &&
|
||||
!noscriptTagInScope &&
|
||||
resourcesFromElement('title', props)
|
||||
) {
|
||||
// We have converted this link exclusively to a resource and no longer
|
||||
// need to emit it
|
||||
return null;
|
||||
@@ -1520,8 +1578,9 @@ function pushScript(
|
||||
props: Object,
|
||||
responseState: ResponseState,
|
||||
textEmbedded: boolean,
|
||||
noscriptTagInScope: boolean,
|
||||
): null {
|
||||
if (enableFloat && resourcesFromScript(props)) {
|
||||
if (enableFloat && !noscriptTagInScope && resourcesFromScript(props)) {
|
||||
if (textEmbedded) {
|
||||
// This link follows text but we aren't writing a tag. while not as efficient as possible we need
|
||||
// to be safe and assume text will follow by inserting a textSeparator
|
||||
@@ -1863,18 +1922,47 @@ export function pushStartInstance(
|
||||
return pushStartMenuItem(target, props, responseState);
|
||||
case 'title':
|
||||
return enableFloat
|
||||
? pushTitle(target, props, responseState)
|
||||
? pushTitle(
|
||||
target,
|
||||
props,
|
||||
responseState,
|
||||
formatContext.noscriptTagInScope,
|
||||
)
|
||||
: pushStartTitle(target, props, responseState);
|
||||
case 'link':
|
||||
return pushLink(target, props, responseState, textEmbedded);
|
||||
return pushLink(
|
||||
target,
|
||||
props,
|
||||
responseState,
|
||||
textEmbedded,
|
||||
formatContext.noscriptTagInScope,
|
||||
);
|
||||
case 'script':
|
||||
return enableFloat
|
||||
? pushScript(target, props, responseState, textEmbedded)
|
||||
? pushScript(
|
||||
target,
|
||||
props,
|
||||
responseState,
|
||||
textEmbedded,
|
||||
formatContext.noscriptTagInScope,
|
||||
)
|
||||
: pushStartGenericElement(target, props, type, responseState);
|
||||
case 'meta':
|
||||
return pushMeta(target, props, responseState, textEmbedded);
|
||||
return pushMeta(
|
||||
target,
|
||||
props,
|
||||
responseState,
|
||||
textEmbedded,
|
||||
formatContext.noscriptTagInScope,
|
||||
);
|
||||
case 'base':
|
||||
return pushBase(target, props, responseState, textEmbedded);
|
||||
return pushBase(
|
||||
target,
|
||||
props,
|
||||
responseState,
|
||||
textEmbedded,
|
||||
formatContext.noscriptTagInScope,
|
||||
);
|
||||
// Newline eating tags
|
||||
case 'listing':
|
||||
case 'pre': {
|
||||
|
||||
@@ -72,6 +72,7 @@ export function createRootFormatContext(): FormatContext {
|
||||
return {
|
||||
insertionMode: HTML_MODE, // We skip the root mode because we don't want to emit the DOCTYPE in legacy mode.
|
||||
selectedValue: null,
|
||||
noscriptTagInScope: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -290,6 +290,9 @@ describe('ReactDOMFloat', () => {
|
||||
<meta property="foo" content="bar" />
|
||||
<link rel="foo" href="bar" onLoad={() => {}} />
|
||||
<title>foo</title>
|
||||
<noscript>
|
||||
<link rel="icon" href="icon" />
|
||||
</noscript>
|
||||
<base target="foo" href="bar" />
|
||||
<script async={true} src="foo" onLoad={() => {}} />
|
||||
</head>
|
||||
@@ -305,6 +308,7 @@ describe('ReactDOMFloat', () => {
|
||||
<link rel="preload" href="foo" as="script" />
|
||||
<meta property="foo" content="bar" />
|
||||
<title>foo</title>
|
||||
<noscript><link rel="icon" href="icon"/></noscript>
|
||||
</head>
|
||||
<body>foo</body>
|
||||
</html>,
|
||||
@@ -317,6 +321,9 @@ describe('ReactDOMFloat', () => {
|
||||
<meta property="foo" content="bar" />
|
||||
<link rel="foo" href="bar" onLoad={() => {}} />
|
||||
<title>foo</title>
|
||||
<noscript>
|
||||
<link rel="icon" href="icon" />
|
||||
</noscript>
|
||||
<base target="foo" href="bar" />
|
||||
<script async={true} src="foo" onLoad={() => {}} />
|
||||
</head>
|
||||
@@ -332,6 +339,7 @@ describe('ReactDOMFloat', () => {
|
||||
<meta property="foo" content="bar" />
|
||||
<title>foo</title>
|
||||
<link rel="foo" href="bar" />
|
||||
<noscript><link rel="icon" href="icon"/></noscript>
|
||||
<script async="" src="foo" />
|
||||
</head>
|
||||
<body>foo</body>
|
||||
@@ -5376,4 +5384,170 @@ describe('ReactDOMFloat', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('noscript', () => {
|
||||
// @gate enableFloat
|
||||
it('should not turn children of noscript into resources', async () => {
|
||||
function SomeResources() {
|
||||
return (
|
||||
<>
|
||||
<link rel="stylesheet" href="foo" precedence="foo" />
|
||||
<title>foo</title>
|
||||
<link rel="foobar" href="foobar" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta property="og:image" content="foo" />
|
||||
<script async={true} src="script" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
function Indirection({level, children}) {
|
||||
if (level > 0) {
|
||||
return <Indirection level={level - 1}>{children}</Indirection>;
|
||||
} else {
|
||||
return children;
|
||||
}
|
||||
}
|
||||
function App() {
|
||||
return (
|
||||
<html>
|
||||
<head>
|
||||
<SomeResources />
|
||||
<noscript>
|
||||
<SomeResources />
|
||||
<Indirection level={3}>
|
||||
<SomeResources />
|
||||
</Indirection>
|
||||
</noscript>
|
||||
<SomeResources />
|
||||
</head>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
{/* the actual resources */}
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
<script async="" src="script" />
|
||||
<title>foo</title>
|
||||
<link rel="foobar" href="foobar" />
|
||||
<meta property="og:image" content="foo" />
|
||||
{/* the noscript children are encoded as a textNode when scripting is enabled */}
|
||||
<noscript>
|
||||
<link rel="stylesheet"
|
||||
href="foo"/><title>foo</title><link rel="foobar"
|
||||
href="foobar"/><meta charSet="utf-8"/><meta
|
||||
property="og:image" content="foo"/><script async=""
|
||||
src="script"></script><link rel="stylesheet"
|
||||
href="foo"/><title>foo</title><link rel="foobar"
|
||||
href="foobar"/><meta charSet="utf-8"/><meta
|
||||
property="og:image" content="foo"/><script async=""
|
||||
src="script"></script>
|
||||
</noscript>
|
||||
</head>
|
||||
<body />
|
||||
</html>,
|
||||
);
|
||||
|
||||
const root = ReactDOMClient.hydrateRoot(document, <App />);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
{/* the actual resources */}
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
<script async="" src="script" />
|
||||
<title>foo</title>
|
||||
<link rel="foobar" href="foobar" />
|
||||
<meta property="og:image" content="foo" />
|
||||
{/* the noscript children are encoded as a textNode when scripting is enabled */}
|
||||
<noscript>
|
||||
<link rel="stylesheet"
|
||||
href="foo"/><title>foo</title><link rel="foobar"
|
||||
href="foobar"/><meta charSet="utf-8"/><meta
|
||||
property="og:image" content="foo"/><script async=""
|
||||
src="script"></script><link rel="stylesheet"
|
||||
href="foo"/><title>foo</title><link rel="foobar"
|
||||
href="foobar"/><meta charSet="utf-8"/><meta
|
||||
property="og:image" content="foo"/><script async=""
|
||||
src="script"></script>
|
||||
</noscript>
|
||||
</head>
|
||||
<body />
|
||||
</html>,
|
||||
);
|
||||
|
||||
root.render(null);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
// stylesheets and scripts currently don't unmount ever
|
||||
// noscript is never hydrated so it also does not get cleared
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="foo" data-precedence="foo" />
|
||||
<script async="" src="script" />
|
||||
</head>
|
||||
<body />
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
|
||||
it('noscript runs on the server but does not emit resources and does not run on the client', async () => {
|
||||
function App() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<div>
|
||||
foo
|
||||
<noscript>
|
||||
<Foo />
|
||||
</noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
function Foo() {
|
||||
Scheduler.unstable_yieldValue('Foo');
|
||||
|
||||
return <title>noscript title</title>;
|
||||
}
|
||||
|
||||
await actIntoEmptyDocument(() => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
|
||||
pipe(writable);
|
||||
});
|
||||
expect(getMeaningfulChildren(container)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div>
|
||||
foo<noscript><title>noscript title</title></noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['Foo']);
|
||||
|
||||
ReactDOMClient.hydrateRoot(document, <App />);
|
||||
expect(Scheduler).toFlushWithoutYielding();
|
||||
expect(getMeaningfulChildren(document)).toEqual(
|
||||
<html>
|
||||
<head />
|
||||
<body>
|
||||
<div>
|
||||
foo<noscript><title>noscript title</title></noscript>
|
||||
</div>
|
||||
</body>
|
||||
</html>,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user