From fb39d010d19082a25fc0e0c47cf9cc4d5446b012 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 16 May 2024 19:16:10 +0200 Subject: [PATCH] Support default exports and non-function client components --- .changeset/nice-carpets-fold.md | 5 + .../src/__fixtures__/client-component.js | 2 + .../src/__fixtures__/client-components.js | 15 ++- .../src/webpack-rsc-client-plugin.test.ts | 23 +++- .../src/webpack-rsc-server-loader.cts | 118 +++++++++++++----- .../src/webpack-rsc-server-loader.test.ts | 39 +++++- .../src/webpack-rsc-ssr-loader.test.ts | 5 +- 7 files changed, 161 insertions(+), 46 deletions(-) create mode 100644 .changeset/nice-carpets-fold.md diff --git a/.changeset/nice-carpets-fold.md b/.changeset/nice-carpets-fold.md new file mode 100644 index 0000000..0461d02 --- /dev/null +++ b/.changeset/nice-carpets-fold.md @@ -0,0 +1,5 @@ +--- +'@mfng/webpack-rsc': minor +--- + +Support default exports and non-function client components diff --git a/packages/webpack-rsc/src/__fixtures__/client-component.js b/packages/webpack-rsc/src/__fixtures__/client-component.js index e505d49..d235eef 100644 --- a/packages/webpack-rsc/src/__fixtures__/client-component.js +++ b/packages/webpack-rsc/src/__fixtures__/client-component.js @@ -10,3 +10,5 @@ export function ClientComponent({action}) { return null; } + +export default ClientComponent; diff --git a/packages/webpack-rsc/src/__fixtures__/client-components.js b/packages/webpack-rsc/src/__fixtures__/client-components.js index bc45141..9676820 100644 --- a/packages/webpack-rsc/src/__fixtures__/client-components.js +++ b/packages/webpack-rsc/src/__fixtures__/client-components.js @@ -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; @@ -30,3 +31,7 @@ function D() { function ComponentE() { return React.createElement(`div`); } + +export default function () { + return null; +} diff --git a/packages/webpack-rsc/src/webpack-rsc-client-plugin.test.ts b/packages/webpack-rsc/src/webpack-rsc-client-plugin.test.ts index 9f7c72c..38e2d30 100644 --- a/packages/webpack-rsc/src/webpack-rsc-client-plugin.test.ts +++ b/packages/webpack-rsc/src/webpack-rsc-client-plugin.test.ts @@ -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 @@ -136,6 +137,8 @@ function ClientComponent({action}) { return null; } +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (ClientComponent); + /***/ }) @@ -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`, @@ -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`, @@ -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; }, }, ]); diff --git a/packages/webpack-rsc/src/webpack-rsc-server-loader.cts b/packages/webpack-rsc/src/webpack-rsc-server-loader.cts index 473c574..f0ed78c 100644 --- a/packages/webpack-rsc/src/webpack-rsc-server-loader.cts +++ b/packages/webpack-rsc/src/webpack-rsc-server-loader.cts @@ -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 = function (source, sourceMap) { this.cacheable(true); @@ -56,6 +57,7 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction(); + const exportNames = new Set(); const exportNamesByLocalName = new Map(); const localTopLevelFunctionsByNode = new WeakMap< @@ -76,15 +78,25 @@ const webpackRscServerLoader: webpack.LoaderDefinitionFunction 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); } @@ -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 { @@ -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`), diff --git a/packages/webpack-rsc/src/webpack-rsc-server-loader.test.ts b/packages/webpack-rsc/src/webpack-rsc-server-loader.test.ts index caf5c56..4721749 100644 --- a/packages/webpack-rsc/src/webpack-rsc-server-loader.test.ts +++ b/packages/webpack-rsc/src/webpack-rsc-server-loader.test.ts @@ -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`, @@ -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(), ); }); @@ -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`, + }, ], }); }); diff --git a/packages/webpack-rsc/src/webpack-rsc-ssr-loader.test.ts b/packages/webpack-rsc/src/webpack-rsc-ssr-loader.test.ts index 8c435e3..5c33288 100644 --- a/packages/webpack-rsc/src/webpack-rsc-ssr-loader.test.ts +++ b/packages/webpack-rsc/src/webpack-rsc-ssr-loader.test.ts @@ -105,7 +105,10 @@ export function ClientComponent({action}) { }, []); return null; -}`.trim(), +} + +export default ClientComponent; +`.trim(), ); });