Skip to content

Commit

Permalink
Handle source maps in webpack loaders
Browse files Browse the repository at this point in the history
This enables setting breakpoints in server actions. In addition, we set
the swc env to node 18 in the webpack server configs, so that no
unecessary generator code is emitted for async functions, which also
messed up the mapped source lines.
  • Loading branch information
unstubbable committed Nov 17, 2023
1 parent cfe455b commit 2b7ed43
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 133 deletions.
23 changes: 10 additions & 13 deletions apps/cloudflare-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ export default function createConfigs(_env, argv) {
const rscSsrLoader = createWebpackRscSsrLoader();
const rscClientLoader = createWebpackRscClientLoader({serverReferencesMap});

/**
* @type {import('webpack').RuleSetUseItem}
*/
const serverSwcLoader = {
loader: `swc-loader`,
options: {env: {targets: {node: 18}}},
};

/**
* @type {import('webpack').Configuration}
*/
Expand Down Expand Up @@ -122,26 +130,16 @@ export default function createConfigs(_env, argv) {
{
issuerLayer: webpackRscLayerName,
test: /\.tsx?$/,
use: [rscServerLoader, `swc-loader`],
use: [rscServerLoader, serverSwcLoader],
exclude: [/node_modules/],
},
{
test: /\.tsx?$/,
use: [rscSsrLoader, `swc-loader`],
use: [rscSsrLoader, serverSwcLoader],
exclude: [/node_modules/],
},
],
},
{
oneOf: [
{
test: /\.js$/,
issuerLayer: webpackRscLayerName,
use: rscServerLoader,
},
{test: /\.js$/, use: rscSsrLoader},
],
},
cssRule,
],
},
Expand Down Expand Up @@ -177,7 +175,6 @@ export default function createConfigs(_env, argv) {
module: {
rules: [
{test: /\.js$/, loader: `source-map-loader`, enforce: `pre`},
{test: /\.js$/, use: rscClientLoader},
{
test: /\.tsx?$/,
use: [rscClientLoader, `swc-loader`],
Expand Down
23 changes: 10 additions & 13 deletions apps/vercel-app/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ export default function createConfigs(_env, argv) {
const rscSsrLoader = createWebpackRscSsrLoader();
const rscClientLoader = createWebpackRscClientLoader({serverReferencesMap});

/**
* @type {import('webpack').RuleSetUseItem}
*/
const serverSwcLoader = {
loader: `swc-loader`,
options: {env: {targets: {node: 18}}},
};

/**
* @type {import('webpack').Configuration}
*/
Expand Down Expand Up @@ -149,27 +157,17 @@ export default function createConfigs(_env, argv) {
{
issuerLayer: webpackRscLayerName,
test: /\.tsx?$/,
use: [rscServerLoader, `swc-loader`],
use: [rscServerLoader, serverSwcLoader],
exclude: [/node_modules/],
},
{
test: /\.tsx?$/,
use: [rscSsrLoader, `swc-loader`],
use: [rscSsrLoader, serverSwcLoader],
// use: `swc-loader`,
exclude: [/node_modules/],
},
],
},
{
oneOf: [
{
test: /\.js$/,
issuerLayer: webpackRscLayerName,
use: rscServerLoader,
},
{test: /\.js$/, use: rscSsrLoader},
],
},
cssRule,
],
},
Expand Down Expand Up @@ -218,7 +216,6 @@ export default function createConfigs(_env, argv) {
module: {
rules: [
{test: /\.js$/, loader: `source-map-loader`, enforce: `pre`},
{test: /\.js$/, use: rscClientLoader},
{
test: /\.tsx?$/,
use: [rscClientLoader, `swc-loader`],
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/webpack-rsc/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mfng/webpack-rsc",
"version": "2.4.0",
"version": "2.5.0",
"description": "A set of Webpack loaders and plugins for React Server Components",
"repository": {
"type": "git",
Expand Down
187 changes: 96 additions & 91 deletions packages/webpack-rsc/src/webpack-rsc-server-loader.cts
Original file line number Diff line number Diff line change
Expand Up @@ -21,109 +21,114 @@ namespace webpackRscServerLoader {

type RegisterReferenceType = 'Server' | 'Client';

function webpackRscServerLoader(
this: webpack.LoaderContext<webpackRscServerLoader.WebpackRscServerLoaderOptions>,
source: string,
): void {
this.cacheable(true);

const {clientReferencesMap} = this.getOptions();
const clientReferences: webpackRscServerLoader.ClientReference[] = [];
const resourcePath = this.resourcePath;

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

let moduleDirective: 'use client' | 'use server' | undefined;
let addedRegisterReferenceCall: RegisterReferenceType | undefined;
const unshiftedNodes = new Set<t.Node>();

traverse.default(ast, {
enter(nodePath) {
const {node} = nodePath;

if (t.isProgram(node)) {
if (node.directives.some(isDirective(`use client`))) {
moduleDirective = `use client`;
} else if (node.directives.some(isDirective(`use server`))) {
moduleDirective = `use server`;
} else {
const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerLoader.WebpackRscServerLoaderOptions> =
function (source, sourceMap) {
this.cacheable(true);

const {clientReferencesMap} = this.getOptions();
const clientReferences: webpackRscServerLoader.ClientReference[] = [];
const resourcePath = this.resourcePath;

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

let moduleDirective: 'use client' | 'use server' | undefined;
let addedRegisterReferenceCall: RegisterReferenceType | undefined;
const unshiftedNodes = new Set<t.Node>();

traverse.default(ast, {
enter(nodePath) {
const {node} = nodePath;

if (t.isProgram(node)) {
if (node.directives.some(isDirective(`use client`))) {
moduleDirective = `use client`;
} else if (node.directives.some(isDirective(`use server`))) {
moduleDirective = `use server`;
} else {
nodePath.skip();
}

return;
}

if (
!moduleDirective ||
(t.isDirective(node) && isDirective(`use client`)(node)) ||
unshiftedNodes.has(node)
) {
nodePath.skip();

return;
}

return;
}
const exportName = getExportName(node);

if (moduleDirective === `use client`) {
if (exportName) {
const id = `${path.relative(process.cwd(), resourcePath)}`;
clientReferences.push({id, exportName});
addedRegisterReferenceCall = `Client`;
nodePath.replaceWith(createExportedClientReference(id, exportName));
nodePath.skip();
} else {
nodePath.remove();
}
} else if (exportName) {
addedRegisterReferenceCall = `Server`;
nodePath.insertAfter(createRegisterServerReference(exportName));
nodePath.skip();
}
},
exit(nodePath) {
if (!t.isProgram(nodePath.node) || !addedRegisterReferenceCall) {
nodePath.skip();

if (
!moduleDirective ||
(t.isDirective(node) && isDirective(`use client`)(node)) ||
unshiftedNodes.has(node)
) {
nodePath.skip();
return;
}

return;
}
const nodes: t.Node[] = [
createRegisterReferenceImport(addedRegisterReferenceCall),
];

const exportName = getExportName(node);
if (addedRegisterReferenceCall === `Client`) {
nodes.push(createClientReferenceProxyImplementation());
}

if (moduleDirective === `use client`) {
if (exportName) {
const id = `${path.relative(process.cwd(), resourcePath)}`;
clientReferences.push({id, exportName});
addedRegisterReferenceCall = `Client`;
nodePath.replaceWith(createExportedClientReference(id, exportName));
nodePath.skip();
} else {
nodePath.remove();
for (const node of nodes) {
unshiftedNodes.add(node);
}
} else if (exportName) {
addedRegisterReferenceCall = `Server`;
nodePath.insertAfter(createRegisterServerReference(exportName));
nodePath.skip();
}
},
exit(nodePath) {
if (!t.isProgram(nodePath.node) || !addedRegisterReferenceCall) {
nodePath.skip();

return;
}

const nodes: t.Node[] = [
createRegisterReferenceImport(addedRegisterReferenceCall),
];

if (addedRegisterReferenceCall === `Client`) {
nodes.push(createClientReferenceProxyImplementation());
}

for (const node of nodes) {
unshiftedNodes.add(node);
}

(nodePath as traverse.NodePath<t.Program>).unshiftContainer(
`body`,
nodes,
);
},
});

if (!moduleDirective) {
return this.callback(null, source);
}

if (clientReferences.length > 0) {
clientReferencesMap.set(resourcePath, clientReferences);
}
(nodePath as traverse.NodePath<t.Program>).unshiftContainer(
`body`,
nodes,
);
},
});

const {code} = generate.default(ast, {sourceFileName: this.resourcePath});
if (!moduleDirective) {
return this.callback(null, source, sourceMap);
}

// TODO: Handle source maps.
if (clientReferences.length > 0) {
clientReferencesMap.set(resourcePath, clientReferences);
}

this.callback(null, code);
}
const {code, map} = generate.default(
ast,
{
sourceFileName: this.resourcePath,
sourceMaps: this.sourceMap,
// @ts-expect-error
inputSourceMap: sourceMap,
},
source,
);

this.callback(null, code, map ?? sourceMap);
};

function isDirective(
value: 'use client' | 'use server',
Expand Down
35 changes: 26 additions & 9 deletions packages/webpack-rsc/src/webpack-rsc-ssr-loader.cts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import generate = require('@babel/generator');
import parser = require('@babel/parser');
import traverse = require('@babel/traverse');
import t = require('@babel/types');
import type {LoaderContext} from 'webpack';
import webpack = require('webpack');

export = function webpackRscSsrLoader(
this: LoaderContext<{}>,
source: string,
): void {
const webpackRscSsrLoader: webpack.LoaderDefinitionFunction = function (
source,
sourceMap,
) {
this.cacheable(true);

const resourcePath = this.resourcePath;
Expand All @@ -17,12 +17,16 @@ export = function webpackRscSsrLoader(
sourceFilename: resourcePath,
});

let hasUseServerDirective = false;

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

if (t.isProgram(node)) {
if (!node.directives.some(isUseServerDirective)) {
if (node.directives.some(isUseServerDirective)) {
hasUseServerDirective = true;
} else {
path.skip();
}

Expand All @@ -46,11 +50,22 @@ export = function webpackRscSsrLoader(
},
});

const {code} = generate.default(ast, {sourceFileName: this.resourcePath});
if (!hasUseServerDirective) {
return this.callback(null, source, sourceMap);
}

// TODO: Handle source maps.
const {code, map} = generate.default(
ast,
{
sourceFileName: this.resourcePath,
sourceMaps: this.sourceMap,
// @ts-expect-error
inputSourceMap: sourceMap,
},
source,
);

this.callback(null, code);
this.callback(null, code, map ?? sourceMap);
};

function isUseServerDirective(directive: t.Directive): boolean {
Expand Down Expand Up @@ -104,3 +119,5 @@ function createExportedServerReferenceStub(
),
);
}

export = webpackRscSsrLoader;
Loading

0 comments on commit 2b7ed43

Please sign in to comment.