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 = ``; + 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 = ``; + 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("
Header
"); + expect(output).toContain("
Content
"); + }); + + it("replaces one HTML comment among multiple", () => { + const source = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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("
Header
"); + expect(output).toContain("
Content
"); + }); + + it("replaces a short mustache comment with a long-form one", () => { + const source = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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("
Header
"); + expect(output).toContain("
Content
"); + }); + + it("replaces a long-form comment with a short-form one", () => { + const source = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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 = ``; + 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(``, { 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 { + +} +`; + 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 = ``; + 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
"); + }); +});