@samitouri / QOS-React / commits / 37906d4dfb

[Flight Parcel] Pass import maps through client references (#32132)

Corresponding Parcel PR: https://github.com/parcel-bundler/parcel/pull/10073 Parcel avoids [cascading cache invalidation](https://philipwalton.com/articles/cascading-cache-invalidation/) by injecting a bundle manifest containing a mapping of stable bundle ids to hashed URLs. When using an HTML entry point, this is done (as of the above PR) via a native import map. This means that if a bundle's hash changes, only that bundle will be invalidated (plus the HTML itself which typically has a short caching policy), not any other bundles that reference it. For RSCs, we cannot currently use native import maps because of client side navigations, where a new HTML file is not requested. Eventually, multiple `<script type="importmap">` elements will be supported (https://github.com/whatwg/html/pull/10528) ([coming Chrome 133](https://chromestatus.com/feature/5121916248260608)), at which point React could potentially inject them. In the meantime, I've added some APIs to Parcel to polyfill this. With this change, an import map can be sent along with a client reference, containing a mapping for any dynamic imports and URL dependencies (e.g. images) that are referenced by the JS bundles. On the client, the import map is extended with these new mappings prior to executing the referenced bundles. This preserves the caching advantages described above while supporting client navigations.

Devon Govett committed Jan 27, 2025 at 12:39 UTC 37906d4dfbe80d71f312f7347bb9ddb930484d28
5 files changed +32 -4
packages/react-server-dom-parcel/src/ReactFlightParcelReferences.js
+3
@@ -22,6 +22,7 @@ export type ClientReference<T> = {
22 $$id: string,
23 $$name: string,
24 $$bundles: Array<string>,
25 + $$importMap?: ?{[string]: string},
26 };
27
28 const CLIENT_REFERENCE_TAG = Symbol.for('react.client.reference');
@@ -39,12 +40,14 @@ export function createClientReference<T>(
40 id: string,
41 exportName: string,
42 bundles: Array<string>,
43 + importMap?: ?{[string]: string},
44 ): ClientReference<T> {
45 return {
46 $$typeof: CLIENT_REFERENCE_TAG,
47 $$id: id,
48 $$name: exportName,
49 $$bundles: bundles,
50 + $$importMap: importMap,
51 };
52 }
53
packages/react-server-dom-parcel/src/client/ReactFlightClientConfigBundlerParcel.js
+11 -1
@@ -11,7 +11,12 @@ import type {Thenable} from 'shared/ReactTypes';
11
12 import type {ImportMetadata} from '../shared/ReactFlightImportMetadata';
13
14 -import {ID, NAME, BUNDLES} from '../shared/ReactFlightImportMetadata';
14 +import {
15 + ID,
16 + NAME,
17 + BUNDLES,
18 + IMPORT_MAP,
19 +} from '../shared/ReactFlightImportMetadata';
20 import {prepareDestinationWithChunks} from 'react-client/src/ReactFlightClientConfig';
21
22 export type ServerManifest = {
@@ -60,9 +65,14 @@ export function resolveServerReference<T>(
65 export function preloadModule<T>(
66 metadata: ClientReference<T>,
67 ): null | Thenable<any> {
68 + if (metadata[IMPORT_MAP]) {
69 + parcelRequire.extendImportMap(metadata[IMPORT_MAP]);
70 + }
71 +
72 if (metadata[BUNDLES].length === 0) {
73 return null;
74 }
75 +
76 return Promise.all(metadata[BUNDLES].map(url => parcelRequire.load(url)));
77 }
78
packages/react-server-dom-parcel/src/server/ReactFlightServerConfigParcelBundler.js
+9
@@ -37,6 +37,15 @@ export function resolveClientReferenceMetadata<T>(
37 config: ClientManifest,
38 clientReference: ClientReference<T>,
39 ): ClientReferenceMetadata {
40 + if (clientReference.$$importMap) {
41 + return [
42 + clientReference.$$id,
43 + clientReference.$$name,
44 + clientReference.$$bundles,
45 + clientReference.$$importMap,
46 + ];
47 + }
48 +
49 return [
50 clientReference.$$id,
51 clientReference.$$name,
packages/react-server-dom-parcel/src/shared/ReactFlightImportMetadata.js
+8 -3
@@ -10,11 +10,16 @@
10 // This is the parsed shape of the wire format which is why it is
11 // condensed to only the essentialy information
12 export type ImportMetadata = [
13 - /* id */ string,
14 - /* name */ string,
15 - /* bundles */ Array<string>,
13 + // eslint does not understand Flow tuple syntax.
14 + /* eslint-disable */
15 + id: string,
16 + name: string,
17 + bundles: Array<string>,
18 + importMap?: {[string]: string},
19 + /* eslint-enable */
20 ];
21
22 export const ID = 0;
23 export const NAME = 1;
24 export const BUNDLES = 2;
25 +export const IMPORT_MAP = 3;
scripts/flow/environment.js
+1
@@ -106,6 +106,7 @@ declare const __turbopack_require__: ((id: string) => any) & {
106 declare var parcelRequire: {
107 (id: string): any,
108 load: (url: string) => Promise<mixed>,
109 + extendImportMap: (importMap: {[string]: string}) => void,
110 meta: {
111 publicUrl: string,
112 },