mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
Add a DOCTYPE to the stream if the <html> tag is rendered (#21680)
This makes it a lot easier to render the whole document using React without needing to patch into the stream. We expect that currently people will still have to patch into the stream to do advanced things but eventually the goal is that you shouldn't need to.
This commit is contained in:
parent
a8f5e77b92
commit
bd45ad05dc
|
|
@ -21,9 +21,14 @@
|
|||
<script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
|
||||
<script type="text/babel">
|
||||
let controller = new AbortController();
|
||||
let stream = ReactDOMFizzServer.renderToReadableStream(<body>Success</body>, {
|
||||
let stream = ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<body>Success</body>
|
||||
</html>,
|
||||
{
|
||||
signal: controller.signal,
|
||||
});
|
||||
}
|
||||
);
|
||||
let response = new Response(stream, {
|
||||
headers: {'Content-Type': 'text/html'},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,8 +28,6 @@ export default function render(url, res) {
|
|||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
res.statusCode = didError ? 500 : 200;
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
// There's no way to render a doctype in React so prepend manually.
|
||||
res.write('<!DOCTYPE html>');
|
||||
startWriting();
|
||||
},
|
||||
onError(x) {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,19 @@ describe('ReactDOMFizzServer', () => {
|
|||
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should emit DOCTYPE at the root of the document', async () => {
|
||||
const stream = ReactDOMFizzServer.renderToReadableStream(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
`"<!DOCTYPE html><html><body>hello world</body></html>"`,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('emits all HTML as one unit if we wait until the end to start', async () => {
|
||||
let hasLoaded = false;
|
||||
|
|
|
|||
|
|
@ -68,6 +68,22 @@ describe('ReactDOMFizzServer', () => {
|
|||
expect(output.result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should emit DOCTYPE at the root of the document', () => {
|
||||
const {writable, output} = getTestWritable();
|
||||
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
|
||||
<html>
|
||||
<body>hello world</body>
|
||||
</html>,
|
||||
writable,
|
||||
);
|
||||
startWriting();
|
||||
jest.runAllTimers();
|
||||
expect(output.result).toMatchInlineSnapshot(
|
||||
`"<!DOCTYPE html><html><body>hello world</body></html>"`,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should start writing after startWriting', () => {
|
||||
const {writable, output} = getTestWritable();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ describe('rendering React components at document', () => {
|
|||
}
|
||||
|
||||
const markup = ReactDOMServer.renderToString(<Root hello="world" />);
|
||||
expect(markup).not.toContain('DOCTYPE');
|
||||
const testDocument = getTestDocument(markup);
|
||||
const body = testDocument.body;
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ function renderToStringImpl(
|
|||
generateStaticMarkup,
|
||||
options ? options.identifierPrefix : undefined,
|
||||
),
|
||||
createRootFormatContext(undefined),
|
||||
createRootFormatContext(),
|
||||
Infinity,
|
||||
onError,
|
||||
undefined,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ function renderToNodeStreamImpl(
|
|||
children,
|
||||
destination,
|
||||
createResponseState(false, options ? options.identifierPrefix : undefined),
|
||||
createRootFormatContext(undefined),
|
||||
createRootFormatContext(),
|
||||
Infinity,
|
||||
onError,
|
||||
onCompleteAll,
|
||||
|
|
|
|||
|
|
@ -87,9 +87,10 @@ export function createResponseState(
|
|||
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
|
||||
// modes. We only include the variants as they matter for the sake of our purposes.
|
||||
// We don't actually provide the namespace therefore we use constants instead of the string.
|
||||
const HTML_MODE = 0;
|
||||
const SVG_MODE = 1;
|
||||
const MATHML_MODE = 2;
|
||||
const ROOT_HTML_MODE = 0; // Used for the root most element tag.
|
||||
export const HTML_MODE = 1;
|
||||
const SVG_MODE = 2;
|
||||
const MATHML_MODE = 3;
|
||||
const HTML_TABLE_MODE = 4;
|
||||
const HTML_TABLE_BODY_MODE = 5;
|
||||
const HTML_TABLE_ROW_MODE = 6;
|
||||
|
|
@ -121,7 +122,7 @@ export function createRootFormatContext(namespaceURI?: string): FormatContext {
|
|||
? SVG_MODE
|
||||
: namespaceURI === 'http://www.w3.org/1998/Math/MathML'
|
||||
? MATHML_MODE
|
||||
: HTML_MODE;
|
||||
: ROOT_HTML_MODE;
|
||||
return createFormatContext(insertionMode, null);
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +161,10 @@ export function getChildFormatContext(
|
|||
// entered plain HTML again.
|
||||
return createFormatContext(HTML_MODE, null);
|
||||
}
|
||||
if (parentContext.insertionMode === ROOT_HTML_MODE) {
|
||||
// We've emitted the root and is now in plain HTML mode.
|
||||
return createFormatContext(HTML_MODE, null);
|
||||
}
|
||||
return parentContext;
|
||||
}
|
||||
|
||||
|
|
@ -1262,6 +1267,8 @@ function startChunkForTag(tag: string): PrecomputedChunk {
|
|||
return tagStartChunk;
|
||||
}
|
||||
|
||||
const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
|
||||
|
||||
export function pushStartInstance(
|
||||
target: Array<Chunk | PrecomputedChunk>,
|
||||
type: string,
|
||||
|
|
@ -1371,6 +1378,21 @@ export function pushStartInstance(
|
|||
assignID,
|
||||
);
|
||||
}
|
||||
case 'html': {
|
||||
if (formatContext.insertionMode === ROOT_HTML_MODE) {
|
||||
// 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,
|
||||
type,
|
||||
responseState,
|
||||
assignID,
|
||||
);
|
||||
}
|
||||
default: {
|
||||
if (type.indexOf('-') === -1 && typeof props.is !== 'string') {
|
||||
// Generic element
|
||||
|
|
@ -1541,6 +1563,7 @@ export function writeStartSegment(
|
|||
id: number,
|
||||
): boolean {
|
||||
switch (formatContext.insertionMode) {
|
||||
case ROOT_HTML_MODE:
|
||||
case HTML_MODE: {
|
||||
writeChunk(destination, startSegmentHTML);
|
||||
writeChunk(destination, responseState.segmentPrefix);
|
||||
|
|
@ -1597,6 +1620,7 @@ export function writeEndSegment(
|
|||
formatContext: FormatContext,
|
||||
): boolean {
|
||||
switch (formatContext.insertionMode) {
|
||||
case ROOT_HTML_MODE:
|
||||
case HTML_MODE: {
|
||||
return writeChunk(destination, endSegmentHTML);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
import type {SuspenseBoundaryID} from './ReactDOMServerFormatConfig';
|
||||
import type {
|
||||
SuspenseBoundaryID,
|
||||
FormatContext,
|
||||
} from './ReactDOMServerFormatConfig';
|
||||
|
||||
import {
|
||||
createResponseState as createResponseStateImpl,
|
||||
|
|
@ -16,6 +19,7 @@ import {
|
|||
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
|
||||
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
|
||||
writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl,
|
||||
HTML_MODE,
|
||||
} from './ReactDOMServerFormatConfig';
|
||||
|
||||
import type {
|
||||
|
|
@ -62,6 +66,13 @@ export function createResponseState(
|
|||
};
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
export type {
|
||||
FormatContext,
|
||||
SuspenseBoundaryID,
|
||||
|
|
@ -69,7 +80,6 @@ export type {
|
|||
} from './ReactDOMServerFormatConfig';
|
||||
|
||||
export {
|
||||
createRootFormatContext,
|
||||
getChildFormatContext,
|
||||
createSuspenseBoundaryID,
|
||||
makeServerID,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user