Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz/Float] Float for stylesheet resources #25243

Merged
merged 35 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
09c091d
[Fizz/Float] Float for stylesheet resources
gnoff Jun 10, 2022
589485d
determine preamble while flushing and make postamble resilient to asy…
gnoff Sep 28, 2022
b562d26
refactor render prep
gnoff Sep 28, 2022
ca98449
setCurrentlyRenderingBoundaryResources => setCurrentlyRenderingBounda…
gnoff Sep 28, 2022
72bbd2c
fixes
gnoff Sep 28, 2022
52b1388
add react-dom to fizz entry externals
gnoff Sep 28, 2022
d3bffc2
refactor default dispatcher initialization
gnoff Sep 28, 2022
24a8638
set default dispatcher in creatRoot and hydra
gnoff Sep 29, 2022
f06f1ad
clarify render start and stop function names
gnoff Sep 29, 2022
3793c2e
make TODO proper TODO and cleanup comments
gnoff Sep 29, 2022
16be133
integrate push/pop dispatcher with renderer prep / reset
gnoff Sep 29, 2022
0dc98d2
forks
gnoff Sep 29, 2022
9956a77
wip insertstyles
gnoff Sep 29, 2022
eec88a3
wip
gnoff Sep 29, 2022
e2bc267
wip fixes
gnoff Sep 29, 2022
0af21ba
query for existing styles in acquisition instead of construction. rem…
gnoff Sep 29, 2022
8da096c
revert to request based preamble
gnoff Sep 29, 2022
02984d7
cleanup style insertion scripts
gnoff Sep 29, 2022
52d2e66
lint
gnoff Sep 29, 2022
db245b0
remove rootDidFlush
gnoff Sep 29, 2022
4987d19
fix test
gnoff Sep 29, 2022
1dba4c0
remove extraneous preamableopen
gnoff Sep 29, 2022
3704af3
add react-dom external to flight entries
gnoff Sep 29, 2022
4d30694
types
gnoff Sep 30, 2022
33fa6e1
make insert style script slightly smaller
gnoff Sep 30, 2022
a0bf190
add react-dom externals
gnoff Sep 30, 2022
763fadc
fix double call of hasStyleResourceDependencies
gnoff Sep 30, 2022
3f3f9d5
remove special casing of precedence prop
gnoff Sep 30, 2022
f1925a8
escape hrefs in document queries
gnoff Sep 30, 2022
b845eff
use a stack for currentResources
gnoff Sep 30, 2022
7c45420
lints
gnoff Sep 30, 2022
17fde2c
flow
gnoff Sep 30, 2022
b544927
escape newline in attribute selector
gnoff Sep 30, 2022
5ebdce8
remove unecessary export
gnoff Sep 30, 2022
1ccdb00
nits
gnoff Sep 30, 2022
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
Prev Previous commit
Next Next commit
determine preamble while flushing and make postamble resilient to asy…
…nc render order
  • Loading branch information
gnoff committed Sep 30, 2022
commit 589485d0bacf889c970094a89d2bf8d5be9cf6c8
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @flow
*/

import type {ArrayWithPreamble} from 'react-server/src/ReactFizzServer';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {Resources, BoundaryResources} from './ReactDOMFloatServer';
export type {Resources, BoundaryResources};
Expand Down Expand Up @@ -93,6 +94,8 @@ export type ResponseState = {
...
};

export const emptyChunk = stringToPrecomputedChunk('');

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

Expand Down Expand Up @@ -283,25 +286,6 @@ export function getChildFormatContext(
return createFormatContext(HTML_MODE, null);
}
if (parentContext.insertionMode === ROOT_HTML_MODE) {
switch (type) {
case 'html': {
return parentContext;
}
case 'head':
case 'title':
case 'base':
case 'link':
case 'style':
case 'meta':
case 'script':
case 'noscript':
case 'template': {
break;
}
default: {
parentContext.preambleOpen = false;
}
}
// We've emitted the root and is now in plain HTML mode.
return createFormatContext(HTML_MODE, null);
}
Expand Down Expand Up @@ -1316,40 +1300,33 @@ function pushStartTitle(
}

