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
11 changes: 8 additions & 3 deletions fixtures/fizz-ssr-browser/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ <h1>Fizz Example</h1>
<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>, {
signal: controller.signal,
});
let stream = ReactDOMFizzServer.renderToReadableStream(
<html>
<body>Success</body>
</html>,
{
signal: controller.signal,
}
);
let response = new Response(stream, {
headers: {'Content-Type': 'text/html'},
});
Expand Down
2 changes: 0 additions & 2 deletions fixtures/ssr/server/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServerNode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy, I forgot this already works

`"<!DOCTYPE html><html><body>hello world</body></html>"`,
);
});

// @gate experimental
it('should start writing after startWriting', () => {
const {writable, output} = getTestWritable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function renderToStringImpl(
generateStaticMarkup,
options ? options.identifierPrefix : undefined,
),
createRootFormatContext(undefined),
createRootFormatContext(),
Infinity,
onError,
undefined,
Expand Down
2 changes: 1 addition & 1 deletion packages/react-dom/src/server/ReactDOMLegacyServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ function renderToNodeStreamImpl(
children,
destination,
createResponseState(false, options ? options.identifierPrefix : undefined),
createRootFormatContext(undefined),
createRootFormatContext(),
Infinity,
onError,
onCompleteAll,
Expand Down
32 changes: 28 additions & 4 deletions packages/react-dom/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -1597,6 +1620,7 @@ export function writeEndSegment(
formatContext: FormatContext,
): boolean {
switch (formatContext.insertionMode) {
case ROOT_HTML_MODE:
case HTML_MODE: {
return writeChunk(destination, endSegmentHTML);
}
Expand Down
14 changes: 12 additions & 2 deletions packages/react-dom/src/server/ReactDOMServerLegacyFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
* @flow
*/

import type {SuspenseBoundaryID} from './ReactDOMServerFormatConfig';
import type {
SuspenseBoundaryID,
FormatContext,
} from './ReactDOMServerFormatConfig';

import {
createResponseState as createResponseStateImpl,
Expand All @@ -16,6 +19,7 @@ import {
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl,
HTML_MODE,
} from './ReactDOMServerFormatConfig';

import type {
Expand Down Expand Up @@ -62,14 +66,20 @@ 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,
OpaqueIDType,
} from './ReactDOMServerFormatConfig';

export {
createRootFormatContext,
getChildFormatContext,
createSuspenseBoundaryID,
makeServerID,
Expand Down