Skip to content

Commit

Permalink
Add client loader to enable importing server actions
Browse files Browse the repository at this point in the history
Made easier by facebook/react#26632
  • Loading branch information
unstubbable committed Apr 17, 2023
1 parent 0b92a75 commit dabd846
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 25 deletions.
13 changes: 11 additions & 2 deletions apps/cloudflare-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from 'path';
import {
WebpackRscClientPlugin,
WebpackRscServerPlugin,
createWebpackRscClientLoader,
createWebpackRscServerLoader,
webpackRscLayerName,
} from '@mfng/webpack-rsc';
Expand Down Expand Up @@ -73,6 +74,7 @@ export default function createConfigs(_env, argv) {
* @type {import('@mfng/webpack-rsc').ClientReferencesMap}
*/
const clientReferencesMap = new Map();
const serverReferencesMap = new Map();
const rscServerLoader = createWebpackRscServerLoader({clientReferencesMap});

/**
Expand Down Expand Up @@ -121,7 +123,7 @@ export default function createConfigs(_env, argv) {
},
plugins: [
new MiniCssExtractPlugin({filename: `server-main.css`, runtime: false}),
new WebpackRscServerPlugin({clientReferencesMap}),
new WebpackRscServerPlugin({clientReferencesMap, serverReferencesMap}),
],
experiments: {outputModule: true, layers: true},
performance: {maxAssetSize: 1_000_000, maxEntrypointSize: 1_000_000},
Expand All @@ -131,6 +133,8 @@ export default function createConfigs(_env, argv) {
stats,
};

const rscClientLoader = createWebpackRscClientLoader({serverReferencesMap});

/**
* @type {import('webpack').Configuration}
*/
Expand All @@ -149,7 +153,12 @@ export default function createConfigs(_env, argv) {
},
module: {
rules: [
{test: /\.tsx?$/, loader: `swc-loader`, exclude: [/node_modules/]},
{test: /\.js$/, use: rscClientLoader},
{
test: /\.tsx?$/,
use: [rscClientLoader, `swc-loader`],
exclude: [/node_modules/],
},
cssRule,
],
},
Expand Down
11 changes: 10 additions & 1 deletion apps/vercel-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import url from 'url';
import {
WebpackRscClientPlugin,
WebpackRscServerPlugin,
createWebpackRscClientLoader,
createWebpackRscServerLoader,
webpackRscLayerName,
} from '@mfng/webpack-rsc';
Expand Down Expand Up @@ -100,6 +101,7 @@ export default function createConfigs(_env, argv) {
* @type {import('@mfng/webpack-rsc').ClientReferencesMap}
*/
const clientReferencesMap = new Map();
const serverReferencesMap = new Map();
const rscServerLoader = createWebpackRscServerLoader({clientReferencesMap});

/**
Expand Down Expand Up @@ -151,6 +153,7 @@ export default function createConfigs(_env, argv) {
new MiniCssExtractPlugin({filename: `server-main.css`, runtime: false}),
new WebpackRscServerPlugin({
clientReferencesMap,
serverReferencesMap,
serverManifestFilename: path.relative(
outputFunctionDirname,
reactServerManifestFilename,
Expand All @@ -168,6 +171,7 @@ export default function createConfigs(_env, argv) {
};

const clientOutputDirname = path.join(outputDirname, `static/client`);
const rscClientLoader = createWebpackRscClientLoader({serverReferencesMap});

/**
* @type {import('webpack').Configuration}
Expand All @@ -187,7 +191,12 @@ export default function createConfigs(_env, argv) {
},
module: {
rules: [
{test: /\.tsx?$/, loader: `swc-loader`, exclude: [/node_modules/]},
{test: /\.js$/, use: rscClientLoader},
{
test: /\.tsx?$/,
use: [rscClientLoader, `swc-loader`],
exclude: [/node_modules/],
},
cssRule,
],
},
Expand Down
59 changes: 43 additions & 16 deletions packages/webpack-rsc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

⚠️ **Experimental**

This library provides a Webpack loader and a pair of Webpack plugins for
integrating React Server Components (RSC) and Server-Side Rendering (SSR) in a
React application that can be deployed to the edge.
This library provides a set of Webpack loaders and plugins for integrating React
Server Components (RSC) and Server-Side Rendering (SSR) in a React application
that can be deployed to the edge.

> Disclaimer: There are many moving parts involved in creating an RSC app that
> also handles SSR, without using a framework like Next.js. This library only
Expand All @@ -21,23 +21,25 @@ To use this library in your React Server Components project, follow these steps:
npm install --save-dev @mfng/webpack-rsc
```

2. Update your `webpack.config.js` to include the loader and plugins provided by
this library. See the example configuration below for reference.
2. Update your `webpack.config.js` to include the loaders and plugins provided
by this library. See the example configuration below for reference.

## Example Webpack Configuration

The following example demonstrates how to use the loader and plugins in a
The following example demonstrates how to use the loaders and plugins in a
Webpack configuration:

```js
import {
WebpackRscClientPlugin,
WebpackRscServerPlugin,
createWebpackRscClientLoader,
createWebpackRscServerLoader,
webpackRscLayerName,
} from '@mfng/webpack-rsc';

const clientReferencesMap = new Map();
const serverReferencesMap = new Map();

const serverConfig = {
name: 'server',
Expand Down Expand Up @@ -68,7 +70,9 @@ const serverConfig = {
},
],
},
plugins: [new WebpackRscServerPlugin({clientReferencesMap})],
plugins: [
new WebpackRscServerPlugin({clientReferencesMap, serverReferencesMap}),
],
experiments: {layers: true},
// ...
};
Expand All @@ -77,7 +81,17 @@ const clientConfig = {
name: 'client',
dependencies: ['server'],
// ...
module: {rules: [{test: /\.tsx?$/, loader: 'swc-loader'}]},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
createWebpackRscClientLoader({serverReferencesMap}),
'swc-loader',
],
},
],
},
plugins: [new WebpackRscClientPlugin({clientReferencesMap})],
// ...
};
Expand All @@ -88,17 +102,17 @@ export default [serverConfig, clientConfig];
**Note:** It's important to specify the names and dependencies of the configs as
shown above, so that the plugins work in the correct order, even in watch mode.

## Webpack Loader and Plugins
## Webpack Loaders and Plugins

This library provides the following Webpack loader and plugins:
This library provides the following Webpack loaders and plugins:

### `createWebpackRscServerLoader`

A function to create the RSC server loader `use` item. This loader is
responsible for replacing client components in a `use client` module with client
references (objects that contain meta data about the client components), and
removing all other parts of the client module. It also populates the
`clientReferencesMap`.
A function to create the RSC server loader `use` item for the server entry
webpack config. This loader is responsible for replacing client components in a
`use client` module with client references (objects that contain meta data about
the client components), and removing all other parts of the client module. It
also populates the given `clientReferencesMap`.

### `WebpackRscServerPlugin`

Expand All @@ -110,7 +124,20 @@ The plugin also handles server references for React server actions by adding
meta data to all exported functions of a `use server` module. Based on this, it
generates the server manifest that is needed for validating the server
references for server actions (also known as mutations) that are sent back from
the client.
the client. It also populates the given `serverReferencesMap`.

### `createWebpackRscClientLoader`

A function to create the RSC client loader `use` item for the client entry
webpack config. This loader is responsible for replacing server actions in a
`use server` module with server references (based on the given
`serverReferencesMap`), and removing all other parts of the server module, so
that the server module can be imported from a client module.

**Note:** Importing server actions from a client module requires that
`callServer` can be imported from a module. Per default `@mfng/core/client` is
used as import source, but this can be customized with the
`callServerImportSource` option.

### `WebpackRscClientPlugin`

Expand Down
4 changes: 2 additions & 2 deletions packages/webpack-rsc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@mfng/webpack-rsc",
"version": "2.0.2",
"description": "A Webpack loader and a pair of plugins for React Server Components",
"version": "2.1.0",
"description": "A set of Webpack loaders and plugins for React Server Components",
"repository": {
"type": "git",
"url": "https://github.com/unstubbable/mfng.git",
Expand Down
13 changes: 11 additions & 2 deletions packages/webpack-rsc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import {createRequire} from 'module';
import type {RuleSetUseItem} from 'webpack';
import type {WebpackRscClientLoaderOptions} from './webpack-rsc-client-loader.cjs';
import type {WebpackRscServerLoaderOptions} from './webpack-rsc-server-loader.cjs';

export * from './webpack-rsc-client-loader.cjs';
export * from './webpack-rsc-client-plugin.js';
export * from './webpack-rsc-server-loader.cjs';
export * from './webpack-rsc-server-plugin.js';

const require = createRequire(import.meta.url);
const loader = require.resolve(`./webpack-rsc-server-loader.cjs`);
const serverLoader = require.resolve(`./webpack-rsc-server-loader.cjs`);
const clientLoader = require.resolve(`./webpack-rsc-client-loader.cjs`);

export function createWebpackRscServerLoader(
options: WebpackRscServerLoaderOptions,
): RuleSetUseItem {
return {loader, options};
return {loader: serverLoader, options};
}

export function createWebpackRscClientLoader(
options: WebpackRscClientLoaderOptions,
): RuleSetUseItem {
return {loader: clientLoader, options};
}
114 changes: 114 additions & 0 deletions packages/webpack-rsc/src/webpack-rsc-client-loader.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import generate from '@babel/generator';
import {parse} from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
import type {LoaderContext} from 'webpack';

export interface WebpackRscClientLoaderOptions {
readonly serverReferencesMap: ServerReferencesMap;
readonly callServerImportSource?: string;
}

export type ServerReferencesMap = Map<string, ServerReferencesModuleInfo>;

export interface ServerReferencesModuleInfo {
readonly moduleId: string | number;
readonly exportNames: string[];
}

export default function webpackRscClientLoader(
this: LoaderContext<WebpackRscClientLoaderOptions>,
source: string,
): void {
this.cacheable(true);

const {serverReferencesMap, callServerImportSource = `@mfng/core/client`} =
this.getOptions();

const loaderContext = this;
const resourcePath = this.resourcePath;

const ast = parse(source, {
sourceType: `module`,
sourceFilename: resourcePath,
});

traverse(ast, {
enter(path) {
const {node} = path;

if (!t.isProgram(node)) {
return;
}

if (!node.directives.some(isUseServerDirective)) {
return;
}

const moduleInfo = serverReferencesMap.get(resourcePath);

if (!moduleInfo) {
loaderContext.emitError(
new Error(
`Could not find server references module info in \`serverReferencesMap\` for ${resourcePath}.`,
),
);

path.replaceWith(t.program([]));

return;
}

const {moduleId, exportNames} = moduleInfo;

path.replaceWith(
t.program([
t.importDeclaration(
[
t.importSpecifier(
t.identifier(`createServerReference`),
t.identifier(`createServerReference`),
),
],
t.stringLiteral(`react-server-dom-webpack/client`),
),
t.importDeclaration(
[
t.importSpecifier(
t.identifier(`callServer`),
t.identifier(`callServer`),
),
],
t.stringLiteral(callServerImportSource),
),
...exportNames.map((exportName) =>
t.exportNamedDeclaration(
t.variableDeclaration(`const`, [
t.variableDeclarator(
t.identifier(exportName),
t.callExpression(t.identifier(`createServerReference`), [
t.stringLiteral(`${moduleId}#${exportName}`),
t.identifier(`callServer`),
]),
),
]),
),
),
]),
);
},
});

const {code} = generate(ast, {sourceFileName: this.resourcePath});

// TODO: Handle source maps.

this.callback(null, code);
}

function isUseServerDirective(directive: t.Directive): boolean {
return (
t.isDirectiveLiteral(directive.value) &&
directive.value.value === `use server`
);
}
Loading

0 comments on commit dabd846

Please sign in to comment.