From 8cf8c4fc04ed9fe7f8b0beb3a98fc268c01f336c Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 25 Dec 2025 13:36:33 +0000 Subject: [PATCH] feat(create-gen-app): add inspect() method to TemplateScaffolder Adds inspect() method that clones/caches a template and reads its .boilerplate.json configuration WITHOUT scaffolding any files. This enables metadata-driven workflows where callers can read the template's type field before deciding how to handle scaffolding. New types: - InspectOptions: options for inspecting a template - InspectResult: result containing template metadata and config The inspect flow: 1. Clone/cache the template repo (or use existing cache if valid) 2. Resolve fromPath via .boilerplates.json convention 3. Read .boilerplate.json from the resolved template path 4. Return config without calling templatizer.process() This is useful for CLI tools like pgpm that want to use a single entry point (pgpm init ) and branch behavior based on the template's declared type. --- .../src/scaffolder/template-scaffolder.ts | 101 ++++++++++++++++++ .../create-gen-app/src/scaffolder/types.ts | 59 ++++++++++ 2 files changed, 160 insertions(+) diff --git a/packages/create-gen-app/src/scaffolder/template-scaffolder.ts b/packages/create-gen-app/src/scaffolder/template-scaffolder.ts index 4ce353e..376c491 100644 --- a/packages/create-gen-app/src/scaffolder/template-scaffolder.ts +++ b/packages/create-gen-app/src/scaffolder/template-scaffolder.ts @@ -10,6 +10,8 @@ import { ScaffoldResult, BoilerplatesConfig, BoilerplateConfig, + InspectOptions, + InspectResult, } from './types'; /** @@ -80,6 +82,36 @@ export class TemplateScaffolder { return this.scaffoldFromRemote(resolvedTemplate, branch, options); } + /** + * Inspect a template without scaffolding. + * Clones/caches the template and reads its .boilerplate.json configuration + * without copying any files to an output directory. + * + * This is useful for metadata-driven workflows where you need to know + * the template's type or other configuration before deciding how to handle it. + * + * @param options - Inspect options + * @returns Inspect result with template metadata + */ + inspect(options: InspectOptions): InspectResult { + const template = options.template ?? this.config.defaultRepo; + if (!template) { + throw new Error( + 'No template specified and no defaultRepo configured. ' + + 'Either pass template in options or set defaultRepo in config.' + ); + } + + const branch = options.branch ?? this.config.defaultBranch; + const resolvedTemplate = this.resolveTemplatePath(template); + + if (this.isLocalPath(resolvedTemplate) && fs.existsSync(resolvedTemplate)) { + return this.inspectLocal(resolvedTemplate, options.fromPath); + } + + return this.inspectRemote(resolvedTemplate, branch, options.fromPath); + } + /** * Read the .boilerplates.json configuration from a template repository root. */ @@ -133,6 +165,75 @@ export class TemplateScaffolder { return this.templatizer; } + private inspectLocal( + templateDir: string, + fromPath?: string + ): InspectResult { + const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath( + templateDir, + fromPath + ); + + const config = this.readBoilerplateConfig(resolvedTemplatePath); + + return { + templateDir, + resolvedFromPath, + resolvedTemplatePath, + cacheUsed: false, + cacheExpired: false, + config, + }; + } + + private inspectRemote( + templateUrl: string, + branch: string | undefined, + fromPath?: string + ): InspectResult { + const normalizedUrl = this.gitCloner.normalizeUrl(templateUrl); + const cacheKey = this.cacheManager.createKey(normalizedUrl, branch); + + const expiredMetadata = this.cacheManager.checkExpiration(cacheKey); + if (expiredMetadata) { + this.cacheManager.clear(cacheKey); + } + + let templateDir: string; + let cacheUsed = false; + + const cachedPath = this.cacheManager.get(cacheKey); + if (cachedPath && !expiredMetadata) { + templateDir = cachedPath; + cacheUsed = true; + } else { + const tempDest = path.join(this.cacheManager.getReposDir(), cacheKey); + this.gitCloner.clone(normalizedUrl, tempDest, { + branch, + depth: 1, + singleBranch: true, + }); + this.cacheManager.set(cacheKey, tempDest); + templateDir = tempDest; + } + + const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath( + templateDir, + fromPath + ); + + const config = this.readBoilerplateConfig(resolvedTemplatePath); + + return { + templateDir, + resolvedFromPath, + resolvedTemplatePath, + cacheUsed, + cacheExpired: Boolean(expiredMetadata), + config, + }; + } + private async scaffoldFromLocal( templateDir: string, options: ScaffoldOptions diff --git a/packages/create-gen-app/src/scaffolder/types.ts b/packages/create-gen-app/src/scaffolder/types.ts index 9be0118..fa77f7a 100644 --- a/packages/create-gen-app/src/scaffolder/types.ts +++ b/packages/create-gen-app/src/scaffolder/types.ts @@ -145,3 +145,62 @@ export interface BoilerplateConfig { */ questions?: Question[]; } + +/** + * Options for inspecting a template without scaffolding. + * Used to read template metadata before deciding how to handle it. + */ +export interface InspectOptions { + /** + * Template repository URL, local path, or org/repo shorthand. + * If not provided, uses the defaultRepo from config. + */ + template?: string; + + /** + * Branch to clone (for remote repositories) + */ + branch?: string; + + /** + * Subdirectory within the template repository to inspect. + * Can be a direct path or a variant name that gets resolved via .boilerplates.json + */ + fromPath?: string; +} + +/** + * Result of inspecting a template. + * Contains metadata about the template without copying any files. + */ +export interface InspectResult { + /** + * Path to the cached/cloned template directory + */ + templateDir: string; + + /** + * The resolved fromPath after .boilerplates.json resolution + */ + resolvedFromPath?: string; + + /** + * Full path to the resolved template directory + */ + resolvedTemplatePath: string; + + /** + * Whether a cached template was used + */ + cacheUsed: boolean; + + /** + * Whether the cache was expired and refreshed + */ + cacheExpired: boolean; + + /** + * The .boilerplate.json configuration from the template, if present + */ + config: BoilerplateConfig | null; +}