[Flight Parcel] Implement prepareDestinationForModule (#31799)

Followup to #31725

This implements `prepareDestinationForModule` in the Parcel Flight
client. On the Parcel side, the `<Resources>` component now only inserts
`<link>` elements for stylesheets (along with a bootstrap script when
needed), and React is responsible for inserting scripts. This ensures
that components that are conditionally dynamic imported during render
are also preloaded.

CSS must be added to the RSC tree using `<Resources>` to avoid FOUC.
This must be manually rendered in both the top-level page, and in any
component that is dynamic imported. It would be nice if there was a way
for React to automatically insert CSS as well, but unfortunately
`prepareDestinationForModule` only knows about client components and not
CSS for server components. Perhaps there could be a way we could
annotate components at code splitting boundaries with the resources they
need? More thoughts in this thread:
https://github.com/facebook/react/pull/31725#discussion_r1884867607
This commit is contained in:
Devon Govett 2024-12-31 13:13:43 -05:00 committed by GitHub
parent c01b8058e6
commit 694d3e1aae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 482 additions and 438 deletions

View File

@ -18,7 +18,7 @@
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
"dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"",
"dev": "concurrently \"npm run dev:watch\" \"sleep 2 && npm run dev:start\"",
"dev:watch": "NODE_ENV=development parcel watch",
"dev:start": "NODE_ENV=development node dist/server.js",
"build": "parcel build",
@ -28,8 +28,8 @@
"packageExports": true
},
"dependencies": {
"@parcel/config-default": "2.0.0-dev.1789",
"@parcel/runtime-rsc": "2.13.3-dev.3412",
"@parcel/config-default": "2.0.0-dev.1795",
"@parcel/runtime-rsc": "2.13.3-dev.3418",
"@types/parcel-env": "^0.0.6",
"@types/express": "*",
"@types/node": "^22.10.1",
@ -37,7 +37,7 @@
"@types/react-dom": "^19",
"concurrently": "^7.3.0",
"express": "^4.18.2",
"parcel": "2.0.0-dev.1787",
"parcel": "2.0.0-dev.1793",
"process": "^0.11.10",
"react": "experimental",
"react-dom": "experimental",

View File

@ -15,8 +15,8 @@ import {injectRSCPayload} from 'rsc-html-stream/server';
// Client dependencies, used for SSR.
// These must run in the same environment as client components (e.g. same instance of React).
import {createFromReadableStream} from 'react-server-dom-parcel/client' with {env: 'react-client'};
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server' with {env: 'react-client'};
import {createFromReadableStream} from 'react-server-dom-parcel/client.edge' with {env: 'react-client'};
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge' with {env: 'react-client'};
import ReactClient, {ReactElement} from 'react' with {env: 'react-client'};
// Page components. These must have "use server-entry" so they are treated as code splitting entry points.
@ -66,8 +66,9 @@ async function render(
// Use client react to render the RSC payload to HTML.
let [s1, s2] = stream.tee();
let data = createFromReadableStream<ReactElement>(s1);
let data: Promise<ReactElement>;
function Content() {
data ??= createFromReadableStream<ReactElement>(s1);
return ReactClient.use(data);
}

View File

@ -2,13 +2,16 @@
declare module 'react-server-dom-parcel/client' {
export function createFromFetch<T>(res: Promise<Response>): Promise<T>;
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
export function encodeReply(value: any): Promise<string | URLSearchParams | FormData>;
type CallServerCallback = <T>(id: string, args: any[]) => Promise<T>;
export function setServerCallback(cb: CallServerCallback): void;
}
declare module 'react-server-dom-parcel/client.edge' {
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
}
declare module 'react-server-dom-parcel/server.edge' {
export function renderToReadableStream(value: any): ReadableStream;
export function loadServerAction(id: string): Promise<(...args: any[]) => any>;
@ -17,5 +20,10 @@ declare module 'react-server-dom-parcel/server.edge' {
}
declare module '@parcel/runtime-rsc' {
import {JSX} from 'react';
export function Resources(): JSX.Element;
}
declare module 'react-dom/server.edge' {
export * from 'react-dom/server';
}

File diff suppressed because it is too large Load Diff

View File

@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = false;

View File

@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;

View File

@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
export * from 'react-client/src/ReactClientConsoleConfigServer';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
export const usedWithSSR = true;

View File

@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes';
import type {ImportMetadata} from '../shared/ReactFlightImportMetadata';
import {ID, NAME, BUNDLES} from '../shared/ReactFlightImportMetadata';
import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
export type ServerManifest = {
[string]: Array<string>,
@ -24,21 +25,14 @@ export type ServerReferenceId = string;
export opaque type ClientReferenceMetadata = ImportMetadata;
// eslint-disable-next-line no-unused-vars
export opaque type ClientReference<T> = {
// Module id.
id: string,
// Export name.
name: string,
// List of bundle URLs, relative to the distDir.
bundles: Array<string>,
};
export opaque type ClientReference<T> = ImportMetadata;
export function prepareDestinationForModule(
moduleLoading: ModuleLoading,
nonce: ?string,
metadata: ClientReferenceMetadata,
) {
return;
prepareDestinationWithChunks(moduleLoading, metadata[BUNDLES], nonce);
}
export function resolveClientReference<T>(
@ -46,11 +40,7 @@ export function resolveClientReference<T>(
metadata: ClientReferenceMetadata,
): ClientReference<T> {
// Reference is already resolved during the build.
return {
id: metadata[ID],
name: metadata[NAME],
bundles: metadata[BUNDLES],
};
return metadata;
}
export function resolveServerReference<T>(
@ -64,20 +54,19 @@ export function resolveServerReference<T>(
if (!bundles) {
throw new Error('Invalid server action: ' + ref);
}
return {
id,
name,
bundles,
};
return [id, name, bundles];
}
export function preloadModule<T>(
metadata: ClientReference<T>,
): null | Thenable<any> {
return Promise.all(metadata.bundles.map(url => parcelRequire.load(url)));
if (metadata[BUNDLES].length === 0) {
return null;
}
return Promise.all(metadata[BUNDLES].map(url => parcelRequire.load(url)));
}
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata.id);
return moduleExports[metadata.name];
const moduleExports = parcelRequire(metadata[ID]);
return moduleExports[metadata[NAME]];
}

View File

@ -0,0 +1,18 @@
/**
* 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
*/
import type {ModuleLoading} from './ReactFlightClientConfigBundlerParcel';
export function prepareDestinationWithChunks(
moduleLoading: ModuleLoading,
bundles: Array<string>,
nonce: ?string,
) {
// In the browser we don't need to prepare our destination since the browser is the Destination
}

View File

@ -0,0 +1,21 @@
/**
* 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
*/
import type {ModuleLoading} from './ReactFlightClientConfigBundlerParcel';
import {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig';
export function prepareDestinationWithChunks(
moduleLoading: ModuleLoading,
bundles: Array<string>,
nonce: ?string,
) {
for (let i = 0; i < bundles.length; i++) {
preinitModuleForSSR(parcelRequire.meta.publicUrl + bundles[i], nonce);
}
}

View File

@ -106,6 +106,9 @@ declare const __turbopack_require__: ((id: string) => any) & {
declare var parcelRequire: {
(id: string): any,
load: (url: string) => Promise<mixed>,
meta: {
publicUrl: string,
},
};
declare module 'fs/promises' {