better formatting

This commit is contained in:
Hellings 2026-10-23 09:12:49 -04:00
parent ffbe14e164
commit 49561f7c95
2 changed files with 109 additions and 31 deletions

116
index.ts
View file

@ -7,23 +7,51 @@ import { experimental_AstroContainer as AstroContainer } from "astro/container";
import ts from "typescript"; import ts from "typescript";
import type { Integration, IntegrationContext } from "muddle/contracts"; import type { Integration, IntegrationContext } from "muddle/contracts";
export interface AstroIntegrationOptions { dir?: string; partials?: boolean } export interface AstroIntegrationOptions {
dir?: string;
partials?: boolean;
}
function routeFor(root: string, file: string): string { function routeFor(root: string, file: string): string {
const path = relative(root, file).replace(/\\/g, "/").replace(/\.astro$/, ""); const path = relative(root, file)
.replace(/\\/g, "/")
.replace(/\.astro$/, "");
return `/${path.replace(/index$/, "").replace(/\/+/g, "/") || ""}` || "/"; return `/${path.replace(/index$/, "").replace(/\/+/g, "/") || ""}` || "/";
} }
export function astroIntegration(options: AstroIntegrationOptions = {}): Integration { export function astroIntegration(
options: AstroIntegrationOptions = {},
): Integration {
return { return {
name: "astro", name: "astro",
async setup(context: IntegrationContext) { async setup(context: IntegrationContext) {
context.loader("astro", { name: "astro", extensions: [".astro"], async load(file) { return { data: {}, content: await renderAstro(file.source, undefined, file.path) }; } }); 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"); const dir = resolve(context.config.root, options.dir ?? "src/pages");
if (!existsSync(dir)) return; if (!existsSync(dir)) return;
for (const file of await astroFiles(dir)) { for (const file of await astroFiles(dir)) {
const route = routeFor(dir, file); 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" } })); context.route(
"GET",
route,
async (request) =>
new Response(
await renderAstro(await readFile(file, "utf8"), request, file),
{
headers: {
"content-type": "text/html; charset=utf-8",
},
},
),
);
} }
}, },
}; };
@ -39,42 +67,92 @@ async function astroFiles(dir: string): Promise<string[]> {
return files; return files;
} }
export async function renderAstro(source: string, request = new Request("http://localhost/"), filename = resolve("inline.astro")): Promise<string> { export async function renderAstro(
source: string,
request = new Request("http://localhost/"),
filename = resolve("inline.astro"),
): Promise<string> {
const component = await compileAstro(source, filename); const component = await compileAstro(source, filename);
const container = await AstroContainer.create(); const container = await AstroContainer.create();
return container.renderToString(component, { request }); return container.renderToString(component, { request });
} }
async function compileAstro(source: string, filename: string, ancestors: string[] = []): Promise<any> { async function compileAstro(
if (ancestors.includes(filename)) throw new Error(`Circular Astro import ${filename}`); source: string,
const result = await transform(source, { filename, internalURL: "astro/runtime/server/index.js" }); filename: string,
let code = result.code.replace(/,\s*createMetadata as \$\$createMetadata/, "").replace(/export const \$\$metadata = [^\n]+\n/g, ""); ancestors: string[] = [],
): Promise<any> {
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)]) { for (const match of [...code.matchAll(/from\s+["']([^"']+)["']/g)]) {
const specifier = match[1]!; const specifier = match[1]!;
let url: string; let url: string;
if (specifier.endsWith(".astro")) { if (specifier.endsWith(".astro")) {
const file = resolve(filename, "..", specifier); const file = resolve(filename, "..", specifier);
const child = await compileAstro(await readFile(file, "utf8"), file, [...ancestors, filename]); const child = await compileAstro(await readFile(file, "utf8"), file, [
...ancestors,
filename,
]);
const key = `__muddle_astro_${crypto.randomUUID().replaceAll("-", "")}`; const key = `__muddle_astro_${crypto.randomUUID().replaceAll("-", "")}`;
(globalThis as Record<string, unknown>)[key] = child; (globalThis as Record<string, unknown>)[key] = child;
url = `data:text/javascript;base64,${Buffer.from(`export default globalThis[${JSON.stringify(key)}]`).toString("base64")}`; url = `data:text/javascript;base64,${Buffer.from(
} else url = specifier.startsWith(".") ? pathToFileURL(resolve(filename, "..", specifier)).href : import.meta.resolve(specifier); `export default globalThis[${JSON.stringify(key)}]`,
code = code.replaceAll(`"${specifier}"`, JSON.stringify(url)).replaceAll(`'${specifier}'`, JSON.stringify(url)); ).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; code = ts.transpileModule(code, {
return (await import(`data:text/javascript;base64,${Buffer.from(code).toString("base64")}`)).default; 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"))`. */ /** Import into TemplateContext with `.import("Card", astroComponent("./Card.astro"))`. */
export function astroComponent(file: string) { export function astroComponent(file: string) {
return { return {
async render(props: Record<string, unknown>, context: { params: Record<string, string>; locals: Record<string, unknown> }) { async render(
props: Record<string, unknown>,
context: {
params: Record<string, string>;
locals: Record<string, unknown>;
},
) {
const path = resolve(file); const path = resolve(file);
const component = await compileAstro(await readFile(path, "utf8"), path); const component = await compileAstro(await readFile(path, "utf8"), path);
const container = await AstroContainer.create(); const container = await AstroContainer.create();
const slots: Record<string, string> = {}; const slots: Record<string, string> = {};
for (const [key, value] of Object.entries(props)) if (typeof value === "function") slots[key === "content" ? "default" : key] = await value(); for (const [key, value] of Object.entries(props)) {
return container.renderToString(component, { props, params: context.params, locals: context.locals, slots }); if (typeof value === "function") {
slots[key === "content" ? "default" : key] = await value();
}
}
return container.renderToString(component, {
props,
params: context.params,
locals: context.locals,
slots,
});
}, },
}; };
} }

View file

@ -1,14 +1,14 @@
{ {
"name": "@muddle/astro", "name": "@muddle/astro",
"type": "module", "type": "module",
"exports": "./index.ts", "exports": "./index.ts",
"version": "0.1.0", "version": "0.1.0",
"peerDependencies": { "peerDependencies": {
"muddle": "*" "muddle": "*"
}, },
"dependencies": { "dependencies": {
"@astrojs/compiler": "2", "@astrojs/compiler": "2",
"astro": "5", "astro": "5",
"typescript": "5.9.3" "typescript": "5.9.3"
} }
} }