Skip to content

Commit 2939812

Browse files
authored
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
1 parent 768cd26 commit 2939812

File tree

4 files changed

+63
-22
lines changed

4 files changed

+63
-22
lines changed

.changeset/thin-cheetahs-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-react-transform": patch
3+
---
4+
5+
Add support for auto transforming Components declared as object properties

packages/react-transform/src/index.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,18 @@ function basename(filename: string | undefined): string | undefined {
6767

6868
const DefaultExportSymbol = Symbol("DefaultExportSymbol");
6969

70+
function getObjectPropertyKey(
71+
node: BabelTypes.ObjectProperty | BabelTypes.ObjectMethod
72+
): string | null {
73+
if (node.key.type === "Identifier") {
74+
return node.key.name;
75+
} else if (node.key.type === "StringLiteral") {
76+
return node.key.value;
77+
}
78+
79+
return null;
80+
}
81+
7082
/**
7183
* If the function node has a name (i.e. is a function declaration with a
7284
* name), return that. Else return null.
@@ -75,11 +87,7 @@ function getFunctionNodeName(path: NodePath<FunctionLike>): string | null {
7587
if (path.node.type === "FunctionDeclaration" && path.node.id) {
7688
return path.node.id.name;
7789
} else if (path.node.type === "ObjectMethod") {
78-
if (path.node.key.type === "Identifier") {
79-
return path.node.key.name;
80-
} else if (path.node.key.type === "StringLiteral") {
81-
return path.node.key.value;
82-
}
90+
return getObjectPropertyKey(path.node);
8391
}
8492

8593
return null;
@@ -122,6 +130,8 @@ function getFunctionNameFromParent(
122130
} else {
123131
return null;
124132
}
133+
} else if (parentPath.node.type === "ObjectProperty") {
134+
return getObjectPropertyKey(parentPath.node);
125135
} else if (parentPath.node.type === "ExportDefaultDeclaration") {
126136
return DefaultExportSymbol;
127137
} else if (
@@ -150,10 +160,10 @@ function getFunctionName(
150160
return getFunctionNameFromParent(path.parentPath);
151161
}
152162

153-
function fnNameStartsWithCapital(name: string | null): boolean {
163+
function isComponentName(name: string | null): boolean {
154164
return name?.match(/^[A-Z]/) != null ?? false;
155165
}
156-
function fnNameStartsWithUse(name: string | null): boolean {
166+
function isCustomHookName(name: string | null): boolean {
157167
return name?.match(/^use[A-Z]/) != null ?? null;
158168
}
159169

@@ -230,14 +240,10 @@ function isComponentFunction(
230240
): boolean {
231241
return (
232242
getData(path.scope, containsJSX) === true && // Function contains JSX
233-
fnNameStartsWithCapital(functionName) // Function name indicates it's a component
243+
isComponentName(functionName) // Function name indicates it's a component
234244
);
235245
}
236246

237-
function isCustomHook(functionName: string | null): boolean {
238-
return fnNameStartsWithUse(functionName); // Function name indicates it's a hook
239-
}
240-
241247
function shouldTransform(
242248
path: NodePath<FunctionLike>,
243249
functionName: string | null,
@@ -255,7 +261,8 @@ function shouldTransform(
255261
if (options.mode == null || options.mode === "auto") {
256262
return (
257263
getData(path.scope, maybeUsesSignal) === true && // Function appears to use signals;
258-
(isComponentFunction(path, functionName) || isCustomHook(functionName))
264+
(isComponentFunction(path, functionName) ||
265+
isCustomHookName(functionName))
259266
);
260267
}
261268

@@ -330,7 +337,7 @@ function transformFunction(
330337
state: PluginPass
331338
) {
332339
let newFunction: FunctionLike;
333-
if (isCustomHook(functionName) || options.experimental?.noTryFinally) {
340+
if (isCustomHookName(functionName) || options.experimental?.noTryFinally) {
334341
// For custom hooks, we don't need to wrap the function body in a
335342
// try/finally block because later code in the function's render body could
336343
// read signals and we want to track and associate those signals with this
@@ -452,9 +459,7 @@ function isComponentLike(
452459
path: NodePath<FunctionLike>,
453460
functionName: string | null
454461
): boolean {
455-
return (
456-
!getData(path, alreadyTransformed) && fnNameStartsWithCapital(functionName)
457-
);
462+
return !getData(path, alreadyTransformed) && isComponentName(functionName);
458463
}
459464

460465
export default function signalsTransform(

packages/react-transform/test/browser/e2e.test.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,39 @@ describe("React Signals babel transfrom - browser E2E tests", () => {
314314
expect(ref.current).to.equal(scratch.firstChild);
315315
});
316316

317+
it("should rerender registry-style declared components", async () => {
318+
const { App, name, lang } = await createComponent(`
319+
import { signal } from "@preact/signals-core";
320+
import { memo } from "react";
321+
322+
const Greeting = {
323+
English: memo(({ name }) => <div>Hello {name.value}</div>),
324+
["Espanol"]: memo(({ name }) => <div>Hola {name.value}</div>),
325+
};
326+
327+
export const name = signal("John");
328+
export const lang = signal("English");
329+
330+
export function App() {
331+
const Component = Greeting[lang.value];
332+
return <Component name={name} />;
333+
}
334+
`);
335+
336+
await render(<App />);
337+
expect(scratch.innerHTML).to.equal("<div>Hello John</div>");
338+
339+
await act(() => {
340+
name.value = "Jane";
341+
});
342+
expect(scratch.innerHTML).to.equal("<div>Hello Jane</div>");
343+
344+
await act(() => {
345+
lang.value = "Espanol";
346+
});
347+
expect(scratch.innerHTML).to.equal("<div>Hola Jane</div>");
348+
});
349+
317350
it("should transform components authored inside a test's body", async () => {
318351
const { name, App } = await createComponent(`
319352
import { signal } from "@preact/signals-core";

packages/react-transform/test/node/index.test.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,9 @@ function runGeneratedTestCases(config: TestCaseConfig) {
153153
});
154154

155155
// e.g. const obj = { C: () => {} };
156-
if (config.comment !== undefined) {
157-
describe("object property components", () => {
158-
runTestCases(config, objectPropertyComp(codeConfig));
159-
});
160-
}
156+
describe("object property components", () => {
157+
runTestCases(config, objectPropertyComp(codeConfig));
158+
});
161159

162160
// e.g. export default () => {};
163161
describe(`default exported components`, () => {

0 commit comments

Comments
 (0)