Skip to content
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

React Server Components #10043

Merged
merged 41 commits into from
Dec 22, 2024
Merged
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
22e4dbe
Evaluate import attributes
devongovett Dec 1, 2024
3fc1a7f
Add react-client and react-server environments
devongovett Dec 1, 2024
06a5a4f
Support "source" exports condition
devongovett Dec 1, 2024
4b7348c
Handle "use client" and "use server" directives
devongovett Dec 1, 2024
acec228
Support "use server-entry" to create isolated bundle groups
devongovett Dec 1, 2024
8cf1718
HMR
devongovett Dec 1, 2024
67353f9
Fix scope hoisted builds
devongovett Dec 2, 2024
fc7110b
Add import.meta.distDir and import.meta.publicUrl
devongovett Dec 2, 2024
a0b8bb4
RSC runtime
devongovett Dec 6, 2024
33eab53
RSC example
devongovett Dec 6, 2024
51a99fa
Register server references in client bundles correctly
devongovett Dec 8, 2024
69ab107
Don't execute module in HMR that haven't already loaded
devongovett Dec 8, 2024
c8cd3a9
Fix dependency collector bug
devongovett Dec 8, 2024
b8b44b6
Ignore of ws module is not available in HMR runtime
devongovett Dec 8, 2024
8aac352
Fix bundle graph check for async bundles
devongovett Dec 8, 2024
a160634
Fix library bundler for bundle graph change
devongovett Dec 9, 2024
86d5195
Handle internalized async dependencies
devongovett Dec 9, 2024
4eb2f66
Don't remove type=module in development to avoid multiple copies of a…
devongovett Dec 9, 2024
9ea9318
prettier
devongovett Dec 9, 2024
a256737
Resolve react relative to app
devongovett Dec 9, 2024
24f9c46
Use react-server-dom-parcel from npm
devongovett Dec 9, 2024
61f02c2
Fix default node engines in test
devongovett Dec 9, 2024
561beac
Allow parallel bundles with different contexts from the root bundle b…
devongovett Dec 10, 2024
d9b984f
Don't transpile dynamic imports in TS plugins
devongovett Dec 10, 2024
215dd5e
Remove error that "Only browser targets are supported in serve mode"
devongovett Dec 10, 2024
0fa0008
Use parcelRequire.load to load bundles
devongovett Dec 10, 2024
c0b0ee6
lint
devongovett Dec 10, 2024
e0a86c2
fix test
devongovett Dec 10, 2024
b1c3ad4
Bump react-server-dom-parcel
devongovett Dec 10, 2024
0b82b13
format
devongovett Dec 10, 2024
f12dac0
Bump react to 19
devongovett Dec 16, 2024
07f24a8
Replace typeof process with 'undefined' to avoid adding polyfill for …
devongovett Dec 16, 2024
1f7c98d
normalize publicUrl
devongovett Dec 16, 2024
1cb5ae5
treat context changes async bundles
devongovett Dec 16, 2024
6c9a184
refactor to let react insert scripts
devongovett Dec 16, 2024
9f297b1
add tests
devongovett Dec 16, 2024
6a13048
Improve inference for targets in serve mode
devongovett Dec 22, 2024
4e7c3a4
Bundle node_modules in the react-server context for now
devongovett Dec 22, 2024
4e0b155
fixes
devongovett Dec 22, 2024
93be6cf
Merge branch 'v2' of github.com:parcel-bundler/parcel into rsc2
devongovett Dec 22, 2024
1f5f005
update versions
devongovett Dec 22, 2024
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
Prev Previous commit
Next Next commit
refactor to let react insert scripts
devongovett committed Dec 16, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 6c9a18452b64afdbe5095f3ad13f84426bc627fc
2 changes: 1 addition & 1 deletion packages/examples/react-server-components/.parcelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"extends": "@parcel/config-default",
"runtimes": ["...", "@parcel/runtime-rsc"]
"runtimes": ["@parcel/runtime-rsc", "..."]
}
8 changes: 5 additions & 3 deletions packages/examples/react-server-components/src/server.tsx
Original file line number Diff line number Diff line change
@@ -6,8 +6,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 from 'react' with {env: 'react-client'};

