From b21983904bde8c09d0358e4fb3dddde7308af099 Mon Sep 17 00:00:00 2001 From: Eason Date: Wed, 26 Nov 2025 17:03:10 +0800 Subject: [PATCH 1/3] wip: refactor: restructure template processing, git, and cache modules into dedicated subdirectories and classes. --- .../cached-template.test.ts.snap | 182 ++--------- .../src/__tests__/cached-template.test.ts | 79 +++-- packages/create-gen-app-test/src/cli.ts | 97 +++++- packages/create-gen-app-test/src/index.ts | 106 +++++-- .../create-gen-app/__tests__/cache.test.ts | 150 --------- .../__tests__/create-gen-flow.test.ts | 57 ---- .../__tests__/create-gen.test.ts | 29 +- .../__tests__/template-cache.test.ts | 270 ---------------- .../__tests__/ttl-expiration.test.ts | 59 ++++ .../create-gen-app-test/APACHE-2.0.txt | 18 -- .../create-gen-app-test/BSD-3-CLAUSE.txt | 28 -- .../create-gen-app-test/GPL-3.0.txt | 18 -- .../create-gen-app-test/ISC.txt | 16 - .../create-gen-app-test/LICENSE | 21 -- .../create-gen-app-test/MIT.txt | 22 -- .../create-gen-app-test/MPL-2.0.txt | 8 - .../create-gen-app-test/README.md | 143 --------- .../create-gen-app-test/UNLICENSE.txt | 22 -- .../create-gen-app-test/package.json | 39 --- packages/create-gen-app/src/cache.ts | 62 ---- .../create-gen-app/src/cache/cache-manager.ts | 214 +++++++++++++ packages/create-gen-app/src/cache/types.ts | 18 ++ packages/create-gen-app/src/clone.ts | 65 ---- packages/create-gen-app/src/git/git-cloner.ts | 119 +++++++ packages/create-gen-app/src/git/types.ts | 17 + packages/create-gen-app/src/index.ts | 102 ++---- packages/create-gen-app/src/template-cache.ts | 292 ------------------ .../src/{ => template}/extract.ts | 2 +- .../src/{ => template}/prompt.ts | 2 +- .../src/{ => template}/replace.ts | 6 +- .../src/template/templatizer.ts | 102 ++++++ packages/create-gen-app/src/template/types.ts | 13 + .../src/utils/npm-version-check.ts | 67 ++++ packages/create-gen-app/src/utils/types.ts | 6 + 34 files changed, 893 insertions(+), 1558 deletions(-) delete mode 100644 packages/create-gen-app/__tests__/cache.test.ts delete mode 100644 packages/create-gen-app/__tests__/create-gen-flow.test.ts delete mode 100644 packages/create-gen-app/__tests__/template-cache.test.ts create mode 100644 packages/create-gen-app/__tests__/ttl-expiration.test.ts delete mode 100644 packages/create-gen-app/create-gen-app-test/APACHE-2.0.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/BSD-3-CLAUSE.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/GPL-3.0.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/ISC.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/LICENSE delete mode 100644 packages/create-gen-app/create-gen-app-test/MIT.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/MPL-2.0.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/README.md delete mode 100644 packages/create-gen-app/create-gen-app-test/UNLICENSE.txt delete mode 100644 packages/create-gen-app/create-gen-app-test/package.json delete mode 100644 packages/create-gen-app/src/cache.ts create mode 100644 packages/create-gen-app/src/cache/cache-manager.ts create mode 100644 packages/create-gen-app/src/cache/types.ts delete mode 100644 packages/create-gen-app/src/clone.ts create mode 100644 packages/create-gen-app/src/git/git-cloner.ts create mode 100644 packages/create-gen-app/src/git/types.ts delete mode 100644 packages/create-gen-app/src/template-cache.ts rename packages/create-gen-app/src/{ => template}/extract.ts (99%) rename packages/create-gen-app/src/{ => template}/prompt.ts (99%) rename packages/create-gen-app/src/{ => template}/replace.ts (97%) create mode 100644 packages/create-gen-app/src/template/templatizer.ts create mode 100644 packages/create-gen-app/src/template/types.ts create mode 100644 packages/create-gen-app/src/utils/npm-version-check.ts create mode 100644 packages/create-gen-app/src/utils/types.ts 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 668769c..bd4cf99 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 @@ -2,61 +2,42 @@ exports[`cached template integration tests first clone with variable replacement should snapshot created directory structure 1`] = ` [ - "module/", - "module/README.md", - "module/__tests__/", - "module/__tests__/basic.test.ts", - "module/jest.config.js", - "module/package.json", - "workspace/", - "workspace/.eslintrc.json", - "workspace/.github/", - "workspace/.github/run-tests.yaml", - "workspace/.gitignore", - "workspace/.prettierrc.json", - "workspace/.vscode/", - "workspace/.vscode/settings.json", - "workspace/LICENSE", - "workspace/Makefile", - "workspace/README.md", - "workspace/bin/", - "workspace/bin/install.sh", - "workspace/docker-compose.yml", - "workspace/launchql.json", - "workspace/lerna.json", - "workspace/package.json", - "workspace/pnpm-workspace.yaml", - "workspace/tsconfig.json", + "LICENSE", + "README.md", + "__tests__/", + "__tests__/basic.test.ts", + "jest.config.js", + "package.json", ] `; exports[`cached template integration tests first clone with variable replacement should snapshot package.json files if they exist 1`] = ` { - "module/package.json": { - "author": "____fullName____ <____email____>", + "package.json": { + "author": "Test User test ", "bugs": { - "url": "https://github.com/____username____/____repoName____/issues", + "url": "https://github.com/Test User test/Test User test/issues", }, - "description": "____moduleDesc____", + "description": "Test Module test", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/____username____/____repoName____", + "homepage": "https://github.com/Test User test/Test User test", "keywords": [], - "license": "____license____", - "name": "____packageIdentifier____", + "license": "MIT", + "name": "integration-test", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "____access____", + "access": "public", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/____username____/____repoName____", + "url": "https://github.com/Test User test/Test User test", }, "scripts": { "lint": "eslint . --fix", @@ -65,108 +46,47 @@ exports[`cached template integration tests first clone with variable replacement }, "version": "0.0.1", }, - "workspace/package.json": { - "author": "____fullName____ <____email____>", - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "eslint": "^9.13.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unused-imports": "^4.3.0", - "jest": "^29.7.0", - "lerna": "^8.2.4", - "pgsql-test": "^2.13.2", - "prettier": "^3.3.3", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - }, - "license": "SEE LICENSE IN LICENSE", - "name": "____moduleName____", - "pnpm": { - "overrides": { - "graphql": "14.7.0", - }, - }, - "private": true, - "publishConfig": { - "access": "restricted", - }, - "repository": { - "type": "git", - "url": "https://github.com/____username____/____moduleName____", - }, - "scripts": { - "lint": "pnpm -r run lint", - }, - "version": "0.0.1", - "workspaces": [ - "packages/*", - ], - }, } `; exports[`cached template integration tests second clone from cache should snapshot created directory structure from cache 1`] = ` [ - "module/", - "module/README.md", - "module/__tests__/", - "module/__tests__/basic.test.ts", - "module/jest.config.js", - "module/package.json", - "workspace/", - "workspace/.eslintrc.json", - "workspace/.github/", - "workspace/.github/run-tests.yaml", - "workspace/.gitignore", - "workspace/.prettierrc.json", - "workspace/.vscode/", - "workspace/.vscode/settings.json", - "workspace/LICENSE", - "workspace/Makefile", - "workspace/README.md", - "workspace/bin/", - "workspace/bin/install.sh", - "workspace/docker-compose.yml", - "workspace/launchql.json", - "workspace/lerna.json", - "workspace/package.json", - "workspace/pnpm-workspace.yaml", - "workspace/tsconfig.json", + "LICENSE", + "README.md", + "__tests__/", + "__tests__/basic.test.ts", + "jest.config.js", + "package.json", ] `; exports[`cached template integration tests second clone from cache should snapshot package.json files from cached template 1`] = ` { - "module/package.json": { - "author": "____fullName____ <____email____>", + "package.json": { + "author": "Test User cached ", "bugs": { - "url": "https://github.com/____username____/____repoName____/issues", + "url": "https://github.com/Test User cached/Test User cached/issues", }, - "description": "____moduleDesc____", + "description": "Test Module cached", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/____username____/____repoName____", + "homepage": "https://github.com/Test User cached/Test User cached", "keywords": [], - "license": "____license____", - "name": "____packageIdentifier____", + "license": "MIT", + "name": "integration-cached", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "____access____", + "access": "public", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/____username____/____repoName____", + "url": "https://github.com/Test User cached/Test User cached", }, "scripts": { "lint": "eslint . --fix", @@ -175,47 +95,5 @@ exports[`cached template integration tests second clone from cache should snapsh }, "version": "0.0.1", }, - "workspace/package.json": { - "author": "____fullName____ <____email____>", - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "eslint": "^9.13.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unused-imports": "^4.3.0", - "jest": "^29.7.0", - "lerna": "^8.2.4", - "pgsql-test": "^2.13.2", - "prettier": "^3.3.3", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - }, - "license": "SEE LICENSE IN LICENSE", - "name": "____moduleName____", - "pnpm": { - "overrides": { - "graphql": "14.7.0", - }, - }, - "private": true, - "publishConfig": { - "access": "restricted", - }, - "repository": { - "type": "git", - "url": "https://github.com/____username____/____moduleName____", - }, - "scripts": { - "lint": "pnpm -r run lint", - }, - "version": "0.0.1", - "workspaces": [ - "packages/*", - ], - }, } `; diff --git a/packages/create-gen-app-test/src/__tests__/cached-template.test.ts b/packages/create-gen-app-test/src/__tests__/cached-template.test.ts index 0eed514..d1ca1b0 100644 --- a/packages/create-gen-app-test/src/__tests__/cached-template.test.ts +++ b/packages/create-gen-app-test/src/__tests__/cached-template.test.ts @@ -4,9 +4,10 @@ import * as path from 'path'; import { appstash, resolve } from 'appstash'; -import { createFromCachedTemplate, TemplateCache } from '../index'; +import { createFromTemplate, CacheManager, GitCloner } from '../index'; +import { buildAnswers, TEST_REPO, TEST_TEMPLATE } from '../test-utils/integration-helpers'; -const DEFAULT_TEMPLATE_URL = 'https://github.com/launchql/pgpm-boilerplates'; +const DEFAULT_TEMPLATE_URL = TEST_REPO; describe('cached template integration tests', () => { let testOutputDir: string; @@ -35,25 +36,36 @@ describe('cached template integration tests', () => { describe('cache functionality', () => { let sharedCachePath: string; - let templateCache: TemplateCache; + let cacheManager: CacheManager; beforeAll(() => { - templateCache = new TemplateCache({ - enabled: true, + cacheManager = new CacheManager({ toolName: testCacheTool, }); }); it('should return null when cache does not exist for new URL', () => { const nonExistentUrl = 'https://github.com/nonexistent/repo-test-123456'; - const cachedRepo = templateCache.get(nonExistentUrl); + const cacheKey = cacheManager.createKey(nonExistentUrl); + const cachedRepo = cacheManager.get(cacheKey); expect(cachedRepo).toBeNull(); }); it('should clone repository to cache', () => { - const cachePath = templateCache.set(DEFAULT_TEMPLATE_URL); + // Use GitCloner + CacheManager to clone and cache + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + + // Clone to cache directory + const cachePath = path.join(cacheManager['reposDir'], cacheKey); + gitCloner.clone(normalizedUrl, cachePath, { depth: 1 }); + + // Register in cache manager + cacheManager.set(cacheKey, cachePath); sharedCachePath = cachePath; + // Verify cache was created correctly expect(fs.existsSync(cachePath)).toBe(true); expect(fs.existsSync(path.join(cachePath, '.git'))).toBe(false); @@ -62,7 +74,11 @@ describe('cached template integration tests', () => { }, 60000); it('should retrieve cached repository', () => { - const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL); + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + const cachedRepo = cacheManager.get(cacheKey); + expect(cachedRepo).not.toBeNull(); expect(cachedRepo).toBe(sharedCachePath); expect(fs.existsSync(cachedRepo!)).toBe(true); @@ -76,16 +92,13 @@ describe('cached template integration tests', () => { beforeAll(async () => { firstOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'first-clone-')); - firstCloneResult = await createFromCachedTemplate({ + firstCloneResult = await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: firstOutputDir, - answers: { - PROJECT_NAME: 'test-project', - AUTHOR: 'Test Author', - DESCRIPTION: 'A test project', - MODULE_NAME: 'testmodule' - }, - cacheTool: testCacheTool + answers: buildAnswers('test'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); }, 60000); @@ -128,11 +141,15 @@ describe('cached template integration tests', () => { }); it('should verify template cache was created', () => { - const templateCache = new TemplateCache({ - enabled: true, + // Verify cache was created by createFromTemplate + const cacheManager = new CacheManager({ toolName: testCacheTool, }); - const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL); + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + const cachedRepo = cacheManager.get(cacheKey); + expect(cachedRepo).not.toBeNull(); expect(fs.existsSync(cachedRepo!)).toBe(true); }); @@ -143,28 +160,24 @@ describe('cached template integration tests', () => { let secondOutputDir: string; beforeAll(async () => { - await createFromCachedTemplate({ + await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: fs.mkdtempSync(path.join(os.tmpdir(), 'warmup-')), - answers: { - PROJECT_NAME: 'warmup', - MODULE_NAME: 'warmup' - }, - cacheTool: testCacheTool + answers: buildAnswers('warmup'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); secondOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'second-clone-')); - secondCloneResult = await createFromCachedTemplate({ + secondCloneResult = await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: secondOutputDir, - answers: { - PROJECT_NAME: 'cached-project', - AUTHOR: 'Cached Author', - DESCRIPTION: 'A cached test project', - MODULE_NAME: 'cachedmodule' - }, - cacheTool: testCacheTool + answers: buildAnswers('cached'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); }, 60000); diff --git a/packages/create-gen-app-test/src/cli.ts b/packages/create-gen-app-test/src/cli.ts index 5496817..b6c4192 100644 --- a/packages/create-gen-app-test/src/cli.ts +++ b/packages/create-gen-app-test/src/cli.ts @@ -6,16 +6,18 @@ import * as path from "path"; import { Inquirerer, ListQuestion } from "inquirerer"; import minimist, { ParsedArgs } from "minimist"; -import { cloneRepo, createGen } from "create-gen-app"; +import { CacheManager, GitCloner, checkNpmVersion } from "create-gen-app"; +import { createFromTemplate } from './index'; const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates.git"; const DEFAULT_PATH = "."; const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output"; +const DEFAULT_TOOL_NAME = "create-gen-app-test"; +const DEFAULT_TTL = 604800000; // 1 week -// Use require for package.json to avoid module resolution issues -const createGenPackageJson = require("create-gen-app/package.json"); -const PACKAGE_VERSION = - (createGenPackageJson as { version?: string }).version ?? "0.0.0"; +// Import package.json for version +import * as createGenPackageJson from "create-gen-app/package.json"; +const PACKAGE_VERSION = createGenPackageJson.version ?? "0.0.0"; const RESERVED_ARG_KEYS = new Set([ "_", @@ -37,6 +39,8 @@ const RESERVED_ARG_KEYS = new Set([ "v", "no-tty", "n", + "clear-cache", + "c", ]); export interface CliResult { @@ -58,9 +62,10 @@ export async function runCli( h: "help", v: "version", n: "no-tty", + c: "clear-cache", }, string: ["repo", "branch", "path", "template", "output"], - boolean: ["force", "help", "version", "no-tty"], + boolean: ["force", "help", "version", "no-tty", "clear-cache"], default: { repo: DEFAULT_REPO, path: DEFAULT_PATH, @@ -77,19 +82,72 @@ export async function runCli( return; } + // Check for updates + try { + const versionCheck = await checkNpmVersion("create-gen-app", PACKAGE_VERSION); + if (versionCheck.isOutdated && versionCheck.latestVersion) { + console.warn( + `\n⚠️ New version available: ${versionCheck.currentVersion} → ${versionCheck.latestVersion}` + ); + console.warn(` Run: npm install -g create-gen-app@latest\n`); + } + } catch { + // Silently ignore version check failures + } + + // Initialize modules + const cacheManager = new CacheManager({ + toolName: DEFAULT_TOOL_NAME, + ttl: DEFAULT_TTL, + }); + + // Handle --clear-cache + if (args["clear-cache"]) { + console.log("Clearing cache..."); + cacheManager.clearAll(); + console.log("✨ Cache cleared successfully!"); + return; + } + + const gitCloner = new GitCloner(); + if (!args.output && args._[0]) { args.output = args._[0]; } - let tempDir: string | null = null; - try { + // Get or clone template + const normalizedUrl = gitCloner.normalizeUrl(args.repo); + const cacheKey = cacheManager.createKey(normalizedUrl, args.branch); + + let templateDir: string; + const cachedPath = cacheManager.get(cacheKey); + const expiredMetadata = cacheManager.checkExpiration(cacheKey); + + if (expiredMetadata) { + console.warn( + `⚠️ Cached template expired (last updated: ${new Date(expiredMetadata.lastUpdated).toLocaleString()})` + ); + console.log('Updating cache...'); + cacheManager.clear(cacheKey); + } + + if (cachedPath && !expiredMetadata) { + console.log(`Using cached template from ${cachedPath}`); + templateDir = cachedPath; + } else { console.log(`Cloning template from ${args.repo}...`); if (args.branch) { console.log(`Using branch ${args.branch}`); } - tempDir = await cloneRepo(args.repo, { branch: args.branch }); + const tempDest = path.join(cacheManager['reposDir'], cacheKey); + gitCloner.clone(normalizedUrl, tempDest, { branch: args.branch, depth: 1 }); + cacheManager.set(cacheKey, tempDest); + templateDir = tempDest; + console.log('Template cached for future runs'); + } - const selectionRoot = path.join(tempDir, args.path); + try { + const selectionRoot = path.join(templateDir, args.path); if ( !fs.existsSync(selectionRoot) || !fs.statSync(selectionRoot).isDirectory() @@ -146,21 +204,22 @@ export async function runCli( args["no-tty"] ?? (args as Record).noTty ); - await createGen({ + // Use the createFromTemplate function which will use the same cache + await createFromTemplate({ templateUrl: args.repo, - fromBranch: args.branch, + branch: args.branch, fromPath, outputDir, - argv: answerOverrides, + answers: answerOverrides, noTty, + toolName: DEFAULT_TOOL_NAME, + ttl: DEFAULT_TTL, }); console.log(`\n✨ Done! Project ready at ${outputDir}`); return { outputDir, template: selectedTemplate }; - } finally { - if (tempDir && fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } + } catch (error) { + throw error; } } @@ -178,12 +237,16 @@ Options: -t, --template Template folder to use (will prompt if omitted) -o, --output Output directory (defaults to ./