diff --git a/package.json b/package.json
index 1e656b2..0e8d234 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"test": "vitest run"
},
"dependencies": {
- "ember-estree": "^0.4.2"
+ "ember-estree": "^0.4.3"
},
"devDependencies": {
"@types/node": "^25.5.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4e0b948..7c321ed 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
ember-estree:
- specifier: ^0.4.2
- version: 0.4.2
+ specifier: ^0.4.3
+ version: 0.4.3
devDependencies:
'@types/node':
specifier: ^25.5.0
@@ -1196,8 +1196,8 @@ packages:
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
- ember-estree@0.4.2:
- resolution: {integrity: sha512-Runf+IVY+hUhlk0QwwljhPYj0/kjza5CCRp/Hlfdk9MawKSxXQqR0WX4f2rChXAGbDIzK7JVopVaet5oIZr8tg==}
+ ember-estree@0.4.3:
+ resolution: {integrity: sha512-742Wp/Dx2g9IZFOd9+JaRTpyflptxhtdMaG7MA2nl8lZOM9zTzlD+FfT3Vx/o+CZX976ZGDBbKg/gr5WErfqxA==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2895,7 +2895,7 @@ snapshots:
eastasianwidth@0.2.0: {}
- ember-estree@0.4.2:
+ ember-estree@0.4.3:
dependencies:
'@glimmer/env': 0.1.7
'@glimmer/syntax': 0.95.0
diff --git a/tests/glimmer-comments.test.ts b/tests/glimmer-comments.test.ts
new file mode 100644
index 0000000..d94b24e
--- /dev/null
+++ b/tests/glimmer-comments.test.ts
@@ -0,0 +1,369 @@
+import { describe, expect, it } from "vitest";
+import { z } from "zmod";
+import { emberParser } from "../src/index.ts";
+
+const j = z.withParser(emberParser);
+
+// ── HTML comments ─────────────────────────────────────────────
+
+describe("Glimmer — HTML comments ()", () => {
+ it("finds an HTML comment in a template", () => {
+ const source = `content
`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const comments = root.find("GlimmerCommentStatement");
+ expect(comments.length).toBe(1);
+ expect(comments.get()?.node.value).toBe(" TODO: remove this ");
+ });
+
+ it("replaces the content of an HTML comment", () => {
+ const source = ``;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).not.toContain("");
+ });
+
+ it("replaces an HTML comment while preserving surrounding elements", () => {
+ const source = `
+
+
+ Content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).not.toContain("");
+ expect(output).toContain("");
+ expect(output).toContain("Content");
+ });
+
+ it("replaces one HTML comment among multiple", () => {
+ const source = `
+
+ content
+
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root
+ .find("GlimmerCommentStatement")
+ .filter((path) => path.node.value.includes("first"))
+ .replaceWith("");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).toContain("");
+ expect(output).not.toContain("");
+ });
+
+ it("removes an HTML comment by replacing with empty string", () => {
+ const source = `text`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ const output = root.toSource();
+
+ expect(output).not.toContain("
+
+
content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const found = root.find("GlimmerCommentStatement", { value: " remove this " });
+ expect(found.length).toBe(1);
+ });
+
+ it("does not find HTML comments when only mustache comments exist", () => {
+ const source = `{{! a mustache comment }}content
`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ expect(root.find("GlimmerCommentStatement").length).toBe(0);
+ });
+});
+
+// ── Short mustache comments {{! }} ────────────────────────────────────
+
+describe("Glimmer — short mustache comments ({{! }})", () => {
+ it("finds a short mustache comment in a template", () => {
+ const source = `{{! TODO: fix this }}content
`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const comments = root.find("GlimmerMustacheCommentStatement");
+ expect(comments.length).toBe(1);
+ expect(comments.get()?.node.value).toBe(" TODO: fix this ");
+ });
+
+ it("replaces the content of a short mustache comment", () => {
+ const source = `{{! old comment }}content
`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! new comment }}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{! new comment }}");
+ expect(output).not.toContain("{{! old comment }}");
+ });
+
+ it("replaces a short mustache comment while preserving surrounding elements", () => {
+ const source = `
+
+ {{! old section label }}
+ Content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! updated section label }}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{! updated section label }}");
+ expect(output).not.toContain("{{! old section label }}");
+ expect(output).toContain("");
+ expect(output).toContain("Content");
+ });
+
+ it("replaces a short mustache comment with a long-form one", () => {
+ const source = `{{! short form }}`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{!-- now long form --}}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{!-- now long form --}}");
+ expect(output).not.toContain("{{! short form }}");
+ });
+
+ it("removes a short mustache comment by replacing with empty string", () => {
+ const source = `{{! placeholder }}text`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("");
+ const output = root.toSource();
+
+ expect(output).not.toContain("{{!");
+ expect(output).toContain("text");
+ });
+});
+
+// ── Long mustache comments {{!-- --}} ─────────────────────────────────
+
+describe("Glimmer — long mustache comments ({{!-- --}})", () => {
+ it("finds a long-form mustache comment in a template", () => {
+ const source = `{{!-- multi-word comment --}}content
`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const comments = root.find("GlimmerMustacheCommentStatement");
+ expect(comments.length).toBe(1);
+ expect(comments.get()?.node.value).toBe(" multi-word comment ");
+ });
+
+ it("replaces the content of a long-form mustache comment", () => {
+ const source = `{{!-- old content --}}`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{!-- new content --}}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{!-- new content --}}");
+ expect(output).not.toContain("{{!-- old content --}}");
+ });
+
+ it("replaces a long-form mustache comment while preserving surrounding elements", () => {
+ const source = `
+
+ {{!-- section: main content area --}}
+ Content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root
+ .find("GlimmerMustacheCommentStatement")
+ .replaceWith("{{!-- section: updated main content area --}}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{!-- section: updated main content area --}}");
+ expect(output).not.toContain("{{!-- section: main content area --}}");
+ expect(output).toContain("");
+ expect(output).toContain("Content");
+ });
+
+ it("replaces a long-form comment with a short-form one", () => {
+ const source = `{{!-- was long --}}`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! now short }}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{! now short }}");
+ expect(output).not.toContain("{{!-- was long --}}");
+ });
+
+ it("finds long-form comment by source slice when distinguishing from short form", () => {
+ const source = `{{! short form }}{{!-- long form --}}`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const longFormComments = root
+ .find("GlimmerMustacheCommentStatement")
+ .filter((path) => source.slice(path.node.start, path.node.end).startsWith("{{!--"));
+
+ expect(longFormComments.length).toBe(1);
+ expect(longFormComments.get()?.node.value).toBe(" long form ");
+
+ longFormComments.replaceWith("{{!-- updated long form --}}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{! short form }}");
+ expect(output).toContain("{{!-- updated long form --}}");
+ expect(output).not.toContain("{{!-- long form --}}");
+ });
+});
+
+// ── Mixed comment types ────────────────────────────────────────────────
+
+describe("Glimmer — mixed comment types", () => {
+ it("finds HTML and mustache comments separately", () => {
+ const source = `
+
+ {{! short mustache }}
+ {{!-- long mustache --}}
+ content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ const htmlComments = root.find("GlimmerCommentStatement");
+ const mustacheComments = root.find("GlimmerMustacheCommentStatement");
+
+ expect(htmlComments.length).toBe(1);
+ expect(mustacheComments.length).toBe(2);
+ });
+
+ it("replaces only HTML comments leaving mustache comments intact", () => {
+ const source = `
+
+ {{! mustache comment }}
+ content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).toContain("{{! mustache comment }}");
+ expect(output).not.toContain("");
+ });
+
+ it("replaces only mustache comments leaving HTML comments intact", () => {
+ const source = `
+
+ {{! mustache comment }}
+ content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! updated mustache comment }}");
+ const output = root.toSource();
+
+ expect(output).toContain("{{! updated mustache comment }}");
+ expect(output).toContain("");
+ expect(output).not.toContain("{{! mustache comment }}");
+ });
+
+ it("replaces all comment types in one pass", () => {
+ const source = `
+
+ {{! short mustache }}
+ {{!-- long mustache --}}
+ content
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! updated mustache }}");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).not.toContain("");
+ expect(output).not.toContain("{{! short mustache }}");
+ expect(output).not.toContain("{{!-- long mustache --}}");
+ expect(output).toContain("content
");
+ });
+
+ it("targets short {{! }} and long {{!-- --}} mustache comments individually", () => {
+ const root = j(`{{! short }}{{!-- long --}}`, { filePath: "test.gjs" });
+
+ root
+ .find("GlimmerMustacheCommentStatement")
+ .filter((path) => !path.node.longForm)
+ .replaceWith("{{! updated short }}");
+
+ root
+ .find("GlimmerMustacheCommentStatement")
+ .filter((path) => path.node.longForm)
+ .replaceWith("{{!-- updated long --}}");
+
+ const output = root.toSource();
+ expect(output).toContain("{{! updated short }}");
+ expect(output).toContain("{{!-- updated long --}}");
+ expect(output).not.toContain("{{! short }}");
+ expect(output).not.toContain("{{!-- long --}}");
+ });
+
+ it("handles comments inside a class-based component in .gjs", () => {
+ const source = `import Component from '@glimmer/component';
+
+export default class MyPage extends Component {
+
+
+ {{! page body }}
+ Content
+
+}
+`;
+ const root = j(source, { filePath: "page.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! updated page body }}");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).toContain("{{! updated page body }}");
+ expect(output).not.toContain("");
+ expect(output).not.toContain("{{! page body }}");
+ expect(output).toContain("import Component");
+ expect(output).toContain("export default class MyPage");
+ expect(output).toContain("Content");
+ });
+
+ it("handles comments nested inside a block expression", () => {
+ const source = `
+ {{#if this.show}}
+
+ {{! conditional mustache comment }}
+ shown
+ {{/if}}
+`;
+ const root = j(source, { filePath: "test.gjs" });
+
+ root.find("GlimmerCommentStatement").replaceWith("");
+ root.find("GlimmerMustacheCommentStatement").replaceWith("{{! updated mustache }}");
+ const output = root.toSource();
+
+ expect(output).toContain("");
+ expect(output).toContain("{{! updated mustache }}");
+ expect(output).toContain("{{#if this.show}}");
+ expect(output).toContain("shown
");
+ });
+});