// Page components. These must have "use server-entry" so they are treated as code splitting entry points.
@@ -81,8 +81,10 @@ async function render(req, res, component, actionResult) {

// Use client react to render the RSC payload to HTML.
let [s1, s2] = stream.tee();
let data = createFromReadableStream(s1);
let data;
function Content() {
// Important: this must be constructed inside a component for preinit scripts to be inserted.
data ??= createFromReadableStream(s1);
return ReactClient.use(data);
}

2 changes: 1 addition & 1 deletion packages/packagers/js/src/index.js
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ export default (new Packager({
},
);

let name = packageName?.contents?.name ?? '';
let name = packageName?.contents ?? '';
return {
parcelRequireName: 'parcelRequire' + hashString(name).slice(-4),
unstable_asyncBundleRuntime: Boolean(
4 changes: 3 additions & 1 deletion packages/runtimes/rsc/package.json
Original file line number Diff line number Diff line change
@@ -18,14 +18,16 @@
"react-server": "./resources.js",
"source": "./src/RSCRuntime.js",
"default": "./lib/RSCRuntime.js"
}
},
"./resources": "./resources.js"
},
"engines": {
"node": ">= 12.0.0",
"parcel": "^2.8.3"
},
"dependencies": {
"@parcel/plugin": "2.13.2",
"@parcel/rust": "2.13.2",
"@parcel/utils": "2.13.2",
"nullthrows": "^1.1.1"
}
232 changes: 142 additions & 90 deletions packages/runtimes/rsc/src/RSCRuntime.js
Original file line number Diff line number Diff line change
@@ -2,12 +2,32 @@

import {Runtime} from '@parcel/plugin';
import nullthrows from 'nullthrows';
import {urlJoin} from '@parcel/utils';
import {urlJoin, normalizeSeparators} from '@parcel/utils';
import path from 'path';
import {hashString} from '@parcel/rust';

export default (new Runtime({
apply({bundle, bundleGraph}) {
if (bundle.type !== 'js') {
async loadConfig({config, options}) {
// This logic must be synced with the packager...
let packageName = await config.getConfigFrom(
options.projectRoot + '/index',
[],
{
packageKey: 'name',
},
);

let name = packageName?.contents ?? '';
return {
parcelRequireName: 'parcelRequire' + hashString(name).slice(-4),
};
},
apply({bundle, bundleGraph, config}) {
if (
bundle.type !== 'js' ||
(bundle.env.context !== 'react-server' &&
bundle.env.context !== 'react-client')
) {
return [];
}

@@ -24,37 +44,42 @@ export default (new Runtime({
Array.isArray(directives) &&
directives.includes('use client')
) {
let usedSymbols = nullthrows(
bundleGraph.getUsedSymbols(resolvedAsset),
);
if (usedSymbols.has('*')) {
// TODO
let browserBundles;
let async = bundleGraph.resolveAsyncDependency(node.value, bundle);
if (async?.type === 'bundle_group') {
browserBundles = bundleGraph
.getBundlesInBundleGroup(async.value)
.filter(b => b.type === 'js' && b.env.isBrowser())
.map(b => normalizeSeparators(b.name));
} else {
browserBundles = bundleGraph
.getReferencedBundles(bundle)
.filter(b => b.type === 'js' && b.env.isBrowser())
.map(b => normalizeSeparators(b.name));
}

let browserBundles = bundleGraph
.getReferencedBundles(bundle)
.filter(b => b.type === 'js' && b.env.isBrowser())
.map(b => b.name);

let code = `import {createClientReference} from "react-server-dom-parcel/server.edge";\n`;
for (let symbol of usedSymbols) {
let resolved = bundleGraph.getSymbolResolution(
resolvedAsset,
symbol,
);
for (let symbol of bundleGraph.getExportedSymbols(
resolvedAsset,
bundle,
)) {
code += `exports[${JSON.stringify(
symbol,
symbol.exportAs,
)}] = createClientReference(${JSON.stringify(
bundleGraph.getAssetPublicId(resolved.asset),
)}, ${JSON.stringify(resolved.exportSymbol)}, ${JSON.stringify(
bundleGraph.getAssetPublicId(symbol.asset),
)}, ${JSON.stringify(symbol.exportSymbol)}, ${JSON.stringify(
browserBundles,
)});\n`;
}

code += `exports.__esModule = true;\n`;

if (node.value.priority === 'lazy') {
code += 'module.exports = Promise.resolve(exports);\n';
}

runtimes.push({
filePath: resolvedAsset.filePath,
filePath: replaceExtension(resolvedAsset.filePath),
code,
dependency: node.value,
env: {sourceType: 'module'},
@@ -66,56 +91,57 @@ export default (new Runtime({
Array.isArray(directives) &&
directives.includes('use server')
) {
let usedSymbols = nullthrows(
bundleGraph.getUsedSymbols(resolvedAsset),
);
if (usedSymbols.has('*')) {
// TODO
}

let code;
if (node.value.env.isServer()) {
// Dependency on a "use server" module from a server environment.
// Mark each export as a server reference that can be passed to a client component as a prop.
code = `import {registerServerReference} from "react-server-dom-parcel/server.edge";\n`;
for (let symbol of usedSymbols) {
let resolved = bundleGraph.getSymbolResolution(
resolvedAsset,
symbol,
);
let publicId = JSON.stringify(
bundleGraph.getAssetPublicId(resolved.asset),
);
let name = JSON.stringify(resolved.exportSymbol);
code += `exports[${JSON.stringify(
symbol,
)}] = registerServerReference(function() {
let originalModule = parcelRequire(${publicId});
let fn = originalModule[${name}];
return fn.apply(this, arguments);
}, ${publicId}, ${name});\n`;
}
let publicId = JSON.stringify(
bundleGraph.getAssetPublicId(resolvedAsset),
);
code += `let originalModule = parcelRequire(${publicId});\n`;
code += `for (let key in originalModule) {\n`;
code += ` Object.defineProperty(exports, key, {\n`;
code += ` enumerable: true,\n`;
code += ` get: () => {\n`;
code += ` let value = originalModule[key];\n`;
code += ` if (typeof value === 'function' && !value.$$typeof) {\n`;
code += ` registerServerReference(value, ${publicId}, key);\n`;
code += ` }\n`;
code += ` return value;\n`;
code += ` }\n`;
code += ` });\n`;
code += `}\n`;
} else {
// Dependency on a "use server" module from a client environment.
// Create a client proxy module that will call the server.
code = `import {createServerReference} from "react-server-dom-parcel/client";\n`;
for (let symbol of usedSymbols) {
let resolved = bundleGraph.getSymbolResolution(
resolvedAsset,
symbol,
);
let usedSymbols = bundleGraph.getUsedSymbols(resolvedAsset);
if (usedSymbols?.has('*')) {
usedSymbols = null;
}
for (let symbol of bundleGraph.getExportedSymbols(
resolvedAsset,
bundle,
)) {
if (usedSymbols && !usedSymbols.has(symbol.exportAs)) {
continue;
}
code += `exports[${JSON.stringify(
symbol,
symbol.exportAs,
)}] = createServerReference(${JSON.stringify(
bundleGraph.getAssetPublicId(resolved.asset),
)}, ${JSON.stringify(resolved.exportSymbol)});\n`;
bundleGraph.getAssetPublicId(symbol.asset),
)}, ${JSON.stringify(symbol.exportSymbol)});\n`;
}
}

code += `exports.__esModule = true;\n`;
if (node.value.priority === 'lazy') {
code += 'module.exports = Promise.resolve(exports);\n';
}

runtimes.push({
filePath: resolvedAsset.filePath,
filePath: replaceExtension(resolvedAsset.filePath),
code,
dependency: node.value,
env: {sourceType: 'module'},
@@ -131,64 +157,68 @@ export default (new Runtime({
) {
// Resolve to an empty module so the client entry does not run on the server.
runtimes.push({
filePath: resolvedAsset.filePath,
filePath: replaceExtension(resolvedAsset.filePath),
code: '',
dependency: node.value,
env: {sourceType: 'module'},
});

// Server dependency on a Resources component.
// Dependency on a Resources component.
} else if (
node.value.env.isServer() &&
node.value.specifier === '@parcel/runtime-rsc'
node.value.specifier === '@parcel/runtime-rsc' ||
node.value.specifier === '@parcel/runtime-rsc/resources'
) {
// Generate a component that renders scripts and stylesheets referenced by the bundle.
// Generate a component that renders link tags for stylesheets referenced by the bundle.
let bundles = bundleGraph.getReferencedBundles(bundle);
let code =
'import React from "react";\nexport function Resources() {\n return <>\n';
let entry;
let imports = '';
for (let b of bundles) {
if (!b.env.isBrowser()) {
continue;
}
let url = urlJoin(b.target.publicUrl, b.name);
if (b.type === 'css') {
code += ` <link rel="stylesheet" href=${JSON.stringify(
code += `<link rel="stylesheet" href=${JSON.stringify(
url,
)} precedence="default" />\n`;
} else if (b.type === 'js') {
code += ` <script type="module" src=${JSON.stringify(
url,
)} />\n`;
imports += `import ${JSON.stringify(url)};`;
}
b.traverseAssets((a, ctx, actions) => {
if (
Array.isArray(a.meta.directives) &&
a.meta.directives.includes('use client-entry')
) {
entry = a;
actions.stop();
}
});
}

code += ' </>;\n}\n';
// React will insert async script tags for client components to preinit them asap.
// Add an inline script element to bootstrap the page, by calling parcelRequire for the client-entry module.
// We use import statements to wait for the dependent bundles to load.
if (entry) {
code += `<script type="module">${imports}${
nullthrows(config).parcelRequireName
}(${JSON.stringify(
bundleGraph.getAssetPublicId(entry),
)})</script>\n`;
}

code += '</>;\n}\n';

let filePath = nullthrows(node.value.sourcePath);
let ext = path.extname(filePath);
runtimes.push({
filePath: filePath.slice(0, -ext.length) + '.jsx',
filePath: replaceExtension(filePath),
code,
dependency: node.value,
env: {sourceType: 'module'},
shouldReplaceResolution: true,
});
}

// Dependency on a client entry asset.
} else if (
Array.isArray(node.value.meta.directives) &&
node.value.meta.directives.includes('use client-entry')
) {
// Add as a conditional entry, when running on the client (not during SSR).
runtimes.push({
filePath: __filename,
code: `if (typeof document !== 'undefined') {
parcelRequire(${JSON.stringify(bundleGraph.getAssetPublicId(node.value))})
}`,
env: {sourceType: 'module'},
isEntry: true,
});
}
});

@@ -197,9 +227,7 @@ parcelRequire(${JSON.stringify(bundleGraph.getAssetPublicId(node.value))})
bundle.env.isServer() &&
bundleGraph.getParentBundles(bundle).length === 0
) {
let code =
'import {registerServerActions} from "react-server-dom-parcel/server.edge";\n';
code += `registerServerActions({\n`;
let serverActions = '';
bundleGraph.traverse(node => {
if (
node.type === 'asset' &&
@@ -211,21 +239,40 @@ parcelRequire(${JSON.stringify(bundleGraph.getAssetPublicId(node.value))})
let referenced = bundleGraph.getReferencedBundles(
bundlesWithAsset[0],
);
bundles.add(bundlesWithAsset[0].name);
bundles.add(normalizeSeparators(bundlesWithAsset[0].name));
for (let r of referenced) {
if (r.type === 'js' && r.env.context === bundle.env.context) {
bundles.add(r.name);
bundles.add(normalizeSeparators(r.name));
}
}
code += ` ${JSON.stringify(
serverActions += ` ${JSON.stringify(
bundleGraph.getAssetPublicId(node.value),
)}: ${JSON.stringify([...bundles])},\n`;
}
});

code += '});\n';
let code = '';
if (serverActions.length > 0) {
code +=
'import {registerServerActions} from "react-server-dom-parcel/server.edge";\n';
code += `registerServerActions({\n`;
code += serverActions;
code += '});\n';
}

// React needs AsyncLocalStorage defined as a global for the edge environment.
// Without this, preinit scripts won't be inserted during SSR.
code += 'if (typeof AsyncLocalHooks === "undefined") {\n';
code += ' try {\n';
code +=
' globalThis.AsyncLocalStorage = require("node:async_hooks").AsyncLocalStorage;\n';
code += ' } catch {}\n';
code += '}\n';

runtimes.push({
filePath: bundle.getMainEntry()?.filePath ?? __filename,
filePath: replaceExtension(
bundle.getMainEntry()?.filePath ?? __filename,
),
code,
isEntry: true,
env: {sourceType: 'module'},
@@ -235,3 +282,8 @@ parcelRequire(${JSON.stringify(bundleGraph.getAssetPublicId(node.value))})
return runtimes;
},
}): Runtime);

function replaceExtension(filePath, extension = '.jsx') {
let ext = path.extname(filePath);
return filePath.slice(0, -ext.length) + extension;
}
15 changes: 12 additions & 3 deletions packages/transformers/js/src/JSTransformer.js
Original file line number Diff line number Diff line change
@@ -707,7 +707,7 @@ export default (new Transformer({
asset.setEnvironment({
context: 'react-server',
sourceType: 'module',
outputFormat: 'esmodule',
outputFormat: 'commonjs',
engines: asset.env.engines,
includeNodeModules: false,
isLibrary: false,
@@ -724,6 +724,11 @@ export default (new Transformer({
asset.bundleBehavior = 'isolated';
}

// Server actions must always be wrapped so they can be parcelRequired.
if (directives.includes('use server')) {
asset.meta.shouldWrap = true;
}

for (let dep of dependencies) {
if (dep.kind === 'WebWorker') {
// Use native ES module output if the worker was created with `type: 'module'` and all targets
@@ -895,7 +900,7 @@ export default (new Transformer({
env = {
...env,
context: 'react-server',
outputFormat: 'esmodule',
outputFormat: 'commonjs',
};
} else if (dep.attributes?.env === 'react-client') {
env = {
@@ -904,6 +909,10 @@ export default (new Transformer({
outputFormat: 'esmodule',
includeNodeModules: true,
};

// This is a hack to prevent creating unnecessary shared bundles between actual client code
// and server code that runs in the client environment (e.g. react).
asset.isBundleSplittable = false;
}

asset.addDependency({
@@ -1030,7 +1039,7 @@ export default (new Transformer({

asset.meta.hasCJSExports = hoist_result.has_cjs_exports;
asset.meta.staticExports = hoist_result.static_cjs_exports;
asset.meta.shouldWrap = hoist_result.should_wrap;
asset.meta.shouldWrap ||= hoist_result.should_wrap;
} else {
if (symbol_result) {
let deps = new Map(