import { exec } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import type { Context } from "../context.js"; import { Folder } from "../fs/folder.js"; import { StaticJsonFile } from "../fs/static-json-file.js"; import { StaticTextFile } from "../fs/static-text-file.js"; import { StaticTypeScriptFile } from "../fs/static-typescript-file.js"; import { Resource } from "../resource.js"; import { ShadcnUI } from "./shadcn.js"; const execAsync = promisify(exec); /** * Type of integrations that can be added to an Astro project */ export type AstroIntegration = | "react" | "preact" | "vue" | "svelte" | "solid" | "lit" | "tailwind" | "mdx" | "sitemap" | "partytown" | "markdoc"; /** * Properties for creating an Astro project */ export interface AstroProjectProps { /** * The name/path of the project */ name: string; /** * The title of the site * @default "Astro Site" */ title?: string; /** * The description of the site * @default "Welcome to my Astro site" */ description?: string; /** * The directory to initialize the project in. * @default {@link name} */ dir?: string; /** * The integrations to add to the project * @default [] */ integrations?: AstroIntegration[]; /** * The TypeScript configuration */ tsconfig?: { /** * Extends from this TypeScript configuration */ extends?: string; /** * References to add to the tsconfig */ references?: string[]; /** * Compiler options to add to the tsconfig */ compilerOptions?: Record; }; /** * Whether to delete the project folder during the delete phase * @default true */ delete?: boolean; /** * Add Shadcn UI to the project * @default false */ shadcn?: { /** * The base color to use * @default "neutral" */ baseColor?: "neutral" | "gray" | "zinc" | "stone" | "slate"; /** * Use default configuration * @default false */ defaults?: boolean; /** * Force overwrite of existing configuration * @default false */ force?: boolean; /** * Mute output * @default false */ silent?: boolean; /** * Use the src directory when creating a new project * @default true */ srcDir?: boolean; /** * Use css variables for theming * @default true */ cssVariables?: boolean; /** * The components to add */ components?: string[]; }; /** * The dependencies to install */ dependencies?: Record; /** * The dev dependencies to install */ devDependencies?: Record; /** * Additional scripts to add to package.json */ scripts?: Record; } /** * Astro project resource */ export interface AstroProject extends AstroProjectProps, Resource { /** * The name/path of the project */ name: string; } /** * Creates a new Astro project * * @example * // Create a basic Astro project * const basicProject = await AstroProject("my-astro-app", { * title: "My Astro Site", * description: "Built with Alchemy" * }); * * @example * // Create an Astro project with React and Tailwind * const reactProject = await AstroProject("astro-react", { * title: "Astro + React", * integrations: ["react", "tailwind"] * }); * * @example * // Create an Astro project with Shadcn UI * const shadcnProject = await AstroProject("astro-shadcn", { * title: "Astro with Shadcn UI", * integrations: ["react", "tailwind"], * shadcn: { * baseColor: "zinc", * components: ["button", "card", "input"] * } * }); */ export const AstroProject = Resource( "project::AstroProject", { alwaysUpdate: true, }, async function ( this: Context, id: string, props: AstroProjectProps ): Promise { const dir = props.dir ?? props.name; if (this.phase === "delete") { try { if (props.delete !== false) { if (await fs.stat(dir).catch(() => null)) { await execAsync(`rm -rf ${dir}`); } } } catch (error) { console.error(`Error deleting project ${id}:`, error); } return this.destroy(); } const cwd = path.resolve(process.cwd(), dir); // Create the project directory await Folder("project-dir", { path: dir, delete: true, }); await Folder(".astro", { path: dir, delete: true, clean: true, }); await setupProject(props); return this(props); /** * Set up the Astro project structure and configuration */ async function setupProject(props: AstroProjectProps) { // Create project structure await ProjectStructure(); // Set up package.json await StaticJsonFile(path.join(dir, "package.json"), { name: props.name, type: "module", version: "0.0.1", scripts: { dev: "astro dev", start: "astro dev", build: "astro build", preview: "astro preview", astro: "astro", ...props.scripts, }, dependencies: props.dependencies || {}, devDependencies: props.devDependencies || {}, }); // Set up tsconfig.json await StaticJsonFile(path.join(dir, "tsconfig.json"), { extends: props.tsconfig?.extends || "astro/tsconfigs/strict", compilerOptions: { baseUrl: ".", paths: { "@/*": ["./src/*"], }, jsx: "react-jsx", jsxImportSource: "react", ...props.tsconfig?.compilerOptions, }, include: ["src/**/*.ts", "src/**/*.tsx", "src/**/*.astro"], references: props.tsconfig?.references?.map((path) => ({ path })), }); // Set up astro.config.mjs await AstroConfig(); // Set up initial files await InitialFiles(); // Install integrations if (props.integrations && props.integrations.length > 0) { await InstallIntegrations(); } // Install dependencies await InstallDependencies(); // Set up Shadcn if requested if (props.shadcn) { await ShadcnUI(id, { ...props.shadcn, cwd: dir, }); } } /** * Create the basic project structure */ async function ProjectStructure() { const folderPaths = [ "src", "src/components", "src/layouts", "src/pages", "public", ]; for (const folderPath of folderPaths) { await Folder(`${dir}-${folderPath}`, { path: path.join(dir, folderPath), }); } } /** * Set up astro.config.mjs */ async function AstroConfig() { const integrations = []; const vitePlugins = []; // Add imports and integrations const configImports = ['import { defineConfig } from "astro/config";']; if (props.integrations?.includes("react")) { configImports.push('import react from "@astrojs/react";'); integrations.push("react()"); } if (props.integrations?.includes("preact")) { configImports.push('import preact from "@astrojs/preact";'); integrations.push("preact()"); } if (props.integrations?.includes("vue")) { configImports.push('import vue from "@astrojs/vue";'); integrations.push("vue()"); } if (props.integrations?.includes("svelte")) { configImports.push('import svelte from "@astrojs/svelte";'); integrations.push("svelte()"); } if (props.integrations?.includes("solid")) { configImports.push('import solid from "@astrojs/solid-js";'); integrations.push("solid()"); } if (props.integrations?.includes("lit")) { configImports.push('import lit from "@astrojs/lit";'); integrations.push("lit()"); } if (props.integrations?.includes("tailwind")) { configImports.push('import tailwindcss from "@tailwindcss/vite";'); vitePlugins.push("tailwindcss()"); } if (props.integrations?.includes("mdx")) { configImports.push('import mdx from "@astrojs/mdx";'); integrations.push("mdx()"); } if (props.integrations?.includes("sitemap")) { configImports.push('import sitemap from "@astrojs/sitemap";'); integrations.push("sitemap()"); } if (props.integrations?.includes("partytown")) { configImports.push('import partytown from "@astrojs/partytown";'); integrations.push("partytown()"); } if (props.integrations?.includes("markdoc")) { configImports.push('import markdoc from "@astrojs/markdoc";'); integrations.push("markdoc()"); } const integrationsStr = integrations.length ? `\n integrations: [${integrations.join(", ")}],` : ""; const vitePluginsStr = vitePlugins.length ? `\n vite: {\n plugins: [${vitePlugins.join(", ")}]\n },` : ""; return StaticTypeScriptFile( path.join(dir, "astro.config.ts"), `${configImports.join("\n")} // https://astro.build/config export default defineConfig({${integrationsStr}${vitePluginsStr} site: "https://example.com", output: "static" });` ); } /** * Create initial files for the project */ async function InitialFiles() { // Create .gitignore await StaticTextFile( path.join(dir, ".gitignore"), `# build output dist/ .output/ # dependencies node_modules/ # logs npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # environment variables .env .env.production # macOS-specific files .DS_Store .astro/ ` ); // Create .env.d.ts await StaticTextFile( path.join(dir, "src", "env.d.ts"), `/// ` ); // Create a basic layout await StaticTextFile( path.join(dir, "src", "layouts", "Layout.astro"), `--- interface Props { title: string; description?: string; } const { title, description = "Welcome to my Astro site" } = Astro.props; --- {title} ` ); // Create a basic index page await StaticTextFile( path.join(dir, "src", "pages", "index.astro"), `--- import Layout from '../layouts/Layout.astro'; ---

Astro Site

To get started, edit src/pages/index.astro and save to see your changes.

` ); // Create public/favicon.svg await StaticTextFile( path.join(dir, "public", "favicon.svg"), ` ` ); } /** * Install Astro integrations */ async function InstallIntegrations() { const exec = (command: string) => execAsync(command, { cwd }); const integrationPackages = []; for (const integration of props.integrations!) { switch (integration) { case "react": integrationPackages.push("@astrojs/react", "react", "react-dom"); break; case "preact": integrationPackages.push("@astrojs/preact", "preact"); break; case "vue": integrationPackages.push("@astrojs/vue", "vue"); break; case "svelte": integrationPackages.push("@astrojs/svelte", "svelte"); break; case "solid": integrationPackages.push("@astrojs/solid-js", "solid-js"); break; case "lit": integrationPackages.push("@astrojs/lit", "lit"); break; case "tailwind": integrationPackages.push("@tailwindcss/vite", "tailwindcss"); // Create a base.css file with @tailwind directives await Folder(path.join(dir, "src", "styles")); await StaticTextFile( path.join(dir, "src", "styles", "global.css"), `@tailwind base; @tailwind components; @tailwind utilities; ` ); break; case "mdx": integrationPackages.push("@astrojs/mdx"); break; case "sitemap": integrationPackages.push("@astrojs/sitemap"); break; case "partytown": integrationPackages.push("@astrojs/partytown"); break; case "markdoc": integrationPackages.push("@astrojs/markdoc"); break; } } if (integrationPackages.length > 0) { await exec(`bun add astro ${integrationPackages.join(" ")}`); } } /** * Install dependencies */ async function InstallDependencies() { const exec = (command: string) => execAsync(command, { cwd }); // Install Astro and general dependencies await exec(`bun add astro`); // Install dev dependencies const devDepsEntries = Object.entries(props.devDependencies || {}); if (devDepsEntries.length > 0) { const devDepsArg = devDepsEntries .map(([name, version]) => `${name}@${version}`) .join(" "); await exec(`bun add -D ${devDepsArg}`); } // Install regular dependencies const depsEntries = Object.entries(props.dependencies || {}); if (depsEntries.length > 0) { const depsArg = depsEntries .map(([name, version]) => `${name}@${version}`) .join(" "); await exec(`bun add ${depsArg}`); } } } );