diff --git a/examples/general/src/app/page.jsx b/examples/general/src/app/page.jsx index 4b51e57..459db49 100644 --- a/examples/general/src/app/page.jsx +++ b/examples/general/src/app/page.jsx @@ -1,4 +1,5 @@ import { Comp } from "./Comp"; +import { Comp as Comp2 } from "./Comp"; export const dynamic = 'force-dynamic'; @@ -6,15 +7,22 @@ export default async function Page() { // throw new Error("This is a Page error"); function renderTest() { - throw new Error("This is a render method error"); - // return Should not be wrapped + // throw new Error("This is a render method error"); + return Should not be wrapped + } + + function ABC() { + // throw new Error("This is a ABC error"); + return
ABC
} return (

Page

+ {renderTest()} +
); } diff --git a/packages/next-rsc-error-handler/src/index.d.ts b/packages/next-rsc-error-handler/src/index.d.ts index 3833acc..1b59daa 100644 --- a/packages/next-rsc-error-handler/src/index.d.ts +++ b/packages/next-rsc-error-handler/src/index.d.ts @@ -1,6 +1,8 @@ import { NextConfig } from "next"; -export interface RscErrorHandlerOptions {} +export interface RscErrorHandlerOptions { + componentName: RegExp; +} export function rscErrorHandler( options: RscErrorHandlerOptions @@ -8,8 +10,7 @@ export function rscErrorHandler( export interface GlobalServerErrorContext { filePath: string; - functionName: string; - options: RscErrorHandlerOptions; + componentName: string; } export type GlobalServerError = ( diff --git a/packages/next-rsc-error-handler/src/index.js b/packages/next-rsc-error-handler/src/index.js index 636f586..26365a8 100644 --- a/packages/next-rsc-error-handler/src/index.js +++ b/packages/next-rsc-error-handler/src/index.js @@ -1,4 +1,6 @@ -const defaultOptions = {}; +const defaultOptions = { + componentName: /^[A-Z]/, +}; export function rscErrorHandler(options = {}) { return function withLoader(nextConfig = {}) { diff --git a/packages/next-rsc-error-handler/src/loader.js b/packages/next-rsc-error-handler/src/loader.js index 4ae0b4f..71708ac 100644 --- a/packages/next-rsc-error-handler/src/loader.js +++ b/packages/next-rsc-error-handler/src/loader.js @@ -6,8 +6,9 @@ import * as t from "@babel/types"; import { getRelativePath, isClientComponent, - isReactElement, - wrapWithFunction, + wrapFunctionDeclaration, + wrapArrowFunction, + getOptionsExpressionLiteral, } from "./utils.js"; const WRAPPER_NAME = "__rscWrapper"; @@ -28,26 +29,31 @@ export default function (source) { let wasWrapped = false; - traverse.default(ast, { - enter(p) { - if (!p.isFunctionDeclaration() && !p.isArrowFunctionExpression()) { - return; - } + function wrapIfComponent(functionName, p, wrapFn) { + if (!options.componentName.test(functionName)) { + return; + } - if (!isReactElement(p)) { - return; - } + const ctx = { + filePath: getRelativePath(resourcePath), + componentName: functionName, + }; + const optionsExpression = getOptionsExpressionLiteral(ctx); - const ctx = { - filePath: getRelativePath(resourcePath), - functionName: getFunctionName(p), - options, - }; + wasWrapped = true; - wasWrapped = true; - wrapWithFunction(p, WRAPPER_NAME, ctx); + wrapFn(p, WRAPPER_NAME, optionsExpression); + } - p.skip(); + traverse.default(ast, { + // TODO add FunctionExpression + FunctionDeclaration(p) { + const functionName = p.node.id?.name ?? ""; + wrapIfComponent(functionName, p, wrapFunctionDeclaration); + }, + ArrowFunctionExpression(p) { + const functionName = getArrowFunctionName(p); + wrapIfComponent(functionName, p, wrapArrowFunction); }, }); @@ -59,19 +65,14 @@ export default function (source) { return output.code; } -function getFunctionName(p) { - if (p.isFunctionDeclaration()) { - return p.node.id?.name; - } - +function getArrowFunctionName(p) { if (p.isArrowFunctionExpression()) { const parent = p.parentPath; if (parent.isVariableDeclarator() && parent.node.id.type === "Identifier") { return parent.node.id.name; } } - - return "(anonymous)"; + return ""; } function addImport(ast) { diff --git a/packages/next-rsc-error-handler/src/utils.js b/packages/next-rsc-error-handler/src/utils.js index 019ddf0..42ec8be 100644 --- a/packages/next-rsc-error-handler/src/utils.js +++ b/packages/next-rsc-error-handler/src/utils.js @@ -10,64 +10,6 @@ export function isClientComponent(source) { return source.includes("__next_internal_client_entry_do_not_use__"); } -/** - * Return true if node is a function and returns jsx. - * @param {Path} p - * @returns {boolean} - */ -export function isReactElement(p) { - let isReactElement = false; - if (p.isFunctionDeclaration() || p.isArrowFunctionExpression()) { - isReactElement = isReturningJSXElement(p); - } - - return isReactElement; -} - -const JSX_FN_NAMES = - process.env.NODE_ENV === "production" - ? ["_jsx", "_jsxs"] - : ["_jsxDEV", "_jsxsDEV"]; - -/** - * Helper function that returns true if node returns a JSX element. - * @param {Path} p - * @returns {boolean} - */ -function isReturningJSXElement(p) { - let foundJSX = false; - - p.traverse({ - CallExpression(innerP) { - const calleePath = innerP.get("callee"); - if ( - t.isIdentifier(calleePath.node) && - JSX_FN_NAMES.includes(calleePath.node.name) - ) { - foundJSX = true; - innerP.stop(); - } - }, - }); - - return foundJSX; -} - -/** - * Wraps FunctionDeclaration or ArrowFunctionExpression with a function call with context. - */ -export function wrapWithFunction(p, wrapFunctionName, context) { - const optionsExpression = getOptionsExpressionLiteral(context); - - if (p.isArrowFunctionExpression()) { - return wrapArrowFunction(p, wrapFunctionName, optionsExpression); - } else if (p.isFunctionDeclaration()) { - return wrapFunctionDeclaration(p, wrapFunctionName, optionsExpression); - } else { - throw new Error("Unsupported type of function"); - } -} - function getOptionsExpression(obj) { return t.objectExpression( Object.entries(obj).map(([key, value]) => @@ -76,7 +18,7 @@ function getOptionsExpression(obj) { ); } -function getOptionsExpressionLiteral(value) { +export function getOptionsExpressionLiteral(value) { if (value === null) { return t.nullLiteral(); } @@ -96,13 +38,17 @@ function getOptionsExpressionLiteral(value) { } } -function wrapArrowFunction(p, wrapFunctionName, optionsNode) { +export function wrapArrowFunction(p, wrapFunctionName, optionsNode) { return p.replaceWith( t.callExpression(t.identifier(wrapFunctionName), [p.node, optionsNode]) ); } -function wrapFunctionDeclaration(p, wrapFunctionName, argumentsExpression) { +export function wrapFunctionDeclaration( + p, + wrapFunctionName, + argumentsExpression +) { const expression = t.functionExpression( null, p.node.params, @@ -112,8 +58,7 @@ function wrapFunctionDeclaration(p, wrapFunctionName, argumentsExpression) { ); if (p.node.id == null) { - // FIXME should work no matter if there is no function name - throw new Error("FunctionDeclaration has no id."); + throw new Error("FunctionDeclaration has no name"); } const originalFunctionIdentifier = t.identifier(p.node.id.name);