Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/babel-memo-naming-2026-2-13.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@alloy-js/babel-plugin-jsx-dom-expressions"
---

Add human-readable expression names to auto-memoized JSX expressions in dev builds to improve debug trace readability.
64 changes: 64 additions & 0 deletions packages/babel-plugin-alloy/test/memo-naming.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { transformSync } from "@babel/core";
import { join } from "node:path";
import { describe, expect, it } from "vitest";

const pluginPath = join(
import.meta.dirname,
"../../babel-plugin-jsx-dom-expressions/index.js",
);

function transform(code: string, addSourceInfo: boolean) {
const result = transformSync(code, {
plugins: [
[
pluginPath,
{
moduleName: "r-custom",
builtIns: ["For", "Show"],
generate: "universal",
addSourceInfo,
},
],
],
filename: "test.js",
});
return result?.code ?? "";
}

describe("memo naming (addSourceInfo)", () => {
it("adds name for simple member expression", () => {
const out = transform("const x = <>{signal.value}</>;", true);
expect(out).toContain('false, "signal.value"');
});

it("adds name for chained member expression", () => {
const out = transform("const x = <>{a.b.c}</>;", true);
expect(out).toContain('false, "a.b.c"');
});

it("adds name for call expression", () => {
const out = transform("const x = <>{getData()}</>;", true);
expect(out).toContain('false, "getData"');
});

it("adds name for numeric computed property", () => {
const out = transform("const x = <>{arr[0]}</>;", true);
expect(out).toContain('false, "arr[0]"');
});

it("adds name for string computed property", () => {
const out = transform('const x = <>{obj["key"]}</>;', true);
expect(out).toContain('false, "obj[key]"');
});

it("adds name for optional chaining", () => {
const out = transform("const x = <>{a?.b}</>;", true);
expect(out).toContain('false, "a.b"');
});

it("does not add name when addSourceInfo is false", () => {
const out = transform("const x = <>{signal.value}</>;", false);
expect(out).toContain("_$memo");
expect(out).not.toContain('"signal.value"');
});
});
54 changes: 53 additions & 1 deletion packages/babel-plugin-jsx-dom-expressions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,51 @@ function toPropertyName(name) {
return name.toLowerCase().replace(/-([a-z])/g, (_, w) => w.toUpperCase());
}

const MAX_EXPR_NAME_LEN = 50;

/**
* Generate a short human-readable description from an AST expression node,
* used to name memoized expressions for debug tracing.
*/
function describeExpression(node, depth = 0) {
if (depth > 4) return "…";
if (t__namespace.isIdentifier(node)) return node.name;
if (t__namespace.isMemberExpression(node) || t__namespace.isOptionalMemberExpression(node)) {
const obj = describeExpression(node.object, depth + 1);
if (!obj) return null;
if (node.computed) {
const prop = t__namespace.isIdentifier(node.property) ? node.property.name :
t__namespace.isStringLiteral(node.property) ? node.property.value :
t__namespace.isNumericLiteral(node.property) ? String(node.property.value) : "…";
return truncName(`${obj}[${prop}]`);
}
return truncName(`${obj}.${node.property.name || "?"}`);
}
if (t__namespace.isCallExpression(node) || t__namespace.isOptionalCallExpression(node)) {
const callee = describeExpression(node.callee, depth + 1);
return callee ? truncName(`${callee}(…)`) : null;
}
if (t__namespace.isConditionalExpression(node)) {
const test = describeExpression(node.test, depth + 1);
return test ? truncName(`${test} ? …`) : null;
}
if (t__namespace.isLogicalExpression(node)) {
const left = describeExpression(node.left, depth + 1);
return left ? truncName(`${left} ${node.operator} …`) : null;
}
if (t__namespace.isTemplateLiteral(node)) return "template";
if (t__namespace.isArrowFunctionExpression(node) || t__namespace.isFunctionExpression(node)) {
// Try to describe the body
const body = t__namespace.isBlockStatement(node.body) ? null : describeExpression(node.body, depth + 1);
return body ? truncName(`() => ${body}`) : null;
}
return null;
}

