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---\n

Body

`, "/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

{title}

; }', ); 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---\n

About

", ); 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"); }); });