commit e93c8f99a685553de9803a98df492e386afe9477 Author: Hellings Date: Fri Oct 23 08:53:07 2026 -0400 Initial commit diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..1f2665f --- /dev/null +++ b/index.ts @@ -0,0 +1,80 @@ +import { existsSync } from "node:fs"; +import { readdir, readFile } from "node:fs/promises"; +import { join, relative, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { transform } from "@astrojs/compiler"; +import { experimental_AstroContainer as AstroContainer } from "astro/container"; +import ts from "typescript"; +import type { Integration, IntegrationContext } from "muddle/contracts"; + +export interface AstroIntegrationOptions { dir?: string; partials?: boolean } + +function routeFor(root: string, file: string): string { + const path = relative(root, file).replace(/\\/g, "/").replace(/\.astro$/, ""); + return `/${path.replace(/index$/, "").replace(/\/+/g, "/") || ""}` || "/"; +} + +export function astroIntegration(options: AstroIntegrationOptions = {}): Integration { + return { + name: "astro", + async setup(context: IntegrationContext) { + context.loader("astro", { name: "astro", extensions: [".astro"], async load(file) { return { data: {}, content: await renderAstro(file.source, undefined, file.path) }; } }); + const dir = resolve(context.config.root, options.dir ?? "src/pages"); + if (!existsSync(dir)) return; + for (const file of await astroFiles(dir)) { + const route = routeFor(dir, file); + context.route("GET", route, async (request) => new Response(await renderAstro(await readFile(file, "utf8"), request, file), { headers: { "content-type": "text/html; charset=utf-8" } })); + } + }, + }; +} + +async function astroFiles(dir: string): Promise { + const files: string[] = []; + for (const entry of await readdir(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) files.push(...(await astroFiles(path))); + else if (entry.name.endsWith(".astro")) files.push(path); + } + return files; +} + +export async function renderAstro(source: string, request = new Request("http://localhost/"), filename = resolve("inline.astro")): Promise { + const component = await compileAstro(source, filename); + const container = await AstroContainer.create(); + return container.renderToString(component, { request }); +} + +async function compileAstro(source: string, filename: string, ancestors: string[] = []): Promise { + if (ancestors.includes(filename)) throw new Error(`Circular Astro import ${filename}`); + const result = await transform(source, { filename, internalURL: "astro/runtime/server/index.js" }); + let code = result.code.replace(/,\s*createMetadata as \$\$createMetadata/, "").replace(/export const \$\$metadata = [^\n]+\n/g, ""); + for (const match of [...code.matchAll(/from\s+["']([^"']+)["']/g)]) { + const specifier = match[1]!; + let url: string; + if (specifier.endsWith(".astro")) { + const file = resolve(filename, "..", specifier); + const child = await compileAstro(await readFile(file, "utf8"), file, [...ancestors, filename]); + const key = `__muddle_astro_${crypto.randomUUID().replaceAll("-", "")}`; + (globalThis as Record)[key] = child; + url = `data:text/javascript;base64,${Buffer.from(`export default globalThis[${JSON.stringify(key)}]`).toString("base64")}`; + } else url = specifier.startsWith(".") ? pathToFileURL(resolve(filename, "..", specifier)).href : import.meta.resolve(specifier); + code = code.replaceAll(`"${specifier}"`, JSON.stringify(url)).replaceAll(`'${specifier}'`, JSON.stringify(url)); + } + code = ts.transpileModule(code, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } }).outputText; + return (await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`)).default; +} + +/** Import into TemplateContext with `.import("Card", astroComponent("./Card.astro"))`. */ +export function astroComponent(file: string) { + return { + async render(props: Record, context: { params: Record; locals: Record }) { + const path = resolve(file); + const component = await compileAstro(await readFile(path, "utf8"), path); + const container = await AstroContainer.create(); + const slots: Record = {}; + for (const [key, value] of Object.entries(props)) if (typeof value === "function") slots[key === "content" ? "default" : key] = await value(); + return container.renderToString(component, { props, params: context.params, locals: context.locals, slots }); + }, + }; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8c3e392 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "@muddle/astro", + "type": "module", + "exports": "./index.ts", + "version": "0.1.0", + "peerDependencies": { + "muddle": "*" + }, + "dependencies": { + "@astrojs/compiler": "2", + "astro": "5", + "typescript": "5.9.3" + } +}