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'; /** * Ensures a certain dependency exists or is excluded */ const ensureDependency = async ( packageJsonObject: any, position: 'dep' | 'devDep' | 'everywhere', constraint: 'exclude' | 'include' | 'latest', dependencyArg: string, ): Promise => { // Parse package name and version, handling scoped packages like @scope/name@version const isScoped = dependencyArg.startsWith('@'); const lastAtIndex = dependencyArg.lastIndexOf('@'); // For scoped packages, the version @ must come after the / // For unscoped packages, any @ indicates a version const hasVersion = isScoped ? lastAtIndex > dependencyArg.indexOf('/') : lastAtIndex >= 0; const packageName = hasVersion ? dependencyArg.slice(0, lastAtIndex) : dependencyArg; const version = hasVersion ? dependencyArg.slice(lastAtIndex + 1) : 'latest'; const targetSections: string[] = []; switch (position) { case 'dep': targetSections.push('dependencies'); break; case 'devDep': targetSections.push('devDependencies'); break; case 'everywhere': targetSections.push('dependencies', 'devDependencies'); break; } for (const section of targetSections) { if (!packageJsonObject[section]) { packageJsonObject[section] = {}; } switch (constraint) { case 'exclude': delete packageJsonObject[section][packageName]; break; case 'include': if (!packageJsonObject[section][packageName]) { packageJsonObject[section][packageName] = version === 'latest' ? '^1.0.0' : version; } break; case 'latest': try { const registry = new plugins.smartnpm.NpmRegistry(); const packageInfo = await registry.getPackageInfo(packageName); const latestVersion = packageInfo['dist-tags'].latest; packageJsonObject[section][packageName] = `^${latestVersion}`; } catch (error) { logVerbose( `Could not fetch latest version for ${packageName}, using existing or default`, ); if (!packageJsonObject[section][packageName]) { packageJsonObject[section][packageName] = version === 'latest' ? '^1.0.0' : version; } } break; } } }; export class PackageJsonFormatter extends BaseFormatter { get name(): string { return 'packagejson'; } async analyze(): Promise { const changes: IPlannedChange[] = []; const packageJsonPath = 'package.json'; // Check if file exists const exists = await plugins.smartfs.file(packageJsonPath).exists(); if (!exists) { logVerbose('package.json does not exist, skipping'); return changes; } // Read current content const currentContent = (await plugins.smartfs .file(packageJsonPath) .encoding('utf8') .read()) as string; // Parse and compute new content const packageJson = JSON.parse(currentContent); // Get gitzone config from npmextra const npmextraConfig = new plugins.npmextra.Npmextra(paths.cwd); const gitzoneData: any = npmextraConfig.dataFor('@git.zone/cli', {}); // Set metadata from gitzone config if (gitzoneData.module) { packageJson.repository = { type: 'git', url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}.git`, }; packageJson.bugs = { url: `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}/issues`, }; packageJson.homepage = `https://${gitzoneData.module.githost}/${gitzoneData.module.gitscope}/${gitzoneData.module.gitrepo}#readme`; } // Ensure module type if (!packageJson.type) { packageJson.type = 'module'; } // Ensure private field exists if (packageJson.private === undefined) { packageJson.private = true; } // Ensure license field exists if (!packageJson.license) { packageJson.license = 'UNLICENSED'; } // Ensure scripts object exists if (!packageJson.scripts) { packageJson.scripts = {}; } // Ensure build script exists if (!packageJson.scripts.build) { packageJson.scripts.build = `echo "Not needed for now"`; } // Ensure buildDocs script exists if (!packageJson.scripts.buildDocs) { packageJson.scripts.buildDocs = `tsdoc`; } // Set files array packageJson.files = [ 'ts/**/*', 'ts_web/**/*', 'dist/**/*', 'dist_*/**/*', 'dist_ts/**/*', 'dist_ts_web/**/*', 'assets/**/*', 'cli.js', 'npmextra.json', 'readme.md', ]; // Handle dependencies await ensureDependency( packageJson, 'devDep', 'exclude', '@push.rocks/tapbundle', ); await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest'); await ensureDependency( packageJson, 'devDep', 'latest', '@git.zone/tsbuild', ); // Set pnpm overrides from assets try { const overridesContent = (await plugins.smartfs .file(plugins.path.join(paths.assetsDir, 'overrides.json')) .encoding('utf8') .read()) as string; const overrides = JSON.parse(overridesContent); packageJson.pnpm = packageJson.pnpm || {}; packageJson.pnpm.overrides = overrides; } catch (error) { logVerbose(`Could not read overrides.json: ${error.message}`); } const newContent = JSON.stringify(packageJson, null, 2); // Only add change if content differs if (newContent !== currentContent) { changes.push({ type: 'modify', path: packageJsonPath, module: this.name, description: 'Format package.json', content: newContent, }); } return changes; } async applyChange(change: IPlannedChange): Promise { if (change.type !== 'modify' || !change.content) return; await this.modifyFile(change.path, change.content); logger.log('info', 'Updated package.json'); } }