UNPKG

@template-tools/template-sync

Version:
509 lines (457 loc) 12.1 kB
import { merge, mergeVersionsLargest, mergeExpressions, mergeSkip, compare } from "hinted-tree-merger"; import { StringContentEntry, ContentEntry } from "content-entry"; import { Merger } from "../merger.mjs"; import { actions2message, aggregateActions, jspath, asScalar, asArray, normalizeTemplateSources } from "../util.mjs"; function moduleNames(object, modules) { if (object !== undefined) { Object.entries(object).forEach(([k, v]) => { if (typeof v === "string") { modules.add(v); } else if (Array.isArray(v)) { v.forEach(e => { if (typeof e === "string") { modules.add(e); } }); } }); } } /** * order in which json keys are written */ const sortedKeys = [ "name", "version", "workspaces", "private", "publishConfig", "files", "sideEffects", "type", "packageManager", "main", "types", "exports", "imports", "umd:main", "jsdelivr", "unpkg", "module", "source", "jsnext:main", "browser", "svelte", "description", "keywords", "author", "maintainers", "contributors", "license", "sustainability", "funding", "bin", "scripts", "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", "bundledDependencies", "overrides", "engines", "os", "cpu", "arch", "repository", "directories", "files", "man", "bugs", "homepage", "config", "pkgbuild", "release", "ava", "nyc", "xo", "native", "template" ]; const propertyKeys = ["description", "version", "name", "main", "browser"]; const MODULE_HINT = { type: "fix", scope: "module", orderBy: [ "node-addons", "node", "import", "require", "module-sync", "svelte", "types", "default"] }; const REMOVE_HINT = { compare, removeEmpty: true }; const DEPENDENCY_HINT = { merge: mergeVersionsLargest, scope: "deps" }; const DEPENDENCY_HINT_FIX = { type: "fix", ...DEPENDENCY_HINT }; const ENGINES_HINT = { compare, merge: mergeVersionsLargest, removeEmpty: true, type: "fix:breaking", scope: "engines" }; const MERGE_HINTS = { "*": { scope: "package", type: "chore" }, "": { orderBy: sortedKeys }, type: MODULE_HINT, types: MODULE_HINT, keywords: { removeEmpty: true, compare, type: "docs" }, repository: { compare }, files: { compare, scope: "files", removeEmpty: true }, exports: { ...REMOVE_HINT, ...MODULE_HINT }, "exports.*": { ...REMOVE_HINT, ...MODULE_HINT }, imports: { ...REMOVE_HINT, ...MODULE_HINT }, bin: REMOVE_HINT, "bin.*": { removeEmpty: true, scope: "bin" }, scripts: { orderBy: [ "preinstall", /^install/, "postinstall", "prepack", /^pack/, "postpack", /^prepare/, "prepublishOnly", "prepublish", /^publish/, "postpublish", "prerestart", /^restart/, "postrestart", "preshrinkwrap", /^shrinkwrap/, "postshrinkwrap", "prestart", /^start/, "poststart", "prestop", /^stop/, "poststop", "pretest", /^test/, "posttest", /^cover/, "preuninstall", /^uninstall/, "postuninstall", "preversion", /^version/, "postversion", /^docs/, /^lint/, /^package/ ] }, "scripts.*": { merge: mergeExpressions, removeEmpty: true, scope: "scripts" }, dependencies: REMOVE_HINT, "dependencies.*": DEPENDENCY_HINT_FIX, devDependencies: REMOVE_HINT, "devDependencies.*": DEPENDENCY_HINT, peerDependencies: REMOVE_HINT, "peerDependencies.*": DEPENDENCY_HINT_FIX, optionalDependencies: REMOVE_HINT, "optionalDependencies.*": DEPENDENCY_HINT_FIX, bundeledDependencies: REMOVE_HINT, "bundeledDependencies.*": DEPENDENCY_HINT, overrides: REMOVE_HINT, "overrides.*": DEPENDENCY_HINT, "engines.*": ENGINES_HINT, "devEngines.*": ENGINES_HINT, postcss: REMOVE_HINT, browserslist: REMOVE_HINT, release: REMOVE_HINT, config: REMOVE_HINT, "config.*": { compare, overwrite: false }, pkgbuild: { orderBy: ["content", "output", "hooks", "backup", "groups"], merge: mergeSkip }, "pkgbuild.*": { merge: mergeSkip }, "pkgbuild.dependencies.*": { merge: mergeVersionsLargest, compare, type: "fix", scope: "pkg" }, template: { merge: mergeSkip } }; /** * Order in which exports are searched * @see {https://nodejs.org/dist/latest/docs/api/packages.html#exports} */ const exportsConditionOrder = [/*"browser", "module", "import", ".",*/ "default", "."]; /** * Merger for package.json */ export class Package extends Merger { static get pattern() { return "package.json"; } static get options() { return { ...super.options, actions: [], keywords: [], optionalDevDependencies: ["cracks", "dont-crack"] }; } /** * Deliver some key properties. * - name * - version * - description * - main * @param {ContentEntry} entry * @return {Promise<Object>} */ static async properties(entry) { const pkg = JSON.parse(await entry.string); const properties = {}; if (pkg.name) { Object.assign(properties, { npm: { name: pkg.name, fullName: pkg.name } }); } if (pkg.template?.usedBy !== undefined) { properties.usedBy = pkg.template.usedBy; } propertyKeys.forEach(key => { const value = pkg[key]; if (value !== undefined && value !== `{{${key}}}`) { switch (key) { case "version": if (value === "0.0.0-semantic-release") { return; } break; case "name": properties.fullName = value; const m = value.match(/^(\@[^\/]+)\/(.*)/); if (m) { properties.npm.organization = m[1]; properties.npm.name = m[2]; properties.name = m[2]; return; } break; } properties[key] = value; } }); function findMainExport(object) { for (const slot of exportsConditionOrder) { switch (typeof object?.[slot]) { case "string": return object[slot]; case "object": return findMainExport(object[slot]); } } } if (properties.main === undefined) { const main = findMainExport(pkg.exports); if (main !== undefined) { properties.main = main; } } Object.assign(properties, pkg.config); return properties; } static async usedDevDependencies(into, entry) { if (await entry.isEmpty) { return into; } moduleNames(JSON.parse(await entry.string).release, into); return into; } static async *commits( context, destinationEntry, sourceEntry, options = this.options ) { const name = destinationEntry.name; const [original, templateContent] = await Promise.all([ destinationEntry.string, sourceEntry.string ]); const originalLastChar = original[original.length - 1]; const targetRepository = context.targetBranch.repository; let target = 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.cloneURL }, bugs: { url: context.targetBranch.issuesURL }, homepage: context.targetBranch.homePageURL }); 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.main !== undefined && !target.main.match(/\{\{main\}\}/)) { properties.main = target.main; } 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), { ...MERGE_HINTS, ...options.mergeHints } ); if ( target.contributors !== 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); aggregateActions(actions, { scope: "package", type: "chore", add: templateValue, path: action.path }); } }); } }); target = context.expand(target); propertyKeys.forEach(key => { if (target[key] === "{{" + key + "}}") { delete target[key]; if (unknownKeys.has(key)) { aggregateActions(actions, { scope: "package", type: "chore", remove: "{{" + key + "}}", path: key }); } } }); if (context.track) { if (target.template === undefined) { target.template = { inheritFrom: [] }; } target.template.inheritFrom = asScalar( normalizeTemplateSources( [...asArray(target.template.inheritFrom), ...context.templateSources], [context.targetBranch.fullCondensedName] ) ); } let merged = JSON.stringify(target, undefined, 2); const lastChar = merged[merged.length - 1]; // keep trailing newline if (originalLastChar === "\n" && lastChar === "}") { merged += "\n"; } if (merged !== original) { yield { entries: [new StringContentEntry(name, undefined, merged)], message: actions2message(actions, options.messagePrefix, name) }; } } } export async function deleteUnusedDevDependencies(context, target, template) { if (target.devDependencies) { try { const usedDevDependencies = new Set(); for await (const [entry, merger] of context.template.entryMerger( context.targetBranch.entries() )) { await merger.factory.usedDevDependencies( usedDevDependencies, entry, merger.options ); } const allKnown = new Set( Object.keys(target.devDependencies).concat( Object.keys(template.devDependencies) ) ); const optionalDevDependencies = new Set(); for await (const [entry, merger] of context.template.entryMerger( context.targetBranch.entries() )) { await merger.factory.optionalDevDependencies( optionalDevDependencies, allKnown, merger.options ); } template.devDependencies = { ...template.devDependencies, ...Object.fromEntries( [...optionalDevDependencies] .filter(d => !usedDevDependencies.has(d)) .map(d => [d, "--delete--"]) ) }; context.debug(`used devDependencies: ${[...usedDevDependencies]}`); } catch (e) { console.error(e); } } }