Skip to content

Commit

Permalink
Add support for auto transforming Components declared as object prope…
Browse files Browse the repository at this point in the history
…rties (#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
  • Loading branch information
andrewiggins authored Nov 22, 2023
1 parent 768cd26 commit 2939812
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-cheetahs-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@preact/signals-react-transform": patch
---

Add support for auto transforming Components declared as object properties
39 changes: 22 additions & 17 deletions packages/react-transform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -75,11 +87,7 @@ function getFunctionNodeName(path: NodePath<FunctionLike>): 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;
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<FunctionLike>,
functionName: string | null,
Expand All @@ -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))
);
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -452,9 +459,7 @@ function isComponentLike(
path: NodePath<FunctionLike>,
functionName: string | null
): boolean {
return (
!getData(path, alreadyTransformed) && fnNameStartsWithCapital(functionName)
);
return !getData(path, alreadyTransformed) && isComponentName(functionName);
}

export default function signalsTransform(
Expand Down
33 changes: 33 additions & 0 deletions packages/react-transform/test/browser/e2e.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => <div>Hello {name.value}</div>),
["Espanol"]: memo(({ name }) => <div>Hola {name.value}</div>),
};
export const name = signal("John");
export const lang = signal("English");
export function App() {
const Component = Greeting[lang.value];
return <Component name={name} />;
}
`);

await render(<App />);
expect(scratch.innerHTML).to.equal("<div>Hello John</div>");

await act(() => {
name.value = "Jane";
});
expect(scratch.innerHTML).to.equal("<div>Hello Jane</div>");

await act(() => {
lang.value = "Espanol";
});
expect(scratch.innerHTML).to.equal("<div>Hola Jane</div>");
});

it("should transform components authored inside a test's body", async () => {
const { name, App } = await createComponent(`
import { signal } from "@preact/signals-core";
Expand Down
8 changes: 3 additions & 5 deletions packages/react-transform/test/node/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`, () => {
Expand Down

0 comments on commit 2939812

Please sign in to comment.