function pushStartHead(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
target: ArrayWithPreamble<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
responseState: ResponseState,
formatContext: FormatContext,
): ReactNodeList {
// Preamble type is nullable for feature off cases but is guaranteed when feature is on
target =
enableFloat &&
formatContext.insertionMode === ROOT_HTML_MODE &&
formatContext.preambleOpen
? preamble
: target;

return pushStartGenericElement(target, props, tag, responseState);
const children = pushStartGenericElement(target, props, tag, responseState);
target._preambleIndex = target.length;
return children;
}

function pushStartHtml(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
target: ArrayWithPreamble<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
responseState: ResponseState,
formatContext: FormatContext,
): ReactNodeList {
if (formatContext.insertionMode === ROOT_HTML_MODE) {
target = enableFloat && formatContext.preambleOpen ? preamble : target;
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
// then we also emit the DOCTYPE as part of the root content as a convenience for
// rendering the whole document.
target.push(DOCTYPE);
}
return pushStartGenericElement(target, props, tag, responseState);
const children = pushStartGenericElement(target, props, tag, responseState);
target._preambleIndex = target.length;
return children;
}

function pushStartGenericElement(
Expand Down Expand Up @@ -1567,8 +1544,7 @@ function startChunkForTag(tag: string): PrecomputedChunk {
const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');

export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
target: ArrayWithPreamble<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
Expand Down Expand Up @@ -1663,23 +1639,9 @@ export function pushStartInstance(
}
// Preamble start tags
case 'head':
return pushStartHead(
target,
preamble,
props,
type,
responseState,
formatContext,
);
return pushStartHead(target, props, type, responseState, formatContext);
case 'html': {
return pushStartHtml(
target,
preamble,
props,
type,
responseState,
formatContext,
);
return pushStartHtml(target, props, type, responseState, formatContext);
}
default: {
if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
Expand Down Expand Up @@ -1722,17 +1684,24 @@ export function pushEndInstance(
case 'track':
case 'wbr': {
// No close tag needed.
break;
return;
}
// Postamble end tags
case 'body':
case 'html':
target = enableFloat ? postamble : target;
// Intentional fallthrough
default: {
target.push(endTag1, stringToChunk(type), endTag2);
case 'body': {
if (enableFloat) {
postamble.unshift(endTag1, stringToChunk(type), endTag2);
return;
}
break;
}
case 'html':
if (enableFloat) {
postamble.push(endTag1, stringToChunk(type), endTag2);
return;
}
break;
}
target.push(endTag1, stringToChunk(type), endTag2);
}

export function writeCompletedRoot(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export {
setCurrentlyRenderingBoundaryResources,
prepareToRender,
cleanupAfterRender,
emptyChunk,
} from './ReactDOMServerFormatConfig';

import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
Expand Down
165 changes: 112 additions & 53 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4320,70 +4320,129 @@ describe('ReactDOMFizzServer', () => {
});

// @gate enableFloat
it('emits html and head start tags (the preamble) before other content if rendered in the shell', async () => {
it('can emit the preamble even if the head renders asynchronously', async () => {
function AsyncNoOutput() {
readText('nooutput');
return null;
}
function AsyncHead() {
readText('head');
return (
<head data-foo="foo">
<title>a title</title>
</head>
);
}
function AsyncBody() {
readText('body');
return (
<body data-bar="bar">
<link rel="preload" as="style" href="foo" />
hello
</body>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<>
<title data-baz="baz">a title</title>
<html data-foo="foo">
<head data-bar="bar" />
<body>a body</body>
</html>
</>,
<html data-html="html">
<AsyncNoOutput />
<AsyncHead />
<AsyncBody />
</html>,
);
pipe(writable);
});
await actIntoEmptyDocument(() => {
resolveText('body');
});
await actIntoEmptyDocument(() => {
resolveText('nooutput');
});
// We need to use actIntoEmptyDocument because act assumes that buffered
// content should be fake streamed into the body which is normally true
// but in this test the entire shell was delayed and we need the initial
// construction to be done to get the parsing right
await actIntoEmptyDocument(() => {
resolveText('head');
});
expect(getVisibleChildren(document)).toEqual(
<html data-foo="foo">
<head data-bar="bar">
<title data-baz="baz">a title</title>
<html data-html="html">
<head data-foo="foo">
<link rel="preload" as="style" href="foo" />
<title>a title</title>
</head>
<body>a body</body>
<body data-bar="bar">hello</body>
</html>,
);
});

// Hydrate the same thing on the client. We expect this to still fail because <title> is not a Resource
// and is unmatched on hydration
const errors = [];
ReactDOMClient.hydrateRoot(
document,
<>
<title data-baz="baz">a title</title>
<html data-foo="foo">
<head data-bar="bar" />
<body>a body</body>
</html>
</>,
{
onRecoverableError: (err, errInfo) => {
errors.push(err.message);
},
},
);
expect(() => {
try {
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('Invalid insertion of HTML node in #document node.');
} catch (e) {
console.log('e', e);
}
}).toErrorDev(
[
'Warning: Expected server HTML to contain a matching <title> in <#document>.',
'Warning: An error occurred during hydration. The server HTML was replaced with client content in <#document>.',
'Warning: validateDOMNesting(...): <title> cannot appear as a child of <#document>',
],
{withoutStack: 1},
// @gate enableFloat
it('does not emit as preamble after the first non-preamble chunk', async () => {
function AsyncNoOutput() {
readText('nooutput');
return null;
}
function AsyncHead() {
readText('head');
return (
<head data-foo="foo">
<title>a title</title>
</head>
);
}
function AsyncBody() {
readText('body');
return (
<body data-bar="bar">
<link rel="preload" as="style" href="foo" />
hello
</body>
);
}
await actIntoEmptyDocument(() => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
<html data-html="html">
<AsyncNoOutput />
<AsyncBody />
<AsyncHead />
</html>,
);
pipe(writable);
});
await actIntoEmptyDocument(() => {
resolveText('body');
});
await actIntoEmptyDocument(() => {
resolveText('nooutput');
});
// We need to use actIntoEmptyDocument because act assumes that buffered
// content should be fake streamed into the body which is normally true
// but in this test the entire shell was delayed and we need the initial
// construction to be done to get the parsing right
await actIntoEmptyDocument(() => {
resolveText('head');
});
// This assertion is a little strange. The html open tag is part of the preamble
// but since the next chunk will be the body open tag which is not preamble it
// emits resources. The browser understands that the link is part of the head and
// constructs the head implicitly which is why it does not have the data-foo attribute.
// When the head finally streams in it is inside the body rather than after it because the
// body closing tag is part of the postamble which stays open until the entire request
// has flushed. This is how the browser would interpret a late head arriving after the
// the body closing tag so while strange it is the expected behavior. One other oddity
// is that <head> in body is elided by html parsers so we end up with just an inlined
// style tag.
expect(getVisibleChildren(document)).toEqual(
<html data-html="html">
<head>
<link rel="preload" as="style" href="foo" />
</head>
<body data-bar="bar">
hello
<title>a title</title>
</body>
</html>,
);
expect(errors).toEqual([
'Hydration failed because the initial UI does not match what was rendered on the server.',
'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
]);
expect(getVisibleChildren(document)).toEqual();
expect(() => {
expect(Scheduler).toFlushWithoutYielding();
}).toThrow('The node to be removed is not a child of this node.');
});

// @gate enableFloat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export type ResponseState = {
nextSuspenseID: number,
};

export const emptyChunk = stringToPrecomputedChunk('');

// Allows us to keep track of what we've already written so we can refer back to it.
export function createResponseState(): ResponseState {
return {
Expand Down Expand Up @@ -140,7 +142,6 @@ export function pushTextInstance(

export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
Expand Down
1 change: 0 additions & 1 deletion packages/react-noop-renderer/src/ReactNoopServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ const ReactNoopServer = ReactFizzServer({
},
pushStartInstance(
target: Array<Uint8Array>,
preamble: Array<Uint8Array>,
type: string,
props: Object,
): ReactNodeList {
Expand Down
Loading