diff --git a/packages/create-gen-app/dev/README.md b/packages/create-gen-app-test/dev/README.md similarity index 97% rename from packages/create-gen-app/dev/README.md rename to packages/create-gen-app-test/dev/README.md index 332f5bcf..cd4b56de 100644 --- a/packages/create-gen-app/dev/README.md +++ b/packages/create-gen-app-test/dev/README.md @@ -4,7 +4,7 @@ This script helps you test `create-gen-app` locally with the pgpm-boilerplates r ## Usage -From the `packages/create-gen-app` directory, run: +From the `packages/create-gen-app-test` directory, run: ```bash pnpm dev [--repo url] [--branch name] [--path dir] [--template name] [--output dir] @@ -73,3 +73,4 @@ Extracting template variables... - The temporary clone directory is automatically cleaned up after generation - You can test different templates without affecting your workspace + diff --git a/packages/create-gen-app/dev/index.ts b/packages/create-gen-app-test/dev/index.ts similarity index 76% rename from packages/create-gen-app/dev/index.ts rename to packages/create-gen-app-test/dev/index.ts index d7d37f8b..d248d146 100644 --- a/packages/create-gen-app/dev/index.ts +++ b/packages/create-gen-app-test/dev/index.ts @@ -3,10 +3,7 @@ import { Inquirerer, ListQuestion } from "inquirerer"; import minimist from "minimist"; import * as path from "path"; -import { cloneRepo } from "../src/clone"; -import { extractVariables } from "../src/extract"; -import { promptUser } from "../src/prompt"; -import { replaceVariables } from "../src/replace"; +import { cloneRepo, extractVariables, promptUser, replaceVariables } from "create-gen-app"; const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates/"; const DEFAULT_DIRECTORY = "."; @@ -40,9 +37,7 @@ async function main() { const templateDir = path.join(tempDir, argv.path); if (!fs.existsSync(templateDir)) { - throw new Error( - `Template path "${argv.path}" does not exist in ${argv.repo}` - ); + throw new Error(`Template path "${argv.path}" does not exist in ${argv.repo}`); } const folders = fs .readdirSync(templateDir, { withFileTypes: true }) @@ -53,9 +48,7 @@ async function main() { throw new Error("No template folders found in repository"); } - console.log( - `\nFound ${folders.length} template(s): ${folders.join(", ")}\n` - ); + console.log(`\nFound ${folders.length} template(s): ${folders.join(", ")}\n`); let selectedFolder = argv.template; if (selectedFolder) { @@ -91,16 +84,10 @@ async function main() { console.log("Extracting template variables..."); const extractedVariables = await extractVariables(selectedTemplateDir); - console.log( - `Found ${extractedVariables.fileReplacers.length} file replacers` - ); - console.log( - `Found ${extractedVariables.contentReplacers.length} content replacers` - ); + console.log(`Found ${extractedVariables.fileReplacers.length} file replacers`); + console.log(`Found ${extractedVariables.contentReplacers.length} content replacers`); if (extractedVariables.projectQuestions) { - console.log( - `Found ${extractedVariables.projectQuestions.questions.length} project questions` - ); + console.log(`Found ${extractedVariables.projectQuestions.questions.length} project questions`); } console.log("\nPrompting for variable values..."); @@ -113,12 +100,7 @@ async function main() { } console.log(`\nGenerating project in ${absoluteOutputDir}...`); - await replaceVariables( - selectedTemplateDir, - absoluteOutputDir, - extractedVariables, - variableAnswers - ); + await replaceVariables(selectedTemplateDir, absoluteOutputDir, extractedVariables, variableAnswers); console.log("\n✅ Project created successfully!"); console.log(`📁 Output directory: ${absoluteOutputDir}\n`); @@ -127,13 +109,11 @@ async function main() { fs.rmSync(tempDir, { recursive: true, force: true }); } } catch (error) { - console.error( - "\n❌ Error:", - error instanceof Error ? error.message : String(error) - ); + console.error("\n❌ Error:", error instanceof Error ? error.message : String(error)); process.exit(1); } } main(); + diff --git a/packages/create-gen-app-test/package.json b/packages/create-gen-app-test/package.json index 8a1f46e8..82203e08 100644 --- a/packages/create-gen-app-test/package.json +++ b/packages/create-gen-app-test/package.json @@ -25,15 +25,19 @@ "clean": "makage clean", "prepublishOnly": "npm run build", "build": "npm run clean && makage build-ts && npm run copy", + "dev": "node -r ts-node/register dev/index.ts", "test": "jest", "test:watch": "jest --watch" }, "dependencies": { "appstash": "workspace:*", - "create-gen-app": "workspace:*" + "create-gen-app": "workspace:*", + "inquirerer": "workspace:*", + "minimist": "^1.2.8" }, "devDependencies": { + "@types/minimist": "^1.2.5", "makage": "0.1.5" }, "keywords": [] -} +} \ No newline at end of file diff --git a/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap b/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap index e15a46c5..668769cb 100644 --- a/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap +++ b/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap @@ -33,30 +33,30 @@ exports[`cached template integration tests first clone with variable replacement exports[`cached template integration tests first clone with variable replacement should snapshot package.json files if they exist 1`] = ` { "module/package.json": { - "author": "__USERFULLNAME__ <__USEREMAIL__>", + "author": "____fullName____ <____email____>", "bugs": { - "url": "https://github.com/__USERNAME__/__REPONAME__/issues", + "url": "https://github.com/____username____/____repoName____/issues", }, - "description": "__MODULEDESC__", + "description": "____moduleDesc____", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/__USERNAME__/__REPONAME__", + "homepage": "https://github.com/____username____/____repoName____", "keywords": [], - "license": "SEE LICENSE IN LICENSE", - "name": "__PACKAGE_IDENTIFIER__", + "license": "____license____", + "name": "____packageIdentifier____", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "__ACCESS__", + "access": "____access____", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/__USERNAME__/__REPONAME__", + "url": "https://github.com/____username____/____repoName____", }, "scripts": { "lint": "eslint . --fix", @@ -66,7 +66,7 @@ exports[`cached template integration tests first clone with variable replacement "version": "0.0.1", }, "workspace/package.json": { - "author": "__USERFULLNAME__ <__USEREMAIL__>", + "author": "____fullName____ <____email____>", "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^22.10.2", @@ -85,7 +85,7 @@ exports[`cached template integration tests first clone with variable replacement "typescript": "^5.6.3", }, "license": "SEE LICENSE IN LICENSE", - "name": "__MODULENAME__", + "name": "____moduleName____", "pnpm": { "overrides": { "graphql": "14.7.0", @@ -97,7 +97,7 @@ exports[`cached template integration tests first clone with variable replacement }, "repository": { "type": "git", - "url": "https://github.com/__USERNAME__/__MODULENAME__", + "url": "https://github.com/____username____/____moduleName____", }, "scripts": { "lint": "pnpm -r run lint", @@ -143,30 +143,30 @@ exports[`cached template integration tests second clone from cache should snapsh exports[`cached template integration tests second clone from cache should snapshot package.json files from cached template 1`] = ` { "module/package.json": { - "author": "__USERFULLNAME__ <__USEREMAIL__>", + "author": "____fullName____ <____email____>", "bugs": { - "url": "https://github.com/__USERNAME__/__REPONAME__/issues", + "url": "https://github.com/____username____/____repoName____/issues", }, - "description": "__MODULEDESC__", + "description": "____moduleDesc____", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/__USERNAME__/__REPONAME__", + "homepage": "https://github.com/____username____/____repoName____", "keywords": [], - "license": "SEE LICENSE IN LICENSE", - "name": "__PACKAGE_IDENTIFIER__", + "license": "____license____", + "name": "____packageIdentifier____", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "__ACCESS__", + "access": "____access____", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/__USERNAME__/__REPONAME__", + "url": "https://github.com/____username____/____repoName____", }, "scripts": { "lint": "eslint . --fix", @@ -176,7 +176,7 @@ exports[`cached template integration tests second clone from cache should snapsh "version": "0.0.1", }, "workspace/package.json": { - "author": "__USERFULLNAME__ <__USEREMAIL__>", + "author": "____fullName____ <____email____>", "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^22.10.2", @@ -195,7 +195,7 @@ exports[`cached template integration tests second clone from cache should snapsh "typescript": "^5.6.3", }, "license": "SEE LICENSE IN LICENSE", - "name": "__MODULENAME__", + "name": "____moduleName____", "pnpm": { "overrides": { "graphql": "14.7.0", @@ -207,7 +207,7 @@ exports[`cached template integration tests second clone from cache should snapsh }, "repository": { "type": "git", - "url": "https://github.com/__USERNAME__/__MODULENAME__", + "url": "https://github.com/____username____/____moduleName____", }, "scripts": { "lint": "pnpm -r run lint", diff --git a/packages/create-gen-app/__tests__/cli.test.ts b/packages/create-gen-app-test/src/__tests__/cli.test.ts similarity index 85% rename from packages/create-gen-app/__tests__/cli.test.ts rename to packages/create-gen-app-test/src/__tests__/cli.test.ts index 511358af..0464e0cc 100644 --- a/packages/create-gen-app/__tests__/cli.test.ts +++ b/packages/create-gen-app-test/src/__tests__/cli.test.ts @@ -1,7 +1,7 @@ import * as fs from "fs"; import * as path from "path"; -import { runCli } from "../src/cli"; +import { runCli } from "../cli"; import { TEST_BRANCH, TEST_REPO, @@ -13,7 +13,7 @@ import { jest.setTimeout(180_000); -describe("CLI integration (GitHub templates)", () => { +describe("CLI integration via create-gen-app-test harness", () => { it("generates a project using the real repo", async () => { const workspace = createTempWorkspace("cli"); const answers = buildAnswers("cli"); @@ -65,12 +65,15 @@ describe("CLI integration (GitHub templates)", () => { }); it("prints version and exits when --version is provided", async () => { - const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined); + const logSpy = jest + .spyOn(console, "log") + .mockImplementation(() => undefined); await runCli(["--version"]); - expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/create-gen-app v/)); + expect(logSpy).toHaveBeenCalledWith( + expect.stringMatching(/create-gen-app v/) + ); logSpy.mockRestore(); }); }); - diff --git a/packages/create-gen-app/src/cli.ts b/packages/create-gen-app-test/src/cli.ts similarity index 84% rename from packages/create-gen-app/src/cli.ts rename to packages/create-gen-app-test/src/cli.ts index eadcc584..6b309d9f 100644 --- a/packages/create-gen-app/src/cli.ts +++ b/packages/create-gen-app-test/src/cli.ts @@ -6,15 +6,15 @@ import * as path from "path"; import { Inquirerer, ListQuestion } from "inquirerer"; import minimist, { ParsedArgs } from "minimist"; -import { cloneRepo } from "./clone"; -import { createGen } from "./index"; -import packageJson from "../package.json"; +import { cloneRepo, createGen } from "create-gen-app"; +import createGenPackageJson from "create-gen-app/package.json"; const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates.git"; const DEFAULT_PATH = "."; const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output"; -const PACKAGE_VERSION = packageJson.version ?? "0.0.0"; +const PACKAGE_VERSION = + (createGenPackageJson as { version?: string }).version ?? "0.0.0"; const RESERVED_ARG_KEYS = new Set([ "_", @@ -43,7 +43,9 @@ export interface CliResult { template: string; } -export async function runCli(rawArgv: string[] = process.argv.slice(2)): Promise { +export async function runCli( + rawArgv: string[] = process.argv.slice(2) +): Promise { const args = minimist(rawArgv, { alias: { r: "repo", @@ -87,8 +89,13 @@ export async function runCli(rawArgv: string[] = process.argv.slice(2)): Promise tempDir = await cloneRepo(args.repo, { branch: args.branch }); const selectionRoot = path.join(tempDir, args.path); - if (!fs.existsSync(selectionRoot) || !fs.statSync(selectionRoot).isDirectory()) { - throw new Error(`Template path "${args.path}" does not exist in ${args.repo}`); + if ( + !fs.existsSync(selectionRoot) || + !fs.statSync(selectionRoot).isDirectory() + ) { + throw new Error( + `Template path "${args.path}" does not exist in ${args.repo}` + ); } const templates = fs @@ -134,7 +141,9 @@ export async function runCli(rawArgv: string[] = process.argv.slice(2)): Promise ensureOutputDir(outputDir, Boolean(args.force)); const answerOverrides = extractAnswerOverrides(args); - const noTty = Boolean(args["no-tty"] ?? (args as Record).noTty); + const noTty = Boolean( + args["no-tty"] ?? (args as Record).noTty + ); await createGen({ templateUrl: args.repo, @@ -156,11 +165,10 @@ export async function runCli(rawArgv: string[] = process.argv.slice(2)): Promise function printHelp(): void { console.log(` -create-gen-app CLI +create-gen-app CLI (test harness) Usage: - create-gen-app [options] [outputDir] - cga [options] [outputDir] + node cli [options] [outputDir] Options: -r, --repo Git repository to clone (default: ${DEFAULT_REPO}) @@ -174,7 +182,7 @@ Options: -h, --help Show this help message You can also pass variable overrides, e.g.: - create-gen-app --template module --PROJECT_NAME my-app + node cli --template module --PROJECT_NAME my-app `); } @@ -193,7 +201,9 @@ async function promptForTemplate(templates: string[]): Promise { }; try { - const answers = (await prompter.prompt({}, [question])) as { template: string }; + const answers = (await prompter.prompt({}, [question])) as { + template: string; + }; return answers.template; } finally { if (typeof (prompter as any).close === "function") { @@ -202,8 +212,13 @@ async function promptForTemplate(templates: string[]): Promise { } } -function resolveOutputDir(outputArg: string | undefined, template?: string): string { - const base = outputArg ?? (template ? path.join(process.cwd(), template) : DEFAULT_OUTPUT_FALLBACK); +function resolveOutputDir( + outputArg: string | undefined, + template?: string +): string { + const base = + outputArg ?? + (template ? path.join(process.cwd(), template) : DEFAULT_OUTPUT_FALLBACK); return path.resolve(base); } @@ -238,4 +253,3 @@ if (require.main === module) { process.exitCode = 1; }); } - diff --git a/packages/create-gen-app-test/src/test-utils/integration-helpers.ts b/packages/create-gen-app-test/src/test-utils/integration-helpers.ts new file mode 100644 index 00000000..596f1e41 --- /dev/null +++ b/packages/create-gen-app-test/src/test-utils/integration-helpers.ts @@ -0,0 +1,47 @@ +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +export const TEST_REPO = + process.env.CREATE_GEN_TEST_REPO ?? + "https://github.com/launchql/pgpm-boilerplates.git"; +export const TEST_BRANCH = + process.env.CREATE_GEN_TEST_BRANCH ?? "license"; +export const TEST_TEMPLATE = + process.env.CREATE_GEN_TEST_TEMPLATE ?? "module"; + +export interface TempWorkspace { + baseDir: string; + outputDir: string; +} + +export function createTempWorkspace(prefix: string): TempWorkspace { + const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), `create-gen-${prefix}-`)); + const outputDir = path.join(baseDir, "output"); + return { baseDir, outputDir }; +} + +export function cleanupWorkspace(workspace: TempWorkspace): void { + fs.rmSync(workspace.baseDir, { recursive: true, force: true }); +} + +export function buildAnswers( + suffix: string, + overrides: Partial> = {} +): Record { + const safeSuffix = suffix.replace(/[^a-z0-9]/gi, "-").toLowerCase(); + return { + USERFULLNAME: `Test User ${suffix}`, + USEREMAIL: `tester-${safeSuffix}@example.com`, + MODULENAME: `Test Module ${suffix}`, + MODULEDESC: `Integration test module ${suffix}`, + REPONAME: `integration-${safeSuffix}`, + USERNAME: `tester-${safeSuffix}`, + ACCESS: "public", + LICENSE: "MIT", + PACKAGE_IDENTIFIER: `integration-${safeSuffix}`, + ...overrides, + }; +} + + diff --git a/packages/create-gen-app/README.md b/packages/create-gen-app/README.md index 73843771..751e06f8 100644 --- a/packages/create-gen-app/README.md +++ b/packages/create-gen-app/README.md @@ -16,52 +16,31 @@

