UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,577 lines (1,537 loc) 175 kB
import { createRequire } from "node:module"; import { $atom, $hook, $inject, $module, $state, Alepha, AlephaError, __alephaRef, z } from "alepha"; import { $command, CliProvider, EnvUtils } from "alepha/command"; import { $logger, ConsoleColorProvider } from "alepha/logger"; import { FileSystemProvider, ShellProvider } from "alepha/system"; import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { readFile, readdir, stat } from "node:fs/promises"; import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"; import pkg from "alepha/package.json" with { type: "json" }; import { analyzer } from "vite-bundle-analyzer"; import { KV_DEFAULT_BINDING } from "alepha/cache"; import { SEND_EMAIL_DEFAULT_BINDING } from "alepha/email/cloudflare"; import { QUEUE_DEFAULT_BINDING, QUEUE_DEFAULT_MAX_RETRIES } from "alepha/queue"; import { promisify } from "node:util"; import { brotliCompress, gzip } from "node:zlib"; import { exec, spawn } from "node:child_process"; import { ServerSwaggerProvider } from "alepha/server/swagger"; import { pathToFileURL } from "node:url"; //#region ../../src/cli/core/atoms/appEntryOptions.ts const appEntryOptions = $atom({ name: "alepha.cli.appEntry.options", schema: z.object({ server: z.text().optional(), browser: z.text().optional(), style: z.text().optional() }), default: {} }); //#endregion //#region ../../src/cli/core/atoms/buildOptions.ts /** * Build options atom for CLI build command. * * Defines the available build configuration options with their defaults. * Options can be overridden via alepha.config.ts or CLI flags. */ const buildOptions = $atom({ name: "alepha.cli.build.options", description: "Build configuration options", schema: z.object({ /** * Generate build stats report. * * - `true` - Generate a static HTML report * - `"json"` - Generate a JSON report */ stats: z.union([z.boolean(), z.enum(["json"])]).optional(), /** * Deployment target for the build output. * * - `docker` - Generate Dockerfile for containerized deployment * - `vercel` - Generate Vercel deployment configuration (forces node runtime) * - `cloudflare` - Generate Cloudflare Workers configuration (forces workerd runtime) */ target: z.enum([ "bare", "docker", "vercel", "cloudflare", "static" ]).optional(), /** * JavaScript runtime for the build output. * * - `node` - Node.js runtime (default) * - `bun` - Bun runtime (uses bun export conditions) * - `workerd` - Cloudflare Workers runtime (auto-set with cloudflare target) * * Note: Some targets force a specific runtime: * - `cloudflare` always uses `workerd` * - `vercel` always uses `node` */ runtime: z.enum([ "node", "bun", "workerd" ]).optional(), /** * Output directory configuration. */ output: z.object({ /** * Root dist directory. * * @default "dist" */ dist: z.string().default("dist").optional(), /** * Public/client subdirectory. * * @default "public" */ public: z.string().default("public").optional() }).optional(), /** * Vercel-specific deployment configuration. * * Note: Set `target: "vercel"` to enable Vercel deployment. * This object is only for additional configuration. */ vercel: z.object({ projectName: z.string().optional(), orgId: z.string().optional(), projectId: z.string().optional(), config: z.object({ crons: z.array(z.object({ path: z.string(), schedule: z.string() })).optional() }).optional() }).optional(), /** * Cloudflare-specific deployment configuration. * * Note: Set `target: "cloudflare"` to enable Cloudflare deployment. * This object is only for additional configuration. */ cloudflare: z.object({ config: z.json().optional() }).optional(), /** * Docker-specific deployment configuration. * * Note: Set `target: "docker"` to enable Docker deployment. * This object is only for additional configuration. */ docker: z.object({ /** * Base image for the Dockerfile (FROM instruction). * * @default "node:24-alpine" for node runtime * @default "oven/bun:alpine" for bun runtime */ from: z.string().optional(), /** * Command to run in the Docker container. * * @default "node" for node runtime * @default "bun" for bun runtime */ command: z.string().optional(), /** * Extra packages to install globally in the generated image. * * Each entry becomes a `RUN npm install --global --no-fund * --no-audit <pkg> …` line inserted after `FROM` and before the * app `COPY`. Use it for CLI tools the running app shells out to * — typical example is `wrangler` for a service that deploys to * Cloudflare on someone else's behalf. * * Ignored in `compile` mode (the distroless base has no `npm`). * * @example install: ["wrangler"] */ install: z.array(z.string()).optional(), /** * Docker build options (used when --image flag is passed). */ image: z.object({ /** * Default image tag (name without version). * * Used when --image is provided without a full override: * - `--image` → `tag:latest` * - `--image=1.3.4` → `tag:1.3.4` * - `--image=other/img:v1` → `other/img:v1` (full override) * * @example "myproject/myapp" * @example "ghcr.io/myorg/myapp" */ tag: z.string(), /** * Additional arguments to pass to `docker build`. * * @example '--platform linux/amd64 --no-cache' */ args: z.string().optional(), /** * Auto-add OCI standard labels (revision, created, version). * * Adds: * - org.opencontainers.image.revision (git commit SHA) * - org.opencontainers.image.created (build timestamp) * - org.opencontainers.image.version (from image tag) */ oci: z.boolean().optional() }).optional(), /** * Compile the server entry to a single static binary using * `bun build --compile`, then package it inside a minimal base image * (distroless by default). Requires `runtime: "bun"`. * * When enabled: * - the binary is produced at `<dist>/app` and the original `dist/server/`, * `dist/index.js` and `dist/package.json` are removed * - the generated Dockerfile uses a distroless base image and does not * run `bun install` (everything is embedded in the binary) * - any non-empty `dependencies` in the externals manifest causes the * task to fail loudly (compile requires fully-bundled output) * * Pass `true` to enable with defaults, or an object to override. */ compile: z.union([z.boolean(), z.object({ /** * Bun target triple, e.g. `bun-linux-x64-musl`, * `bun-linux-arm64-musl`, or `bun-linux-x64-modern-musl` * (AVX2 required). * * @default derived from host arch — always linux-musl. */ target: z.string().optional(), /** * Base image for the generated Dockerfile. * * @default "gcr.io/distroless/static-debian12" */ base: z.string().optional(), /** * Minify the compiled output. * * @default true */ minify: z.boolean().optional() })]).optional() }).optional(), /** * Static site deployment configuration. * * Note: Set `target: "static"` to enable static site generation. */ static: z.object({ /** * Surge domain for deployment. * * If set, a CNAME file is written to dist/public/. * If not set, a domain is auto-generated from package.json name. * * @example "my-app.surge.sh" * @example "my-custom-domain.com" */ domain: z.string().optional() }).optional(), /** * PWA (Progressive Web App) configuration. * * Generates a web app manifest and enables installability. * Requires a client-side bundle (React). */ pwa: z.object({ /** * Full application name displayed on the splash screen * and in the OS app switcher. */ name: z.string(), /** * Short name displayed on the home screen icon. * Falls back to `name` if omitted. */ shortName: z.string().optional(), /** * Theme color used for the browser toolbar and OS chrome. * * @default "#ffffff" */ themeColor: z.string().optional(), /** * Background color for the splash screen. * * @default "#ffffff" */ backgroundColor: z.string().optional(), /** * Display mode for the installed PWA. * * - `standalone` - Looks like a native app (default) * - `fullscreen` - Uses entire screen (games, immersive) * - `minimal-ui` - Like standalone with minimal browser UI * - `browser` - Standard browser tab * * @default "standalone" */ display: z.enum([ "standalone", "fullscreen", "minimal-ui", "browser" ]).optional(), /** * Enable offline support via service worker. * * TODO: Not yet implemented. */ offline: z.boolean().optional() }).optional() }), default: {} }); //#endregion //#region ../../src/cli/core/atoms/devOptions.ts /** * Dev options atom for CLI dev command. * * Defines the available dev configuration options with their defaults. * Options can be overridden via alepha.config.ts or CLI flags. */ const devOptions = $atom({ name: "alepha.cli.dev.options", description: "Dev configuration options", schema: z.object({ /** * Disable Vite React plugin. */ noViteReactPlugin: z.boolean().default(false).optional() }), default: {} }); //#endregion //#region ../../src/cli/core/providers/AppEntryProvider.ts /** * Service for locating entry files in Alepha projects. * * Resolves application entry points for the CLI build pipeline. */ var AppEntryProvider = class { fs = $inject(FileSystemProvider); options = $state(appEntryOptions); serverEntries = [ "main.server.ts", "main.server.tsx", "main.ts", "main.tsx" ]; browserEntries = [ "main.browser.ts", "main.browser.tsx", "main.ts", "main.tsx" ]; styleEntries = [ "main.css", "styles.css", "style.css" ]; /** * Get application entry points. * * Server entry is required, an error is thrown if not found. * Browser entry is optional. * * It will first check for custom entries in options, see appEntryOptions. */ async getAppEntry(root) { const appEntry = { root, server: "" }; if (this.options.server) { const serverPath = this.fs.join(root, this.options.server); if (!await this.fs.exists(serverPath)) throw new AlephaError(`Custom server entry not found: ${serverPath}`); appEntry.server = this.options.server; } if (this.options.browser) { const browserPath = this.fs.join(root, this.options.browser); if (!await this.fs.exists(browserPath)) throw new AlephaError(`Custom browser entry not found: ${browserPath}`); appEntry.browser = this.options.browser; } if (this.options.style) { const stylePath = this.fs.join(root, this.options.style); if (!await this.fs.exists(stylePath)) throw new AlephaError(`Custom style entry not found: ${stylePath}`); appEntry.style = this.options.style; } const srcFiles = await this.fs.ls(this.fs.join(root, "src")); if (!appEntry.server) { for (const entry of this.serverEntries) if (srcFiles.includes(entry)) { appEntry.server = this.fs.join("src", entry); break; } } if (!appEntry.server) { const srcDir = this.fs.join(root, "src"); throw new AlephaError(`No server entry found. Tried: ${this.serverEntries.map((e) => this.fs.join(srcDir, e)).join(", ")}. Add a main.server.ts file or configure a custom entry in alepha.config.ts.`); } if (!appEntry.browser) { for (const entry of this.browserEntries) if (srcFiles.includes(entry)) { appEntry.browser = this.fs.join("src", entry); break; } } if (!appEntry.style) { for (const entry of this.styleEntries) if (srcFiles.includes(entry)) { appEntry.style = this.fs.join("src", entry); break; } } return appEntry; } }; //#endregion //#region ../../src/cli/core/services/ViteUtils.ts /** * Vite integration utilities for the Alepha CLI. * * Centralizes all Vite-specific code: lazy loading, plugin creation, * buffered logger, dev server management. * When Vite is replaced, only this file needs to change. */ var ViteUtils = class { fs = $inject(FileSystemProvider); viteDevServer; /** * Lazy-load Vite (with rolldown-vite fallback). */ async importVite() { try { return createRequire(import.meta.url)("rolldown-vite"); } catch { try { return createRequire(import.meta.url)("vite"); } catch { throw new AlephaError("Vite is not installed. Please install it with `npm install vite`."); } } } /** * Lazy-load @vitejs/plugin-react (optional). */ async importViteReact() { try { const { default: viteReact } = createRequire(import.meta.url)("@vitejs/plugin-react"); return viteReact; } catch {} } /** * Create a Vite logger that buffers all messages instead of printing them. * Useful for silent builds that only show output on failure. */ createBufferedLogger() { const entries = []; const loggedErrors = /* @__PURE__ */ new WeakSet(); const warnedMessages = /* @__PURE__ */ new Set(); let hasWarned = false; return { get hasWarned() { return hasWarned; }, info(msg) { entries.push({ level: "info", msg, timestamp: /* @__PURE__ */ new Date() }); }, warn(msg) { hasWarned = true; entries.push({ level: "warn", msg, timestamp: /* @__PURE__ */ new Date() }); }, warnOnce(msg) { if (warnedMessages.has(msg)) return; warnedMessages.add(msg); hasWarned = true; entries.push({ level: "warn", msg, timestamp: /* @__PURE__ */ new Date() }); }, error(msg, options) { if (options?.error) loggedErrors.add(options.error); entries.push({ level: "error", msg, timestamp: /* @__PURE__ */ new Date() }); }, clearScreen() {}, hasErrorLogged(error) { return loggedErrors.has(error); }, flush() { for (const entry of entries) { const prefix = entry.level === "error" ? "\x1B[31m✖\x1B[0m" : entry.level === "warn" ? "\x1B[33m⚠\x1B[0m" : "\x1B[36mℹ\x1B[0m"; console.log(`${prefix} ${entry.msg}`); } }, getEntries() { return [...entries]; }, clear() { entries.length = 0; warnedMessages.clear(); hasWarned = false; } }; } /** * Vite plugin that reads tsconfig.json `compilerOptions.paths` and converts * them to Vite `resolve.alias` entries. Enables `@/*` → `src/*` style imports * with zero config beyond tsconfig.json. */ createTsconfigPathsPlugin() { return { name: "alepha-tsconfig-paths", async config(config) { const root = config.root || process.cwd(); const tsconfigPath = join(root, "tsconfig.json"); let content; try { content = await readFile(tsconfigPath, "utf-8"); } catch { return; } const clean = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); let tsconfig; try { tsconfig = JSON.parse(clean); } catch { return; } const paths = tsconfig?.compilerOptions?.paths; if (!paths || typeof paths !== "object") return; const alias = {}; for (const [pattern, targets] of Object.entries(paths)) { if (!Array.isArray(targets) || targets.length === 0) continue; const target = targets[0]; const aliasKey = pattern.replace(/\*$/, ""); const resolved = resolve(root, target.replace(/\*$/, "").replace(/^\.\//, "")); alias[aliasKey] = aliasKey.endsWith("/") ? `${resolved}/` : resolved; } if (Object.keys(alias).length === 0) return; return { resolve: { alias } }; } }; } /** * Vite plugin that generates a preload manifest for SSR module preloading. * * Collects lazy import paths from $page definitions during transform, * generates a manifest mapping short keys to resolved source paths, * and injects only the short key into $page definitions. */ createSsrPreloadPlugin() { let root = ""; const preloadMap = /* @__PURE__ */ new Map(); function generateKey(sourcePath) { return createHash("md5").update(sourcePath).digest("hex").slice(0, 8); } return { name: "alepha-preload", configResolved(config) { root = config.root; }, transform(code, id) { if (!id.match(/\.[tj]sx?$/)) return null; if (id.includes("node_modules")) return null; if (!code.includes("$page") || !code.includes("lazy")) return null; const insertions = []; const pageStartRegex = /\$page\s*\(\s*\{/g; let pageMatch = pageStartRegex.exec(code); while (pageMatch !== null) { const objectStartIndex = pageMatch.index + pageMatch[0].length - 1; let braceCount = 1; let i = objectStartIndex + 1; while (i < code.length && braceCount > 0) { if (code[i] === "{") braceCount++; else if (code[i] === "}") braceCount--; i++; } if (braceCount !== 0) { pageMatch = pageStartRegex.exec(code); continue; } const objectEndIndex = i - 1; const pageContent = code.slice(objectStartIndex, objectEndIndex + 1); if (pageContent.includes("alepha.page.preload")) { pageMatch = pageStartRegex.exec(code); continue; } const lazyMatch = /lazy\s*:\s*\(\s*\)\s*=>\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/.exec(pageContent); if (!lazyMatch) { pageMatch = pageStartRegex.exec(code); continue; } const importPath = lazyMatch[1]; const currentDir = dirname(id); let resolvedPath; if (importPath.startsWith(".")) resolvedPath = resolve(currentDir, importPath); else if (importPath.startsWith("/")) resolvedPath = resolve(root, importPath.slice(1)); else { pageMatch = pageStartRegex.exec(code); continue; } let relativePath = relative(root, resolvedPath); relativePath = relativePath.replace(/\\/g, "/"); if (!relativePath.match(/\.[tj]sx?$/)) relativePath = `${relativePath}.tsx`; else if (relativePath.endsWith(".jsx")) relativePath = relativePath.replace(/\.jsx$/, ".tsx"); else if (relativePath.endsWith(".js")) relativePath = relativePath.replace(/\.js$/, ".ts"); const key = generateKey(relativePath); preloadMap.set(key, relativePath); const preloadProperty = `${!code.slice(0, objectEndIndex).trimEnd().endsWith(",") ? "," : ""} [Symbol.for("alepha.page.preload")]: "${key}"`; insertions.push({ position: objectEndIndex, text: preloadProperty }); pageMatch = pageStartRegex.exec(code); } if (insertions.length === 0) return null; let result = code; for (let j = insertions.length - 1; j >= 0; j--) { const { position, text } = insertions[j]; result = result.slice(0, position) + text + result.slice(position); } return { code: result, map: null }; }, writeBundle(options) { const outDir = options.dir || ""; if (outDir.includes("server")) return; if (preloadMap.size > 0) { const viteDir = join(outDir, ".vite"); if (!existsSync(viteDir)) mkdirSync(viteDir, { recursive: true }); const manifest = Object.fromEntries(preloadMap); writeFileSync(join(viteDir, "preload-manifest.json"), JSON.stringify(manifest, null, 2)); } } }; } generateIndexHtml(entry, opts) { const style = entry.style; const browser = entry.browser ?? entry.server; return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>App</title> <meta name="viewport" content="width=device-width, initial-scale=1"/>${opts?.pwa ? "\n<link rel=\"manifest\" href=\"/manifest.webmanifest\" />" : ""} ${style ? `<link rel="stylesheet" href="/${style}" />` : ""} </head> <body> <div id="root"></div> <script type="module" src="/${browser}"><\/script> </body> </html> `.trim(); } /** * We need to close the Vite dev server after build is done. */ onReady = $hook({ on: "ready", priority: "last", handler: async () => { await this.viteDevServer?.close(); } }); onStop = $hook({ on: "stop", handler: async () => { await this.viteDevServer?.close(); } }); async runAlepha(opts) { const { createServer } = await this.importVite(); process.env.NODE_ENV = opts.mode; process.env.ALEPHA_CLI_IMPORT = "true"; process.env.LOG_LEVEL ??= "warn"; process.env.APP_SECRET ??= "123456"; /** * 01/26 Vite 7 * "runnerImport" doesn't work as expected here. (e.g. build docs fail) * -> We still use devServer and ssrLoadModule for now. * -> This is clearly a bad stuff, we need to find better way. */ this.viteDevServer = await createServer({ server: { middlewareMode: true }, appType: "custom", logLevel: "silent", plugins: [this.createTsconfigPathsPlugin()] }); await this.viteDevServer.ssrLoadModule(opts.entry.server); delete process.env.ALEPHA_CLI_IMPORT; const alepha = globalThis.__alepha; if (!alepha) throw new AlephaError("Alepha instance not found after loading entry module"); return alepha; } }; //#endregion //#region ../../src/cli/core/providers/ViteBuildProvider.ts var ViteBuildProvider = class { alepha; appEntry; viteUtils = $inject(ViteUtils); async init(opts) { const alepha = await this.viteUtils.runAlepha({ entry: opts.entry, mode: "production" }); this.alepha = alepha; this.appEntry = opts.entry; return alepha; } hasClient() { if (!this.alepha) throw new AlephaError("ViteBuildProvider not initialized"); try { this.alepha.inject("ReactServerProvider"); return true; } catch { return false; } } }; //#endregion //#region ../../src/cli/core/services/AlephaCliUtils.ts /** * Core utility service for CLI commands. * * Provides: * - Command execution * - File editing helpers * - Drizzle/ORM utilities * - Environment loading */ var AlephaCliUtils = class { log = $logger(); fs = $inject(FileSystemProvider); envUtils = $inject(EnvUtils); boot = $inject(AppEntryProvider); shell = $inject(ShellProvider); viteUtils = $inject(ViteUtils); alepha = $inject(Alepha); /** * Execute a command with inherited stdio. */ async exec(command, options = {}) { await this.shell.run(command, { root: options.root, env: options.env, resolve: !options.global, capture: options.capture }); } /** * Resolve the absolute path to a toolchain binary that ships embedded in * `alepha`'s own `dependencies` (typescript, vite, vitest, @biomejs/biome, * drizzle-kit). * * The CLI runs the result via `node <path>` so the toolchain works under * every package manager — including pnpm with a strict node-linker, where * a transitive dependency's bin is NOT hoisted into the project's * `node_modules/.bin`. Resolution starts from `alepha`'s own location, so * the version is whatever `alepha` shipped — the project never pins it. * * @param pkg - npm package name (e.g. `"typescript"`) * @param binName - which `bin` entry to use (e.g. `"tsc"`); defaults to the * package's only/first bin */ resolveBin(pkg, binName) { const require = createRequire(import.meta.url); let pkgDir; for (const nm of require.resolve.paths(pkg) ?? []) { const candidate = join(nm, pkg); if (existsSync(join(candidate, "package.json"))) { pkgDir = candidate; break; } } if (!pkgDir) throw new AlephaError(`Cannot locate package '${pkg}' — is it installed alongside alepha?`); const bin = JSON.parse(readFileSync(join(pkgDir, "package.json"), "utf8")).bin; if (!bin) throw new AlephaError(`Package '${pkg}' declares no 'bin' entry`); const rel = typeof bin === "string" ? bin : bin[binName ?? pkg] ?? Object.values(bin)[0]; if (!rel) throw new AlephaError(`Package '${pkg}' has no bin named '${binName}'`); return join(pkgDir, rel); } /** * Write a configuration file to node_modules/.alepha directory. */ async writeConfigFile(name, content, root = process.cwd()) { const dir = this.fs.join(root, "node_modules", ".alepha"); await this.fs.mkdir(dir, { recursive: true }).catch(() => null); const path = this.fs.join(dir, name); await this.fs.writeFile(path, content); this.log.debug(`Config file written: ${path}`); return path; } async loadAlephaFromServerEntryFile(opts) { let entry; if ("root" in opts) entry = await this.boot.getAppEntry(opts.root); else entry = opts.entry; return await this.viteUtils.runAlepha({ entry, mode: opts.mode }); } /** * Load environment variables from a .env file. */ async loadEnv(root, files = [".env"]) { await this.envUtils.loadEnv(root, files); } async exists(root, path) { return this.fs.exists(this.fs.join(root, path)); } /** * Check if a command is installed and available in the system PATH. */ isInstalledAsync(cmd) { return this.shell.isInstalled(cmd); } /** * Get the current git revision (commit SHA). * * @returns The short commit SHA or "unknown" if not in a git repo */ async getGitRevision() { try { return (await this.shell.run("git rev-parse --short HEAD", { capture: true })).trim(); } catch { return "unknown"; } } /** * Get the user's email from git config. * * @returns The git user email or undefined if not configured */ async getGitEmail() { try { return (await this.shell.run("git config user.email", { capture: true })).trim() || void 0; } catch { return; } } }; //#endregion //#region ../../src/cli/core/alephaPackageJson.ts const alephaPackageJson = pkg; const version = pkg.version; //#endregion //#region ../../src/cli/core/services/PackageManagerUtils.ts /** * Utility service for package manager operations. * * Handles detection, installation, and cleanup for: * - Yarn * - npm * - pnpm * - Bun */ var PackageManagerUtils = class { log = $logger(); fs = $inject(FileSystemProvider); alepha = $inject(Alepha); /** * Detect the package manager used in the project. * Checks current directory first, then workspace root if in a monorepo. */ async getPackageManager(root, pm) { if (pm) return pm; if (this.alepha.isBun()) return "bun"; if (await this.fs.exists(this.fs.join(root, "bun.lock"))) return "bun"; if (await this.fs.exists(this.fs.join(root, "yarn.lock"))) return "yarn"; if (await this.fs.exists(this.fs.join(root, "pnpm-lock.yaml"))) return "pnpm"; if (await this.fs.exists(this.fs.join(root, "package-lock.json"))) return "npm"; const workspace = await this.getWorkspaceContext(root); if (workspace.packageManager) return workspace.packageManager; return "npm"; } /** * Detect workspace context when inside a monorepo package. * * Checks if we're inside a workspace package by walking up to 3 levels * for workspace indicators like lockfiles and config files. * This covers both standard layouts (packages/my-pkg) and deeper nesting * (packages/scope/my-pkg). * * @param root - The current package directory * @returns Workspace context with root path, PM, and config presence */ async getWorkspaceContext(root) { const noContext = { isPackage: false, workspaceRoot: null, packageManager: null, config: { biomeJson: false, editorconfig: false, tsconfigJson: false } }; for (let depth = 2; depth <= 3; depth++) { const segments = Array.from({ length: depth }, () => ".."); const candidate = this.fs.join(root, ...segments); if (candidate === root) break; const result = await this.checkWorkspaceRoot(candidate); if (result) return result; } return noContext; } async checkWorkspaceRoot(candidate) { const [hasYarnLock, hasPnpmLock, hasNpmLock, hasBunLock] = await Promise.all([ this.fs.exists(this.fs.join(candidate, "yarn.lock")), this.fs.exists(this.fs.join(candidate, "pnpm-lock.yaml")), this.fs.exists(this.fs.join(candidate, "package-lock.json")), this.fs.exists(this.fs.join(candidate, "bun.lock")) ]); if (!(hasYarnLock || hasPnpmLock || hasNpmLock || hasBunLock)) return null; const [hasBiome, hasEditorConfig, hasTsConfig, hasPackageJson] = await Promise.all([ this.fs.exists(this.fs.join(candidate, "biome.json")), this.fs.exists(this.fs.join(candidate, ".editorconfig")), this.fs.exists(this.fs.join(candidate, "tsconfig.json")), this.fs.exists(this.fs.join(candidate, "package.json")) ]); if (!hasPackageJson) return null; let packageManager = null; if (hasYarnLock) packageManager = "yarn"; else if (hasPnpmLock) packageManager = "pnpm"; else if (hasBunLock) packageManager = "bun"; else if (hasNpmLock) packageManager = "npm"; return { isPackage: true, workspaceRoot: candidate, packageManager, config: { biomeJson: hasBiome, editorconfig: hasEditorConfig, tsconfigJson: hasTsConfig } }; } /** * Get the install command for a package. */ async getInstallCommand(root, packageName, dev = true) { const pm = await this.getPackageManager(root); let cmd; switch (pm) { case "yarn": cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`; break; case "pnpm": cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`; break; case "bun": cmd = `bun add ${dev ? "-d" : ""} ${packageName}`; break; default: cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`; } return cmd.replace(/\s+/g, " ").trim(); } /** * Check if a dependency is installed in the project. */ async hasDependency(root, packageName) { try { const pkg = await this.readPackageJson(root); return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]); } catch { return false; } } /** * Check if Expo is present in the project. */ async hasExpo(root) { return this.hasDependency(root, "expo"); } /** * Check if React is present in the project. */ async hasReact(root) { return this.hasDependency(root, "react"); } /** * Install a dependency if it's missing from the project. * Optionally checks workspace root for the dependency in monorepo setups. */ async ensureDependency(root, packageName, options = {}) { const { dev = true, checkWorkspace = false } = options; if (await this.hasDependency(root, packageName)) { this.log.debug(`Dependency '${packageName}' is already installed`); return; } if (checkWorkspace) { const workspace = await this.getWorkspaceContext(root); if (workspace.workspaceRoot) { if (await this.hasDependency(workspace.workspaceRoot, packageName)) { this.log.debug(`Dependency '${packageName}' is already installed in workspace root`); return; } } } const cmd = await this.getInstallCommand(root, packageName, dev); if (options.run) await options.run(cmd, { alias: `add ${packageName}`, root }); else if (options.exec) { this.log.debug(`Installing ${packageName}`); await options.exec(cmd, { global: true, root }); } } async ensureYarn(root) { const yarnrcPath = this.fs.join(root, ".yarnrc.yml"); if (!await this.fs.exists(yarnrcPath)) await this.fs.writeFile(yarnrcPath, "nodeLinker: node-modules"); await this.removeAllPmFilesExcept(root, "yarn"); } async ensureBun(root) { await this.removeAllPmFilesExcept(root, "bun"); } async ensurePnpm(root) { await this.removeAllPmFilesExcept(root, "pnpm"); } async ensureNpm(root) { await this.removeAllPmFilesExcept(root, "npm"); } async removeAllPmFilesExcept(root, except) { if (except !== "yarn") await this.removeYarn(root); if (except !== "pnpm") await this.removePnpm(root); if (except !== "npm") await this.removeNpm(root); if (except !== "bun") await this.removeBun(root); } async removeYarn(root) { await this.removeFiles(root, [ ".yarn", ".yarnrc.yml", "yarn.lock" ]); await this.editPackageJson(root, (pkg) => { pkg.packageManager = void 0; return pkg; }); } async removePnpm(root) { await this.removeFiles(root, ["pnpm-lock.yaml", "pnpm-workspace.yaml"]); await this.editPackageJson(root, (pkg) => { pkg.packageManager = void 0; return pkg; }); } async removeNpm(root) { await this.removeFiles(root, ["package-lock.json"]); } async removeBun(root) { await this.removeFiles(root, ["bun.lockb", "bun.lock"]); } async readPackageJson(root) { const content = await this.fs.createFile({ path: this.fs.join(root, "package.json") }).text(); return JSON.parse(content); } async writePackageJson(root, content) { await this.fs.writeFile(this.fs.join(root, "package.json"), JSON.stringify(content, null, 2)); } async editPackageJson(root, editFn) { try { const updated = editFn(await this.readPackageJson(root)); await this.writePackageJson(root, updated); } catch {} } async ensurePackageJson(root, modes) { const packageJsonPath = this.fs.join(root, "package.json"); if (!await this.fs.exists(packageJsonPath)) { const content = { name: basename(root) || "app", private: true, ...this.generatePackageJsonContent(modes) }; await this.writePackageJson(root, content); return content; } const packageJson = await this.readPackageJson(root); const newContent = this.generatePackageJsonContent(modes); packageJson.type = "module"; packageJson.dependencies ??= {}; packageJson.devDependencies ??= {}; packageJson.scripts ??= {}; Object.assign(packageJson.dependencies, newContent.dependencies); Object.assign(packageJson.devDependencies, newContent.devDependencies); Object.assign(packageJson.scripts, newContent.scripts); await this.writePackageJson(root, packageJson); return packageJson; } generatePackageJsonContent(modes) { const alephaDeps = alephaPackageJson.devDependencies; const dependencies = { alepha: `^${version}` }; const devDependencies = {}; const scripts = { dev: "alepha dev", build: "alepha build", test: "alepha test", lint: "alepha lint", typecheck: "alepha typecheck", verify: "alepha verify" }; if (modes.tailwind) { devDependencies.tailwindcss = alephaDeps.tailwindcss; devDependencies["@tailwindcss/vite"] = alephaDeps["@tailwindcss/vite"]; } if (modes.react) { dependencies.react = alephaDeps.react; dependencies["react-dom"] = alephaDeps["react-dom"]; devDependencies["@types/react"] = alephaDeps["@types/react"]; } return { type: "module", dependencies, devDependencies, scripts }; } async removeFiles(root, files) { await Promise.all(files.map((file) => this.fs.rm(this.fs.join(root, file), { force: true, recursive: true }))); } }; //#endregion //#region ../../src/cli/core/templates/agentMd.ts const agentMd = () => { return `# AGENTS.md This is an **Alepha** project. ## Rules - Always check \`node_modules/alepha/src/\` before suggesting npm packages - Use \`t\` from Alepha for schemas (not Zod) - Use \`protected\` instead of \`private\` for class members - Import with file extensions: \`import { User } from "./User.ts"\` ## Commands \`\`\`bash alepha lint # Format and lint alepha typecheck # Type checking alepha test # Run tests alepha build # Build alepha platform plan # Show planned cloud topology (requires platform plugin) alepha platform up # Provision + deploy to a configured environment alepha platform status # Inspect deployed resources \`\`\` ## Testing - Specs live in \`test/\`, named \`*.spec.ts\`. - Run with \`alepha test\` (Vitest, embedded in alepha — nothing to install). - \`test/dummy.spec.ts\` is the starting example; \`Alepha.create()\` is the entry point and \`.inject(...)\` resolves providers. ## Cloud deployment (Cloudflare Workers) Add the \`platform\` plugin to \`alepha.config.ts\` to manage cloud provisioning, deploy, secrets, and DB migrations end-to-end: \`\`\`ts import { defineConfig } from "alepha/cli/config"; import { platform } from "alepha/cli/platform"; export default defineConfig({ plugins: [ platform({ environments: { production: { adapter: "cloudflare", domain: "yourapp.com", // zone: "yourapp.com", // required only for wildcard domains // jurisdiction: "eu", // optional: EU data residency }, }, }), ], }); \`\`\` Then: \`alepha platform up --env production\` (auth via \`wrangler login\` on first run). Supported adapters: \`cloudflare\`, \`vercel\`. The Cloudflare adapter provisions D1 (or Hyperdrive when \`DATABASE_URL\` is postgres), KV, R2, Queues, and pushes secrets via \`wrangler secret bulk\`. Set \`build.target: "cloudflare"\` in \`alepha.config.ts\` if you only want the build artifact without the orchestrator. ## Documentation - Framework source: \`node_modules/alepha/src/\` - Docs: https://alepha.dev/llms.txt `.trim(); }; //#endregion //#region ../../src/cli/core/templates/alephaConfigTs.ts /** * Template for alepha.config.ts with documented options. */ const alephaConfigTs = () => { return `import { defineConfig } from "alepha/cli/config"; // import { platform } from "alepha/cli/platform"; export default defineConfig({ // // entry: { // server: "src/main.server.ts", // browser: "src/main.browser.ts", // style: "src/main.css", // }, // // build: { // target: "docker", // runtime: "node", // }, // // env: { // VITE_BUILD_DATE: new Date().toISOString(), // VITE_VERSION: pkg.version, // }, // // // Deploy to Cloudflare in ~10s: \`alepha platform up --env production\` // // Requires \`wrangler login\` once. D1, R2, KV, Queues and cron triggers // // are auto-provisioned from your $repository / $bucket / $cache / $queue // // / $scheduler primitives — no wrangler.toml to maintain. // plugins: [ // platform({ // environments: { // production: { adapter: "cloudflare", domain: "myapp.com" }, // preview: { adapter: "cloudflare" }, // workers.dev subdomain // }, // }), // ], }); `; }; //#endregion //#region ../../src/cli/core/templates/apiHelloControllerTs.ts const apiHelloControllerTs = (options = {}) => { return `import { $action } from "alepha/server"; import { helloResponseSchema } from "../schemas/helloResponseSchema.ts"; export class HelloController { hello = $action({ path: "/hello", schema: { response: helloResponseSchema, }, handler: () => ({ appName: "${(options.appName || "my-app").split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")}", serverTime: new Date().toISOString(), }), }); } `.trim(); }; //#endregion //#region ../../src/cli/core/templates/apiHelloResponseSchemaTs.ts const apiHelloResponseSchemaTs = () => { return `import { type Static, z } from "alepha"; export const helloResponseSchema = z.object({ appName: z.text(), serverTime: z.datetime(), }); export type HelloResponse = Static<typeof helloResponseSchema>; `.trim(); }; //#endregion //#region ../../src/cli/core/templates/apiIndexTs.ts const apiIndexTs = (options = {}) => { const { appName = "app" } = options; return ` import { $module } from "alepha"; import { HelloController } from "./controllers/HelloController.ts"; export const ApiModule = $module({ name: "${appName}.api", services: [HelloController], }); `.trim(); }; //#endregion //#region ../../src/cli/core/templates/biomeJson.ts const biomeJson = () => ` { "$schema": "https://biomejs.dev/schemas/latest/schema.json", "vcs": { "enabled": true, "clientKind": "git" }, "files": { "ignoreUnknown": true, "includes": ["**", "!node_modules", "!dist"] }, "formatter": { "enabled": true, "useEditorconfig": true }, "linter": { "enabled": true, "rules": { "recommended": true, "a11y": { "useFocusableInteractive": "off", "useSemanticElements": "off", "useKeyWithClickEvents": "off", "useAriaPropsForRole": "off", "noLabelWithoutControl": "off" }, "correctness": { "useExhaustiveDependencies": "off" }, "suspicious": { "noArrayIndexKey": "off", "noExplicitAny": "off", "noDocumentCookie": "off" }, "style": { "noNonNullAssertion": "off" } }, "domains": { "react": "recommended" } }, "css": { "parser": { "cssModules": false, "tailwindDirectives": true } }, "assist": { "actions": { "source": { "organizeImports": "on" } } } } `.trim(); //#endregion //#region ../../src/cli/core/templates/dummySpecTs.ts /** * Starter spec written by `alepha init`. It doubles as a worked example: * it shows the test location (`test/`), the `*.spec.ts` naming, and the * canonical Alepha entry point — `Alepha.create()` + `.inject(...)`. * * Tests run with `alepha test` (Vitest, embedded in alepha). Replace this * with real specs as the project grows. */ const dummySpecTs = () => ` import { Alepha } from "alepha"; import { expect, test } from "vitest"; test("alepha app can be created", () => { const alepha = Alepha.create(); expect(alepha).toBeDefined(); expect(alepha.inject).toBeTypeOf("function"); }); `.trim(); //#endregion //#region ../../src/cli/core/templates/editorconfig.ts const editorconfig = () => ` # https://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true indent_style = space indent_size = 2 `.trim(); //#endregion //#region ../../src/cli/core/templates/gitignore.ts const gitignore = () => ` # Dependencies node_modules/ # Build outputs dist/ .vite/ # Environment files .env .env.* !.env.example # IDE .idea/ *.swp *.swo # OS .DS_Store Thumbs.db # Logs *.log logs/ # Test coverage coverage/ # Yarn .yarn/* !.yarn/patches !.yarn/plugins !.yarn/releases !.yarn/sdks !.yarn/versions .pnp.* `.trim(); //#endregion //#region ../../src/cli/core/templates/logoSvg.ts const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300"> <svg xmlns="http://www.w3.org/2000/svg"> <defs> <linearGradient id="b" x1="142.3" x2="159.6" y1="123.3" y2="176.7" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#33a72c"/> <stop offset="1" stop-color="#2d8d40"/> </linearGradient> <linearGradient id="c" x1="61.6" x2="100.7" y1="218.5" y2="174" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#298e35"/> <stop offset="1" stop-color="#327952"/> </linearGradient> <linearGradient id="d" x1="262.7" x2="242.2" y1="178.4" y2="220.1" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#32a62d"/> <stop offset="1" stop-color="#2d8d40"/> </linearGradient> <linearGradient id="e" x1="81.2" x2="69.1" y1="126.7" y2="92.3" gradientUnits="userSpaceOnUse"> <stop offset="0" stop-color="#299a2c"/> <stop offset="1" stop-color="#51be40"/> </linearGradient> <clipPath id="a"> <path fill="none" d="M0 0h300v241H0z"/> </clipPath> </defs> <g fill="none" stroke-miterlimit="10" clip-path="url(#a)" font-family="none" font-size="none" font-weight="300" style="mix-blend-mode:normal" text-anchor="middle"> <path fill="url(#b)" d="M54 174a182 182 0 0 1 106-51c19-2 37-6 54-15l16-11 3-4 1 1 1 4 1 6v9c0 6-2 12-5 18l-1 4-7 8c-5 7-12 14-19 19l-4 2-11 7v1l-1-1-3 2a120 120 0 0 1-30 8l-8 1-20-1c-21-3-44-10-64 0-5 2-9 4-12 8l-9 9h1l14-9c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-5 1h-8l-7-1H15c0-3 4-7 5-9l7-11 5-8c6-10 14-19 22-28z"/> <path fill="#1d524a" d="M178 141c9-1 17-4 24-8l8-4-6 4c-9 7-20 11-32 13l-39 5h-5l-15 3 16 13 18 15-20-1c-21-3-44-10-64 0-5 2-9 4-12 8l-9 9h1l14-9c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-5 1h-8l-7-1H15c0-3 4-7 5-9l7-11 5-8c6-10 14-19 22-28l4-2 23-14 11-5c21-7 43-9 65-9l21-3z"/> <path fill="url(#c)" d="M57 189c7-4 15-6 23-7h7l25 3 3 1-2 5-3 5c-4 7-10 14-15 18l-4 3c-12 8-25 12-39 13h-5l-4-9-2-5 5-2 9-4 10-5 10-4c5-2 10-3 14-6l3-1h-1a1136 1136 0 0 0-43 14c1-6 6-14 9-19 1 0 0 0 0 0z"/> <path fill="#2d6f4d" d="m41 216 5-2 9-4 5 3c9 4 18 6 27 4h4c-12 8-25 12-39 13h-5l-4-9-2-5zm16-27s1 0 0 0c-3 5-8 13-9 19l-15 8c0-3 1-9 3-11l3-3 4-4 14-9z"/> <path fill="#49b63b" d="M42 198h1l-4 4 3-4z"/> <path fill="#2d6f4d" d="m113 154 15-3 11 2 18 8c11 6 23 8 35 5l8-2-11 7v1l-1-1-3 2a120 120 0 0 1-30 8l-8 1-18-15-16-13z"/> <path fill="#49b63b" d="M81 158c2-4 11-7 15-8 18-6 36-7 55-8l12-1h15l-21 3c-22 0-44 2-65 9l-11 5z"/> <path fill="#2d913b" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1 1 5v6l6 12 5 10 12 26 2 5 4 10 7 17 4 8c-9 5-20 9-30 12l-16 3-7-14-6-14-5-10-1-3-4 7-13 27-6 11-6 2-24 9 2-4c11-16 14-36 11-56l-3 1-9 5 1-3 9-21 2-5z"/> <path fill="#2d6f4d" d="m175 17 6 12 5 10 12 26 2 5 4 10 7 17 4 8c-9 5-20 9-30 12l-16 3-7-14-6-14-5-10-1-3c0-2 5-8 6-10l1-2c9-13 17-28 18-45v-5z"/> <path fill="#1d524a" d="m175 17 6 12 5 10 12 26 2 5-3-1-13-8-7 4 3 7-4 1-2-3-5-12-5 7a136 136 0 0 1-10 15l-3 2-1-3c0-2 5-8 6-10l1-2c9-13 17-28 18-45v-5z"/> <path fill="#2d6f4d" d="m186 39 12 26 2 5-3-1-13-8-7 4-6-12 8-7 7-7z"/> <path fill="#2d913b" d="m177 65 7-4 13 8 3 1 4 10-3 7-7 14-2 8-1-4-6-19a99 99 0 0 0-8-21zm-3 5 2 3 12 32h-1l-8 5c-4 1-9 0-13-2l-1-1c-1-8 2-15 4-22 1-5 2-10 5-15z"/> <path fill="#1d524a" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1c-3 1-3 2-4 4l-11 14-5 5v1c-11 11-22 24-31 37l-5 6-6 11h1l6 12c3 7 8 13 14 17l-6 11-6 2-24 9 2-4c11-16 14-36 11-56l-3 1-9 5 1-3 9-21 2-5z"/> <path fill="#49b63b" d="m110 52 2-5 9-14c7-8 19-14 29-18l10-3 12-6 1-1 1 1c-3 1-3 2-4 4l-11 14-5 5-8 7-13 11-5 6c-5-2-10-5-15-3l-3 2z"/> <path fill="#2d913b" d="m110 52 3-2c5-2 10 1 15 3l-12 13-6 9-3 1-9 5 1-3 9-21 2-5z"/> <path fill="#2d6f4d" d="M121 92c10-5 19-9 28-17l7-6c-1 2-6 8-6 10l-4 7-13 27c-6-4-11-10-14-17l-6-12 4-5 4 13z"/> <path fill="#49b63b" d="m154 30-1 11c0 4-3 9-6 12-6 9-19 10-25 19-1 0-3 2-4 1l5-6c9-13 20-26 31-37z"/> <path fill="url(#d)" d="m230 135 3 6 11 17 2 5 4 5 3 6 9 14 19 36 4 5c-2 2-19 2-22 2l-5-1h-2c-12 0-25-4-35-10l-4-3c-10-8-15-16-21-27l-7-18v-1l11-7 4-2c7-5 14-12 19-19l7-8z"/> <path fill="#1d524a" d="m230 135 3 6 11 17 2 5 4 5 3 6h-1c-10-8-20-9-32-4l23 23 13 12 1 2-20-14-13-12-4-3-5-6c-2 0-2 3-2 5l-1 10c-1 8-2 11 1 19l2 6 2 5c-10-8-15-16-21-27l-7-18v-1l11-7 4-2c7-5 14-12 19-19l7-8z"/> <path fill="#2d6f4d" d="m233 141 11 17 2 5 4 5 3 6h-1c-10-8-20-9-32-4l-7-9 15-15 5-5zm-16 76-2-5-2-6c-3-8-2-11-1-19l1-10c0-2 0-5 2-5l5 6 4 3h-1c0 4 8 22 11 26 6 10 15 18 25 22 2 0 3 0 4 2l-5-1h-2c-12 0-25-4-35-10l-4-3z"/> <path fill="#2d913b" d="M110 75c3 20 0 40-11 56l-2 4a145 145 0 0 0-43 31l-5 4-11 15v-7l-1-10a143 143 0 0 1 5-29l9-20c13-18 32-29 52-37l4-3 3-4z"/> <path fill="url(#e)" d="m42 139 9-20c13-18 32-29 52-37l4-3-3 7c-4 5-8 11-13 15l-3 3c-6 6-13 12-18 19l-8 11-20 5z"/> <path fill="#1d524a" d="M91 101c0 2-4 6-6 8-5 5-10 11-14 18l-9 13-4 9 5 1c4 0 16-9 19-7-10 7-20 14-28 23l-5 4-11 15v-7l-1-10a143 143 0 0 1 5-29l20-5 8-11c5-7 12-13 18-19l3-3z"/> <path fill="#2d6f4d" d="m62 134-9 16-3 7-2 4c-3 3-8 5-10 8l-1-1a143 143 0 0 1 5-29l20-5zm0 6 11-5 7-4c5-1 14-2 19 0l-2 4-15 8c-3-2-15 7-19 7l-5-1 4-9z"/> </g> </svg> </svg> `; //#endregion //#region ../../src/cli/core/templates/mainBrowserTs.ts const mainBrowserTs = () => ` import { Alepha, run } from "alepha"; import { WebModule } from "./web/index.ts"; const alepha = Alepha.create(); alepha.with(WebModule); run(alepha); `.trim(); //#endregion //#region ../../src/cli/core/templates/mainCss.ts const mainCss = (opts = {}) => { if (opts.tailwind) return `@import "tailwindcss"; /* Add your styles here */ `; return `/** * Global styles for your application. * * Options: * - Tailwind CSS: Use \`alepha init --tailwind\` to add Tailwind CSS * - Raw CSS: Write your own styles below */ /* Add your styles here */ `; }; //#endregion //#region ../../src/cli/core/templates/mainServerTs.ts const mainServerTs = (options = {}) => { const { api = false, react = false } = options; const imports = []; const withs = []; if (api) { imports.push(`import { ApiModule } from "./api/index.ts";`); withs.push(`alepha.with(ApiModule);`); } if (react) { imports.push(`import { WebModule } from "./web/index.ts";`); withs.push(`alepha.with(WebModule);`); } return ` import { Alepha, run } from "alepha"; ${imports.length > 0 ? `${imports.join("\n")}\n` : ""} const alepha = Alepha.create(); ${withs.length > 0 ? `\n${withs.join("\n")}` : ""} run(alepha); `.trim(); }; //#endregion //#region ../../src/cli/core/templates/tsconfigJson.ts const tsconfigJson = () => ` { "extends": "alepha/tsconfig.base", "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } `.trim(); //#endregion //#region ../../src/cli/core/templates/viteConfigTs.ts const viteConfigTs = () => { return `import tailwindcss from "@tailwindcss/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [tailwindcss()], }); `; }; //#endregion //#region ../../src/cli/core/templates/vitestConfigTs.ts /** * Minimal Vitest config for a freshly scaffolded Alepha project. * * Defines `test.root` explicitly so Vitest doesn't walk up the directory * tree and inherit a parent monorepo config (which can pull in unrelated * setup files, database connections, etc.). */ const vitestConfigTs = () => `import { defineConfig } from "vitest/config"; export default defineConfig({ test: { root: ".", globals: true, }, }); `; //#endregion //#region ../../src/cli/core/templates/vscodeSettingsJson.ts /** * `.vscode/settings.json` — points the editor's TypeScript language server * at the `typescript` copy embedded in `alepha`'s dependencies, so the IDE * type-checks with the exact same compiler version as `alepha typecheck`. * * Without this, VS Code falls back to its own bundled TypeScript, which * drifts from whatever `alepha` ships — the classic "green in CI, red * squiggles in the editor" skew. * * The path assumes a hoisting package manager (yarn node-modules / npm / * bun), where `alepha`