Skip to content

[Flight Parcel] Implement prepareDestinationForModule #31799

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW this took me a long time to debug why preinits weren't working. This must be initialized during render so the current request is set in AsyncLocalStorage. Might be nice if there was some kind of error/warning instead of silently not inserting any scripts.

Copy link
Collaborator

@sebmarkbage sebmarkbage Dec 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An issue is that it's legit to have an optimistic preload or preinit in module scope. E.g. the first time you load a module on the client. The silent ignoring allows those same modules to keep working with SSR or RSC albeit without the preload.

However, this is also related to another issue that it should be possible to consume values from the RSC stream (like formState) before SSR:ing the payload. It's also useful for other routing concerns. You can see this awkwardness in the fixtures/flight where it has to parse it twice to deal with the cycle of somethings having to be outside the render and somethings inside.

This is also related to console replaying when doing RSC to RSC since the replays have to be in the scope that needs to replay.

Another case is expanding the React.cache scope to longer than just one RSC render.

We have plans to add something that allows expanding the scopes for these AsyncLocalStorage contexts. It's just a little unclear the exact shape atm.

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
Loading