Skip to content

Commit

Permalink
Automatically inject CSS resources into RSC stream (#10067)
Browse files Browse the repository at this point in the history
  • Loading branch information
devongovett authored Jan 7, 2025
1 parent 337c31c commit e3a2b72
Show file tree
Hide file tree
Showing 29 changed files with 1,109 additions and 285 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ packages/*/*/test/dist
packages/*/*/test/mochareporters.json
packages/*/*/test/fixtures
packages/*/*/test/.parcel-cache
packages/examples/react-server-components/src/server.js
vendor
tmp
.parcel-cache
Expand Down
16 changes: 11 additions & 5 deletions packages/core/integration-tests/test/output-formats.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,12 +552,18 @@ describe('output formats', function () {
};

let out = [];
await run(b, {
require: () => external,
output(o) {
out.push(o);
await run(
b,
{
output(o) {
out.push(o);
},
},
});
{},
{
external: () => external,
},
);

assert.deepEqual(out, [1, 2]);
});
Expand Down
177 changes: 98 additions & 79 deletions packages/core/integration-tests/test/react-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,8 @@ describe('react server components', function () {
targets: {
default: {
context: 'react-server',
outputFormat: 'commonjs',
},
},
'@parcel/resolver-default': {
packageExports: true,
},
'@parcel/bundler-default': {
minBundleSize: 0,
},
}),
);

