Skip to content

Commit d205821

Browse files
committed
Add signal names and debug pre/postamble in babel react
1 parent 5400848 commit d205821

3 files changed

Lines changed: 352 additions & 10 deletions

File tree

extension/src/components/Graph.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export function GraphVisualization({
1414
}: {
1515
updates: Signal<(SignalUpdate | Divider)[]>;
1616
}) {
17-
console.log(updates);
1817
const svgRef = useRef<SVGSVGElement>(null);
1918

2019
// Build graph data from updates signal using a computed

packages/react-transform/src/index.ts

Lines changed: 204 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,119 @@ function isJSXAlternativeCall(
361361
return false;
362362
}
363363

364+
function isSignalCall(path: NodePath<BabelTypes.CallExpression>): boolean {
365+
const callee = path.get("callee");
366+
367+
// Check direct function calls like signal(), computed(), useSignal(), useComputed()
368+
if (callee.isIdentifier()) {
369+
const name = callee.node.name;
370+
return (
371+
name === "signal" ||
372+
name === "computed" ||
373+
name === "useSignal" ||
374+
name === "useComputed"
375+
);
376+
}
377+
378+
return false;
379+
}
380+
381+
function getVariableNameFromDeclarator(
382+
path: NodePath<BabelTypes.CallExpression>
383+
): string | null {
384+
// Walk up the AST to find a variable declarator
385+
let currentPath: NodePath | null = path;
386+
while (currentPath) {
387+
if (
388+
currentPath.isVariableDeclarator() &&
389+
currentPath.node.id.type === "Identifier"
390+
) {
391+
return currentPath.node.id.name;
392+
}
393+
currentPath = currentPath.parentPath;
394+
}
395+
return null;
396+
}
397+
398+
function hasNameInOptions(
399+
t: typeof BabelTypes,
400+
args: NodePath<
401+
| BabelTypes.Expression
402+
| BabelTypes.SpreadElement
403+
| BabelTypes.JSXNamespacedName
404+
| BabelTypes.ArgumentPlaceholder
405+
>[]
406+
): boolean {
407+
// Check if there's a second argument with a name property
408+
if (args.length >= 2) {
409+
const optionsArg = args[1];
410+
if (optionsArg.isObjectExpression()) {
411+
return optionsArg.node.properties.some(prop => {
412+
if (t.isObjectProperty(prop) && !prop.computed) {
413+
if (t.isIdentifier(prop.key, { name: "name" })) {
414+
return true;
415+
}
416+
if (t.isStringLiteral(prop.key) && prop.key.value === "name") {
417+
return true;
418+
}
419+
}
420+
return false;
421+
});
422+
}
423+
}
424+
return false;
425+
}
426+
427+
function injectSignalName(
428+
t: typeof BabelTypes,
429+
path: NodePath<BabelTypes.CallExpression>,
430+
variableName: string,
431+
filename: string | undefined
432+
): void {
433+
const args = path.get("arguments");
434+
435+
// Create enhanced name with filename and line number
436+
let nameValue = variableName;
437+
if (filename) {
438+
const baseName = basename(filename);
439+
const lineNumber = path.node.loc?.start.line;
440+
if (baseName && lineNumber) {
441+
nameValue = `${variableName} (${baseName}:${lineNumber})`;
442+
}
443+
}
444+
445+
const name = t.stringLiteral(nameValue);
446+
447+
if (args.length === 0) {
448+
// No arguments, add both value and options
449+
const nameOption = t.objectExpression([
450+
t.objectProperty(t.identifier("name"), name),
451+
]);
452+
path.node.arguments.push(t.identifier("undefined"), nameOption);
453+
} else if (args.length === 1) {
454+
// One argument (value), add options object
455+
const nameOption = t.objectExpression([
456+
t.objectProperty(t.identifier("name"), name),
457+
]);
458+
path.node.arguments.push(nameOption);
459+
} else if (args.length >= 2) {
460+
// Two or more arguments, modify existing options object
461+
const optionsArg = args[1];
462+
if (optionsArg.isObjectExpression()) {
463+
// Add name property to existing options object
464+
optionsArg.node.properties.push(
465+
t.objectProperty(t.identifier("name"), name)
466+
);
467+
} else {
468+
// Replace second argument with options object containing name
469+
const nameOption = t.objectExpression([
470+
t.objectProperty(t.identifier("name"), name),
471+
]);
472+
args[1].replaceWith(nameOption);
473+
}
474+
}
475+
}
476+
364477
function hasValuePropertyInPattern(pattern: BabelTypes.ObjectPattern): boolean {
365478
for (const property of pattern.properties) {
366479
if (BabelTypes.isObjectProperty(property)) {
@@ -381,24 +494,67 @@ try {
381494
STORE_IDENTIFIER.f();
382495
}`;
383496

497+
const debugTryCatchTemplate = template.statements(
498+
`var STORE_IDENTIFIER = HOOK_IDENTIFIER(HOOK_USAGE);
499+
try {
500+
if (window.__PREACT_SIGNALS_DEVTOOLS__) {
501+
window.__PREACT_SIGNALS_DEVTOOLS__.enterComponent(
502+
COMPONENT_NAME
503+
);
504+
}
505+
BODY
506+
} finally {
507+
STORE_IDENTIFIER.f();
508+
if (window.__PREACT_SIGNALS_DEVTOOLS__) {
509+
window.__PREACT_SIGNALS_DEVTOOLS__.exitComponent();
510+
}
511+
}`,
512+
{
513+
placeholderWhitelist: new Set([
514+
"COMPONENT_NAME_2",
515+
"STORE_IDENTIFIER",
516+
"HOOK_USAGE",
517+
"HOOK_IDENTIFIER",
518+
"BODY",
519+
"COMPONENT_NAME",
520+
"STORE_IDENTIFIER",
521+
]),
522+
placeholderPattern: false,
523+
}
524+
);
525+
384526
function wrapInTryFinally(
385527
t: typeof BabelTypes,
386528
path: NodePath<FunctionLike>,
387529
state: PluginPass,
388-
hookUsage: HookUsage
530+
hookUsage: HookUsage,
531+
componentName: string,
532+
isDebug: boolean
389533
): BabelTypes.BlockStatement {
390534
const stopTrackingIdentifier = path.scope.generateUidIdentifier("effect");
391535

392-
return t.blockStatement(
393-
tryCatchTemplate({
536+
if (isDebug) {
537+
const statements = debugTryCatchTemplate({
538+
COMPONENT_NAME: t.stringLiteral(componentName),
394539
STORE_IDENTIFIER: stopTrackingIdentifier,
395540
HOOK_IDENTIFIER: get(state, getHookIdentifier)(),
396541
HOOK_USAGE: hookUsage,
397542
BODY: t.isBlockStatement(path.node.body)
398-
? path.node.body.body // TODO: Is it okay to elide the block statement here?
543+
? path.node.body.body
399544
: t.returnStatement(path.node.body),
400-
})
401-
);
545+
});
546+
return t.blockStatement(statements);
547+
} else {
548+
const statements = tryCatchTemplate({
549+
STORE_IDENTIFIER: stopTrackingIdentifier,
550+
HOOK_IDENTIFIER: get(state, getHookIdentifier)(),
551+
HOOK_USAGE: hookUsage,
552+
BODY: t.isBlockStatement(path.node.body)
553+
? path.node.body.body
554+
: t.returnStatement(path.node.body),
555+
});
556+
return t.blockStatement(statements);
557+
}
402558
}
403559

404560
function prependUseSignals<T extends FunctionLike>(
@@ -426,7 +582,8 @@ function transformFunction(
426582
options: PluginOptions,
427583
path: NodePath<FunctionLike>,
428584
functionName: string | null,
429-
state: PluginPass
585+
state: PluginPass,
586+
filename: string
430587
) {
431588
const isHook = isCustomHookName(functionName);
432589
const isComponent = isComponentName(functionName);
@@ -440,7 +597,14 @@ function transformFunction(
440597

441598
let newBody: BabelTypes.BlockStatement;
442599
if (hookUsage !== UNMANAGED) {
443-
newBody = wrapInTryFinally(t, path, state, hookUsage);
600+
newBody = wrapInTryFinally(
601+
t,
602+
path,
603+
state,
604+
hookUsage,
605+
`${functionName || "Unknown"}:${basename(filename)}`,
606+
isComponent && !!options.experimental?.debug
607+
);
444608
} else {
445609
newBody = prependUseSignals(t, path, state);
446610
}
@@ -608,6 +772,17 @@ export interface PluginOptions {
608772
*/
609773
detectTransformedJSX?: boolean;
610774
experimental?: {
775+
/**
776+
* If set to true the plugin will inject names into all invocations of
777+
*
778+
* - computed/useComputed
779+
* - signal/useSignal
780+
*
781+
* these names hook into @preact/signals-debug.
782+
*
783+
* @default false
784+
*/
785+
debug?: boolean;
611786
/**
612787
* If set to true, the component body will not be wrapped in a try/finally
613788
* block and instead the next component render or a microtick will stop
@@ -673,7 +848,14 @@ export default function signalsTransform(
673848
}
674849

675850
if (shouldTransform(path, functionName, state.opts)) {
676-
transformFunction(t, state.opts, path, functionName, state);
851+
transformFunction(
852+
t,
853+
state.opts,
854+
path,
855+
functionName,
856+
state,
857+
this.filename || ""
858+
);
677859
log(true, path, functionName, this.filename);
678860
} else if (isComponentLike(path, functionName)) {
679861
log(false, path, functionName, this.filename);
@@ -719,6 +901,19 @@ export default function signalsTransform(
719901
setOnFunctionScope(path, containsJSX, true, this.filename);
720902
}
721903
}
904+
905+
// Handle signal naming
906+
if (options.experimental?.debug && isSignalCall(path)) {
907+
const args = path.get("arguments");
908+
909+
// Only inject name if it doesn't already have one
910+
if (!hasNameInOptions(t, args)) {
911+
const variableName = getVariableNameFromDeclarator(path);
912+
if (variableName) {
913+
injectSignalName(t, path, variableName, this.filename);
914+
}
915+
}
916+
}
722917
},
723918

724919
MemberExpression(path) {

0 commit comments

Comments
 (0)