Skip to content

Commit

Permalink
Merge pull request #76 from unstubbable/default-export
Browse files Browse the repository at this point in the history
Support default exports and non-function client components
  • Loading branch information
unstubbable committed May 16, 2024
2 parents 154f593 + fb39d01 commit 3612c08
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-carpets-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@mfng/webpack-rsc': minor
---

Support default exports and non-function client components
2 changes: 2 additions & 0 deletions packages/webpack-rsc/src/__fixtures__/client-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export function ClientComponent({action}) {

return null;
}

export default ClientComponent;
15 changes: 10 additions & 5 deletions packages/webpack-rsc/src/__fixtures__/client-components.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
'use client';

import * as React from 'react';
import {ClientComponentWithServerAction} from './client-component-with-server-action.js';

export function ComponentA() {
export function ComponentA(arg0) {
return React.createElement(`div`);
}

export const MemoizedComponentA = React.memo(ComponentA);

export const ComponentB = function () {
return React.createElement(`div`);
};

export const foo = 1;

export const ComponentC = () => {
return React.createElement(ClientComponentWithServerAction);
};
export class ClassComponent extends React.Component {}

export {ClientComponentWithServerAction as ComponentC} from './client-component-with-server-action.js';

const bar = 2;

Expand All @@ -30,3 +31,7 @@ function D() {
function ComponentE() {
return React.createElement(`div`);
}

export default function () {
return null;
}
23 changes: 18 additions & 5 deletions packages/webpack-rsc/src/webpack-rsc-client-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ describe(`WebpackRscClientPlugin`, () => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "ClientComponent": () => (/* binding */ ClientComponent)
/* harmony export */ "ClientComponent": () => (/* binding */ ClientComponent),
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "../../node_modules/react/index.js");
// @ts-nocheck
Expand All @@ -136,6 +137,8 @@ function ClientComponent({action}) {
return null;
}
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ClientComponent);
/***/ })
Expand All @@ -147,6 +150,10 @@ function ClientComponent({action}) {
clientReferencesMap.set(
path.resolve(currentDirname, `__fixtures__/client-component.js`),
[
{
id: `__fixtures__/client-component.js#`,
exportName: ``,
},
{
id: `__fixtures__/client-component.js#ClientComponent`,
exportName: `ClientComponent`,
Expand All @@ -164,6 +171,11 @@ function ClientComponent({action}) {
);

expect(manifest).toEqual({
'__fixtures__/client-component.js#': {
chunks: [`client0`, `client0.main.js`],
id: `./src/__fixtures__/client-component.js`,
name: ``,
},
'__fixtures__/client-component.js#ClientComponent': {
chunks: [`client0`, `client0.main.js`],
id: `./src/__fixtures__/client-component.js`,
Expand Down Expand Up @@ -308,16 +320,17 @@ function ClientComponent({action}) {
[701],
{
431: (e, n, c) => {
c.r(n), c.d(n, { ClientComponent: () => t });
var s = c(423);
function t({ action: e }) {
c.r(n), c.d(n, { ClientComponent: () => s, default: () => u });
var t = c(423);
function s({ action: e }) {
return (
s.useEffect(() => {
t.useEffect(() => {
e().then(console.log);
}, []),
null
);
}
const u = s;
},
},
]);
Expand Down
118 changes: 89 additions & 29 deletions packages/webpack-rsc/src/webpack-rsc-server-loader.cts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface ExtendedFunctionInfo extends FunctionInfo {
readonly exportName?: string;
}

// TODO: Refactor to better separate logic for server and client modules.
const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerLoader.WebpackRscServerLoaderOptions> =
function (source, sourceMap) {
this.cacheable(true);
Expand All @@ -56,6 +57,7 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerL
let moduleDirective: 'use client' | 'use server' | undefined;
let addedRegisterReferenceCall: RegisterReferenceType | undefined;
const unshiftedNodes = new Set<t.Node>();
const exportNames = new Set<string>();
const exportNamesByLocalName = new Map<string, string>();

const localTopLevelFunctionsByNode = new WeakMap<
Expand All @@ -76,15 +78,25 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerL
}

if (t.isExportNamedDeclaration(node)) {
for (const exportName of Object.keys(
// TODO: This is potentially to broad.
t.getBindingIdentifiers(node, false, true),
)) {
exportNames.add(exportName);
}

for (const exportSpecifier of node.specifiers) {
if (
t.isExportSpecifier(exportSpecifier) &&
t.isIdentifier(exportSpecifier.exported)
) {
exportNamesByLocalName.set(
exportSpecifier.local.name,
exportSpecifier.exported.name,
);
const {
exported: {name: exportName},
local: {name: localName},
} = exportSpecifier;

exportNames.add(exportName);
exportNamesByLocalName.set(localName, exportName);
}
}
}
Expand Down Expand Up @@ -118,30 +130,35 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerL
return nodePath.skip();
}

if (moduleDirective === `use client`) {
if (t.isExportDefaultDeclaration(node)) {
const exportName = ``;

const id = `${path.relative(
process.cwd(),
resourcePath,
)}#${exportName}`;

clientReferences.push({id, exportName});
addedRegisterReferenceCall = `Client`;

nodePath.replaceWith(
createDefaultExportedClientReference(id, resourcePath),
);

return nodePath.skip();
}

return nodePath.remove();
}

const extendedFunctionInfo = getExtendedFunctionInfo(
node,
localTopLevelFunctionsByNode,
exportNamesByLocalName,
);

if (moduleDirective === `use client`) {
if (!extendedFunctionInfo?.exportName) {
return nodePath.remove();
}

const {exportName} = extendedFunctionInfo;

const id = `${path.relative(
process.cwd(),
resourcePath,
)}#${exportName}`;

clientReferences.push({id, exportName});
addedRegisterReferenceCall = `Client`;

nodePath.replaceWith(createExportedClientReference(id, exportName));
nodePath.skip();
} else if (extendedFunctionInfo) {
if (extendedFunctionInfo) {
const {localName, exportName, hasUseServerDirective} =
extendedFunctionInfo;

Expand All @@ -165,20 +182,39 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction<webpackRscServerL
}
},
exit(nodePath) {
if (!t.isProgram(nodePath.node) || !addedRegisterReferenceCall) {
if (!t.isProgram(nodePath.node)) {
nodePath.skip();

return;
}

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

if (moduleDirective === `use client` && exportNames.size > 0) {
nodes.unshift(createClientReferenceProxyImplementation());

for (const exportName of exportNames) {
const id = `${path.relative(
process.cwd(),
resourcePath,
)}#${exportName}`;

clientReferences.push({id, exportName});
addedRegisterReferenceCall = `Client`;
nodes.push(createNamedExportedClientReference(id, exportName));
}
}

if (!addedRegisterReferenceCall) {
nodePath.skip();

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

nodes.unshift(
createRegisterReferenceImport(addedRegisterReferenceCall),
);

for (const node of nodes) {
unshiftedNodes.add(node);
}
Expand Down Expand Up @@ -291,7 +327,7 @@ function getFunctionInfo(node: t.Node): FunctionInfo | undefined {
return localName ? {localName, hasUseServerDirective} : undefined;
}

function createExportedClientReference(
function createNamedExportedClientReference(
id: string,
exportName: string,
): t.ExportNamedDeclaration {
Expand All @@ -311,6 +347,30 @@ function createExportedClientReference(
);
}

function createDefaultExportedClientReference(
id: string,
resourcePath: string,
): t.ExportNamedDeclaration | t.ExportDefaultDeclaration {
return t.exportDefaultDeclaration(
t.callExpression(t.identifier(`registerClientReference`), [
t.arrowFunctionExpression(
[],
t.blockStatement([
t.throwStatement(
t.newExpression(t.identifier(`Error`), [
t.stringLiteral(
`Attempted to call the default export of ${resourcePath} from the server but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.`,
),
]),
),
]),
),
t.stringLiteral(id),
t.stringLiteral(``),
]),
);
}

function createClientReferenceProxyImplementation(): t.FunctionDeclaration {
return t.functionDeclaration(
t.identifier(`createClientReferenceProxy`),
Expand Down
39 changes: 33 additions & 6 deletions packages/webpack-rsc/src/webpack-rsc-server-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ async function callLoader(
}

describe(`webpackRscServerLoader`, () => {
test(`keeps only the 'use client' directive, and exported functions that are transformed to client references`, async () => {
test(`keeps only the 'use client' directive, and client references for all exports`, async () => {
const resourcePath = path.resolve(
currentDirname,
`__fixtures__/client-components.js`,
Expand All @@ -69,11 +69,18 @@ function createClientReferenceProxy(exportName) {
};
}
export const ComponentA = registerClientReference(createClientReferenceProxy("ComponentA"), "${idPrefix}#ComponentA", "ComponentA");
export const MemoizedComponentA = registerClientReference(createClientReferenceProxy("MemoizedComponentA"), "${idPrefix}#MemoizedComponentA", "MemoizedComponentA");
export const ComponentB = registerClientReference(createClientReferenceProxy("ComponentB"), "${idPrefix}#ComponentB", "ComponentB");
export const foo = registerClientReference(createClientReferenceProxy("foo"), "${idPrefix}#foo", "foo");
export const ClassComponent = registerClientReference(createClientReferenceProxy("ClassComponent"), "${idPrefix}#ClassComponent", "ClassComponent");
export const ComponentC = registerClientReference(createClientReferenceProxy("ComponentC"), "${idPrefix}#ComponentC", "ComponentC");
export const ComponentF = registerClientReference(createClientReferenceProxy("ComponentF"), "${idPrefix}#ComponentF", "ComponentF");
export const ComponentD = registerClientReference(createClientReferenceProxy("ComponentD"), "${idPrefix}#ComponentD", "ComponentD");
export const bar = registerClientReference(createClientReferenceProxy("bar"), "${idPrefix}#bar", "bar");
export const ComponentE = registerClientReference(createClientReferenceProxy("ComponentE"), "${idPrefix}#ComponentE", "ComponentE");
export const ComponentF = registerClientReference(createClientReferenceProxy("ComponentF"), "${idPrefix}#ComponentF", "ComponentF");
export default registerClientReference(() => {
throw new Error("Attempted to call the default export of ${resourcePath} from the server but it's on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");
}, "${idPrefix}#", "");
`.trim(),
);
});
Expand All @@ -90,30 +97,50 @@ export const ComponentE = registerClientReference(createClientReferenceProxy("Co

expect(Object.fromEntries(clientReferencesMap.entries())).toEqual({
[resourcePath]: [
{
exportName: ``,
id: `src/__fixtures__/client-components.js#`,
},
{
exportName: `ComponentA`,
id: `src/__fixtures__/client-components.js#ComponentA`,
},
{
exportName: `MemoizedComponentA`,
id: `src/__fixtures__/client-components.js#MemoizedComponentA`,
},
{
exportName: `ComponentB`,
id: `src/__fixtures__/client-components.js#ComponentB`,
},
{
exportName: `ComponentC`,
id: `src/__fixtures__/client-components.js#ComponentC`,
exportName: `foo`,
id: `src/__fixtures__/client-components.js#foo`,
},
{
exportName: `ComponentF`,
id: `src/__fixtures__/client-components.js#ComponentF`,
exportName: `ClassComponent`,
id: `src/__fixtures__/client-components.js#ClassComponent`,
},
{
exportName: `ComponentC`,
id: `src/__fixtures__/client-components.js#ComponentC`,
},
{
exportName: `ComponentD`,
id: `src/__fixtures__/client-components.js#ComponentD`,
},
{
exportName: `bar`,
id: `src/__fixtures__/client-components.js#bar`,
},
{
exportName: `ComponentE`,
id: `src/__fixtures__/client-components.js#ComponentE`,
},
{
exportName: `ComponentF`,
id: `src/__fixtures__/client-components.js#ComponentF`,
},
],
});
});
Expand Down
5 changes: 4 additions & 1 deletion packages/webpack-rsc/src/webpack-rsc-ssr-loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ export function ClientComponent({action}) {
}, []);
return null;
}`.trim(),
}
export default ClientComponent;
`.trim(),
);
});

Expand Down

0 comments on commit 3612c08

Please sign in to comment.