diff --git a/examples/general/src/app/Comp.jsx b/examples/general/src/app/Comp.jsx index 1cd65ad..dc22a1a 100644 --- a/examples/general/src/app/Comp.jsx +++ b/examples/general/src/app/Comp.jsx @@ -1,4 +1,8 @@ +'use client'; + +import { Inner } from './Inner'; + export function Comp() { - throw new Error('Comp error thrown'); - return Math.random() > 0.5 ?
Comp content
: 'Comp content'; + // throw new Error('Comp error thrown'); + return
Comp content
; } diff --git a/examples/general/src/app/Inner.jsx b/examples/general/src/app/Inner.jsx new file mode 100644 index 0000000..9b7e722 --- /dev/null +++ b/examples/general/src/app/Inner.jsx @@ -0,0 +1,5 @@ +'use client'; + +export function Inner() { + return
Inner
; +} \ No newline at end of file diff --git a/examples/general/src/app/page.jsx b/examples/general/src/app/page.jsx index 459db49..6f5b7b7 100644 --- a/examples/general/src/app/page.jsx +++ b/examples/general/src/app/page.jsx @@ -1,5 +1,6 @@ import { Comp } from "./Comp"; import { Comp as Comp2 } from "./Comp"; +import { Inner } from "./Inner"; export const dynamic = 'force-dynamic'; @@ -23,6 +24,7 @@ export default async function Page() { {renderTest()} + ); } diff --git a/packages/next-rsc-error-handler/README.md b/packages/next-rsc-error-handler/README.md index 6d23abc..c2f34cd 100644 --- a/packages/next-rsc-error-handler/README.md +++ b/packages/next-rsc-error-handler/README.md @@ -2,7 +2,7 @@ Webpack plugin that allow to handle RSC errors on the server side. -**This plugin requires all the client components to be marked with `'use client;'`** +**This plugin does not allow dual client and server components** ## Get started diff --git a/packages/next-rsc-error-handler/src/loader.js b/packages/next-rsc-error-handler/src/loader.js index 41e5117..905f42e 100644 --- a/packages/next-rsc-error-handler/src/loader.js +++ b/packages/next-rsc-error-handler/src/loader.js @@ -2,6 +2,7 @@ import parser from "@babel/parser"; import traverse from "@babel/traverse"; import generate from "@babel/generator"; import * as t from "@babel/types"; +import path from "node:path"; import { getRelativePath, @@ -14,15 +15,36 @@ import { const WRAPPER_NAME = "__rscWrapper"; const WRAPPER_PATH = "next-rsc-error-handler/inserted/wrapper"; +const clientComponents = new Set(); +const serverComponents = new Set(); + export default function (source) { - if (isClientComponent(source)) { + const resourcePath = this.resourcePath; + const relativePath = getRelativePath(resourcePath); + + if (isRoute(relativePath)) { return source; } - const resourcePath = this.resourcePath; - const filePath = getRelativePath(resourcePath); + const noExtRelativePath = dropExtension(relativePath); + const isTrulyClientComponent = isClientComponent(source); + + if (isTrulyClientComponent || clientComponents.has(noExtRelativePath)) { + if (!isTrulyClientComponent && serverComponents.has(noExtRelativePath)) { + throw new Error(`${relativePath} is used on both client and server`); + } + + const ast = parser.parse(source, { + sourceType: "module", + plugins: ["typescript", "jsx"], + }); + + traverse.default(ast, { + ImportDeclaration(p) { + clientComponents.add(getImportRelativePath(resourcePath, p)); + }, + }); - if (isRoute(filePath)) { return source; } @@ -41,17 +63,21 @@ export default function (source) { } const ctx = { - filePath, + filePath: relativePath, componentName: functionName, }; const optionsExpression = getOptionsExpressionLiteral(ctx); wasWrapped = true; - wrapFn(p, WRAPPER_NAME, optionsExpression); } + const innerServerComponents = new Set(); + traverse.default(ast, { + ImportDeclaration(p) { + innerServerComponents.add(getImportRelativePath(resourcePath, p)); + }, // TODO add FunctionExpression FunctionDeclaration(p) { const functionName = p.node.id?.name ?? ""; @@ -67,19 +93,21 @@ export default function (source) { return source; } + innerServerComponents.forEach((c) => serverComponents.add(c)); + addImport(ast); const output = generate.default(ast); return output.code; } -function isInApp(resourcePath) { - return /^(src(\/|\\))?app(\/|\\)/.test(resourcePath); +function isInApp(relativePath) { + return /^(src(\/|\\))?app(\/|\\)/.test(relativePath); } -function isRoute(resourcePath) { +function isRoute(relativePath) { return ( - isInApp(resourcePath) && /(\/|\\)route\.(c|m)?(t|j)s$/.test(resourcePath) + isInApp(relativePath) && /(\/|\\)route\.(c|m)?(t|j)s$/.test(relativePath) ); } @@ -101,3 +129,15 @@ function addImport(ast) { ast.program.body.unshift(wrapperImport); } + +function dropExtension(relativePath) { + return relativePath.replace(/\.[^/.]+$/, ""); +} + +function getImportRelativePath(resourcePath, p) { + return dropExtension( + getRelativePath( + path.resolve(path.dirname(resourcePath), p.node.source.value) + ) + ); +}