[Flight Parcel] Implement findSourceMapURL (#32294)

This implements `findSourceMapURL` in react-server-dom-parcel, enabling
source maps for replayed server errors on the client. It utilizes a new
endpoint in the Parcel dev server that returns the source map for a
given bundle/file. The error overlay UI has also been updated to handle
these stacks. See https://github.com/parcel-bundler/parcel/pull/10082

Also updated the fixture to the latest Parcel canary. A few APIs have
changed. We do have a higher level library wrapper now (`@parcel/rsc`
added in https://github.com/parcel-bundler/parcel/pull/10074) but I left
the fixture using the lower level APIs directly here since it is easier
to see how react-server-dom-parcel is used.
This commit is contained in:
Devon Govett 2025-02-04 14:17:13 -05:00 committed by GitHub
parent 0a82580bfc
commit f82c662b8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 638 additions and 586 deletions

View File

@ -1,4 +0,0 @@
{
"extends": "@parcel/config-default",
"runtimes": ["...", "@parcel/runtime-rsc"]
}

View File

@ -1,15 +1,11 @@
{
"name": "flight-parcel",
"private": true,
"workspaces": [
"examples/*"
],
"source": "src/server.tsx",
"server": "dist/server.js",
"targets": {
"server": {
"source": "src/server.tsx",
"context": "react-server",
"outputFormat": "commonjs",
"includeNodeModules": {
"express": false
}
@ -18,18 +14,11 @@
"scripts": {
"predev": "cp -r ../../build/oss-experimental/* ./node_modules/",
"prebuild": "cp -r ../../build/oss-experimental/* ./node_modules/",
"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",
"dev": "parcel",
"build": "parcel build",
"start": "node dist/server.js"
},
"@parcel/resolver-default": {
"packageExports": true
},
"dependencies": {
"@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,15 +26,11 @@
"@types/react-dom": "^19",
"concurrently": "^7.3.0",
"express": "^4.18.2",
"parcel": "2.0.0-dev.1793",
"parcel": "canary",
"process": "^0.11.10",
"react": "experimental",
"react-dom": "experimental",
"react-server-dom-parcel": "experimental",
"rsc-html-stream": "^0.0.4",
"ws": "^8.8.1"
},
"@parcel/bundler-default": {
"minBundleSize": 0
"rsc-html-stream": "^0.0.4"
}
}

View File

@ -2,7 +2,6 @@
import './client';
import './Todos.css';
import {Resources} from '@parcel/runtime-rsc';
import {Dialog} from './Dialog';
import {TodoDetail} from './TodoDetail';
import {TodoCreate} from './TodoCreate';
@ -13,7 +12,6 @@ export async function Todos({id}: {id?: number}) {
<html style={{colorScheme: 'dark light'}}>
<head>
<title>Todos</title>
<Resources />
</head>
<body>
<header>

View File

@ -72,7 +72,9 @@ async function render(
return ReactClient.use(data);
}
let htmlStream = await renderHTMLToReadableStream(<Content />);
let htmlStream = await renderHTMLToReadableStream(<Content />, {
bootstrapScriptContent: (Todos as any).bootstrapScript,
});
let response = htmlStream.pipeThrough(injectRSCPayload(s2));
Readable.fromWeb(response as NodeReadableStream).pipe(res);
} else {

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,17 @@ import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryR
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export type {TemporaryReferenceSet};
function findSourceMapURL(filename: string, environmentName: string) {
const devServer = parcelRequire.meta.devServer;
if (devServer != null) {
const qs = new URLSearchParams();
qs.set('filename', filename);
qs.set('env', environmentName);
return devServer + '/__parcel_source_map?' + qs.toString();
}
return null;
}
type CallServerCallback = <A, T>(id: string, args: A) => Promise<T>;
let callServer: CallServerCallback | null = null;
@ -57,6 +68,9 @@ export function createServerReference<A: Iterable<any>, T>(
return createServerReferenceImpl(
id + '#' + exportName,
callCurrentServerCallback,
undefined,
findSourceMapURL,
exportName,
);
}
@ -107,7 +121,7 @@ export function createFromReadableStream<T>(
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
undefined, // TODO: findSourceMapUrl
__DEV__ ? findSourceMapURL : undefined,
__DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
__DEV__ && options && options.environmentName
? options.environmentName
@ -131,7 +145,7 @@ export function createFromFetch<T>(
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
undefined, // TODO: findSourceMapUrl
__DEV__ ? findSourceMapURL : undefined,
__DEV__ ? (options ? options.replayConsoleLogs !== false : true) : false, // defaults to true
__DEV__ && options && options.environmentName
? options.environmentName

View File

@ -30,6 +30,17 @@ import type {TemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryR
export {createTemporaryReferenceSet} from 'react-client/src/ReactFlightTemporaryReferences';
export type {TemporaryReferenceSet};
function findSourceMapURL(filename: string, environmentName: string) {
const devServer = parcelRequire.meta.devServer;
if (devServer != null) {
const qs = new URLSearchParams();
qs.set('filename', filename);
qs.set('env', environmentName);
return devServer + '/__parcel_source_map?' + qs.toString();
}
return null;
}
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
@ -42,7 +53,13 @@ export function createServerReference<A: Iterable<any>, T>(
id: string,
exportName: string,
): (...A) => Promise<T> {
return createServerReferenceImpl(id + '#' + exportName, noServerCall);
return createServerReferenceImpl(
id + '#' + exportName,
noServerCall,
undefined,
findSourceMapURL,
exportName,
);
}
type EncodeFormActionCallback = <A>(
@ -69,7 +86,7 @@ function createResponseFromOptions(options?: Options) {
options && options.temporaryReferences
? options.temporaryReferences
: undefined,
undefined, // TODO: findSourceMapUrl
__DEV__ ? findSourceMapURL : undefined,
__DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
__DEV__ && options && options.environmentName
? options.environmentName

View File

@ -21,6 +21,17 @@ import {
import {createServerReference as createServerReferenceImpl} from 'react-client/src/ReactFlightReplyClient';
function findSourceMapURL(filename: string, environmentName: string) {
const devServer = parcelRequire.meta.devServer;
if (devServer != null) {
const qs = new URLSearchParams();
qs.set('filename', filename);
qs.set('env', environmentName);
return devServer + '/__parcel_source_map?' + qs.toString();
}
return null;
}
function noServerCall() {
throw new Error(
'Server Functions cannot be called during initial render. ' +
@ -33,7 +44,13 @@ export function createServerReference<A: Iterable<any>, T>(
id: string,
exportName: string,
): (...A) => Promise<T> {
return createServerReferenceImpl(id + '#' + exportName, noServerCall);
return createServerReferenceImpl(
id + '#' + exportName,
noServerCall,
undefined,
findSourceMapURL,
exportName,
);
}
type EncodeFormActionCallback = <A>(
@ -60,7 +77,7 @@ export function createFromNodeStream<T>(
options ? options.encodeFormAction : undefined,
options && typeof options.nonce === 'string' ? options.nonce : undefined,
undefined, // TODO: If encodeReply is supported, this should support temporaryReferences
undefined, // TODO: findSourceMapUrl
__DEV__ ? findSourceMapURL : undefined,
__DEV__ && options ? options.replayConsoleLogs === true : false, // defaults to false
__DEV__ && options && options.environmentName
? options.environmentName

View File

@ -109,6 +109,7 @@ declare var parcelRequire: {
extendImportMap: (importMap: {[string]: string}) => void,
meta: {
publicUrl: string,
devServer: string | null,
},
};