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

Add non-standard allowing variants of ReactDOMServer render methods (fixes #10064) #12568

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function initModules() {
const {
resetModules,
itRenders,
itRendersNonStandard,
clientCleanRender,
} = ReactDOMServerIntegrationUtils(initModules);

Expand Down Expand Up @@ -604,6 +605,16 @@ describe('ReactDOMServerIntegration', () => {
expect(e.getAttribute('foo')).toBe('bar');
});

itRendersNonStandard(
'non-standard attributes for non-standard elements',
async render => {
const e = await render(
<non-standard {...{'[non-standard]': 'test'}} />,
);
expect(e.getAttribute('[non-standard]')).toBe('test');
},
);

itRenders('SVG tags with dashes in them', async render => {
const e = await render(
<svg>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,40 @@ describe('ReactServerRenderingBrowser', () => {
);
});

it('returns the same non-standard results as react-dom/server', () => {
class NiceNonStandard extends React.Component {
render() {
return (
<greeting-answer {...{'[type]': 'nice'}}>
I am feeling very good today, thanks, how are you?
</greeting-answer>
);
}
}
function GreetingNonStandard() {
return (
<div>
<greeting-question {...{'[type]': 'inquisitive'}}>
How are you?
</greeting-question>
<NiceNonStandard />
</div>
);
}
expect(
ReactDOMServerBrowser.renderToStringNonStandard(<GreetingNonStandard />),
).toEqual(
ReactDOMServer.renderToStringNonStandard(<GreetingNonStandard />),
);
expect(
ReactDOMServerBrowser.renderToStaticMarkupNonStandard(
<GreetingNonStandard />,
),
).toEqual(
ReactDOMServer.renderToStaticMarkupNonStandard(<GreetingNonStandard />),
);
});

it('throws meaningfully for server-only APIs', () => {
expect(() => ReactDOMServerBrowser.renderToNodeStream(<div />)).toThrow(
'ReactDOMServer.renderToNodeStream(): The streaming API is not available ' +
Expand All @@ -63,5 +97,17 @@ describe('ReactServerRenderingBrowser', () => {
'ReactDOMServer.renderToStaticNodeStream(): The streaming API is not available ' +
'in the browser. Use ReactDOMServer.renderToStaticMarkup() instead.',
);
expect(() =>
ReactDOMServerBrowser.renderToNodeStreamNonStandard(<div />),
).toThrow(
'ReactDOMServer.renderToNodeStreamNonStandard(): The streaming API is not available ' +
'in the browser. Use ReactDOMServer.renderToStringNonStandard() instead.',
);
expect(() =>
ReactDOMServerBrowser.renderToStaticNodeStreamNonStandard(<div />),
).toThrow(
'ReactDOMServer.renderToStaticNodeStreamNonStandard(): The streaming API is not available ' +
'in the browser. Use ReactDOMServer.renderToStaticMarkupNonStandard() instead.',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,23 @@ module.exports = function(initModules) {
}, errorCount);
}

async function renderIntoString(reactElement, errorCount = 0) {
async function renderIntoString(
reactElement,
errorCount = 0,
allowNonStandard,
) {
return await expectErrors(
() =>
new Promise(resolve =>
resolve(ReactDOMServer.renderToString(reactElement)),
),
new Promise(resolve => {
let result;
if (allowNonStandard) {
result = ReactDOMServer.renderToStringNonStandard(reactElement);
} else {
result = ReactDOMServer.renderToString(reactElement);
}

resolve(result);
}),
errorCount,
);
}
Expand All @@ -98,7 +109,17 @@ module.exports = function(initModules) {
// element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function serverRender(reactElement, errorCount = 0) {
const markup = await renderIntoString(reactElement, errorCount);
const markup = await renderIntoString(reactElement, errorCount, false);
const domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
}

// Renders text using SSR and then stuffs it into a DOM node; returns the DOM
// element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function serverRenderNonStandard(reactElement, errorCount = 0) {
const markup = await renderIntoString(reactElement, errorCount, true);
const domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
Expand All @@ -118,12 +139,26 @@ module.exports = function(initModules) {
}
}

async function renderIntoStream(reactElement, errorCount = 0) {
async function renderIntoStream(
reactElement,
errorCount = 0,
allowNonStandard,
) {
return await expectErrors(
() =>
new Promise(resolve => {
let writable = new DrainWritable();
ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);

let nodeStream;
if (allowNonStandard) {
nodeStream = ReactDOMServer.renderToNodeStreamNonStandard(
reactElement,
);
} else {
nodeStream = ReactDOMServer.renderToNodeStream(reactElement);
}

nodeStream.pipe(writable);
writable.on('finish', () => resolve(writable.buffer));
}),
errorCount,
Expand All @@ -134,7 +169,17 @@ module.exports = function(initModules) {
// returns the DOM element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function streamRender(reactElement, errorCount = 0) {
const markup = await renderIntoStream(reactElement, errorCount);
const markup = await renderIntoStream(reactElement, errorCount, false);
const domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
}

// Renders non-standard text using node stream SSR and then stuffs it into a
// DOM node; returns the DOM element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function streamRenderNonStandard(reactElement, errorCount = 0) {
const markup = await renderIntoStream(reactElement, errorCount, true);
const domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
Expand Down Expand Up @@ -221,6 +266,23 @@ module.exports = function(initModules) {
itClientRenders(desc, testFn);
}

// Runs a DOM rendering test for rendering to a non-standard string on server.
// Non-standard strings cannot render on client without errors.
//
// testFn is a test that has one arg, which is a render function. the render
// function takes in a ReactElement and an optional expected error count and
// returns a promise of a DOM Element.
//
// You should only perform tests that examine the DOM of the results of
// render; you should not depend on the interactivity of the returned DOM element,
// as that will not work in the server string scenario.
function itRendersNonStandard(desc, testFn) {
it(`renders ${desc} with server non-standard string render`, () =>
testFn(serverRenderNonStandard));
it(`renders ${desc} with server non-standard stream render`, () =>
testFn(streamRenderNonStandard));
}

// run testFn in three different rendering scenarios:
// -- render on client without any server markup "clean client render"
// -- render on client on top of good server-generated string markup
Expand Down Expand Up @@ -319,6 +381,7 @@ module.exports = function(initModules) {
expectMarkupMismatch,
expectMarkupMatch,
itRenders,
itRendersNonStandard,
itClientRenders,
itThrowsWhenRendering,
asyncReactDOMRender,
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/server/DOMMarkupOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ export function createMarkupForProperty(name: string, value: mixed): string {
export function createMarkupForCustomAttribute(
name: string,
value: mixed,
allowNonStandard: boolean,
): string {
if (!isAttributeNameSafe(name) || value == null) {
if ((!allowNonStandard && !isAttributeNameSafe(name)) || value == null) {
return '';
}
return name + '=' + quoteAttributeValueForBrowser(value);
Expand Down
30 changes: 26 additions & 4 deletions packages/react-dom/src/server/ReactDOMNodeStreamRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import ReactPartialRenderer from './ReactPartialRenderer';

// This is a Readable Node.js stream which wraps the ReactDOMPartialRenderer.
class ReactMarkupReadableStream extends Readable {
constructor(element, makeStaticMarkup) {
constructor(element, makeStaticMarkup, allowNonStandard) {
// Calls the stream.Readable(options) constructor. Consider exposing built-in
// features like highWaterMark in the future.
super({});
this.partialRenderer = new ReactPartialRenderer(element, makeStaticMarkup);
this.partialRenderer = new ReactPartialRenderer(
element,
makeStaticMarkup,
allowNonStandard,
);
}

_read(size) {
Expand All @@ -32,7 +36,7 @@ class ReactMarkupReadableStream extends Readable {
* See https://reactjs.org/docs/react-dom-stream.html#rendertonodestream
*/
export function renderToNodeStream(element) {
return new ReactMarkupReadableStream(element, false);
return new ReactMarkupReadableStream(element, false, false);
}

/**
Expand All @@ -41,5 +45,23 @@ export function renderToNodeStream(element) {
* See https://reactjs.org/docs/react-dom-stream.html#rendertostaticnodestream
*/
export function renderToStaticNodeStream(element) {
return new ReactMarkupReadableStream(element, true);
return new ReactMarkupReadableStream(element, true, false);
}

/**
* Render a ReactElement to its initial non-standard HTML. This should only be
* used on the server.
* See https://reactjs.org/docs/react-dom-stream.html#rendertonodestream
*/
export function renderToNodeStreamNonStandard(element) {
return new ReactMarkupReadableStream(element, false, true);
}

/**
* Similar to renderToNodeStreamNonStandard, except this doesn't create extra
* DOM attributes such as data-react-id that React uses internally.
* See https://reactjs.org/docs/react-dom-stream.html#rendertostaticnodestream
*/
export function renderToStaticNodeStreamNonStandard(element) {
return new ReactMarkupReadableStream(element, true, true);
}
27 changes: 26 additions & 1 deletion packages/react-dom/src/server/ReactDOMServerBrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
import ReactVersion from 'shared/ReactVersion';
import invariant from 'fbjs/lib/invariant';

import {renderToString, renderToStaticMarkup} from './ReactDOMStringRenderer';
import {
renderToString,
renderToStaticMarkup,
renderToStringNonStandard,
renderToStaticMarkupNonStandard,
} from './ReactDOMStringRenderer';

function renderToNodeStream() {
invariant(
Expand All @@ -26,11 +31,31 @@ function renderToStaticNodeStream() {
);
}

function renderToNodeStreamNonStandard() {
invariant(
false,
'ReactDOMServer.renderToNodeStreamNonStandard(): The streaming API is not available ' +
'in the browser. Use ReactDOMServer.renderToStringNonStandard() instead.',
);
}

function renderToStaticNodeStreamNonStandard() {
invariant(
false,
'ReactDOMServer.renderToStaticNodeStreamNonStandard(): The streaming API is not available ' +
'in the browser. Use ReactDOMServer.renderToStaticMarkupNonStandard() instead.',
);
}

// Note: when changing this, also consider https://github.com/facebook/react/issues/11526
export default {
renderToString,
renderToStaticMarkup,
renderToStringNonStandard,
renderToStaticMarkupNonStandard,
renderToNodeStream,
renderToStaticNodeStream,
renderToNodeStreamNonStandard,
renderToStaticNodeStreamNonStandard,
version: ReactVersion,
};
13 changes: 12 additions & 1 deletion packages/react-dom/src/server/ReactDOMServerNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,28 @@

import ReactVersion from 'shared/ReactVersion';

import {renderToString, renderToStaticMarkup} from './ReactDOMStringRenderer';
import {
renderToString,
renderToStaticMarkup,
renderToStringNonStandard,
renderToStaticMarkupNonStandard,
} from './ReactDOMStringRenderer';
import {
renderToNodeStream,
renderToStaticNodeStream,
renderToNodeStreamNonStandard,
renderToStaticNodeStreamNonStandard,
} from './ReactDOMNodeStreamRenderer';

// Note: when changing this, also consider https://github.com/facebook/react/issues/11526
export default {
renderToString,
renderToStaticMarkup,
renderToStringNonStandard,
renderToStaticMarkupNonStandard,
renderToNodeStream,
renderToStaticNodeStream,
renderToNodeStreamNonStandard,
renderToStaticNodeStreamNonStandard,
version: ReactVersion,
};
26 changes: 24 additions & 2 deletions packages/react-dom/src/server/ReactDOMStringRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ReactPartialRenderer from './ReactPartialRenderer';
* See https://reactjs.org/docs/react-dom-server.html#rendertostring
*/
export function renderToString(element) {
const renderer = new ReactPartialRenderer(element, false);
const renderer = new ReactPartialRenderer(element, false, false);
const markup = renderer.read(Infinity);
return markup;
}
Expand All @@ -24,7 +24,29 @@ export function renderToString(element) {
* See https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup
*/
export function renderToStaticMarkup(element) {
const renderer = new ReactPartialRenderer(element, true);
const renderer = new ReactPartialRenderer(element, true, false);
const markup = renderer.read(Infinity);
return markup;
}

/**
* Render a ReactElement to its initial non-standard HTML. This should only be
* used on the server.
* See https://reactjs.org/docs/react-dom-server.html#rendertostring
*/
export function renderToStringNonStandard(element) {
const renderer = new ReactPartialRenderer(element, false, true);
const markup = renderer.read(Infinity);
return markup;
}

/**
* Similar to renderToStringNonStandard, except this doesn't create extra DOM
* attributes such as data-react-id that React uses internally.
* See https://reactjs.org/docs/react-dom-server.html#rendertostaticmarkup
*/
export function renderToStaticMarkupNonStandard(element) {
const renderer = new ReactPartialRenderer(element, true, true);
const markup = renderer.read(Infinity);
return markup;
}