UNPKG

npm-template-sync

Version:

Keep npm package in sync with its template

446 lines (373 loc) 12.5 kB
import { createContext } from "expression-expander"; import micromatch from "micromatch"; import { LogLevelMixin, makeLogEvent } from "loglevel-mixin"; import { StringContentEntry } from "content-entry"; import { Travis } from "./travis.mjs"; import { Readme } from "./readme.mjs"; import { Package } from "./package.mjs"; import { Rollup } from "./rollup.mjs"; import { License } from "./license.mjs"; import { MergeAndRemoveLineSet } from "./merge-and-remove-line-set.mjs"; import { MergeLineSet } from "./merge-line-set.mjs"; import { NpmIgnore } from "./npm-ignore.mjs"; import { ReplaceIfEmpty } from "./replace-if-empty.mjs"; import { Replace } from "./replace.mjs"; import { JSONFile } from "./json-file.mjs"; import { JSDoc } from "./jsdoc.mjs"; import { Context } from "./context.mjs"; import { jspath, mergeTemplateFiles } from "./util.mjs"; /** * context prepared to execute one package * @param {Context} context * @param {string} targetBranchName * * @property {Object} ctx * @property {Map<string,Object>} files */ export const PreparedContext = LogLevelMixin( class _PreparedContext { static get mergers() { return [ Rollup, Travis, Readme, Package, JSONFile, JSDoc, Travis, MergeAndRemoveLineSet, MergeLineSet, NpmIgnore, License, ReplaceIfEmpty, Replace ]; } static async from(context, targetBranchName) { const pc = new PreparedContext(context, targetBranchName); await pc.initialize(); return pc; } static async execute(context, targetBranchName) { const pc = new PreparedContext(context, targetBranchName); await pc.initialize(); return pc.execute(); } constructor(context, targetBranchName) { Object.defineProperties(this, { ctx: { value: createContext({ properties: Object.assign({}, context.properties), keepUndefinedValues: true, leftMarker: "{{", rightMarker: "}}", markerRegexp: "{{([^}]+)}}", evaluate: (expression, context, path) => jspath(this.properties, expression) }) }, files: { value: new Map() }, context: { value: context }, targetBranchName: { value: targetBranchName } }); } get mergers() { return this.constructor.mergers; } get provider() { return this.context.provider; } get templateBranchName() { return this.context.templateBranchName; } get properties() { return this.ctx.properties; } expand(...args) { return this.ctx.expand(...args); } evaluate(expression) { return jspath(this.properties, expression); } log(level, arg) { this.context.log( makeLogEvent(level, arg, { branch: this.targetBranch ? this.targetBranch.fullCondensedName : undefined }) ); } async initialize() { const context = this.context; const targetBranch = await context.provider.branch(this.targetBranchName); if (targetBranch === undefined) { throw new Error(`Unable to find branch ${this.targetBranchName}`); } if (targetBranch.provider.name === "GithubProvider") { this.properties.github = { user: targetBranch.owner.name, repo: targetBranch.repository.name }; } if (targetBranch.repository.owner !== undefined) { Object.assign(this.properties.license, { owner: targetBranch.owner.name }); } if ( targetBranch.repository.description !== undefined && this.properties.description === undefined ) { this.properties.description = targetBranch.repository.description; } const pkg = new Package("package.json"); Object.assign(this.properties, await pkg.properties(targetBranch)); this.debug({ message: "detected properties", properties: this.properties }); if (this.properties.usedBy !== undefined) { Object.defineProperties(this, { templateBranch: { value: targetBranch } }); return; } let templateBranch; if (context.templateBranchName === undefined) { try { templateBranch = await context.provider.branch( this.properties.templateRepo ); } catch (e) {} if (templateBranch === undefined) { throw new Error( `Unable to extract template repo url from ${targetBranch.name} ${pkg.name}` ); } } else { templateBranch = await context.provider.branch(this.templateBranchName); } Object.defineProperties(this, { targetBranch: { value: targetBranch }, templateBranch: { value: templateBranch } }); this.debug({ message: "initialized for", targetBranch, templateBranch }); } static async createFiles(branch, mapping = Context.defaultMapping) { const files = []; for await (const entry of branch) { files.push(entry); } let alreadyPresent = new Set(); return mapping .map(m => { const found = micromatch(files.map(f => f.name), m.pattern); const notAlreadyProcessed = found.filter(f => !alreadyPresent.has(f)); alreadyPresent = new Set([...Array.from(alreadyPresent), ...found]); return notAlreadyProcessed.map(f => { const merger = this.mergers.find(merger => merger.name === m.merger) || ReplaceIfEmpty; return new merger(f, m.options); }); }) .reduce((last, current) => Array.from([...last, ...current]), []); } addFile(file) { file.logLevel = this.logLevel; this.files.set(file.name, file); } /** * all used dev modules * @return {Set<string>} */ async usedDevModules() { const usedModuleSets = await Promise.all( Array.from(this.files.values()).map(async file => { if (file.name === "package.json") { return file.usedDevModules( file.targetEntry(this, { ignoreMissing: true }) ); } else { const m = await file.merge(this); return file.usedDevModules(m.content); } }) ); return usedModuleSets.reduce( (sum, current) => new Set([...sum, ...current]), new Set() ); } optionalDevModules(modules) { return Array.from(this.files.values()) .map(file => file.optionalDevModules(modules)) .reduce((sum, current) => new Set([...sum, ...current]), new Set()); } async trackUsedModule(targetBranch) { const templateBranch = this.templateBranch; let templatePullRequest; let newTemplatePullRequest = false; const templateAddBranchName = "npm-template-trac-usage/1"; let templatePRBranch = await templateBranch.repository.branch( templateAddBranchName ); const pkg = new Package("package.json"); const templatePackage = await (templatePRBranch ? templatePRBranch : templateBranch ).entry(pkg.name); const templatePackageContent = await templatePackage.getString(); const templatePackageJson = templatePackageContent === undefined || templatePackageContent === "" ? {} : JSON.parse(templatePackageContent); if (this.context.trackUsedByModule) { const name = targetBranch.fullCondensedName; if (templatePackageJson.template === undefined) { templatePackageJson.template = {}; } if (!Array.isArray(templatePackageJson.template.usedBy)) { templatePackageJson.template.usedBy = []; } if (!templatePackageJson.template.usedBy.find(n => n === name)) { templatePackageJson.template.usedBy.push(name); templatePackageJson.template.usedBy = templatePackageJson.template.usedBy.sort(); if (templatePRBranch === undefined) { templatePRBranch = await templateBranch.createBranch( templateAddBranchName ); newTemplatePullRequest = true; } await templatePRBranch.commit(`fix: add ${name}`, [ new StringContentEntry( "package.json", JSON.stringify(templatePackageJson, undefined, 2) ) ]); if (newTemplatePullRequest) { templatePullRequest = await templateBranch.createPullRequest( templatePRBranch, { title: `add ${name}`, body: `add tracking info for ${name}` } ); } } } return { templatePackageJson, templatePRBranch, templatePullRequest }; } async execute() { if (this.properties.usedBy !== undefined) { for (const r of this.properties.usedBy) { try { await PreparedContext.execute(this.context, r); } catch (e) { this.error(e); } } } else { return this.executeSingleRepo(); } } /** * @return {Promise<PullRequest>} */ async executeSingleRepo() { const templateBranch = this.templateBranch; const targetBranch = this.targetBranch; this.debug({ message: "executeSingleRepo", templateBranch, targetBranch }); const { templatePackageJson } = await this.trackUsedModule(targetBranch); /* collect files form template cascade */ let templateFiles = []; if (templatePackageJson.template) { if (templatePackageJson.template.files) { templateFiles.push(...templatePackageJson.template.files); } if (templatePackageJson.template.inheritFrom) { const inheritFromBranch = await this.provider.branch( templatePackageJson.template.inheritFrom ); const pc = await inheritFromBranch.entry("package.json"); const pkg = JSON.parse(await pc.getString()); if (pkg.template && pkg.template.files) { mergeTemplateFiles(templateFiles,pkg.template.files); } } } const files = await PreparedContext.createFiles( templateBranch, templateFiles ); files.forEach(f => this.addFile(f)); this.trace({ message: "got files", files: files.map(f => f.name) }); const merges = (await Promise.all( files.map(async f => f.merge(this)) )).filter(m => m !== undefined && m.changed); if (merges.length === 0) { this.info("-"); return; } this.info(merges.map(m => `${m.messages[0]}`).join(",")); if (this.dry) { this.info("dry run"); return; } let newPullRequestRequired = false; const prBranchName = "npm-template-sync/1"; let prBranch = await this.targetBranch.repository.branch(prBranchName); if (prBranch === undefined) { newPullRequestRequired = true; prBranch = await this.targetBranch.createBranch(prBranchName); } const messages = merges.reduce((result, merge) => { merge.messages.forEach(m => result.push(m)); return result; }, []); await prBranch.commit( messages.join("\n"), merges.map(m => new StringContentEntry(m.name, m.content)) ); if (newPullRequestRequired) { try { const pullRequest = await targetBranch.createPullRequest(prBranch, { title: `merge from ${templateBranch}`, body: merges .map( m => `${m.name} --- - ${m.messages.join("\n- ")} ` ) .join("\n") }); this.info({ message: "new PR", pr: pullRequest }); return pullRequest; } catch (err) { this.error(err); } } else { const pullRequest = new targetBranch.provider.pullRequestClass( targetBranch, prBranch, "old" ); this.info({ message: "update PR", pr: pullRequest }); return pullRequest; } } } );