UNPKG

npm-template-sync

Version:

Keep npm package in sync with its template

428 lines (378 loc) 10.4 kB
import { merge, mergeVersionsLargest, mergeExpressions, mergeSkip, compare } from "hinted-tree-merger"; import { StringContentEntry } from "content-entry"; import { Merger } from "../merger.mjs"; import { Rollup } from "./rollup.mjs"; import { optionalDevDependencies, usedDevDependencies } from "../detect-dependencies.mjs"; import { actions2messages, aggregateActions, jspath, asScalar, asArray, defaultEncodingOptions } from "../util.mjs"; function moduleNames(object) { if (object === undefined) return new Set(); const modules = new Set(); Object.keys(object).forEach(k => { const v = object[k]; if (typeof v === "string") { modules.add(v); } else if (Array.isArray(v)) { v.forEach(e => { if (typeof e === "string") { modules.add(e); } }); } }); return modules; } /** * order in which json keys are written */ const sortedKeys = [ "name", "version", "private", "publishConfig", "files", "sideEffects", "type", "main", "umd:main", "jsdelivr", "unpkg", "module", "source", "jsnext:main", "browser", "svelte", "description", "keywords", "author", "maintainers", "contributors", "license", "sustainability", "bin", "scripts", "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "bundledDependencies", "engines", "os", "cpu", "arch", "repository", "directories", "files", "man", "bugs", "homepage", "config", "systemd", "pacman", "release", "ava", "nyc", "xo", "native", "template" ]; const propertyKeys = [ "description", "version", "name", "main", "module", "browser" ]; const REMOVE_HINT = { compare, removeEmpty: true }; const DEPENDENCY_HINT = { merge: mergeVersionsLargest }; /** * Merger for package.json */ export class Package extends Merger { static get pattern() { return "package.json"; } static get defaultOptions() { return { ...super.defaultOptions, actions: [], keywords: [], optionalDevDependencies: ["cracks", "dont-crack"] }; } /** * Deliver some key properties * @param {ContentEntry} entry * @return {Object} */ static async properties(entry) { const pkg = JSON.parse(await entry.getString(defaultEncodingOptions)); const properties = { npm: { name: pkg.name, fullName: pkg.name } }; if (pkg.name !== undefined) { const m = pkg.name.match(/^(\@[^\/]+)\/(.*)/); if (m) { properties.npm.organization = m[1]; properties.npm.name = m[2]; } } if (pkg.template !== undefined) { if (pkg.template.repository !== undefined) { properties.templateSources = asArray(pkg.template.repository.url); } if (pkg.template.inheritFrom !== undefined) { properties.templateSources = asArray(pkg.template.inheritFrom); } if (pkg.template.usedBy !== undefined) { properties.usedBy = pkg.template.usedBy; } } propertyKeys.forEach(key => { if (pkg[key] !== undefined && pkg[key] !== `{{${key}}}`) { if (!(key === "version" && pkg[key] === "0.0.0-semantic-release")) { properties[key] = pkg[key]; } } }); Object.assign(properties, pkg.config); return properties; } static optionalDevDependencies(modules = new Set()) { return new Set(["cracks", "dont-crack"].filter(m => modules.has(m))); } static async usedDevDependencies(entry) { const content = await entry.getString(); const pkg = content.length === 0 ? {} : JSON.parse(content); return moduleNames(pkg.release); } static async merge( context, destinationEntry, sourceEntry, options = this.defaultOptions ) { const messages = []; const name = destinationEntry.name; const templateContent = await sourceEntry.getString(); const original = await destinationEntry.getString(); const originalLastChar = original[original.length - 1]; const targetRepository = context.targetBranch.repository; let target = original === undefined || original === "" ? {} : JSON.parse(original); const unknownKeys = new Set(); propertyKeys.forEach(key => { if (target[key] === "{{" + key + "}}") { unknownKeys.add(key); } }); target = context.expand(target); const template = context.expand({ ...(templateContent.length ? JSON.parse(templateContent) : {}), repository: { type: targetRepository.type, url: targetRepository.url }, bugs: { url: context.targetBranch.issuesURL }, homepage: context.targetBranch.homePageURL, template: { inheritFrom: asScalar( [...context.template.initialBranches].map( branch => branch.fullCondensedName ) ) } }); const properties = context.properties; if (target.name === undefined || target.name === "") { const m = targetRepository.name.match(/^([^\/]+)\/(.*)/); target.name = m ? m[2] : context.targetBranch.repository.name; } if (target.module !== undefined && !target.module.match(/\{\{module\}\}/)) { properties.module = target.module; } await deleteUnusedDevDependencies(context, target, template); Object.entries(options.keywords).forEach(([r, keyword]) => { if (target.name.match(new RegExp(r))) { if (template.keywords === undefined) { template.keywords = []; } template.keywords.push(keyword); } }); const actions = {}; target = merge( target, template, "", (action, hint) => aggregateActions(actions, action, hint), { "": { orderBy: sortedKeys }, "*": { scope: "package", type: "chore" }, keywords: { removeEmpty: true, compare, type: "docs" }, repository: { compare }, files: { compare, scope: "files", removeEmpty: true }, bin: REMOVE_HINT, "bin.*": { removeEmpty: true, scope: "bin" }, scripts: { orderBy: [ "install", "pack", "prepare", "publish", "restart", "shrinkwrap", "start", "stop", "pretest", "test", "posttest", "cover", "uninstall", "version", "docs", "lint", "package" ] }, "scripts.*": { merge: mergeExpressions, removeEmpty: true, scope: "scripts" }, dependencies: REMOVE_HINT, "dependencies.*": { ...DEPENDENCY_HINT, type: "fix" }, devDependencies: REMOVE_HINT, "devDependencies.*": DEPENDENCY_HINT, peerDependencies: REMOVE_HINT, "peerDependencies.*": DEPENDENCY_HINT, optionalDependencies: REMOVE_HINT, "optionalDependencies.*": DEPENDENCY_HINT, bundeledDependencies: REMOVE_HINT, "bundeledDependencies.*": DEPENDENCY_HINT, "engines.*": { compare, merge: mergeVersionsLargest, removeEmpty: true, type: "fix", scope: "engines" }, release: REMOVE_HINT, config: REMOVE_HINT, "config.*": { compare, overwrite: false }, "pacman.*": { overwrite: false }, "pacman.depends.*": { merge: mergeVersionsLargest, compare, type: "fix", scope: "pacman" }, "template.usedBy": { merge: mergeSkip }, "template.repository": { remove: true }, ...options.mergeHints } ); if ( target.contributors !== undefined && target.author !== undefined && target.author.name !== undefined ) { const m = target.author.name.match(/(^[^<]+)<([^>]+)>/); if (m !== undefined) { const name = String(m[1]).replace(/^\s+|\s+$/g, ""); const email = m[2]; if ( target.contributors.find(c => c.name === name && c.email === email) ) { delete target.author; } } } options.actions.forEach(action => { if (action.op === "replace") { const templateValue = jspath(template, action.path); jspath(target, action.path, (targetValue, setter) => { if (templateValue !== targetValue) { setter(templateValue); messages.push( `chore(package): set ${action.path}='${templateValue}' as in template` ); } }); } }); target = context.expand(target); propertyKeys.forEach(key => { if (target[key] === "{{" + key + "}}") { delete target[key]; if (unknownKeys.has(key)) { messages.push( `chore(package): remove unknown value for ${key} ({{${key}}})` ); } } }); let merged = JSON.stringify(target, undefined, 2); const lastChar = merged[merged.length - 1]; // keep trailing newline if (originalLastChar === "\n" && lastChar === "}") { merged += "\n"; } return merged === original ? undefined : { entry: new StringContentEntry(name, merged), message: [ ...messages, ...actions2messages(actions, options.messagePrefix, name) ].join("\n") }; } } export async function deleteUnusedDevDependencies(context, target, template) { if (target.devDependencies) { try { const mm = [Package, Rollup].map(m => [m, m.pattern]); const udd = await usedDevDependencies(mm, context.targetBranch); const allKnown = new Set([ ...Object.keys(target.devDependencies), ...Object.keys(template.devDependencies) ]); context.debug(`used devDependencies: ${[...udd]}`); [...(await optionalDevDependencies(mm, allKnown))] .filter(m => !udd.has(m)) .forEach(m => { if (template.devDependencies === undefined) { template.devDependencies = {}; } template.devDependencies[m] = "--delete--"; context.debug(`devDependency: ${m}`); }); } catch (e) { console.log(e); } } }