function truncName(s) {
return s != null && s.length > MAX_EXPR_NAME_LEN ? s.slice(0, MAX_EXPR_NAME_LEN - 1) + "…" : s;
}

function transformCondition(path, inline, deep) {
const config = getConfig(path);
const expr = path.node;
Expand Down Expand Up @@ -865,7 +910,14 @@ function createTemplate(path, result, wrap) {
}
}
if (wrap && result.dynamic && config.memoWrapper) {
return t__namespace.callExpression(registerImportMethod(path, config.memoWrapper), [result.exprs[0]]);
const args = [result.exprs[0]];
if (config.addSourceInfo) {
const name = describeExpression(
t__namespace.isArrowFunctionExpression(result.exprs[0]) ? result.exprs[0].body : result.exprs[0]
);
if (name) args.push(t__namespace.booleanLiteral(false), t__namespace.stringLiteral(name));
}
return t__namespace.callExpression(registerImportMethod(path, config.memoWrapper), args);
}
return result.exprs[0];
}
Expand Down
45 changes: 45 additions & 0 deletions packages/babel-plugin-jsx-dom-expressions/src/shared/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,51 @@ export function wrappedByText(list, startIndex) {
return false;
}

const MAX_EXPR_NAME_LEN = 50;

/**
* Generate a short human-readable description from an AST expression node,
* used to name memoized expressions for debug tracing.
*/
export function describeExpression(node, depth = 0) {
if (depth > 4) return "…";
if (t.isIdentifier(node)) return node.name;
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const obj = describeExpression(node.object, depth + 1);
if (!obj) return null;
if (node.computed) {
const prop = t.isIdentifier(node.property) ? node.property.name :
t.isStringLiteral(node.property) ? node.property.value :
t.isNumericLiteral(node.property) ? String(node.property.value) : "…";
return truncName(`${obj}[${prop}]`);
}
return truncName(`${obj}.${node.property.name || "?"}`);
}
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
const callee = describeExpression(node.callee, depth + 1);
return callee ? truncName(`${callee}(…)`) : null;
}
if (t.isConditionalExpression(node)) {
const test = describeExpression(node.test, depth + 1);
return test ? truncName(`${test} ? …`) : null;
}
if (t.isLogicalExpression(node)) {
const left = describeExpression(node.left, depth + 1);
return left ? truncName(`${left} ${node.operator} …`) : null;
}
if (t.isTemplateLiteral(node)) return "template";
if (t.isArrowFunctionExpression(node) || t.isFunctionExpression(node)) {
// Try to describe the body
const body = t.isBlockStatement(node.body) ? null : describeExpression(node.body, depth + 1);
return body ? truncName(`() => ${body}`) : null;
}
return null;
}

function truncName(s) {
return s != null && s.length > MAX_EXPR_NAME_LEN ? s.slice(0, MAX_EXPR_NAME_LEN - 1) + "…" : s;
}

export function transformCondition(path, inline, deep) {
const config = getConfig(path);
const expr = path.node;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as t from "@babel/types";
import { getConfig, getNumberedId, registerImportMethod } from "../shared/utils";
import { describeExpression, getConfig, getNumberedId, registerImportMethod } from "../shared/utils";
import { setAttr } from "./element";

export function createTemplate(path, result, wrap) {
Expand Down Expand Up @@ -29,7 +29,14 @@ export function createTemplate(path, result, wrap) {
}
}
if (wrap && result.dynamic && config.memoWrapper) {
return t.callExpression(registerImportMethod(path, config.memoWrapper), [result.exprs[0]]);
const args = [result.exprs[0]];
if (config.addSourceInfo) {
const name = describeExpression(
t.isArrowFunctionExpression(result.exprs[0]) ? result.exprs[0].body : result.exprs[0]
);
if (name) args.push(t.booleanLiteral(false), t.stringLiteral(name));
}
return t.callExpression(registerImportMethod(path, config.memoWrapper), args);
}
return result.exprs[0];
}
Expand Down