-A TypeScript-first CLI/library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults. +A TypeScript-first library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults. ## Features - Clone any Git repo (or GitHub `org/repo` shorthand) and optionally select a branch + subdirectory - Extract template variables from filenames and file contents using the safer `____variable____` convention - Merge auto-discovered variables with `.questions.{json,js}` (questions win, including `ignore` patterns) -- Interactive prompts powered by `inquirerer`, with CLI flag overrides (`--VAR value`) and non-TTY mode for CI -- Built-in CLI (`create-gen-app` / `cga`) that discovers templates, prompts once, and writes output safely +- Interactive prompts powered by `inquirerer`, with flexible override mapping (`argv` support) and non-TTY mode for CI - License scaffolding: choose from MIT, Apache-2.0, ISC, GPL-3.0, BSD-3-Clause, Unlicense, or MPL-2.0 and generate a populated `LICENSE` +- Built-in template caching powered by `appstash`, so repeat runs skip `git clone` (configurable via `cache` options) ## Installation ```bash npm install create-gen-app -# or for CLI only -npm install -g create-gen-app ``` -## CLI Usage - -```bash -# interactively pick a template from launchql/pgpm-boilerplates -create-gen-app --output ./workspace - -# short alias -cga --template module --branch main --output ./module \ - --USERFULLNAME "Jane Dev" --USEREMAIL jane@example.com - -# point to a different repo/branch/path -cga --repo github:my-org/my-templates --branch release \ - --path ./templates --template api --output ./api -``` - -Key flags: - -- `--repo`, `--branch`, `--path` – choose the Git repo, branch/tag, and subdirectory that contains templates -- `--template` – folder inside `--path` (auto-prompted if omitted) -- `--output` – destination directory (defaults to `./