Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions packages/create-gen-app/src/scaffolder/template-scaffolder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
ScaffoldResult,
BoilerplatesConfig,
BoilerplateConfig,
InspectOptions,
InspectResult,
} from './types';

/**
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions packages/create-gen-app/src/scaffolder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}