212 lines
7.4 KiB
TypeScript
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");
|
|
});
|
|
});
|