mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
[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:
parent
c01b8058e6
commit
694d3e1aae
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
10
fixtures/flight-parcel/types.d.ts
vendored
10
fixtures/flight-parcel/types.d.ts
vendored
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]];
|
||||
}
|
||||
|
|
|
|||
18
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js
vendored
Normal file
18
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelBrowser.js
vendored
Normal 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
|
||||
}
|
||||
21
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js
vendored
Normal file
21
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigTargetParcelServer.js
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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' {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user