Expand Down Expand Up @@ -383,7 +376,6 @@ describe('react server components', function () {
'client.jsx',
'react-jsx-dev-runtime.development.js',
'index.js',
'jsx-dev-runtime.js',
'react.development.js',
],
},
Expand Down Expand Up @@ -487,7 +479,6 @@ describe('react server components', function () {
assets: [
'client.jsx',
'index.js',
'jsx-dev-runtime.js',
'react-jsx-dev-runtime.development.js',
'react.development.js',
],
Expand Down Expand Up @@ -610,7 +601,6 @@ describe('react server components', function () {
// Client: shared bundle.
assets: [
'index.js',
'jsx-dev-runtime.js',
'react-jsx-dev-runtime.development.js',
'react.development.js',
],
Expand All @@ -620,32 +610,27 @@ describe('react server components', function () {
);
});

it('should support <Resources>', async function () {
it('should support inject CSS resources', async function () {
await fsFixture(overlayFS, dir)`
index.jsx:
import {Server} from './Page.jsx';
function render() {
return <Server />;
}
output = {render};
Page.jsx:
import {Client} from './Client';
import {Resources} from '@parcel/runtime-rsc';
"use server-entry";
import './server.css';
export function Server() {
return <><Resources /><Client /></>;
return <h1>Server</h1>;
}
output = {Resources};
server.css:
.server { color: red }
Client.jsx:
"use client";
import './client.css';
export function Client() {
return <p>Client</p>;
}
client.css:
.client { color: green }
`;

let b = await bundle(path.join(dir, '/Page.jsx'), {
let b = await bundle(path.join(dir, '/index.jsx'), {
inputFS: overlayFS,
targets: ['default'],
defaultTargetOptions: {
Expand All @@ -657,22 +642,24 @@ describe('react server components', function () {
b,
[
{
assets: ['Page.jsx', 'resources.js'],
assets: ['index.jsx'],
},
{
assets: ['Client.jsx'],
assets: ['Page.jsx'],
},
{
assets: ['server.css', 'client.css'],
assets: ['server.css'],
},
],
{skipNodeModules: true},
);

let res = (await run(b, null, {require: false})).output;
let resources = res.Resources();
let res = (await run(b, {output: null}, {require: false})).output;
let output = res.render();

let link = resources.props.children;
output.type.$$typeof;
let rendered = output.type();
let link = rendered.props.children[0];
assert.equal(link.type, 'link');
assert.equal(link.props.rel, 'stylesheet');
assert.equal(link.props.precedence, 'default');
Expand All @@ -687,14 +674,17 @@ describe('react server components', function () {

it('should generate an inline script for bootstrap with "use client-entry"', async function () {
await fsFixture(overlayFS, dir)`
index.jsx:
import {Server} from './Page';
output = {Server};
Page.jsx:
"use server-entry";
import {Client} from './Client';
import {Resources} from '@parcel/runtime-rsc';
import './client-entry.jsx';
export function Server() {
return <><Resources /><Client /></>;
return <Client />;
}
output = {Resources};
client-entry.jsx:
"use client-entry";
Expand All @@ -707,7 +697,7 @@ describe('react server components', function () {
}
`;

let b = await bundle(path.join(dir, '/Page.jsx'), {
let b = await bundle(path.join(dir, '/index.jsx'), {
inputFS: overlayFS,
targets: ['default'],
defaultTargetOptions: {
Expand All @@ -719,7 +709,10 @@ describe('react server components', function () {
b,
[
{
assets: ['Page.jsx', 'resources.js'],
assets: ['index.jsx'],
},
{
assets: ['Page.jsx'],
},
{
assets: ['client-entry.jsx', 'Client.jsx'],
Expand All @@ -729,12 +722,11 @@ describe('react server components', function () {
);

let res = await run(b, null, {require: false});
let resources = res.output.Resources();
let parcelRequireName = nullthrows(
Object.keys(res).find(k => /^parcelRequire(.+)$/.test(k)),
);
let clientEntry;
b.getBundles()[1].traverseAssets(a => {
b.getBundles()[2].traverseAssets(a => {
if (
Array.isArray(a.meta.directives) &&
a.meta.directives.includes('use client-entry')
Expand All @@ -743,41 +735,35 @@ describe('react server components', function () {
}
});

let script = resources.props.children;
assert.equal(script.type, 'script');
assert.equal(script.props.type, 'module');
assert.equal(
script.props.children,
`import "/${path.basename(
b.getBundles()[1].filePath,
)}";${parcelRequireName}("${b.getAssetPublicId(
res.output.Server.bootstrapScript,
`Promise.all([import("/${path.basename(
b.getBundles()[2].filePath,
)}")]).then(()=>${parcelRequireName}("${b.getAssetPublicId(
nullthrows(clientEntry),
)}")`,
)}"))`,
);
});

it('dynamic import works with client components', async function () {
it('dynamic import of server component to client component', async function () {
await fsFixture(overlayFS, dir)`
Page.jsx:
import {Resources} from '@parcel/runtime-rsc/resources';
import './server.css';
export async function Server() {
let {Client} = await import('./Client');
return [<Resources />, <Client />];
return <Client />;
}
output = {Server, Resources};
output = {Server};
server.css:
.server { color: red }
Client.jsx:
"use client";
import {Resources} from '@parcel/runtime-rsc/resources';
import './client.css';
export function Client() {
return <p><Resources />Client</p>;
return <p>Client</p>;
}
output = {Resources};
client.css:
.client { color: green }
Expand All @@ -795,10 +781,10 @@ describe('react server components', function () {
b,
[
{
assets: ['Page.jsx', 'resources.js'],
assets: ['Page.jsx'],
},
{
assets: ['Client.jsx', 'resources.js'],
assets: ['Client.jsx'],
},
{
assets: ['server.css'],
Expand All @@ -811,19 +797,20 @@ describe('react server components', function () {
);

let res = (await run(b, null, {require: false})).output;
let result = await res.Server();
let output = await res.Server();
output.type.$$typeof;
let result = output.type();
assert.equal(
result[1].type.$$typeof,
result.props.children[1].type.$$typeof,
Symbol.for('react.client.reference'),
);
assert.equal(result[1].type.$$name, 'Client');
assert.equal(typeof result[1].type.$$id, 'string');
assert.deepEqual(result[1].type.$$bundles, [
assert.equal(result.props.children[1].type.$$name, 'Client');
assert.equal(typeof result.props.children[1].type.$$id, 'string');
assert.deepEqual(result.props.children[1].type.$$bundles, [
path.basename(b.getBundles()[1].filePath),
]);

let resources = res.Resources();
let link = resources.props.children;
let link = result.props.children[0];
assert.equal(link.type, 'link');
assert.equal(link.props.rel, 'stylesheet');
assert.equal(link.props.precedence, 'default');
Expand All @@ -834,31 +821,61 @@ describe('react server components', function () {
nullthrows(
b
.getBundles()
.find(b => b.type === 'css' && b.name.startsWith('Page')),
.find(b => b.type === 'css' && b.name.startsWith('Client')),
).filePath,
),
);
});

let entryAsset;
b.getBundles()[1].traverseAssets(a => {
if (a.filePath.endsWith('Client.jsx')) {
entryAsset = a;
it('dynamic import of server component to server component', async function () {
await fsFixture(overlayFS, dir)`
Page.jsx:
export async function Server() {
let {Dynamic} = await import('./Dynamic');
return <Dynamic />;
}
output = {Server};
Dynamic.jsx:
import './dynamic.css';
export function Dynamic() {
return <p>Dynamic</p>;
}
dynamic.css:
.dynamic { color: green }
`;

let b = await bundle(path.join(dir, '/Page.jsx'), {
inputFS: overlayFS,
targets: ['default'],
defaultTargetOptions: {
shouldScopeHoist,
},
});

let client = (await runBundle(
assertBundles(
b,
b.getBundles()[1],
{output: null},
{require: false, entryAsset},
): any);
let parcelRequire = nullthrows(
Object.keys(client).find(k => k.startsWith('parcelRequire')),
[
{
assets: ['Page.jsx'],
},
{
assets: ['Dynamic.jsx'],
},
{
assets: ['dynamic.css'],
},
],
{skipNodeModules: true},
);
client[parcelRequire](b.getAssetPublicId(nullthrows(entryAsset)));
resources = client.output.Resources();

link = resources.props.children;
let res = (await run(b, null, {require: false})).output;
let output = await res.Server();
output.type.$$typeof;
let result = output.type();

let link = result.props.children[0];
assert.equal(link.type, 'link');
assert.equal(link.props.rel, 'stylesheet');
assert.equal(link.props.precedence, 'default');
Expand All @@ -869,7 +886,9 @@ describe('react server components', function () {
nullthrows(
b
.getBundles()
.find(b => b.type === 'css' && b.name.startsWith('Client')),
.find(
b => b.type === 'css' && b.name.startsWith('Dynamic'),
),
).filePath,
),
);
Expand Down
Loading

0 comments on commit e3a2b72

Please sign in to comment.