muddle/tests/backport.test.ts
2026-10-23 09:12:49 -04:00

212 lines
7.4 KiB
TypeScript

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(
'<main class="page"><h1>Hello</h1><img src="hero.png"></main>',
);
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---\n<Layout title="Hello"><Card title={title} /><p>Body</p></Layout>`,
"/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("<main><slot /></main>")).toContain("block content");
});
test("ports Zola inheritance, partials and template expressions", () => {
const page = zolaToSpug(
`{% extends "base.html" %}\n{% block content %}<h1>{{ page.title }}</h1>{% 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 <section className="card"><h2>{title}</h2><Button variant="outline">Open</Button></section>; }',
);
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 <button>{open}</button>; }";
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"),
"<main><h1>Hi</h1></main>",
);
await Bun.write(
join(root, "src/pages/about.astro"),
"---\nimport Shell from '../layouts/Shell.astro';\n---\n<Shell><p>About</p></Shell>",
);
await Bun.write(
join(root, "src/layouts/Shell.astro"),
"<html><body><slot /></body></html>",
);
await Bun.write(
join(root, "src/components/Card.astro"),
"<article><slot /></article>",
);
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("<main><h1>Hi</h1>");
expect(await engine.render("about.spug")).toContain(
"<html><body><p>About</p>",
);
});
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 %}<h1>{{ config.title }}</h1>{% endblock %}',
);
await Bun.write(
join(root, "templates/partials/nav.html"),
"<nav>Nav</nav>",
);
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 <button className="btn">Save</button>; }',
},
{ 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");
});
});