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 ;\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 { 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 ;`; + expect(print(toTree(source))).toBe("export default "); + }); + it("prints an empty File", () => { const tree = toTree(``, { filePath: "m.js" });