Skip to content

Commit

Permalink
[Flight Parcel] Implement prepareDestinationForModule (#31799)
Browse files Browse the repository at this point in the history
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:
#31725 (comment)
  • Loading branch information
devongovett authored Dec 31, 2024
1 parent c01b805 commit 694d3e1
Show file tree
Hide file tree
Showing 11 changed files with 497 additions and 453 deletions.
8 changes: 4 additions & 4 deletions fixtures/flight-parcel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -28,16 +28,16 @@
"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",
"@types/react": "^19",
"@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",
Expand Down
7 changes: 4 additions & 3 deletions fixtures/flight-parcel/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down
10 changes: 9 additions & 1 deletion fixtures/flight-parcel/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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';
}
847 changes: 424 additions & 423 deletions fixtures/flight-parcel/yarn.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand All @@ -24,33 +25,22 @@ 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>(
bundlerConfig: null,
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>(
Expand All @@ -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]];
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions scripts/flow/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' {
Expand Down

0 comments on commit 694d3e1

Please sign in to comment.