From 0357428c339cf41b63c6570ef12e89af0d7307fe Mon Sep 17 00:00:00 2001
From: NullVoxPopuli-ai-agent
<268630448+NullVoxPopuli-ai-agent@users.noreply.github.com>
Date: Tue, 25 Aug 2026 13:53:57 -0400
Subject: [PATCH] Keep the export default wrapper around a default-exported
template
`export default ...` spliced the GlimmerTemplate in
place of the whole ExportDefaultDeclaration, so the tree and `print()`
both lost the `export default`. The declaration is now what gets spliced.
Co-Authored-By: Claude Fable 5
Claude-Session: https://claude.ai/code/session_01FsXgxh6vNRWMCNcAQ3g6TN
---
src/parse.js | 22 ++++++++++++++++++----
tests/parse.test.js | 8 ++++++++
tests/print-file.test.js | 5 +++++
3 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/src/parse.js b/src/parse.js
index 4ce476c..f36a1f9 100644
--- a/src/parse.js
+++ b/src/parse.js
@@ -237,9 +237,25 @@ export function toTree(source, options = {}) {
function walkWithKeys(node, parentPath) {
if (!node || !node.type) return;
+ const path = { node, parent: parentPath?.node ?? null, parentPath };
+
if (hasTemplates && PLACEHOLDER_TYPES.has(node.type)) {
const parseResult = matchPlaceholder(node);
- if (parseResult) {
+ if (parseResult && node.type === "ExportDefaultDeclaration") {
+ // `export default ...`: keep the export wrapper
+ // and splice its declaration, so the tree (and `print`) still carry
+ // the `export default`.
+ const ast = processPlaceholder(parseResult, node.declaration);
+ node.declaration = ast;
+ setParent(ast, node);
+ if (hasVisitors && !seen.has(node)) {
+ seen.add(node);
+ const handler = visitors[node.type];
+ if (handler) handler(node, path);
+ walkWithKeys(ast, path);
+ }
+ return;
+ } else if (parseResult) {
// Splice in place: write the GlimmerTemplate directly into the parent's
// slot instead of allocating new ancestor objects. This preserves node
// identity for every ancestor, which matters for WeakMap-keyed data
@@ -250,14 +266,12 @@ export function toTree(source, options = {}) {
setParent(ast, parent);
// Recurse into the Glimmer subtree so visitors fire on its nodes too.
// The Glimmer root's parentPath reflects its true JS parent — the
- // placeholder (UnaryExpression / StaticBlock) is an internal artifact.
+ // placeholder (`void` expression / StaticBlock) is an internal artifact.
if (hasVisitors) walkWithKeys(ast, parentPath);
return;
}
}
- const path = { node, parent: parentPath?.node ?? null, parentPath };
-
if (hasVisitors && !seen.has(node)) {
seen.add(node);
const handler = visitors[node.type];
diff --git a/tests/parse.test.js b/tests/parse.test.js
index cc37a64..a78419b 100644
--- a/tests/parse.test.js
+++ b/tests/parse.test.js
@@ -184,6 +184,14 @@ describe("parse", () => {
]);
});
+ it("keeps the export default wrapper around a default-exported template", () => {
+ const source = `export default hi;\n`;
+ const ast = parse(source);
+
+ expect(ast.program.body.map((node) => node.type)).toEqual(["ExportDefaultDeclaration"]);
+ expect(ast.program.body[0].declaration.type).toBe("GlimmerTemplate");
+ });
+
it("handles multiple classes with templates", () => {
const source = `class A extends Component {
A
diff --git a/tests/print-file.test.js b/tests/print-file.test.js
index a301f47..00031b0 100644
--- a/tests/print-file.test.js
+++ b/tests/print-file.test.js
@@ -11,6 +11,11 @@ describe("print(File)", () => {
`);
});
+ it("round-trips a default-exported template", () => {
+ const source = `export default hi;`;
+ expect(print(toTree(source))).toBe("export default hi");
+ });
+
it("prints an empty File", () => {
const tree = toTree(``, { filePath: "m.js" });