Skip to content

Commit 76babcb

Browse files
authored
Add debug logging to inspect what components are transformed by plugin (#442)
1 parent 4c433c3 commit 76babcb

File tree

7 files changed

+118
-85
lines changed

7 files changed

+118
-85
lines changed

.changeset/twenty-pillows-scream.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@preact/signals-react-transform": patch
3+
---
4+
5+
Add debug logging to inspect what components are transformed by plugin

packages/react-transform/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"@babel/helper-module-imports": "^7.22.5",
4949
"@babel/helper-plugin-utils": "^7.22.5",
5050
"@preact/signals-react": "workspace:^1.3.6",
51+
"debug": "^4.3.4",
5152
"use-sync-external-store": "^1.2.0"
5253
},
5354
"peerDependencies": {
@@ -59,6 +60,7 @@
5960
"@types/babel__core": "^7.20.1",
6061
"@types/babel__helper-module-imports": "^7.18.0",
6162
"@types/babel__helper-plugin-utils": "^7.10.0",
63+
"@types/debug": "^4.1.12",
6264
"@types/prettier": "^2.7.3",
6365
"@types/react": "^18.0.18",
6466
"@types/react-dom": "^18.0.6",

packages/react-transform/src/index.ts

+84-71
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
template,
88
} from "@babel/core";
99
import { isModule, addNamed } from "@babel/helper-module-imports";
10+
import type { VisitNodeObject } from "@babel/traverse";
11+
import debug from "debug";
1012

