Skip to content

Commit 694d3e1

Browse files
authored
[Flight Parcel] Implement prepareDestinationForModule (facebook#31799)
Followup to facebook#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: facebook#31725 (comment)
1 parent c01b805 commit 694d3e1

11 files changed

+497
-453
lines changed

fixtures/flight-parcel/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"scripts": {
1919
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
2020
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
21-
"dev": "concurrently \"npm run dev:watch\" \"npm run dev:start\"",
21+
"dev": "concurrently \"npm run dev:watch\" \"sleep 2 && npm run dev:start\"",
2222
"dev:watch": "NODE_ENV=development parcel watch",
2323
"dev:start": "NODE_ENV=development node dist/server.js",
2424
"build": "parcel build",
@@ -28,16 +28,16 @@
2828
"packageExports": true
2929
},
3030
"dependencies": {
31-
"@parcel/config-default": "2.0.0-dev.1789",
32-
"@parcel/runtime-rsc": "2.13.3-dev.3412",
31+
"@parcel/config-default": "2.0.0-dev.1795",
32+
"@parcel/runtime-rsc": "2.13.3-dev.3418",
3333
"@types/parcel-env": "^0.0.6",
3434
"@types/express": "*",
3535
"@types/node": "^22.10.1",
3636
"@types/react": "^19",
3737
"@types/react-dom": "^19",
3838
"concurrently": "^7.3.0",
3939
"express": "^4.18.2",
40-
"parcel": "2.0.0-dev.1787",
40+
"parcel": "2.0.0-dev.1793",
4141
"process": "^0.11.10",
4242
"react": "experimental",
4343
"react-dom": "experimental",

fixtures/flight-parcel/src/server.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import {injectRSCPayload} from 'rsc-html-stream/server';
1515

1616
// Client dependencies, used for SSR.
1717
// These must run in the same environment as client components (e.g. same instance of React).
18-
import {createFromReadableStream} from 'react-server-dom-parcel/client' with {env: 'react-client'};
19-
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server' with {env: 'react-client'};
18+
import {createFromReadableStream} from 'react-server-dom-parcel/client.edge' with {env: 'react-client'};
19+
import {renderToReadableStream as renderHTMLToReadableStream} from 'react-dom/server.edge' with {env: 'react-client'};
2020
import ReactClient, {ReactElement} from 'react' with {env: 'react-client'};
2121

2222
// Page components. These must have "use server-entry" so they are treated as code splitting entry points.
@@ -66,8 +66,9 @@ async function render(
6666

6767
// Use client react to render the RSC payload to HTML.
6868
let [s1, s2] = stream.tee();
69-
let data = createFromReadableStream<ReactElement>(s1);
69+
let data: Promise<ReactElement>;
7070
function Content() {
71+
data ??= createFromReadableStream<ReactElement>(s1);
7172
return ReactClient.use(data);
7273
}
7374

fixtures/flight-parcel/types.d.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22

33
declare module 'react-server-dom-parcel/client' {
44
export function createFromFetch<T>(res: Promise<Response>): Promise<T>;
5-
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
65
export function encodeReply(value: any): Promise<string | URLSearchParams | FormData>;
76

87
type CallServerCallback = <T>(id: string, args: any[]) => Promise<T>;
98
export function setServerCallback(cb: CallServerCallback): void;
109
}
1110

11+
declare module 'react-server-dom-parcel/client.edge' {
12+
export function createFromReadableStream<T>(stream: ReadableStream): Promise<T>;
13+
}
14+
1215
declare module 'react-server-dom-parcel/server.edge' {
1316
export function renderToReadableStream(value: any): ReadableStream;
1417
export function loadServerAction(id: string): Promise<(...args: any[]) => any>;
@@ -17,5 +20,10 @@ declare module 'react-server-dom-parcel/server.edge' {
1720
}
1821

1922
declare module '@parcel/runtime-rsc' {
23+
import {JSX} from 'react';
2024
export function Resources(): JSX.Element;
2125
}
26+
27+
declare module 'react-dom/server.edge' {
28+
export * from 'react-dom/server';
29+
}

fixtures/flight-parcel/yarn.lock

Lines changed: 424 additions & 423 deletions
Large diffs are not rendered by default.

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser-parcel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
1313
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
1414
export * from 'react-client/src/ReactClientConsoleConfigBrowser';
1515
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
16+
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser';
1617
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1718
export const usedWithSSR = false;

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-parcel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
1313
export * from 'react-client/src/ReactFlightClientStreamConfigWeb';
1414
export * from 'react-client/src/ReactClientConsoleConfigServer';
1515
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
16+
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
1617
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1718
export const usedWithSSR = true;

packages/react-client/src/forks/ReactFlightClientConfig.dom-node-parcel.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export const rendererPackageName = 'react-server-dom-parcel';
1313
export * from 'react-client/src/ReactFlightClientStreamConfigNode';
1414
export * from 'react-client/src/ReactClientConsoleConfigServer';
1515
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel';
16+
export * from 'react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer';
1617
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1718
export const usedWithSSR = true;

packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js

Lines changed: 11 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {Thenable} from 'shared/ReactTypes';
1212
import type {ImportMetadata} from '../shared/ReactFlightImportMetadata';
1313

1414
import {ID, NAME, BUNDLES} from '../shared/ReactFlightImportMetadata';
15+
import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
1516

1617
export type ServerManifest = {
1718
[string]: Array<string>,
@@ -24,33 +25,22 @@ export type ServerReferenceId = string;
2425
export opaque type ClientReferenceMetadata = ImportMetadata;
2526

2627
// eslint-disable-next-line no-unused-vars
27-
export opaque type ClientReference<T> = {
28-
// Module id.
29-
id: string,
30-
// Export name.
31-
name: string,
32-
// List of bundle URLs, relative to the distDir.
33-
bundles: Array<string>,
34-
};
28+
export opaque type ClientReference<T> = ImportMetadata;
3529

3630
export function prepareDestinationForModule(
3731
moduleLoading: ModuleLoading,
3832
nonce: ?string,
3933
metadata: ClientReferenceMetadata,
4034
) {
41-
return;
35+
prepareDestinationWithChunks(moduleLoading, metadata[BUNDLES], nonce);
4236
}
4337

4438
export function resolveClientReference<T>(
4539
bundlerConfig: null,
4640
metadata: ClientReferenceMetadata,
4741
): ClientReference<T> {
4842
// Reference is already resolved during the build.
49-
return {
50-
id: metadata[ID],
51-
name: metadata[NAME],
52-
bundles: metadata[BUNDLES],
53-
};
43+
return metadata;
5444
}
5545

5646
export function resolveServerReference<T>(
@@ -64,20 +54,19 @@ export function resolveServerReference<T>(
6454
if (!bundles) {
6555
throw new Error('Invalid server action: ' + ref);
6656
}
67-
return {
68-
id,
69-
name,
70-
bundles,
71-
};
57+
return [id, name, bundles];
7258
}
7359

7460
export function preloadModule<T>(
7561
metadata: ClientReference<T>,
7662
): null | Thenable<any> {
77-
return Promise.all(metadata.bundles.map(url => parcelRequire.load(url)));
63+
if (metadata[BUNDLES].length === 0) {
64+
return null;
65+
}
66+
return Promise.all(metadata[BUNDLES].map(url => parcelRequire.load(url)));
7867
}
7968

8069
export function requireModule<T>(metadata: ClientReference<T>): T {
81-
const moduleExports = parcelRequire(metadata.id);
82-
return moduleExports[metadata.name];
70+
const moduleExports = parcelRequire(metadata[ID]);
71+
return moduleExports[metadata[NAME]];
8372
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ModuleLoading} from './ReactFlightClientConfigBundlerParcel';
11+
12+
export function prepareDestinationWithChunks(
13+
moduleLoading: ModuleLoading,
14+
bundles: Array<string>,
15+
nonce: ?string,
16+
) {
17+
// In the browser we don't need to prepare our destination since the browser is the Destination
18+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {ModuleLoading} from './ReactFlightClientConfigBundlerParcel';
11+
import {preinitModuleForSSR} from 'react-client/src/ReactFlightClientConfig';
12+
13+
export function prepareDestinationWithChunks(
14+
moduleLoading: ModuleLoading,
15+
bundles: Array<string>,
16+
nonce: ?string,
17+
) {
18+
for (let i = 0; i < bundles.length; i++) {
19+
preinitModuleForSSR(parcelRequire.meta.publicUrl + bundles[i], nonce);
20+
}
21+
}

scripts/flow/environment.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ declare const __turbopack_require__: ((id: string) => any) & {
106106
declare var parcelRequire: {
107107
(id: string): any,
108108
load: (url: string) => Promise<mixed>,
109+
meta: {
110+
publicUrl: string,
111+
},
109112
};
110113

111114
declare module 'fs/promises' {

0 commit comments

Comments
 (0)