import { BaseFormatter } from '../classes.baseformatter.js'; import type { IPlannedChange } from '../interfaces.format.js'; import * as plugins from '../mod.plugins.js'; import * as paths from '../../paths.js'; import { logger, logVerbose } from '../../gitzone.logging.js'; export class TemplatesFormatter extends BaseFormatter { get name(): string { return 'templates'; } /** * Render a template directory through smartscaf and return a map of path → content. */ private async renderTemplate(templateName: string): Promise> { const templateDir = plugins.path.join(paths.templatesDir, templateName); const scafTemplate = new plugins.smartscaf.ScafTemplate(templateDir); await scafTemplate.readTemplateFromDir(); const gitzoneData = this.project.gitzoneConfig?.data; if (gitzoneData) { await scafTemplate.supplyVariables({ module: gitzoneData.module, projectType: gitzoneData.projectType, }); } const renderedFiles = await scafTemplate.renderToMemory(); const fileMap = new Map(); for (const file of renderedFiles) { fileMap.set(file.path, file.contents.toString()); } return fileMap; } async analyze(): Promise { const changes: IPlannedChange[] = []; const project = this.project; const projectType = project.gitzoneConfig?.data?.projectType; // VSCode template - for all projects const vscodeChanges = await this.analyzeTemplate('vscode', [ { templatePath: '.vscode/settings.json', destPath: '.vscode/settings.json' }, { templatePath: '.vscode/launch.json', destPath: '.vscode/launch.json' }, ]); changes.push(...vscodeChanges); // CI and other templates based on projectType switch (projectType) { case 'npm': case 'wcc': const accessLevel = (project.gitzoneConfig?.data as any)?.release?.accessLevel || project.gitzoneConfig?.data?.npmciOptions?.npmAccessLevel; const ciTemplate = accessLevel === 'public' ? 'ci_default' : 'ci_default_private'; const ciChanges = await this.analyzeTemplate(ciTemplate, [ { templatePath: '.gitea/workflows/default_nottags.yaml', destPath: '.gitea/workflows/default_nottags.yaml' }, { templatePath: '.gitea/workflows/default_tags.yaml', destPath: '.gitea/workflows/default_tags.yaml' }, ]); changes.push(...ciChanges); break; case 'service': case 'website': const dockerCiChanges = await this.analyzeTemplate('ci_docker', [ { templatePath: '.gitea/workflows/docker_nottags.yaml', destPath: '.gitea/workflows/docker_nottags.yaml' }, { templatePath: '.gitea/workflows/docker_tags.yaml', destPath: '.gitea/workflows/docker_tags.yaml' }, ]); changes.push(...dockerCiChanges); const dockerfileChanges = await this.analyzeTemplate('dockerfile_service', [ { templatePath: 'Dockerfile', destPath: 'Dockerfile' }, { templatePath: 'dockerignore', destPath: '.dockerignore' }, ]); changes.push(...dockerfileChanges); const cliChanges = await this.analyzeTemplate('cli', [ { templatePath: 'cli.js', destPath: 'cli.js' }, { templatePath: 'cli.ts.js', destPath: 'cli.ts.js' }, ]); changes.push(...cliChanges); break; } // Update templates based on projectType if (projectType === 'website') { const websiteChanges = await this.analyzeTemplate('website_update', [ { templatePath: 'html/index.html', destPath: 'html/index.html' }, ]); changes.push(...websiteChanges); } else if (projectType === 'wcc') { const wccChanges = await this.analyzeTemplate('wcc_update', [ { templatePath: 'html/index.html', destPath: 'html/index.html' }, { templatePath: 'html/index.ts', destPath: 'html/index.ts' }, ]); changes.push(...wccChanges); } return changes; } private async analyzeTemplate( templateName: string, files: Array<{ templatePath: string; destPath: string }>, ): Promise { const changes: IPlannedChange[] = []; const templateDir = plugins.path.join(paths.templatesDir, templateName); const templateExists = await plugins.smartfs.directory(templateDir).exists(); if (!templateExists) { logVerbose(`Template ${templateName} not found`); return changes; } let renderedFiles: Map; try { renderedFiles = await this.renderTemplate(templateName); } catch (error) { logVerbose(`Failed to render template ${templateName}: ${error.message}`); return changes; } for (const file of files) { // Look up by templatePath first, then destPath (frontmatter may rename files) const processedContent = renderedFiles.get(file.templatePath) || renderedFiles.get(file.destPath); if (!processedContent) { logVerbose(`Template file ${file.templatePath} not found in rendered output`); continue; } const destExists = await plugins.smartfs.file(file.destPath).exists(); let currentContent = ''; if (destExists) { currentContent = (await plugins.smartfs .file(file.destPath) .encoding('utf8') .read()) as string; } if (processedContent !== currentContent) { changes.push({ type: destExists ? 'modify' : 'create', path: file.destPath, module: this.name, description: `Apply template ${templateName}/${file.templatePath}`, content: processedContent, }); } } return changes; } async applyChange(change: IPlannedChange): Promise { if (!change.content) return; if (change.type === 'create') { await this.createFile(change.path, change.content); } else { await this.modifyFile(change.path, change.content); } logger.log('info', `Applied template to ${change.path}`); } }