1113
interface PluginArgs {
1214
types: typeof BabelTypes;
@@ -23,6 +25,11 @@ const maybeUsesSignal = "maybeUsesSignal";
2325
const containsJSX = "containsJSX";
2426
const alreadyTransformed = "alreadyTransformed";
2527

28+
const logger = {
29+
transformed: debug("signals:react-transform:transformed"),
30+
skipped: debug("signals:react-transform:skipped"),
31+
};
32+
2633
const get = (pass: PluginPass, name: any) =>
2734
pass.get(`${dataNamespace}/${name}`);
2835
const set = (pass: PluginPass, name: string, v: any) =>
@@ -143,28 +150,11 @@ function getFunctionName(
143150
return getFunctionNameFromParent(path.parentPath);
144151
}
145152

146-
function fnNameStartsWithCapital(
147-
path: NodePath<FunctionLike>,
148-
filename: string | undefined
149-
): boolean {
150-
const name = getFunctionName(path);
151-
if (!name) return false;
152-
if (name === DefaultExportSymbol) {
153-
return basename(filename)?.match(/^[A-Z]/) != null ?? false;
154-
}
155-
return name.match(/^[A-Z]/) != null;
153+
function fnNameStartsWithCapital(name: string | null): boolean {
154+
return name?.match(/^[A-Z]/) != null ?? false;
156155
}
157-
function fnNameStartsWithUse(
158-
path: NodePath<FunctionLike>,
159-
filename: string | undefined
160-
): boolean {
161-
const name = getFunctionName(path);
162-
if (!name) return false;
163-
if (name === DefaultExportSymbol) {
164-
return basename(filename)?.match(/^use[A-Z]/) != null ?? false;
165-
}
166-
167-
return name.match(/^use[A-Z]/) != null;
156+
function fnNameStartsWithUse(name: string | null): boolean {
157+
return name?.match(/^use[A-Z]/) != null ?? null;
168158
}
169159

170160
function hasLeadingComment(path: NodePath, comment: RegExp): boolean {
@@ -236,41 +226,36 @@ function isOptedOutOfSignalTracking(path: NodePath | null): boolean {
236226

237227
function isComponentFunction(
238228
path: NodePath<FunctionLike>,
239-
filename: string | undefined
229+
functionName: string | null
240230
): boolean {
241231
return (
242232
getData(path.scope, containsJSX) === true && // Function contains JSX
243-
fnNameStartsWithCapital(path, filename) // Function name indicates it's a component
233+
fnNameStartsWithCapital(functionName) // Function name indicates it's a component
244234
);
245235
}
246236

247-
function isCustomHook(
248-
path: NodePath<FunctionLike>,
249-
filename: string | undefined
250-
): boolean {
251-
return fnNameStartsWithUse(path, filename); // Function name indicates it's a hook
237+
function isCustomHook(functionName: string | null): boolean {
238+
return fnNameStartsWithUse(functionName); // Function name indicates it's a hook
252239
}
253240

254241
function shouldTransform(
255242
path: NodePath<FunctionLike>,
256-
filename: string | undefined,
243+
functionName: string | null,
257244
options: PluginOptions
258245
): boolean {
259-
if (getData(path, alreadyTransformed) === true) return false;
260-
261246
// Opt-out takes first precedence
262247
if (isOptedOutOfSignalTracking(path)) return false;
263248
// Opt-in opts in to transformation regardless of mode
264249
if (isOptedIntoSignalTracking(path)) return true;
265250

266251
if (options.mode === "all") {
267-
return isComponentFunction(path, filename);
252+
return isComponentFunction(path, functionName);
268253
}
269254

270255
if (options.mode == null || options.mode === "auto") {
271256
return (
272257
getData(path.scope, maybeUsesSignal) === true && // Function appears to use signals;
273-
(isComponentFunction(path, filename) || isCustomHook(path, filename))
258+
(isComponentFunction(path, functionName) || isCustomHook(functionName))
274259
);
275260
}
276261

@@ -341,11 +326,11 @@ function transformFunction(
341326
t: typeof BabelTypes,
342327
options: PluginOptions,
343328
path: NodePath<FunctionLike>,
344-
filename: string | undefined,
329+
functionName: string | null,
345330
state: PluginPass
346331
) {
347332
let newFunction: FunctionLike;
348-
if (isCustomHook(path, filename) || options.experimental?.noTryFinally) {
333+
if (isCustomHook(functionName) || options.experimental?.noTryFinally) {
349334
// For custom hooks, we don't need to wrap the function body in a
350335
// try/finally block because later code in the function's render body could
351336
// read signals and we want to track and associate those signals with this
@@ -435,10 +420,70 @@ export interface PluginOptions {
435420
};
436421
}
437422

423+
function log(
424+
transformed: boolean,
425+
path: NodePath<FunctionLike>,
426+
functionName: string | null,
427+
currentFile: string | undefined
428+
) {
429+
if (!logger.transformed.enabled && !logger.skipped.enabled) return;
430+
431+
let cwd = "";
432+
if (typeof process !== undefined && typeof process.cwd == "function") {
433+
cwd = process.cwd().replace(/\\([^ ])/g, "/$1");
434+
cwd = cwd.endsWith("/") ? cwd : cwd + "/";
435+
}
436+
437+
const relativePath = currentFile?.replace(cwd, "") ?? "";
438+
const lineNum = path.node.loc?.start.line;
439+
functionName = functionName ?? "<anonymous>";
440+
441+
if (transformed) {
442+
logger.transformed(`${functionName} (${relativePath}:${lineNum})`);
443+
} else {
444+
logger.skipped(`${functionName} (${relativePath}:${lineNum}) %o`, {
445+
hasSignals: getData(path.scope, maybeUsesSignal) ?? false,
446+
hasJSX: getData(path.scope, containsJSX) ?? false,
447+
});
448+
}
449+
}
450+
451+
function isComponentLike(
452+
path: NodePath<FunctionLike>,
453+
functionName: string | null
454+
): boolean {
455+
return (
456+
!getData(path, alreadyTransformed) && fnNameStartsWithCapital(functionName)
457+
);
458+
}
459+
438460
export default function signalsTransform(
439461
{ types: t }: PluginArgs,
440462
options: PluginOptions
441463
): PluginObj {
464+
// TODO: Consider alternate implementation, where on enter of a function
465+
// expression, we run our own manual scan the AST to determine if the
466+
// function uses signals and is a component. This manual scan once upon
467+
// seeing a function would probably be faster than running an entire
468+
// babel pass with plugins on components twice.
469+
const visitFunction: VisitNodeObject<PluginPass, FunctionLike> = {
470+
exit(path, state) {
471+
if (getData(path, alreadyTransformed) === true) return false;
472+
473+
let functionName = getFunctionName(path);
474+
if (functionName === DefaultExportSymbol) {
475+
functionName = basename(this.filename) ?? null;
476+
}
477+
478+
if (shouldTransform(path, functionName, state.opts)) {
479+
transformFunction(t, state.opts, path, functionName, state);
480+
log(true, path, functionName, this.filename);
481+
} else if (isComponentLike(path, functionName)) {
482+
log(false, path, functionName, this.filename);
483+
}
484+
},
485+
};
486+
442487
return {
443488
name: "@preact/signals-transform",
444489
visitor: {
@@ -462,42 +507,10 @@ export default function signalsTransform(
462507
},
463508
},
464509

465-
ArrowFunctionExpression: {
466-
// TODO: Consider alternate implementation, where on enter of a function
467-
// expression, we run our own manual scan the AST to determine if the
468-
// function uses signals and is a component. This manual scan once upon
469-
// seeing a function would probably be faster than running an entire
470-
// babel pass with plugins on components twice.
471-
exit(path, state) {
472-
if (shouldTransform(path, this.filename, options)) {
473-
transformFunction(t, options, path, this.filename, state);
474-
}
475-
},
476-
},
477-
478-
FunctionExpression: {
479-
exit(path, state) {
480-
if (shouldTransform(path, this.filename, options)) {
481-
transformFunction(t, options, path, this.filename, state);
482-
}
483-
},
484-
},
485-
486-
FunctionDeclaration: {
487-
exit(path, state) {
488-
if (shouldTransform(path, this.filename, options)) {
489-
transformFunction(t, options, path, this.filename, state);
490-
}
491-
},
492-
},
493-
494-
ObjectMethod: {
495-
exit(path, state) {
496-
if (shouldTransform(path, this.filename, options)) {
497-
transformFunction(t, options, path, this.filename, state);
498-
}
499-
},
500-
},
510+
ArrowFunctionExpression: visitFunction,
511+
FunctionExpression: visitFunction,
512+
FunctionDeclaration: visitFunction,
513+
ObjectMethod: visitFunction,
501514

502515
MemberExpression(path) {
503516
if (isValueMemberExpression(path)) {

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
// To help interactively debug a specific test case, add the test ids of the
2020
// test cases you want to debug to the `debugTestIds` array, e.g. (["258",
2121
// "259"]). Set to true to debug all tests.
22-
const DEBUG_TEST_IDS: string[] | true = true;
22+
const DEBUG_TEST_IDS: string[] | true = [];
2323

2424
const format = (code: string) => prettier.format(code, { parser: "babel" });
2525

packages/react/test/browser/updates.test.tsx

+8-9
Original file line numberDiff line numberDiff line change
@@ -263,17 +263,16 @@ describe("@preact/signals-react updating", () => {
263263
it("should update memo'ed component via signals", async () => {
264264
const sig = signal("foo");
265265

266-
function Inner() {
266+
function UseMemoTestInner() {
267267
const value = sig.value;
268268
return <p>{value}</p>;
269269
}
270270

271-
function App() {
272-
sig.value;
273-
return useMemo(() => <Inner />, []);
271+
function UseMemoTestApp() {
272+
return useMemo(() => <UseMemoTestInner />, []);
274273
}
275274

276-
await render(<App />);
275+
await render(<UseMemoTestApp />);
277276
expect(scratch.textContent).to.equal("foo");
278277

279278
await act(() => {
@@ -326,10 +325,10 @@ describe("@preact/signals-react updating", () => {
326325
it("should consistently rerender in strict mode (with memo)", async () => {
327326
const sig = signal(-1);
328327

329-
const Test = memo(() => <p>{sig.value}</p>);
328+
const ReactMemoTest = memo(() => <p>{sig.value}</p>);
330329
const App = () => (
331330
<StrictMode>
332-
<Test />
331+
<ReactMemoTest />
333332
</StrictMode>
334333
);
335334

@@ -347,7 +346,7 @@ describe("@preact/signals-react updating", () => {
347346
it("should render static markup of a component", async () => {
348347
const count = signal(0);
349348

350-
const Test = () => {
349+
const StaticMarkupTest = () => {
351350
return (
352351
<pre>
353352
{renderToStaticMarkup(<code>{count}</code>)}
@@ -356,7 +355,7 @@ describe("@preact/signals-react updating", () => {
356355
);
357356
};
358357

359-
await render(<Test />);
358+
await render(<StaticMarkupTest />);
360359
expect(scratch.textContent).to.equal("<code>0</code><code>0</code>");
361360

362361
for (let i = 0; i < 3; i++) {

pnpm-lock.yaml

+16-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)