mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(cli): add alchemy and improve cli tooling and structure (#520)
This commit is contained in:
@@ -1,13 +1,7 @@
|
||||
import { join } from "node:path";
|
||||
import consola from "consola";
|
||||
import { execa } from "execa";
|
||||
import {
|
||||
ensureDirSync,
|
||||
existsSync,
|
||||
readFileSync,
|
||||
readJsonSync,
|
||||
removeSync,
|
||||
} from "fs-extra";
|
||||
import { ensureDir, existsSync, readFile, readJson, remove } from "fs-extra";
|
||||
import * as JSONC from "jsonc-parser";
|
||||
import { FailedToExitError } from "trpc-cli";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
@@ -17,6 +11,8 @@ async function runCli(argv: string[], cwd: string) {
|
||||
const previous = process.cwd();
|
||||
process.chdir(cwd);
|
||||
try {
|
||||
consola.info(`Running CLI command: bts ${argv.join(" ")}`);
|
||||
|
||||
const cli = createBtsCli();
|
||||
await cli
|
||||
.run({
|
||||
@@ -37,12 +33,12 @@ async function runCli(argv: string[], cwd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function createTmpDir(_prefix: string) {
|
||||
async function createTmpDir(_prefix: string) {
|
||||
const dir = join(__dirname, "..", ".smoke");
|
||||
if (existsSync(dir)) {
|
||||
removeSync(dir);
|
||||
await remove(dir);
|
||||
}
|
||||
ensureDirSync(dir);
|
||||
await ensureDir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -50,6 +46,10 @@ async function runCliExpectingError(args: string[], cwd: string) {
|
||||
const previous = process.cwd();
|
||||
process.chdir(cwd);
|
||||
try {
|
||||
consola.info(
|
||||
`Running CLI command (expecting error): bts ${args.join(" ")}`,
|
||||
);
|
||||
|
||||
const cli = createBtsCli();
|
||||
let threw = false;
|
||||
await cli
|
||||
@@ -72,15 +72,15 @@ async function runCliExpectingError(args: string[], cwd: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertScaffoldedProject(dir: string) {
|
||||
async function assertScaffoldedProject(dir: string) {
|
||||
const pkgJsonPath = join(dir, "package.json");
|
||||
expect(existsSync(pkgJsonPath)).toBe(true);
|
||||
const pkg = readJsonSync(pkgJsonPath);
|
||||
const pkg = await readJson(pkgJsonPath);
|
||||
expect(typeof pkg.name).toBe("string");
|
||||
expect(Array.isArray(pkg.workspaces)).toBe(true);
|
||||
}
|
||||
|
||||
function assertProjectStructure(
|
||||
async function assertProjectStructure(
|
||||
dir: string,
|
||||
options: {
|
||||
hasWeb?: boolean;
|
||||
@@ -107,6 +107,13 @@ function assertProjectStructure(
|
||||
expect(existsSync(join(dir, "package.json"))).toBe(true);
|
||||
expect(existsSync(join(dir, ".gitignore"))).toBe(true);
|
||||
|
||||
try {
|
||||
const pmConfig = (await readBtsConfig(dir)) as { packageManager?: string };
|
||||
if (pmConfig && pmConfig.packageManager === "bun") {
|
||||
expect(existsSync(join(dir, "bunfig.toml"))).toBe(true);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (hasWeb) {
|
||||
expect(existsSync(join(dir, "apps", "web", "package.json"))).toBe(true);
|
||||
const webDir = join(dir, "apps", "web");
|
||||
@@ -132,6 +139,26 @@ function assertProjectStructure(
|
||||
hasAppDir ||
|
||||
hasPublicDir,
|
||||
).toBe(true);
|
||||
|
||||
const bts = (await readBtsConfig(dir)) as {
|
||||
webDeploy?: string;
|
||||
serverDeploy?: string;
|
||||
frontend?: string[];
|
||||
};
|
||||
if (bts.webDeploy === "wrangler") {
|
||||
expect(existsSync(join(dir, "apps", "web", "wrangler.jsonc"))).toBe(true);
|
||||
}
|
||||
|
||||
if (
|
||||
bts.webDeploy === "alchemy" &&
|
||||
bts.serverDeploy !== "alchemy" &&
|
||||
bts.frontend &&
|
||||
bts.frontend.length > 0
|
||||
) {
|
||||
const webRunner = join(dir, "apps", "web", "alchemy.run.ts");
|
||||
consola.info(`Checking Alchemy web runner at: ${webRunner}`);
|
||||
expect(existsSync(webRunner)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasNative) {
|
||||
@@ -154,8 +181,42 @@ function assertProjectStructure(
|
||||
expect(existsSync(join(dir, "apps", "server", "src", "index.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(existsSync(join(dir, "apps", "server", "tsconfig.json"))).toBe(true);
|
||||
|
||||
const bts = (await readBtsConfig(dir)) as {
|
||||
serverDeploy?: string;
|
||||
webDeploy?: string;
|
||||
};
|
||||
if (bts.serverDeploy === "wrangler") {
|
||||
expect(existsSync(join(dir, "apps", "server", "wrangler.jsonc"))).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
if (bts.serverDeploy === "alchemy") {
|
||||
const serverRunner = join(dir, "apps", "server", "alchemy.run.ts");
|
||||
const serverEnv = join(dir, "apps", "server", "env.d.ts");
|
||||
consola.info(`Checking Alchemy server runner at: ${serverRunner}`);
|
||||
consola.info(`Checking Alchemy env types at: ${serverEnv}`);
|
||||
expect(existsSync(serverRunner)).toBe(true);
|
||||
expect(existsSync(serverEnv)).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const btsAll = (await readBtsConfig(dir)) as {
|
||||
serverDeploy?: string;
|
||||
webDeploy?: string;
|
||||
};
|
||||
if (btsAll.serverDeploy === "alchemy" && btsAll.webDeploy === "alchemy") {
|
||||
const rootRunner = join(dir, "alchemy.run.ts");
|
||||
const serverEnv = join(dir, "apps", "server", "env.d.ts");
|
||||
consola.info(`Checking Alchemy root runner at: ${rootRunner}`);
|
||||
consola.info(`Checking Alchemy env types at: ${serverEnv}`);
|
||||
expect(existsSync(rootRunner)).toBe(true);
|
||||
expect(existsSync(serverEnv)).toBe(true);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (hasConvexBackend) {
|
||||
const hasPackagesDir = existsSync(join(dir, "packages"));
|
||||
const hasConvexRelated =
|
||||
@@ -208,11 +269,11 @@ function assertProjectStructure(
|
||||
}
|
||||
|
||||
expect(existsSync(join(dir, "bts.jsonc"))).toBe(true);
|
||||
const btsConfig = readFileSync(join(dir, "bts.jsonc"), "utf8");
|
||||
const btsConfig = await readFile(join(dir, "bts.jsonc"), "utf8");
|
||||
expect(btsConfig).toContain("Better-T-Stack configuration");
|
||||
}
|
||||
|
||||
function assertBtsConfig(
|
||||
async function assertBtsConfig(
|
||||
dir: string,
|
||||
expectedConfig: Partial<{
|
||||
frontend: string[];
|
||||
@@ -225,11 +286,13 @@ function assertBtsConfig(
|
||||
api: string;
|
||||
runtime: string;
|
||||
packageManager: string;
|
||||
webDeploy: string;
|
||||
serverDeploy: string;
|
||||
}>,
|
||||
) {
|
||||
const btsConfigPath = join(dir, "bts.jsonc");
|
||||
expect(existsSync(btsConfigPath)).toBe(true);
|
||||
const content = readFileSync(btsConfigPath, "utf8");
|
||||
const content = await readFile(btsConfigPath, "utf8");
|
||||
|
||||
type BtsConfig = {
|
||||
frontend?: string[];
|
||||
@@ -242,6 +305,8 @@ function assertBtsConfig(
|
||||
api?: string;
|
||||
runtime?: string;
|
||||
packageManager?: string;
|
||||
webDeploy?: string;
|
||||
serverDeploy?: string;
|
||||
};
|
||||
|
||||
const errors: JSONC.ParseError[] = [];
|
||||
@@ -286,13 +351,19 @@ function assertBtsConfig(
|
||||
if (expectedConfig.packageManager) {
|
||||
expect(config.packageManager).toBe(expectedConfig.packageManager);
|
||||
}
|
||||
if (expectedConfig.webDeploy) {
|
||||
expect(config.webDeploy).toBe(expectedConfig.webDeploy);
|
||||
}
|
||||
if (expectedConfig.serverDeploy) {
|
||||
expect(config.serverDeploy).toBe(expectedConfig.serverDeploy);
|
||||
}
|
||||
}
|
||||
|
||||
function readBtsConfig(dir: string) {
|
||||
async function readBtsConfig(dir: string) {
|
||||
const btsConfigPath = join(dir, "bts.jsonc");
|
||||
if (!existsSync(btsConfigPath)) return {} as Record<string, unknown>;
|
||||
|
||||
const content = readFileSync(btsConfigPath, "utf8");
|
||||
const content = await readFile(btsConfigPath, "utf8");
|
||||
const errors: JSONC.ParseError[] = [];
|
||||
const parsed = JSONC.parse(content, errors, {
|
||||
allowTrailingComma: true,
|
||||
@@ -309,7 +380,7 @@ describe("create-better-t-stack smoke", () => {
|
||||
let workdir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
workdir = createTmpDir("cli");
|
||||
workdir = await createTmpDir("cli");
|
||||
consola.start("Building CLI...");
|
||||
const buildProc = execa("bun", ["run", "build"], {
|
||||
cwd: join(__dirname, ".."),
|
||||
@@ -329,7 +400,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
consola.info("Programmatic CLI mode");
|
||||
});
|
||||
|
||||
// Exhaustive matrix: all frontends x standard backends (no db, no orm, no api, no auth)
|
||||
describe("frontend x backend matrix (no db, no api)", () => {
|
||||
const FRONTENDS = [
|
||||
"tanstack-router",
|
||||
@@ -391,15 +461,15 @@ describe("create-better-t-stack smoke", () => {
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
assertScaffoldedProject(projectDir);
|
||||
assertProjectStructure(projectDir, {
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertProjectStructure(projectDir, {
|
||||
hasWeb: WEB_FRONTENDS.has(frontend),
|
||||
hasNative:
|
||||
frontend === "native-nativewind" ||
|
||||
frontend === "native-unistyles",
|
||||
hasServer: true,
|
||||
});
|
||||
assertBtsConfig(projectDir, {
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: [frontend],
|
||||
backend,
|
||||
database: "none",
|
||||
@@ -474,9 +544,9 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
afterAll(() => {
|
||||
afterAll(async () => {
|
||||
try {
|
||||
removeSync(workdir);
|
||||
await remove(workdir);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
@@ -1101,6 +1171,52 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds with PostgreSQL + Drizzle", async () => {
|
||||
const projectName = "app-postgres-drizzle";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"drizzle",
|
||||
"--api",
|
||||
"trpc",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
assertScaffoldedProject(projectDir);
|
||||
assertProjectStructure(projectDir, {
|
||||
hasWeb: true,
|
||||
hasServer: true,
|
||||
hasDatabase: true,
|
||||
});
|
||||
assertBtsConfig(projectDir, {
|
||||
database: "postgres",
|
||||
orm: "drizzle",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds with MongoDB + Mongoose", async () => {
|
||||
const projectName = "app-mongo-mongoose";
|
||||
await runCli(
|
||||
@@ -1457,6 +1573,116 @@ describe("create-better-t-stack smoke", () => {
|
||||
workdir,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects Turso db-setup with non-SQLite database", async () => {
|
||||
await runCliExpectingError(
|
||||
[
|
||||
"invalid-combo",
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"prisma",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"turso",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("YOLO mode", () => {
|
||||
it("bypasses db-setup/database validation (Turso + Postgres + Prisma)", async () => {
|
||||
const projectName = "app-yolo-turso-postgres";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"bun",
|
||||
"--database",
|
||||
"postgres",
|
||||
"--orm",
|
||||
"prisma",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"turso",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
"--yolo",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
database: "postgres",
|
||||
orm: "prisma",
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses web-deploy requires web frontend (none + wrangler)", async () => {
|
||||
const projectName = "app-yolo-webdeploy-no-frontend";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"none",
|
||||
"--backend",
|
||||
"none",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--addons",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
"--yolo",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
backend: "none",
|
||||
webDeploy: "wrangler",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("runtime compatibility", () => {
|
||||
@@ -1500,10 +1726,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
runtime: "workers",
|
||||
orm: "drizzle",
|
||||
});
|
||||
|
||||
expect(
|
||||
existsSync(join(projectDir, "apps", "server", "wrangler.jsonc")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects incompatible runtime and backend combinations", async () => {
|
||||
@@ -1810,7 +2032,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Git and install flag variations
|
||||
it("scaffolds with git enabled", async () => {
|
||||
const projectName = "app-with-git";
|
||||
await runCli(
|
||||
@@ -1855,6 +2076,8 @@ describe("create-better-t-stack smoke", () => {
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--directory-conflict",
|
||||
"overwrite",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
@@ -1887,7 +2110,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
expect(existsSync(join(projectDir, "node_modules"))).toBe(true);
|
||||
});
|
||||
|
||||
// Additional addons beyond turborepo and biome
|
||||
it("scaffolds with PWA addon", async () => {
|
||||
const projectName = "app-addon-pwa";
|
||||
await runCli(
|
||||
@@ -2008,7 +2230,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Authentication combinations
|
||||
it("scaffolds with authentication enabled", async () => {
|
||||
const projectName = "app-with-auth";
|
||||
await runCli(
|
||||
@@ -2055,7 +2276,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// MySQL database
|
||||
it("scaffolds with MySQL + Prisma", async () => {
|
||||
const projectName = "app-mysql-prisma";
|
||||
await runCli(
|
||||
@@ -2138,7 +2358,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// oRPC API with more frontends
|
||||
it("scaffolds oRPC with Next.js", async () => {
|
||||
const projectName = "app-orpc-next";
|
||||
await runCli(
|
||||
@@ -2303,7 +2522,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Backend next combinations
|
||||
it("scaffolds with Next.js backend", async () => {
|
||||
const projectName = "app-backend-next";
|
||||
await runCli(
|
||||
@@ -2345,7 +2563,6 @@ describe("create-better-t-stack smoke", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Node runtime combinations
|
||||
it("scaffolds with Node runtime", async () => {
|
||||
const projectName = "app-node-runtime";
|
||||
await runCli(
|
||||
@@ -2459,14 +2676,16 @@ describe("create-better-t-stack smoke", () => {
|
||||
"app-orpc-solid",
|
||||
"app-backend-next",
|
||||
"app-node-runtime",
|
||||
].forEach((n) => projectNames.add(n));
|
||||
].forEach((n) => {
|
||||
projectNames.add(n);
|
||||
});
|
||||
|
||||
const detectPackageManager = (
|
||||
const detectPackageManager = async (
|
||||
projectDir: string,
|
||||
): "bun" | "pnpm" | "npm" => {
|
||||
): Promise<"bun" | "pnpm" | "npm"> => {
|
||||
const bts = readBtsConfig(projectDir) as { packageManager?: string };
|
||||
const pkgJsonPath = join(projectDir, "package.json");
|
||||
const pkg = existsSync(pkgJsonPath) ? readJsonSync(pkgJsonPath) : {};
|
||||
const pkg = existsSync(pkgJsonPath) ? await readJson(pkgJsonPath) : {};
|
||||
const pkgMgrField =
|
||||
(pkg.packageManager as string | undefined) || bts.packageManager;
|
||||
|
||||
@@ -2531,7 +2750,7 @@ describe("create-better-t-stack smoke", () => {
|
||||
consola.info(`${dirName} not found, skipping`);
|
||||
return;
|
||||
}
|
||||
const pm = detectPackageManager(projectDir);
|
||||
const pm = await detectPackageManager(projectDir);
|
||||
|
||||
consola.info(`Processing ${dirName} (pm=${pm})`);
|
||||
try {
|
||||
@@ -2552,11 +2771,11 @@ describe("create-better-t-stack smoke", () => {
|
||||
}
|
||||
|
||||
const pkgJsonPath = join(projectDir, "package.json");
|
||||
const pkg = readJsonSync(pkgJsonPath);
|
||||
const pkg = await readJson(pkgJsonPath);
|
||||
const scripts = pkg.scripts || {};
|
||||
consola.info(`Scripts: ${Object.keys(scripts).join(", ")}`);
|
||||
|
||||
const bts = readBtsConfig(projectDir) as {
|
||||
const bts = (await readBtsConfig(projectDir)) as {
|
||||
backend?: string;
|
||||
frontend?: string[];
|
||||
};
|
||||
@@ -2603,33 +2822,25 @@ describe("create-better-t-stack smoke", () => {
|
||||
|
||||
if (scripts["check-types"]) {
|
||||
consola.start(`Type checking ${dirName}...`);
|
||||
try {
|
||||
const typeRes = await runScript(
|
||||
pm,
|
||||
projectDir,
|
||||
"check-types",
|
||||
[],
|
||||
120_000,
|
||||
);
|
||||
if (typeRes.exitCode === 0) {
|
||||
consola.success(`${dirName} type check passed`);
|
||||
} else {
|
||||
consola.warn(
|
||||
`${dirName} type check failed (exit code ${typeRes.exitCode}) - likely due to missing generated files`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
consola.warn(
|
||||
`${dirName} type check failed - likely due to missing generated files:`,
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
const typeRes = await runScript(
|
||||
pm,
|
||||
projectDir,
|
||||
"check-types",
|
||||
[],
|
||||
120_000,
|
||||
);
|
||||
expect(typeRes.exitCode).toBe(0);
|
||||
consola.success(`${dirName} type check passed`);
|
||||
}
|
||||
|
||||
if (!scripts.build && !scripts["check-types"]) {
|
||||
consola.info(
|
||||
`No build or check-types script for ${dirName}, skipping`,
|
||||
);
|
||||
} else if (!scripts.build && scripts["check-types"]) {
|
||||
consola.info(
|
||||
`Only check-types script available for ${dirName}, type checking will be performed`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
consola.error(`${dirName} failed`, error);
|
||||
@@ -2639,4 +2850,279 @@ describe("create-better-t-stack smoke", () => {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
describe("deploy combinations", () => {
|
||||
it("scaffolds workers runtime + web deploy wrangler", async () => {
|
||||
const projectName = "app-web-wrangler";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + web deploy alchemy", async () => {
|
||||
const projectName = "app-web-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--web-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy alchemy (server-only)", async () => {
|
||||
const projectName = "app-server-only-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--directory-conflict",
|
||||
"overwrite",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
serverDeploy: "alchemy",
|
||||
});
|
||||
consola.info("Verifying server-only Alchemy artifacts");
|
||||
expect(
|
||||
existsSync(join(projectDir, "apps", "server", "alchemy.run.ts")),
|
||||
).toBe(true);
|
||||
expect(existsSync(join(projectDir, "apps", "server", "env.d.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(existsSync(join(projectDir, "alchemy.run.ts"))).toBe(false);
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy wrangler", async () => {
|
||||
const projectName = "app-server-wrangler";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds workers runtime + server deploy alchemy", async () => {
|
||||
const projectName = "app-server-alchemy";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"hono",
|
||||
"--runtime",
|
||||
"workers",
|
||||
"--server-deploy",
|
||||
"alchemy",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "hono",
|
||||
runtime: "workers",
|
||||
});
|
||||
});
|
||||
|
||||
it("scaffolds web deploy wrangler with backend none (no server deploy)", async () => {
|
||||
const projectName = "app-web-wrangler-only";
|
||||
await runCli(
|
||||
[
|
||||
projectName,
|
||||
"--yes",
|
||||
"--frontend",
|
||||
"tanstack-router",
|
||||
"--backend",
|
||||
"none",
|
||||
"--web-deploy",
|
||||
"wrangler",
|
||||
"--database",
|
||||
"none",
|
||||
"--orm",
|
||||
"none",
|
||||
"--api",
|
||||
"none",
|
||||
"--no-auth",
|
||||
"--addons",
|
||||
"none",
|
||||
"--db-setup",
|
||||
"none",
|
||||
"--examples",
|
||||
"none",
|
||||
"--package-manager",
|
||||
"bun",
|
||||
"--no-install",
|
||||
"--no-git",
|
||||
],
|
||||
workdir,
|
||||
);
|
||||
|
||||
const projectDir = join(workdir, projectName);
|
||||
await assertScaffoldedProject(projectDir);
|
||||
await assertBtsConfig(projectDir, {
|
||||
frontend: ["tanstack-router"],
|
||||
backend: "none",
|
||||
webDeploy: "wrangler",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { join } from "node:path";
|
||||
import { ensureDirSync, existsSync, readFileSync, removeSync } from "fs-extra";
|
||||
import { ensureDir, existsSync, readFile, remove } from "fs-extra";
|
||||
import { parse as parseJsonc } from "jsonc-parser";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { init } from "../src/index";
|
||||
@@ -9,13 +9,13 @@ let testCounter = 0;
|
||||
let tmpDir: string;
|
||||
let originalCwd: string;
|
||||
|
||||
function createTmpDir() {
|
||||
async function createTmpDir() {
|
||||
testCounter++;
|
||||
const dir = join(__dirname, "..", `.prog-test-${testCounter}`);
|
||||
if (existsSync(dir)) {
|
||||
removeSync(dir);
|
||||
await remove(dir);
|
||||
}
|
||||
ensureDirSync(dir);
|
||||
await ensureDir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ function assertProjectExists(dir: string) {
|
||||
expect(existsSync(join(dir, "bts.jsonc"))).toBe(true);
|
||||
}
|
||||
|
||||
function assertBtsConfig(
|
||||
async function assertBtsConfig(
|
||||
dir: string,
|
||||
expectedConfig: Partial<{
|
||||
frontend: string[];
|
||||
@@ -40,7 +40,7 @@ function assertBtsConfig(
|
||||
const configPath = join(dir, "bts.jsonc");
|
||||
expect(existsSync(configPath)).toBe(true);
|
||||
|
||||
const configContent = readFileSync(configPath, "utf-8");
|
||||
const configContent = await readFile(configPath, "utf-8");
|
||||
const config: BetterTStackConfig = parseJsonc(configContent);
|
||||
|
||||
if (expectedConfig.frontend) {
|
||||
@@ -67,16 +67,16 @@ function assertBtsConfig(
|
||||
}
|
||||
|
||||
describe("Programmatic API - Fast Tests", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
originalCwd = process.cwd();
|
||||
tmpDir = createTmpDir();
|
||||
tmpDir = await createTmpDir();
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
process.chdir(originalCwd);
|
||||
if (existsSync(tmpDir)) {
|
||||
removeSync(tmpDir);
|
||||
await remove(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,7 +137,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
frontend: ["next"],
|
||||
});
|
||||
}, 15000);
|
||||
@@ -151,7 +151,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
backend: "fastify",
|
||||
});
|
||||
}, 15000);
|
||||
@@ -166,7 +166,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
database: "postgres",
|
||||
orm: "prisma",
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
api: "orpc",
|
||||
});
|
||||
}, 15000);
|
||||
@@ -195,7 +195,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
runtime: "node",
|
||||
});
|
||||
}, 15000);
|
||||
@@ -209,7 +209,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
addons: ["biome"],
|
||||
});
|
||||
}, 15000);
|
||||
@@ -258,7 +258,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
git: false,
|
||||
yolo: false,
|
||||
}),
|
||||
).rejects.toThrow(/requires Mongoose or Prisma/);
|
||||
).rejects.toThrow(/Drizzle ORM does not support MongoDB/);
|
||||
});
|
||||
|
||||
test("handles auth without database", async () => {
|
||||
@@ -305,7 +305,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
addons: ["biome", "turborepo"],
|
||||
});
|
||||
}, 15000);
|
||||
@@ -321,7 +321,7 @@ describe("Programmatic API - Fast Tests", () => {
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
assertBtsConfig(result.projectDirectory, {
|
||||
await assertBtsConfig(result.projectDirectory, {
|
||||
database: "sqlite",
|
||||
orm: "drizzle",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user