mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[Fizz] Add option to inject bootstrapping script tags after the shell is injected (#22594)
* Add option to inject bootstrap scripts These are emitted right after the shell as flushed. * Update ssr fixtures to use bootstrapScripts instead of manual script tag * Add option to FB renderer too
This commit is contained in:
parent
3677c019af
commit
cdb8a1d19d
|
|
@ -21,6 +21,7 @@ export default function render(url, res) {
|
|||
});
|
||||
let didError = false;
|
||||
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
onCompleteShell() {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
res.statusCode = didError ? 500 : 200;
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ export default class Chrome extends Component {
|
|||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
}}
|
||||
/>
|
||||
<script src={assets['main.js']} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4265,7 +4265,7 @@ longest@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
|
||||
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0:
|
||||
loose-envify@^1.0.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
|
@ -5945,14 +5945,6 @@ sax@^1.2.1, sax@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
scheduler@^0.20.1:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@
|
|||
"concurrently": "^5.3.0",
|
||||
"express": "^4.17.1",
|
||||
"nodemon": "^2.0.6",
|
||||
"react": "18.0.0-alpha-7ec4c5597",
|
||||
"react-dom": "18.0.0-alpha-7ec4c5597",
|
||||
"react": "link:../../build/node_modules/react",
|
||||
"react-dom": "link:../../build/node_modules/react-dom",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"resolve": "1.12.0",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ module.exports = function render(url, res) {
|
|||
<App assets={assets} />
|
||||
</DataProvider>,
|
||||
{
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
onCompleteShell() {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
res.statusCode = didError ? 500 : 200;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export default function Html({assets, children, title}) {
|
|||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
}}
|
||||
/>
|
||||
<script async src={assets['main.js']} />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
5277
fixtures/ssr2/yarn.lock
Normal file
5277
fixtures/ssr2/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -21,6 +21,7 @@ let useSyncExternalStore;
|
|||
let useSyncExternalStoreExtra;
|
||||
let PropTypes;
|
||||
let textCache;
|
||||
let window;
|
||||
let document;
|
||||
let writable;
|
||||
let CSPnonce = null;
|
||||
|
|
@ -56,6 +57,7 @@ describe('ReactDOMFizzServer', () => {
|
|||
runScripts: 'dangerously',
|
||||
},
|
||||
);
|
||||
window = jsdom.window;
|
||||
document = jsdom.window.document;
|
||||
container = document.getElementById('container');
|
||||
|
||||
|
|
@ -338,11 +340,18 @@ describe('ReactDOMFizzServer', () => {
|
|||
);
|
||||
}
|
||||
|
||||
let bootstrapped = false;
|
||||
window.__INIT__ = function() {
|
||||
bootstrapped = true;
|
||||
// Attempt to hydrate the content.
|
||||
ReactDOM.hydrateRoot(container, <App isClient={true} />);
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<App isClient={false} />,
|
||||
|
||||
{
|
||||
bootstrapScriptContent: '__INIT__();',
|
||||
onError(x) {
|
||||
loggedErrors.push(x);
|
||||
},
|
||||
|
|
@ -351,10 +360,8 @@ describe('ReactDOMFizzServer', () => {
|
|||
pipe(writable);
|
||||
});
|
||||
expect(loggedErrors).toEqual([]);
|
||||
expect(bootstrapped).toBe(true);
|
||||
|
||||
// Attempt to hydrate the content.
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App isClient={true} />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// We're still loading because we're waiting for the server to stream more content.
|
||||
|
|
@ -507,17 +514,27 @@ describe('ReactDOMFizzServer', () => {
|
|||
);
|
||||
}
|
||||
|
||||
let bootstrapped = false;
|
||||
window.__INIT__ = function() {
|
||||
bootstrapped = true;
|
||||
// Attempt to hydrate the content.
|
||||
ReactDOM.hydrateRoot(container, <App />);
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />, {
|
||||
bootstrapScriptContent: '__INIT__();',
|
||||
});
|
||||
pipe(writable);
|
||||
});
|
||||
|
||||
// We're still showing a fallback.
|
||||
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
|
||||
|
||||
// We already bootstrapped.
|
||||
expect(bootstrapped).toBe(true);
|
||||
|
||||
// Attempt to hydrate the content.
|
||||
const root = ReactDOM.createRoot(container, {hydrate: true});
|
||||
root.render(<App />);
|
||||
Scheduler.unstable_flushAll();
|
||||
|
||||
// We're still loading because we're waiting for the server to stream more content.
|
||||
|
|
|
|||
|
|
@ -71,6 +71,22 @@ describe('ReactDOMFizzServer', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should emit bootstrap script src at the end', async () => {
|
||||
const stream = ReactDOMFizzServer.renderToReadableStream(
|
||||
<div>hello world</div>,
|
||||
{
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
);
|
||||
const result = await readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
`"<div>hello world</div><script>INIT();</script><script src=\\"init.js\\" async=\\"\\"></script><script type=\\"module\\" src=\\"init.mjs\\" async=\\"\\"></script>"`,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('emits all HTML as one unit if we wait until the end to start', async () => {
|
||||
let hasLoaded = false;
|
||||
|
|
|
|||
|
|
@ -82,6 +82,24 @@ describe('ReactDOMFizzServer', () => {
|
|||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should emit bootstrap script src at the end', () => {
|
||||
const {writable, output} = getTestWritable();
|
||||
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
|
||||
<div>hello world</div>,
|
||||
{
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
},
|
||||
);
|
||||
pipe(writable);
|
||||
jest.runAllTimers();
|
||||
expect(output.result).toMatchInlineSnapshot(
|
||||
`"<div>hello world</div><script>INIT();</script><script src=\\"init.js\\" async=\\"\\"></script><script type=\\"module\\" src=\\"init.mjs\\" async=\\"\\"></script>"`,
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should start writing after pipe', () => {
|
||||
const {writable, output} = getTestWritable();
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ type Options = {|
|
|||
identifierPrefix?: string,
|
||||
namespaceURI?: string,
|
||||
nonce?: string,
|
||||
bootstrapScriptContent?: string,
|
||||
bootstrapScripts?: Array<string>,
|
||||
bootstrapModules?: Array<string>,
|
||||
progressiveChunkSize?: number,
|
||||
signal?: AbortSignal,
|
||||
onCompleteShell?: () => void,
|
||||
|
|
@ -43,6 +46,9 @@ function renderToReadableStream(
|
|||
createResponseState(
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.nonce : undefined,
|
||||
options ? options.bootstrapScriptContent : undefined,
|
||||
options ? options.bootstrapScripts : undefined,
|
||||
options ? options.bootstrapModules : undefined,
|
||||
),
|
||||
createRootFormatContext(options ? options.namespaceURI : undefined),
|
||||
options ? options.progressiveChunkSize : undefined,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ type Options = {|
|
|||
identifierPrefix?: string,
|
||||
namespaceURI?: string,
|
||||
nonce?: string,
|
||||
bootstrapScriptContent?: string,
|
||||
bootstrapScripts?: Array<string>,
|
||||
bootstrapModules?: Array<string>,
|
||||
progressiveChunkSize?: number,
|
||||
onCompleteShell?: () => void,
|
||||
onCompleteAll?: () => void,
|
||||
|
|
@ -51,6 +54,9 @@ function createRequestImpl(children: ReactNodeList, options: void | Options) {
|
|||
createResponseState(
|
||||
options ? options.identifierPrefix : undefined,
|
||||
options ? options.nonce : undefined,
|
||||
options ? options.bootstrapScriptContent : undefined,
|
||||
options ? options.bootstrapScripts : undefined,
|
||||
options ? options.bootstrapModules : undefined,
|
||||
),
|
||||
createRootFormatContext(options ? options.namespaceURI : undefined),
|
||||
options ? options.progressiveChunkSize : undefined,
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export const isPrimaryRenderer = true;
|
|||
|
||||
// Per response, global state that is not contextual to the rendering subtree.
|
||||
export type ResponseState = {
|
||||
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
|
||||
startInlineScript: PrecomputedChunk,
|
||||
placeholderPrefix: PrecomputedChunk,
|
||||
segmentPrefix: PrecomputedChunk,
|
||||
|
|
@ -73,11 +74,19 @@ export type ResponseState = {
|
|||
};
|
||||
|
||||
const startInlineScript = stringToPrecomputedChunk('<script>');
|
||||
const endInlineScript = stringToPrecomputedChunk('</script>');
|
||||
|
||||
const startScriptSrc = stringToPrecomputedChunk('<script src="');
|
||||
const startModuleSrc = stringToPrecomputedChunk('<script type="module" src="');
|
||||
const endAsyncScript = stringToPrecomputedChunk('" async=""></script>');
|
||||
|
||||
// Allows us to keep track of what we've already written so we can refer back to it.
|
||||
export function createResponseState(
|
||||
identifierPrefix: string | void,
|
||||
nonce: string | void,
|
||||
bootstrapScriptContent: string | void,
|
||||
bootstrapScripts: Array<string> | void,
|
||||
bootstrapModules: Array<string> | void,
|
||||
): ResponseState {
|
||||
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
|
||||
const inlineScriptWithNonce =
|
||||
|
|
@ -86,7 +95,34 @@ export function createResponseState(
|
|||
: stringToPrecomputedChunk(
|
||||
'<script nonce="' + escapeTextForBrowser(nonce) + '">',
|
||||
);
|
||||
const bootstrapChunks = [];
|
||||
if (bootstrapScriptContent !== undefined) {
|
||||
bootstrapChunks.push(
|
||||
inlineScriptWithNonce,
|
||||
stringToChunk(escapeTextForBrowser(bootstrapScriptContent)),
|
||||
endInlineScript,
|
||||
);
|
||||
}
|
||||
if (bootstrapScripts !== undefined) {
|
||||
for (let i = 0; i < bootstrapScripts.length; i++) {
|
||||
bootstrapChunks.push(
|
||||
startScriptSrc,
|
||||
stringToChunk(escapeTextForBrowser(bootstrapScripts[i])),
|
||||
endAsyncScript,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (bootstrapModules !== undefined) {
|
||||
for (let i = 0; i < bootstrapModules.length; i++) {
|
||||
bootstrapChunks.push(
|
||||
startModuleSrc,
|
||||
stringToChunk(escapeTextForBrowser(bootstrapModules[i])),
|
||||
endAsyncScript,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
bootstrapChunks: bootstrapChunks,
|
||||
startInlineScript: inlineScriptWithNonce,
|
||||
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
|
||||
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
|
||||
|
|
@ -1370,6 +1406,18 @@ export function pushEndInstance(
|
|||
}
|
||||
}
|
||||
|
||||
export function writeCompletedRoot(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
const bootstrapChunks = responseState.bootstrapChunks;
|
||||
let result = true;
|
||||
for (let i = 0; i < bootstrapChunks.length; i++) {
|
||||
result = writeChunk(destination, bootstrapChunks[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Structural Nodes
|
||||
|
||||
// A placeholder is a node inside a hidden partial tree that can be filled in later, but before
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const isPrimaryRenderer = false;
|
|||
|
||||
export type ResponseState = {
|
||||
// Keep this in sync with ReactDOMServerFormatConfig
|
||||
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
|
||||
startInlineScript: PrecomputedChunk,
|
||||
placeholderPrefix: PrecomputedChunk,
|
||||
segmentPrefix: PrecomputedChunk,
|
||||
|
|
@ -50,6 +51,7 @@ export function createResponseState(
|
|||
const responseState = createResponseStateImpl(identifierPrefix, undefined);
|
||||
return {
|
||||
// Keep this in sync with ReactDOMServerFormatConfig
|
||||
bootstrapChunks: responseState.bootstrapChunks,
|
||||
startInlineScript: responseState.startInlineScript,
|
||||
placeholderPrefix: responseState.placeholderPrefix,
|
||||
segmentPrefix: responseState.segmentPrefix,
|
||||
|
|
@ -95,6 +97,7 @@ export {
|
|||
writeStartPendingSuspenseBoundary,
|
||||
writeEndPendingSuspenseBoundary,
|
||||
writePlaceholder,
|
||||
writeCompletedRoot,
|
||||
} from './ReactDOMServerFormatConfig';
|
||||
|
||||
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
|
||||
|
|
|
|||
|
|
@ -164,6 +164,13 @@ export function pushEndInstance(
|
|||
target.push(END);
|
||||
}
|
||||
|
||||
export function writeCompletedRoot(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// IDs are formatted as little endian Uint16
|
||||
function formatID(id: number): Uint8Array {
|
||||
if (id > 0xffff) {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ const ReactNoopServer = ReactFizzServer({
|
|||
target.push(POP);
|
||||
},
|
||||
|
||||
writeCompletedRoot(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
): boolean {
|
||||
return true;
|
||||
},
|
||||
|
||||
writePlaceholder(
|
||||
destination: Destination,
|
||||
responseState: ResponseState,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ import {
|
|||
|
||||
type Options = {
|
||||
identifierPrefix?: string,
|
||||
bootstrapScriptContent?: string,
|
||||
bootstrapScripts: Array<string>,
|
||||
bootstrapModules: Array<string>,
|
||||
progressiveChunkSize?: number,
|
||||
onError: (error: mixed) => void,
|
||||
};
|
||||
|
|
@ -46,7 +49,13 @@ function renderToStream(children: ReactNodeList, options: Options): Stream {
|
|||
};
|
||||
const request = createRequest(
|
||||
children,
|
||||
createResponseState(options ? options.identifierPrefix : undefined),
|
||||
createResponseState(
|
||||
options ? options.identifierPrefix : undefined,
|
||||
undefined,
|
||||
options ? options.bootstrapScriptContent : undefined,
|
||||
options ? options.bootstrapScripts : undefined,
|
||||
options ? options.bootstrapModules : undefined,
|
||||
),
|
||||
createRootFormatContext(undefined),
|
||||
options ? options.progressiveChunkSize : undefined,
|
||||
options.onError,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,21 @@ describe('ReactDOMServerFB', () => {
|
|||
expect(result).toMatchInlineSnapshot(`"<div>hello world</div>"`);
|
||||
});
|
||||
|
||||
it('should emit bootstrap script src at the end', () => {
|
||||
const stream = ReactDOMServer.renderToStream(<div>hello world</div>, {
|
||||
bootstrapScriptContent: 'INIT();',
|
||||
bootstrapScripts: ['init.js'],
|
||||
bootstrapModules: ['init.mjs'],
|
||||
onError(x) {
|
||||
console.error(x);
|
||||
},
|
||||
});
|
||||
const result = readResult(stream);
|
||||
expect(result).toMatchInlineSnapshot(
|
||||
`"<div>hello world</div><script>INIT();</script><script src=\\"init.js\\" async=\\"\\"></script><script type=\\"module\\" src=\\"init.mjs\\" async=\\"\\"></script>"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('emits all HTML as one unit if we wait until the end to start', async () => {
|
||||
let hasLoaded = false;
|
||||
let resolve;
|
||||
|
|
|
|||
2
packages/react-server/src/ReactFizzServer.js
vendored
2
packages/react-server/src/ReactFizzServer.js
vendored
|
|
@ -36,6 +36,7 @@ import {
|
|||
closeWithError,
|
||||
} from './ReactServerStreamConfig';
|
||||
import {
|
||||
writeCompletedRoot,
|
||||
writePlaceholder,
|
||||
writeStartCompletedSuspenseBoundary,
|
||||
writeStartPendingSuspenseBoundary,
|
||||
|
|
@ -1779,6 +1780,7 @@ function flushCompletedQueues(
|
|||
if (completedRootSegment !== null && request.pendingRootTasks === 0) {
|
||||
flushSegment(request, destination, completedRootSegment);
|
||||
request.completedRootSegment = null;
|
||||
writeCompletedRoot(destination, request.responseState);
|
||||
}
|
||||
|
||||
// We emit client rendering instructions for already emitted boundaries first.
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ export const pushStartCompletedSuspenseBoundary =
|
|||
$$$hostConfig.pushStartCompletedSuspenseBoundary;
|
||||
export const pushEndCompletedSuspenseBoundary =
|
||||
$$$hostConfig.pushEndCompletedSuspenseBoundary;
|
||||
export const writeCompletedRoot = $$$hostConfig.writeCompletedRoot;
|
||||
export const writePlaceholder = $$$hostConfig.writePlaceholder;
|
||||
export const writeStartCompletedSuspenseBoundary =
|
||||
$$$hostConfig.writeStartCompletedSuspenseBoundary;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user