Skip to content

Commit

Permalink
During SSR we should preinitialize imports so they can begin to be fe…
Browse files Browse the repository at this point in the history
…tched before bootstrap in the browser
  • Loading branch information
gnoff committed Sep 12, 2023
1 parent 7a3cb8f commit cc7fdfd
Show file tree
Hide file tree
Showing 38 changed files with 779 additions and 223 deletions.
4 changes: 1 addition & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -415,9 +415,7 @@ module.exports = {
},
},
{
files: [
'packages/react-native-renderer/**/*.js',
],
files: ['packages/react-native-renderer/**/*.js'],
globals: {
nativeFabricUIManager: 'readonly',
},
Expand Down
1 change: 1 addition & 0 deletions fixtures/flight/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v18
21 changes: 18 additions & 3 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const compress = require('compression');
const chalk = require('chalk');
const express = require('express');
const http = require('http');
const React = require('react');

const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');
Expand Down Expand Up @@ -62,6 +63,11 @@ if (process.env.NODE_ENV === 'development') {
webpackMiddleware(compiler, {
publicPath: paths.publicUrlOrPath.slice(0, -1),
serverSideRender: true,
headers: () => {
return {
'Cache-Control': 'no-store, must-revalidate',
};
},
})
);
app.use(webpackHotMiddleware(compiler));
Expand Down Expand Up @@ -121,12 +127,13 @@ app.all('/', async function (req, res, next) {
buildPath = path.join(__dirname, '../build/');
}
// Read the module map from the virtual file system.
const moduleMap = JSON.parse(
const ssrManifest = JSON.parse(
await virtualFs.readFile(
path.join(buildPath, 'react-ssr-manifest.json'),
'utf8'
)
);

// Read the entrypoints containing the initial JS to bootstrap everything.
// For other pages, the chunks in the RSC payload are enough.
const mainJSChunks = JSON.parse(
Expand All @@ -138,10 +145,18 @@ app.all('/', async function (req, res, next) {
// For HTML, we're a "client" emulator that runs the client code,
// so we start by consuming the RSC payload. This needs a module
// map that reverse engineers the client-side path to the SSR path.
const root = await createFromNodeStream(rscResponse, moduleMap);
let root;
let Root = () => {
if (root) {
return root;
}
return React.use(
(root = createFromNodeStream(rscResponse, ssrManifest))
);
};
// Render it into HTML by resolving the client components
res.set('Content-type', 'text/html');
const {pipe} = renderToPipeableStream(root, {
const {pipe} = renderToPipeableStream(React.createElement(Root), {
bootstrapScripts: mainJSChunks,
});
pipe(res);
Expand Down
20 changes: 17 additions & 3 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import type {LazyComponent} from 'react/src/ReactLazy';
import type {
ClientReference,
ClientReferenceMetadata,
SSRManifest,
SSRModuleMap,
StringDecoder,
ModuleLoading,
} from './ReactFlightClientConfig';

import type {
Expand All @@ -36,6 +37,7 @@ import {
readPartialStringChunk,
readFinalStringChunk,
createStringDecoder,
prepareDestinationForModule,
} from './ReactFlightClientConfig';

import {registerServerReference} from './ReactFlightReplyClient';
Expand Down Expand Up @@ -178,8 +180,10 @@ Chunk.prototype.then = function <T>(
};

export type Response = {
_bundlerConfig: SSRManifest,
_bundlerConfig: SSRModuleMap,
_moduleLoading: ModuleLoading,
_callServer: CallServerCallback,
_nonce: ?string,
_chunks: Map<number, SomeChunk<any>>,
_fromJSON: (key: string, value: JSONValue) => any,
_stringDecoder: StringDecoder,
Expand Down Expand Up @@ -706,13 +710,17 @@ function missingCall() {
}

export function createResponse(
bundlerConfig: SSRManifest,
bundlerConfig: SSRModuleMap,
moduleLoading: ModuleLoading,
callServer: void | CallServerCallback,
nonce: void | string,
): Response {
const chunks: Map<number, SomeChunk<any>> = new Map();
const response: Response = {
_bundlerConfig: bundlerConfig,
_moduleLoading: moduleLoading,
_callServer: callServer !== undefined ? callServer : missingCall,
_nonce: nonce,
_chunks: chunks,
_stringDecoder: createStringDecoder(),
_fromJSON: (null: any),
Expand Down Expand Up @@ -774,6 +782,12 @@ function resolveModule(
clientReferenceMetadata,
);

prepareDestinationForModule(
response._moduleLoading,
response._nonce,
clientReferenceMetadata,
);

// TODO: Add an option to encode modules that are lazy loaded.
// For now we preload all modules as early as possible since it's likely
// that we'll need them.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@

declare var $$$config: any;

export opaque type SSRManifest = mixed;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = mixed;
Expand All @@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference;
export const preloadModule = $$$config.preloadModule;
export const requireModule = $$$config.requireModule;
export const dispatchHint = $$$config.dispatchHint;
export const prepareDestinationForModule =
$$$config.prepareDestinationForModule;
export const usedWithSSR = true;

export opaque type Source = mixed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = false;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = mixed;
Expand All @@ -20,4 +21,5 @@ export const resolveClientReference: any = null;
export const resolveServerReference: any = null;
export const preloadModule: any = null;
export const requireModule: any = null;
export const prepareDestinationForModule: any = null;
export const usedWithSSR = true;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

export type Response = any;
export opaque type SSRManifest = mixed;
export opaque type ModuleLoading = mixed;
export opaque type SSRModuleMap = mixed;
export opaque type ServerManifest = mixed;
export opaque type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = mixed;
Expand All @@ -20,4 +21,5 @@ export const resolveClientReference: any = null;
export const resolveServerReference: any = null;
export const preloadModule: any = null;
export const requireModule: any = null;
export const prepareDestinationForModule: any = null;
export const usedWithSSR = true;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
*/

export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackServer';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerNode';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigTargetWebpackServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import type {HintCode, HintModel} from '../server/ReactFlightServerConfigDOM';
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;

import {getCrossOriginString} from './crossOriginStrings';

export function dispatchHint<Code: HintCode>(
code: Code,
model: HintModel<Code>,
Expand Down Expand Up @@ -110,3 +112,17 @@ export function dispatchHint<Code: HintCode>(
function refineModel<T>(code: T, model: HintModel<any>): HintModel<T> {
return model;
}

export function preinitScriptForSSR(
href: string,
nonce: ?string,
crossOrigin: ?string,
) {
const dispatcher = ReactDOMCurrentDispatcher.current;
if (dispatcher) {
dispatcher.preinitScript(href, {
crossOrigin: getCrossOriginString(crossOrigin),
nonce,
});
}
}
30 changes: 30 additions & 0 deletions packages/react-dom-bindings/src/shared/crossOriginStrings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/

export opaque type CrossOriginString: string = string;

export function getCrossOriginString(input: ?string): ?CrossOriginString {
if (typeof input === 'string') {
return input === 'use-credentials' ? input : '';
}
return undefined;
}

export function getCrossOriginStringAs(
as: ?string,
input: ?string,
): ?CrossOriginString {
if (as === 'font') {
return '';
}
if (typeof input === 'string') {
return input === 'use-credentials' ? input : '';
}
return undefined;
}
59 changes: 29 additions & 30 deletions packages/react-dom/src/shared/ReactDOMFloat.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* @flow
*/
import type {
CrossOriginEnum,
PreconnectOptions,
PreloadOptions,
PreloadModuleOptions,
Expand All @@ -18,6 +17,11 @@ import type {
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const Dispatcher = ReactDOMSharedInternals.Dispatcher;

import {
getCrossOriginString,
getCrossOriginStringAs,
} from 'react-dom-bindings/src/shared/crossOriginStrings';

export function prefetchDNS(href: string) {
if (__DEV__) {
if (typeof href !== 'string' || !href) {
Expand Down Expand Up @@ -74,7 +78,7 @@ export function preconnect(href: string, options?: ?PreconnectOptions) {
const dispatcher = Dispatcher.current;
if (dispatcher && typeof href === 'string') {
const crossOrigin = options
? getCrossOrigin('preconnect', options.crossOrigin)
? getCrossOriginString(options.crossOrigin)
: null;
dispatcher.preconnect(href, crossOrigin);
}
Expand Down Expand Up @@ -117,7 +121,7 @@ export function preload(href: string, options: PreloadOptions) {
typeof options.as === 'string'
) {
const as = options.as;
const crossOrigin = getCrossOrigin(as, options.crossOrigin);
const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin);
dispatcher.preload(href, as, {
crossOrigin,
integrity:
Expand Down Expand Up @@ -172,7 +176,10 @@ export function preloadModule(href: string, options?: ?PreloadModuleOptions) {
const dispatcher = Dispatcher.current;
if (dispatcher && typeof href === 'string') {
if (options) {
const crossOrigin = getCrossOrigin(options.as, options.crossOrigin);
const crossOrigin = getCrossOriginStringAs(
options.as,
options.crossOrigin,
);
dispatcher.preloadModule(href, {
as:
typeof options.as === 'string' && options.as !== 'script'
Expand Down Expand Up @@ -218,7 +225,7 @@ export function preinit(href: string, options: PreinitOptions) {
typeof options.as === 'string'
) {
const as = options.as;
const crossOrigin = getCrossOrigin(as, options.crossOrigin);
const crossOrigin = getCrossOriginStringAs(as, options.crossOrigin);
const integrity =
typeof options.integrity === 'string' ? options.integrity : undefined;
const fetchPriority =
Expand Down Expand Up @@ -296,38 +303,30 @@ export function preinitModule(href: string, options?: ?PreinitModuleOptions) {
}
const dispatcher = Dispatcher.current;
if (dispatcher && typeof href === 'string') {
if (
options == null ||
(typeof options === 'object' &&
(options.as == null || options.as === 'script'))
) {
const crossOrigin = options
? getCrossOrigin(undefined, options.crossOrigin)
: undefined;
dispatcher.preinitModuleScript(href, {
crossOrigin,
integrity:
options && typeof options.integrity === 'string'
? options.integrity
: undefined,
});
if (typeof options === 'object' && options !== null) {
if (options.as == null || options.as === 'script') {
const crossOrigin = getCrossOriginStringAs(
options.as,
options.crossOrigin,
);
dispatcher.preinitModuleScript(href, {
crossOrigin,
integrity:
typeof options.integrity === 'string'
? options.integrity
: undefined,
nonce: typeof options.nonce === 'string' ? options.nonce : undefined,
});
}
} else if (options == null) {
dispatcher.preinitModuleScript(href);
}
}
// We don't error because preinit needs to be resilient to being called in a variety of scopes
// and the runtime may not be capable of responding. The function is optimistic and not critical
// so we favor silent bailout over warning or erroring.
}

function getCrossOrigin(as: ?string, crossOrigin: ?string): ?CrossOriginEnum {
return as === 'font'
? ''
: typeof crossOrigin === 'string'
? crossOrigin === 'use-credentials'
? 'use-credentials'
: ''
: undefined;
}

function getValueDescriptorExpectingObjectForWarning(thing: any): string {
return thing === null
? '`null`'
Expand Down
Loading

0 comments on commit cc7fdfd

Please sign in to comment.