UNPKG

xrelease

Version:

xrelease (pronounced cross-release) helps you setup automated releases for your project for any language

193 lines (174 loc) 6 kB
import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import type { ComponentConfig, TemplateConfig } from '../types/index.js'; import ora from 'ora'; export const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '../templates'); export const TEMPLATES: Record<string, Record<string, TemplateConfig[]>> = { workflow: { node: [ { source: 'github-workflows/release.node.yml', destination: '.github/workflows/release.yml', }, ], go: [ { source: 'github-workflows/release.go.yml', destination: '.github/workflows/release.yml', }, ], }, changelog: { node: [ { source: 'configs/versionrc.json', destination: '.versionrc', }, ], go: [ { source: 'configs/versionrc.json', destination: '.versionrc', }, ], }, }; export async function setupTemplates(components: ComponentConfig, templates: typeof TEMPLATES, destDir = process.cwd()): Promise<void> { const spinner = ora(); spinner.start('Setting up project templates...'); try { // Create package.json if it doesn't exist (for version tracking) spinner.start('Creating package.json...'); const pkgStatus = await setupPackageJson(destDir, components.language); if (pkgStatus === 'exists') { spinner.succeed('package.json already exists'); } else { spinner.succeed('Created package.json with default version: 0.1.0. Adjust as needed.'); } // Process workflow templates if (components.workflow) { const language = components.language || 'node'; // Default to Node.js const workflow = templates.workflow[language]; spinner.start(`Setting up ${language}: ${workflow[0].destination}`); await processTemplates(workflow, destDir); spinner.succeed(`${language} workflow configured successfully`); } // Process changelog templates if (components.changelog) { const language = components.language || 'node'; const changelog = templates.changelog[language]; spinner.start(`Setting up ${language}: ${changelog[0].destination}`); await processTemplates(changelog, destDir); spinner.succeed(`${language} changelog configured successfully`); } spinner.succeed('Templates configured successfully'); } catch (error) { spinner.fail(`Failed to setup templates`); throw new Error(`Failed to setup templates: ${error instanceof Error ? error.message : 'Unknown error'}`); } } export async function setupPackageJson(dir = process.cwd(), language = 'node'): Promise<string> { try { await fs.access(path.join(dir, 'package.json')); //TODO: check for esm module compatibility and provide a warning if it's not compatible //TODO: check for test command and provide a warning if it is not set return 'exists'; } catch { let packageName = path.basename(dir); // Only try to get name from go.mod if language is Go if (language === 'go') { try { const goModContent = await fs.readFile(path.join(dir, 'go.mod'), 'utf-8'); const moduleMatch = goModContent.match(/^module\s+([^\s]+)/m); if (moduleMatch) { // Extract just the last part of the module path packageName = moduleMatch[1].split('/').pop() || packageName; } } catch { // go.mod doesn't exist or can't be read, use directory name } } await fs.writeFile( path.join(dir, 'package.json'), JSON.stringify( { name: packageName, version: '0.1.0', type: 'module', scripts: { test: 'echo "add your test command here"', }, private: true, }, null, 2 ) ); return 'created'; } } export async function processTemplates(templates: TemplateConfig[], baseDestDir = process.cwd()): Promise<void> { for (const template of templates) { const sourcePath = path.join(TEMPLATE_DIR, template.source); const destPath = path.join(baseDestDir, template.destination); // Ensure destination directory exists const destDir = path.dirname(destPath); await fs.mkdir(destDir, { recursive: true }); // Read and transform template content let content = await fs.readFile(sourcePath, 'utf-8'); if (template.transform) { content = template.transform(content); } // Write to destination await fs.writeFile(destPath, content); } } /** * Updates version strings in a file using regex pattern and template. * Supports both ${version} for new version and ${N} for regex capture groups. * * @example * // Update go.mod module path while preserving the path * await updateVersionInFile({ * path: 'go.mod', * pattern: '^module\\s+([^\\s]+)', * template: 'module ${1}', * version: '1.0.0' * }); * * // Update package.json version * await updateVersionInFile({ * path: 'package.json', * pattern: '"version":\\s*"([^"]+)"', * template: '"version": "${version}"', * version: '1.0.0' * }); */ export async function updateVersionInFile({ path: filePath, pattern, template, version, }: { path: string; pattern: string; template: string; version: string; }): Promise<void> { const content = await fs.readFile(filePath, 'utf-8'); const regex = new RegExp(pattern); // Handle both ${version} and ${N} capture group references const newContent = content.replace(regex, (...args) => { let result = template; // Replace ${version} with the new version result = result.replace(/\$\{version\}/g, version); // Replace ${N} with capture groups (args[1] is first group, args[2] second, etc) for (let i = 1; i < args.length - 2; i++) { result = result.replace(new RegExp(`\\$\\{${i}\\}`, 'g'), args[i]); } return result; }); await fs.writeFile(filePath, newContent); } //TODO: move the xrelease template logic to this file