import { describe, expect, test } from "bun:test";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { analyzeReactComponent, astroToSpug, backportProject, cssToStcss, htmlToSpug, reactToSpug, zolaToSpug } from "../core/backport.ts";
import { shadcnIntegration } from "../integrations/shadcn.ts";
import { sassLoader } from "../views/css/sass.ts";
import { TemplateEngine } from "../views/engine/runtime.ts";
describe("backport", () => {
test("rebuilds normalized CSS as STCSS", () => {
const result = cssToStcss(".card { color: red; &:hover { color: blue; } }");
expect(result).toContain(".card {");
expect(result).toContain("color: red;");
});
test("turns HTML structures into indented SPUG", () => {
const result = htmlToSpug('Hello
');
expect(result).toContain("main.page");
expect(result).toContain(" h1");
expect(result).toContain('img(src="hero.png")');
});
test("ports Astro layouts, slots and imported components", () => {
const page = astroToSpug(`---\nimport Layout from "../layouts/Layout.astro";\nimport Card from "../components/Card.astro";\n---\nBody
`, "/site/src/pages/index.astro", "/site/src/pages");
expect(page).toContain("extends layouts/Layout.spug");
expect(page).toContain("block content");
expect(page).toContain("component card");
expect(page).toContain("title: title");
expect(astroToSpug("")).toContain("block content");
});
test("ports Zola inheritance, partials and template expressions", () => {
const page = zolaToSpug(`{% extends "base.html" %}\n{% block content %}
{{ page.title }}
{% include "partials/nav.html" %}{% endblock %}`);
expect(page).toContain("extends ./base.spug");
expect(page).toContain("block content");
expect(page).toContain("include ./partials/nav.spug");
expect(page).toContain("#{ page.title }");
});
test("ports React JSX components through the TypeScript AST", () => {
const component = reactToSpug('import { Button } from "./button"; export function Card({ title }) { return ; }');
expect(component).toContain("section(class=\"card\")");
expect(component).toContain("= title");
expect(component).toContain("component button");
expect(component).toContain('variant: "outline"');
});
test("analyzes hooks and lowers static state into a javascript block", () => {
const source = 'function Toggle() { const [open] = useState(false); useEffect(() => {}, []); return ; }';
expect(analyzeReactComponent(source).hooks).toEqual(["useEffect", "useState"]);
expect(reactToSpug(source)).toContain("javascript!");
expect(reactToSpug(source)).toContain("const open = false;");
});
test("creates a Muddle project from an Astro folder", async () => {
const root = await mkdtemp(join(tmpdir(), "muddle-backport-"));
await Bun.write(join(root, "astro.config.mjs"), "export default {}");
await mkdir(join(root, "src/pages"), { recursive: true });
await mkdir(join(root, "src/styles"), { recursive: true });
await mkdir(join(root, "src/content/posts"), { recursive: true });
await mkdir(join(root, "src/layouts"), { recursive: true });
await mkdir(join(root, "src/components"), { recursive: true });
await Bun.write(join(root, "src/pages/index.astro"), "Hi
");
await Bun.write(join(root, "src/pages/about.astro"), "---\nimport Shell from '../layouts/Shell.astro';\n---\nAbout
");
await Bun.write(join(root, "src/layouts/Shell.astro"), "");
await Bun.write(join(root, "src/components/Card.astro"), "");
await Bun.write(join(root, "src/styles/site.css"), ".page { color: red; }");
await Bun.write(join(root, "src/content/posts/hello.md"), "---\ntitle: Hello\n---\n\nBody");
const destination = join(root, "muddle");
const result = await backportProject(root, destination);
expect(result.source).toBe("astro");
expect(await readFile(join(destination, "modules/app/views/index.spug"), "utf8")).toContain("main");
expect(await readFile(join(destination, "styles/site.stcss"), "utf8")).toContain(".page");
expect(await readFile(join(destination, "content/posts/hello.md"), "utf8")).toContain("title: Hello");
expect(await readFile(join(destination, "ct.config.ts"), "utf8")).toContain('name: "posts"');
const engine = new TemplateEngine({ viewsDir: join(destination, "modules/app/views") });
expect(await engine.render("index.spug")).toContain("Hi
");
expect(await engine.render("about.spug")).toContain("About
");
});
test("carries Zola configuration and partial templates into the Muddle project", async () => {
const root = await mkdtemp(join(tmpdir(), "muddle-zola-"));
await mkdir(join(root, "templates/partials"), { recursive: true });
await mkdir(join(root, "content/posts"), { recursive: true });
await Bun.write(join(root, "config.toml"), 'title = "Zola notes"\nbase_url = "https://notes.example"');
await Bun.write(join(root, "templates/index.html"), '{% extends "base.html" %}{% block content %}{{ config.title }}
{% endblock %}');
await Bun.write(join(root, "templates/partials/nav.html"), "");
await Bun.write(join(root, "content/posts/first.md"), "---\ntitle: First\n---\n\nPost");
const destination = join(root, "muddle");
await backportProject(root, destination);
expect(await readFile(join(destination, "modules/app/site.config.ts"), "utf8")).toContain("Zola notes");
expect(await readFile(join(destination, "modules/app/views/partials/nav.spug"), "utf8")).toContain("nav");
expect(await readFile(join(destination, "modules/app/views/index.spug"), "utf8")).toContain("extends ./base.spug");
});
test("compiles Sass before handing CSS to STCSS", async () => {
const root = await mkdtemp(join(tmpdir(), "muddle-sass-"));
const path = join(root, "site.scss");
await writeFile(path, "$ink: #123456; .card { color: $ink; }");
const loaded = await sassLoader().load({ path, relativePath: "styles/site.scss", source: await readFile(path, "utf8") });
expect(loaded.content).toContain("color: #123456;");
});
test("pulls declarative ShadCN registry components into Spug and STCSS", async () => {
const root = await mkdtemp(join(tmpdir(), "muddle-shadcn-"));
const integration = shadcnIntegration({
components: ["button"],
fetch: async () => new Response(JSON.stringify({ files: [
{ path: "button.tsx", content: "export function Button() { return ; }" },
{ path: "button.css", content: ".btn { color: red; }" },
] })),
});
await integration.setup?.({ config: { root } as any, route() {}, loader() {} });
expect(await readFile(join(root, "modules/app/views/components/button.spug"), "utf8")).toContain("button");
expect(await readFile(join(root, "styles/shadcn/button.stcss"), "utf8")).toContain(".btn");
});
});