From 2939812a972b62830e0a839dcc9a8024ab5c7bc8 Mon Sep 17 00:00:00 2001 From: Andre Wiggins <459878+andrewiggins@users.noreply.github.com> Date: Tue, 21 Nov 2023 21:48:00 -0800 Subject: [PATCH] Add support for auto transforming Components declared as object properties (#444) * Add support for auto transforming Components declared as object properties * Refactor object property key retrieval in react-transform * Refactor component and custom hook name checking functions --- .changeset/thin-cheetahs-tie.md | 5 +++ packages/react-transform/src/index.ts | 39 +++++++++++-------- .../react-transform/test/browser/e2e.test.tsx | 33 ++++++++++++++++ .../react-transform/test/node/index.test.tsx | 8 ++-- 4 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 .changeset/thin-cheetahs-tie.md diff --git a/.changeset/thin-cheetahs-tie.md b/.changeset/thin-cheetahs-tie.md new file mode 100644 index 000000000..63835f30c --- /dev/null +++ b/.changeset/thin-cheetahs-tie.md @@ -0,0 +1,5 @@ +--- +"@preact/signals-react-transform": patch +--- + +Add support for auto transforming Components declared as object properties diff --git a/packages/react-transform/src/index.ts b/packages/react-transform/src/index.ts index 8536fdc15..cd6530452 100644 --- a/packages/react-transform/src/index.ts +++ b/packages/react-transform/src/index.ts @@ -67,6 +67,18 @@ function basename(filename: string | undefined): string | undefined { const DefaultExportSymbol = Symbol("DefaultExportSymbol"); +function getObjectPropertyKey( + node: BabelTypes.ObjectProperty | BabelTypes.ObjectMethod +): string | null { + if (node.key.type === "Identifier") { + return node.key.name; + } else if (node.key.type === "StringLiteral") { + return node.key.value; + } + + return null; +} + /** * If the function node has a name (i.e. is a function declaration with a * name), return that. Else return null. @@ -75,11 +87,7 @@ function getFunctionNodeName(path: NodePath): string | null { if (path.node.type === "FunctionDeclaration" && path.node.id) { return path.node.id.name; } else if (path.node.type === "ObjectMethod") { - if (path.node.key.type === "Identifier") { - return path.node.key.name; - } else if (path.node.key.type === "StringLiteral") { - return path.node.key.value; - } + return getObjectPropertyKey(path.node); } return null; @@ -122,6 +130,8 @@ function getFunctionNameFromParent( } else { return null; } + } else if (parentPath.node.type === "ObjectProperty") { + return getObjectPropertyKey(parentPath.node); } else if (parentPath.node.type === "ExportDefaultDeclaration") { return DefaultExportSymbol; } else if ( @@ -150,10 +160,10 @@ function getFunctionName( return getFunctionNameFromParent(path.parentPath); } -function fnNameStartsWithCapital(name: string | null): boolean { +function isComponentName(name: string | null): boolean { return name?.match(/^[A-Z]/) != null ?? false; } -function fnNameStartsWithUse(name: string | null): boolean { +function isCustomHookName(name: string | null): boolean { return name?.match(/^use[A-Z]/) != null ?? null; } @@ -230,14 +240,10 @@ function isComponentFunction( ): boolean { return ( getData(path.scope, containsJSX) === true && // Function contains JSX - fnNameStartsWithCapital(functionName) // Function name indicates it's a component + isComponentName(functionName) // Function name indicates it's a component ); } -function isCustomHook(functionName: string | null): boolean { - return fnNameStartsWithUse(functionName); // Function name indicates it's a hook -} - function shouldTransform( path: NodePath, functionName: string | null, @@ -255,7 +261,8 @@ function shouldTransform( if (options.mode == null || options.mode === "auto") { return ( getData(path.scope, maybeUsesSignal) === true && // Function appears to use signals; - (isComponentFunction(path, functionName) || isCustomHook(functionName)) + (isComponentFunction(path, functionName) || + isCustomHookName(functionName)) ); } @@ -330,7 +337,7 @@ function transformFunction( state: PluginPass ) { let newFunction: FunctionLike; - if (isCustomHook(functionName) || options.experimental?.noTryFinally) { + if (isCustomHookName(functionName) || options.experimental?.noTryFinally) { // For custom hooks, we don't need to wrap the function body in a // try/finally block because later code in the function's render body could // read signals and we want to track and associate those signals with this @@ -452,9 +459,7 @@ function isComponentLike( path: NodePath, functionName: string | null ): boolean { - return ( - !getData(path, alreadyTransformed) && fnNameStartsWithCapital(functionName) - ); + return !getData(path, alreadyTransformed) && isComponentName(functionName); } export default function signalsTransform( diff --git a/packages/react-transform/test/browser/e2e.test.tsx b/packages/react-transform/test/browser/e2e.test.tsx index f9aaae793..0f71543aa 100644 --- a/packages/react-transform/test/browser/e2e.test.tsx +++ b/packages/react-transform/test/browser/e2e.test.tsx @@ -314,6 +314,39 @@ describe("React Signals babel transfrom - browser E2E tests", () => { expect(ref.current).to.equal(scratch.firstChild); }); + it("should rerender registry-style declared components", async () => { + const { App, name, lang } = await createComponent(` + import { signal } from "@preact/signals-core"; + import { memo } from "react"; + + const Greeting = { + English: memo(({ name }) =>
Hello {name.value}
), + ["Espanol"]: memo(({ name }) =>
Hola {name.value}
), + }; + + export const name = signal("John"); + export const lang = signal("English"); + + export function App() { + const Component = Greeting[lang.value]; + return ; + } + `); + + await render(); + expect(scratch.innerHTML).to.equal("
Hello John
"); + + await act(() => { + name.value = "Jane"; + }); + expect(scratch.innerHTML).to.equal("
Hello Jane
"); + + await act(() => { + lang.value = "Espanol"; + }); + expect(scratch.innerHTML).to.equal("
Hola Jane
"); + }); + it("should transform components authored inside a test's body", async () => { const { name, App } = await createComponent(` import { signal } from "@preact/signals-core"; diff --git a/packages/react-transform/test/node/index.test.tsx b/packages/react-transform/test/node/index.test.tsx index f4ec2b0f0..14dd3f4de 100644 --- a/packages/react-transform/test/node/index.test.tsx +++ b/packages/react-transform/test/node/index.test.tsx @@ -153,11 +153,9 @@ function runGeneratedTestCases(config: TestCaseConfig) { }); // e.g. const obj = { C: () => {} }; - if (config.comment !== undefined) { - describe("object property components", () => { - runTestCases(config, objectPropertyComp(codeConfig)); - }); - } + describe("object property components", () => { + runTestCases(config, objectPropertyComp(codeConfig)); + }); // e.g. export default () => {}; describe(`default exported components`, () => {