UNPKG

alepha

Version:

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

728 lines (559 loc) 29.5 kB
import { Alepha, Json } from "alepha"; import { CliProvider } from "alepha/command"; import { FileSystemProvider, MemoryFileSystemProvider, MemoryShellProvider, ShellProvider, } from "alepha/system"; import { describe, expect, it } from "vitest"; import { InitCommand } from "../commands/init.ts"; describe("alepha init", () => { const createTestEnv = () => { const alepha = Alepha.create() .with({ provide: FileSystemProvider, use: MemoryFileSystemProvider }) .with({ provide: ShellProvider, use: MemoryShellProvider }); const fs = alepha.inject(MemoryFileSystemProvider); const shell = alepha.inject(MemoryShellProvider); const cli = alepha.inject(CliProvider); const cmd = alepha.inject(InitCommand); const json = alepha.inject(Json); return { alepha, fs, shell, cli, cmd, json }; }; const setupProject = async ( fs: MemoryFileSystemProvider, json: Json, name = "test-app", ) => { await fs.writeFile("/project/package.json", json.stringify({ name })); }; // ───────────────────────────────────────────────────────────────────────────── // Configuration Files // ───────────────────────────────────────────────────────────────────────────── describe("configuration files", () => { it("should create tsconfig.json with alepha base", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/tsconfig.json")).toBe(true); const tsconfig = await fs.readJsonFile<{ extends: string }>( "/project/tsconfig.json", ); expect(tsconfig.extends).toBe("alepha/tsconfig.base"); }); it("should create biome.json", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/biome.json")).toBe(true); expect(fs.wasWrittenMatching("/project/biome.json", /biomejs\.dev/)).toBe( true, ); }); it("should create .editorconfig", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/.editorconfig")).toBe(true); expect( fs.wasWrittenMatching("/project/.editorconfig", /root\s*=\s*true/), ).toBe(true); }); it("should create alepha.config.ts with documented options", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/alepha.config.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/alepha.config.ts", /defineConfig/), ).toBe(true); expect(fs.wasWrittenMatching("/project/alepha.config.ts", /entry:/)).toBe( true, ); expect(fs.wasWrittenMatching("/project/alepha.config.ts", /build:/)).toBe( true, ); }); it("should create .vscode/settings.json pointing at the embedded tsdk", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/.vscode/settings.json")).toBe(true); expect( fs.wasWrittenMatching( "/project/.vscode/settings.json", /node_modules\/typescript\/lib/, ), ).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // AI Agent Files (always generated) // ───────────────────────────────────────────────────────────────────────────── describe("AI agent files", () => { it("should create both AGENTS.md and CLAUDE.md", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/AGENTS.md")).toBe(true); expect(fs.wasWritten("/project/CLAUDE.md")).toBe(true); }); it("should write CLAUDE.md as a stub importing AGENTS.md", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); const claude = await fs.readTextFile("/project/CLAUDE.md"); expect(claude.trim()).toBe("@AGENTS.md"); }); it("should include Alepha instructions in agent file", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWrittenMatching("/project/AGENTS.md", /Alepha/)).toBe(true); expect(fs.wasWrittenMatching("/project/AGENTS.md", /alepha lint/)).toBe( true, ); }); }); // ───────────────────────────────────────────────────────────────────────────── // Package Manager Detection // ───────────────────────────────────────────────────────────────────────────── describe("package manager detection", () => { it("should use yarn when yarn.lock exists", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/yarn.lock", ""); await cli.run(cmd.init, { root: "/project" }); expect(shell.wasCalled("yarn install")).toBe(true); expect(shell.wasCalled("yarn set version stable")).toBe(true); }); it("should use npm when package-lock.json exists", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/package-lock.json", "{}"); await cli.run(cmd.init, { root: "/project" }); expect(shell.wasCalled("npm install")).toBe(true); }); it("should use pnpm when pnpm-lock.yaml exists", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/pnpm-lock.yaml", ""); await cli.run(cmd.init, { root: "/project" }); expect(shell.wasCalled("pnpm install")).toBe(true); }); it("should use bun when bun.lock exists", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/bun.lock", ""); await cli.run(cmd.init, { root: "/project" }); expect(shell.wasCalled("bun install")).toBe(true); }); it("should respect --pm flag over lockfile detection", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/yarn.lock", ""); // yarn lockfile exists await cli.run(cmd.init, { argv: "--pm=npm", root: "/project" }); expect(shell.wasCalled("npm install")).toBe(true); expect(shell.wasCalled("yarn install")).toBe(false); }); it("should accept --pm=yarn", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--pm=yarn", root: "/project" }); expect(shell.wasCalled("yarn install")).toBe(true); }); it("should accept --pm=pnpm", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--pm=pnpm", root: "/project" }); expect(shell.wasCalled("pnpm install")).toBe(true); }); it("should accept --pm=bun", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--pm=bun", root: "/project" }); expect(shell.wasCalled("bun install")).toBe(true); }); it("should reject invalid --pm value", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await expect( cli.run(cmd.init, { argv: "--pm=invalid", root: "/project" }), ).rejects.toThrowError(/Invalid flag/); }); it("should show enum values in help for --pm flag", async () => { const { alepha, cli, cmd } = createTestEnv(); await alepha.start(); // Verify printHelp works with the init command (which has an enum flag) expect(() => cli.printHelp(cmd.init)).not.toThrow(); }); }); // ───────────────────────────────────────────────────────────────────────────── // Minimal Project Structure (default, no flags) // ───────────────────────────────────────────────────────────────────────────── describe("minimal project structure (default)", () => { it("should create src/main.server.ts", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/src/main.server.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.server.ts", /Alepha\.create/), ).toBe(true); }); it("should not create api structure without --api flag", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/src/api/index.ts")).toBe(false); expect( fs.wasWritten("/project/src/api/controllers/HelloController.ts"), ).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────────── // API Project Structure (--api flag) // ───────────────────────────────────────────────────────────────────────────── describe("--api flag", () => { it("should create src/main.server.ts with ApiModule import", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--api", root: "/project" }); expect(fs.wasWritten("/project/src/main.server.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.server.ts", /ApiModule/), ).toBe(true); }); it("should create src/api/index.ts with app name from directory", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json, "my-cool-app"); await cli.run(cmd.init, { argv: "--api", root: "/project" }); expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/src/api/index.ts", /\$module/), ).toBe(true); }); it("should create example HelloController", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--api", root: "/project" }); expect( fs.wasWritten("/project/src/api/controllers/HelloController.ts"), ).toBe(true); expect( fs.wasWrittenMatching( "/project/src/api/controllers/HelloController.ts", /\$action/, ), ).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // React Project Structure (--react flag) // ───────────────────────────────────────────────────────────────────────────── describe("--react flag", () => { it("should create web directory structure", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--react", root: "/project" }); expect(fs.wasWritten("/project/src/web/index.ts")).toBe(true); expect(fs.wasWritten("/project/src/web/AppRouter.ts")).toBe(true); expect(fs.wasWritten("/project/src/web/components/Home.tsx")).toBe(true); }); it("should create main.browser.ts for client-side entry", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--react", root: "/project" }); expect(fs.wasWritten("/project/src/main.browser.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.browser.ts", /WebModule/), ).toBe(true); }); it("should create main.css", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--react", root: "/project" }); expect(fs.wasWritten("/project/src/main.css")).toBe(true); }); it("should not create api structure without --api flag", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--react", root: "/project" }); // React alone should not create API structure expect(fs.wasWritten("/project/src/api/index.ts")).toBe(false); expect( fs.wasWritten("/project/src/api/controllers/HelloController.ts"), ).toBe(false); }); it("should create both api and web with --api --react", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--api --react", root: "/project" }); // Both structures should be created expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true); expect(fs.wasWritten("/project/src/web/index.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.server.ts", /ApiModule/), ).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.server.ts", /WebModule/), ).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Test Setup (always scaffolded — Vitest ships embedded in alepha) // ───────────────────────────────────────────────────────────────────────────── describe("test setup", () => { it("should always create test directory with dummy.spec.ts", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/test/dummy.spec.ts")).toBe(true); expect( fs.wasWrittenMatching( "/project/test/dummy.spec.ts", /describe|test|it/, ), ).toBe(true); }); it("should NOT pin vitest in package.json (embedded in alepha)", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); const pkg = await fs.readJsonFile<{ dependencies?: Record<string, string>; devDependencies?: Record<string, string>; }>("/project/package.json"); // The toolchain (vitest, vite, biome, tsc, drizzle-kit) ships embedded // as dependencies of `alepha` — the project never declares it. expect(pkg.devDependencies?.vitest).toBeUndefined(); expect(pkg.dependencies?.vitest).toBeUndefined(); }); it("should add test script to package.json", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { root: "/project" }); const pkg = await fs.readJsonFile<{ scripts?: Record<string, string> }>( "/project/package.json", ); expect(pkg.scripts?.test).toBe("alepha test"); }); }); // ───────────────────────────────────────────────────────────────────────────── // Tailwind CSS (--tailwind flag) // ───────────────────────────────────────────────────────────────────────────── describe("--tailwind flag", () => { it("should add tailwindcss devDependencies", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--tailwind", root: "/project" }); const pkg = await fs.readJsonFile<{ devDependencies?: Record<string, string>; }>("/project/package.json"); expect(pkg.devDependencies?.tailwindcss).toBeDefined(); expect(pkg.devDependencies?.["@tailwindcss/vite"]).toBeDefined(); }); it("should create vite.config.ts with tailwind plugin", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--tailwind", root: "/project" }); expect(fs.wasWritten("/project/vite.config.ts")).toBe(true); expect( fs.wasWrittenMatching("/project/vite.config.ts", /tailwindcss/), ).toBe(true); expect( fs.wasWrittenMatching("/project/vite.config.ts", /@tailwindcss\/vite/), ).toBe(true); expect( fs.wasWrittenMatching("/project/vite.config.ts", /defineConfig/), ).toBe(true); }); it("should add @import tailwindcss to main.css", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--tailwind", root: "/project" }); expect(fs.wasWritten("/project/src/main.css")).toBe(true); expect( fs.wasWrittenMatching("/project/src/main.css", /@import "tailwindcss"/), ).toBe(true); }); it("should imply --react", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--tailwind", root: "/project" }); // React web structure should be created expect(fs.wasWritten("/project/src/web/index.ts")).toBe(true); expect(fs.wasWritten("/project/src/main.browser.ts")).toBe(true); }); it("should not create vite.config.ts without --tailwind", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--react", root: "/project" }); expect(fs.wasWritten("/project/vite.config.ts")).toBe(false); }); }); // ───────────────────────────────────────────────────────────────────────────── // Non-empty directory guard (codegen flags) // ───────────────────────────────────────────────────────────────────────────── describe("non-empty directory guard", () => { it("should reject codegen into non-empty directory", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/src/existing.ts", "export {}"); await expect( cli.run(cmd.init, { argv: "--api", root: "/project" }), ).rejects.toThrowError(/Target directory is not empty/); }); it("should allow codegen when only package.json exists", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await cli.run(cmd.init, { argv: "--api", root: "/project" }); expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true); }); it("should allow codegen into non-empty directory with --force", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/src/existing.ts", "export {}"); await cli.run(cmd.init, { argv: "--api --force", root: "/project", }); expect(fs.wasWritten("/project/src/api/index.ts")).toBe(true); }); it("should allow non-codegen init in non-empty directory", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/src/existing.ts", "export {}"); // No codegen flags — should not throw await cli.run(cmd.init, { root: "/project" }); expect(fs.wasWritten("/project/tsconfig.json")).toBe(true); }); it("should check each codegen flag independently", async () => { for (const flag of ["--react", "--tailwind"]) { const { fs, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/src/existing.ts", "export {}"); await expect( cli.run(cmd.init, { argv: flag, root: "/project" }), ).rejects.toThrowError(/Target directory is not empty/); } }); }); // ───────────────────────────────────────────────────────────────────────────── // Path Argument // ───────────────────────────────────────────────────────────────────────────── describe("path argument", () => { it("should create project in subdirectory when path provided", async () => { const { fs, cli, cmd, json } = createTestEnv(); await fs.writeFile( "/project/subdir/package.json", json.stringify({ name: "subdir-app" }), ); await cli.run(cmd.init, { argv: "subdir", root: "/project" }); expect(fs.wasWritten("/project/subdir/tsconfig.json")).toBe(true); expect(fs.wasWritten("/project/subdir/biome.json")).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Linter Execution // ───────────────────────────────────────────────────────────────────────────── describe("post-install tasks", () => { it("should run linter after install", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupProject(fs, json); await fs.writeFile("/project/yarn.lock", ""); await cli.run(cmd.init, { root: "/project" }); expect(shell.wasCalled("yarn run lint")).toBe(true); }); }); // ───────────────────────────────────────────────────────────────────────────── // Workspace Package Detection // ───────────────────────────────────────────────────────────────────────────── describe("workspace package detection", () => { const setupWorkspace = async ( fs: MemoryFileSystemProvider, json: Json, pm: "yarn" | "npm" | "pnpm" | "bun" = "yarn", ) => { // Setup workspace root at /workspace await fs.writeFile( "/workspace/package.json", json.stringify({ name: "monorepo", workspaces: ["packages/*"] }), ); await fs.writeFile("/workspace/biome.json", "{}"); await fs.writeFile("/workspace/.editorconfig", "root=true"); await fs.writeFile("/workspace/tsconfig.json", "{}"); // Setup lockfile based on PM const lockfiles: Record<string, string> = { yarn: "yarn.lock", npm: "package-lock.json", pnpm: "pnpm-lock.yaml", bun: "bun.lock", }; await fs.writeFile(`/workspace/${lockfiles[pm]}`, ""); // Setup package inside workspace (2 levels down) await fs.writeFile( "/workspace/packages/my-pkg/package.json", json.stringify({ name: "my-pkg" }), ); }; it("should always create biome.json even when workspace root has it", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(fs.wasWritten("/workspace/packages/my-pkg/biome.json")).toBe(true); }); it("should skip .editorconfig when workspace root has it", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(fs.wasWritten("/workspace/packages/my-pkg/.editorconfig")).toBe( false, ); }); it("should skip tsconfig.json when workspace root has it", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(fs.wasWritten("/workspace/packages/my-pkg/tsconfig.json")).toBe( false, ); }); it("should detect yarn from workspace root lockfile", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json, "yarn"); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(shell.wasCalled("yarn install")).toBe(true); }); it("should detect pnpm from workspace root lockfile", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json, "pnpm"); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(shell.wasCalled("pnpm install")).toBe(true); }); it("should run install from workspace root when in package", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json, "yarn"); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); // Install should be run with workspace root const installCalls = shell.getCallsMatching(/yarn install/); expect(installCalls.length).toBeGreaterThan(0); expect(installCalls[0].options.root).toBe("/workspace"); }); it("should not setup PM files when in workspace package", async () => { const { fs, shell, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json, "yarn"); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); // Should not run yarn set version stable in package expect(shell.wasCalled("yarn set version stable")).toBe(false); }); it("should still create package.json in the package", async () => { const { fs, cli, cmd, json } = createTestEnv(); await setupWorkspace(fs, json); await cli.run(cmd.init, { root: "/workspace/packages/my-pkg" }); expect(fs.wasWritten("/workspace/packages/my-pkg/package.json")).toBe( true, ); }); }); });