Files
create-better-t-stack/apps/cli/test/test-utils.ts
2025-09-14 11:29:01 +05:30

246 lines
7.2 KiB
TypeScript

import { rm } from "node:fs/promises";
import { join } from "node:path";
import { ensureDir } from "fs-extra";
import { trpcServer } from "trpc-cli";
import { expect } from "vitest";
import { router } from "../src/index";
import type { CreateInput, InitResult } from "../src/types";
import {
AddonsSchema,
APISchema,
AuthSchema,
BackendSchema,
DatabaseSchema,
DatabaseSetupSchema,
ExamplesSchema,
FrontendSchema,
ORMSchema,
PackageManagerSchema,
RuntimeSchema,
ServerDeploySchema,
WebDeploySchema,
} from "../src/types";
// Create tRPC caller for direct function calls instead of subprocess
const t = trpcServer.initTRPC.create();
const defaultContext = {};
/**
* Clean up the entire .smoke directory
*/
export async function cleanupSmokeDirectory() {
const smokeDir = join(process.cwd(), ".smoke");
try {
await rm(smokeDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
}
export interface TestResult {
success: boolean;
result?: InitResult;
error?: string;
projectDir?: string;
}
export interface TestConfig extends CreateInput {
projectName?: string;
expectError?: boolean;
expectedErrorMessage?: string;
}
/**
* Run tRPC test using direct function calls instead of subprocess
* This delegates all validation to the CLI's existing logic - much simpler!
*/
export async function runTRPCTest(config: TestConfig): Promise<TestResult> {
const smokeDir = join(process.cwd(), ".smoke");
await ensureDir(smokeDir);
// Store original environment
const originalProgrammatic = process.env.BTS_PROGRAMMATIC;
try {
// Set programmatic mode to ensure errors are thrown instead of process.exit
process.env.BTS_PROGRAMMATIC = "1";
const caller = t.createCallerFactory(router)(defaultContext);
const projectName = config.projectName || "default-app";
const projectPath = join(smokeDir, projectName);
// Determine if we should use --yes or not
// Only core stack flags conflict with --yes flag (from CLI error message)
const coreStackFlags: (keyof TestConfig)[] = [
"database",
"orm",
"backend",
"runtime",
"frontend",
"addons",
"examples",
"auth",
"dbSetup",
"api",
"webDeploy",
"serverDeploy",
];
const hasSpecificCoreConfig = coreStackFlags.some(
(flag) => config[flag] !== undefined,
);
// Only use --yes if no core stack flags are provided and not explicitly disabled
const willUseYesFlag =
config.yes !== undefined ? config.yes : !hasSpecificCoreConfig;
// Provide defaults for missing core stack options to avoid prompts
// But don't provide core stack defaults when yes: true is explicitly set
const coreStackDefaults = willUseYesFlag
? {}
: {
frontend: ["tanstack-router"] as Frontend[],
backend: "hono" as Backend,
runtime: "bun" as Runtime,
api: "trpc" as API,
database: "sqlite" as Database,
orm: "drizzle" as ORM,
auth: "none" as Auth,
addons: ["none"] as Addons[],
examples: ["none"] as Examples[],
dbSetup: "none" as DatabaseSetup,
webDeploy: "none" as WebDeploy,
serverDeploy: "none" as ServerDeploy,
};
// Build options object - let the CLI handle all validation
const options: CreateInput = {
renderTitle: false,
install: config.install ?? false,
git: config.git ?? true,
packageManager: config.packageManager ?? "bun",
directoryConflict: "overwrite",
verbose: true, // Need verbose to get the result
disableAnalytics: true,
yes: willUseYesFlag,
...coreStackDefaults,
...config,
};
// Remove our test-specific properties
const {
projectName: _,
expectError: __,
expectedErrorMessage: ___,
...cleanOptions
} = options as TestConfig;
const result = await caller.init([projectPath, cleanOptions]);
return {
success: result?.success ?? false,
result: result?.success ? result : undefined,
error: result?.success ? undefined : result?.error,
projectDir: result?.success ? result?.projectDirectory : undefined,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
} finally {
// Always restore original environment
if (originalProgrammatic === undefined) {
delete process.env.BTS_PROGRAMMATIC;
} else {
process.env.BTS_PROGRAMMATIC = originalProgrammatic;
}
}
}
export function expectSuccess(result: TestResult) {
if (!result.success) {
console.error("Test failed:");
console.error("Error:", result.error);
if (result.result) {
console.error("Result:", result.result);
}
}
expect(result.success).toBe(true);
expect(result.result).toBeDefined();
}
export function expectError(result: TestResult, expectedMessage?: string) {
expect(result.success).toBe(false);
if (expectedMessage) {
expect(result.error).toContain(expectedMessage);
}
}
// Helper function to create properly typed test configs
export function createTestConfig(
config: Partial<TestConfig> & { projectName: string },
): TestConfig {
return config as TestConfig;
}
/**
* Extract enum values from a Zod enum schema
*/
function extractEnumValues<T extends string>(schema: {
options: readonly T[];
}): readonly T[] {
return schema.options;
}
// Inferred types and values from Zod schemas - no duplication with types.ts!
export type PackageManager = (typeof PackageManagerSchema)["options"][number];
export type Database = (typeof DatabaseSchema)["options"][number];
export type ORM = (typeof ORMSchema)["options"][number];
export type Backend = (typeof BackendSchema)["options"][number];
export type Runtime = (typeof RuntimeSchema)["options"][number];
export type Frontend = (typeof FrontendSchema)["options"][number];
export type Addons = (typeof AddonsSchema)["options"][number];
export type Examples = (typeof ExamplesSchema)["options"][number];
export type Auth = (typeof AuthSchema)["options"][number];
export type API = (typeof APISchema)["options"][number];
export type WebDeploy = (typeof WebDeploySchema)["options"][number];
export type ServerDeploy = (typeof ServerDeploySchema)["options"][number];
export type DatabaseSetup = (typeof DatabaseSetupSchema)["options"][number];
// Test data generators inferred from Zod schemas
export const PACKAGE_MANAGERS = extractEnumValues(PackageManagerSchema);
export const DATABASES = extractEnumValues(DatabaseSchema);
export const ORMS = extractEnumValues(ORMSchema);
export const BACKENDS = extractEnumValues(BackendSchema);
export const RUNTIMES = extractEnumValues(RuntimeSchema);
export const FRONTENDS = extractEnumValues(FrontendSchema);
export const ADDONS = extractEnumValues(AddonsSchema);
export const EXAMPLES = extractEnumValues(ExamplesSchema);
export const AUTH_PROVIDERS = extractEnumValues(AuthSchema);
export const API_TYPES = extractEnumValues(APISchema);
export const WEB_DEPLOYS = extractEnumValues(WebDeploySchema);
export const SERVER_DEPLOYS = extractEnumValues(ServerDeploySchema);
export const DB_SETUPS = extractEnumValues(DatabaseSetupSchema);
// Convenience functions for common test patterns
export function createBasicConfig(
overrides: Partial<TestConfig> = {},
): TestConfig {
return {
projectName: "test-app",
yes: true, // Use defaults
install: false,
git: true,
...overrides,
};
}
export function createCustomConfig(config: Partial<TestConfig>): TestConfig {
return {
projectName: "test-app",
install: false,
git: true,
...config,
};
}