sync-monorepo-packages
Version:
Synchronize files and metadata across packages in a monorepo
112 lines (111 loc) • 3.53 kB
JavaScript
import createDebug from 'debug';
import {readFile, writeFile} from 'node:fs/promises';
import {basename, join} from 'node:path';
import {applyPatch, createPatch} from 'rfc6902';
import {SyncMonorepoPackagesError} from './error.js';
import {findPackageJsons, PACKAGE_JSON, walkUp} from './find-package.js';
import {PkgChangeResult} from './model.js';
import {pick} from './util.js';
const debug = createDebug('sync-monorepo-packages:sync-package');
/**
* These are the default fields synced from the monorepo root `package.json` to
* its sub-packages.
*/
export const DEFAULT_FIELDS = Object.freeze([
'keywords',
'author',
'repository',
'license',
'engines',
'publishConfig',
]);
const pluralize = (word, count) => (count === 1 ? word : `${word}s`);
const readPackageJson = async (pkgJsonPath) => {
debug('reading %s', pkgJsonPath);
return JSON.parse(await readFile(pkgJsonPath, 'utf8'));
};
const writePackageJson = async (pkgJsonPath, pkg) => {
await writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
};
/**
* Resolves the path to the source `package.json`. If a directory path is given,
* appends `package.json`. Returns `undefined` if no input is given.
*/
const resolveSourcePkgPath = (sourcePath) => {
if (!sourcePath) {
return undefined;
}
return basename(sourcePath) === PACKAGE_JSON
? sourcePath
: join(sourcePath, PACKAGE_JSON);
};
/**
* Synchronizes selected fields from a source `package.json` to all destination
* packages in a monorepo.
*
* Yields a {@link PkgChangeResult} for each package that has (or would have)
* been modified.
*
* @example
*
* ```ts
* for await (const change of syncPackageJsons({dryRun: true})) {
* console.log(change.toString());
* }
* ```
*
* @param opts - Sync options
*/
export const syncPackageJsons = async function* ({
dryRun = false,
fields = [...DEFAULT_FIELDS],
lerna: lernaJsonPath,
packages = [],
source: sourcePkgPathInput,
} = {}) {
let sourcePkgPath = resolveSourcePkgPath(sourcePkgPathInput);
if (!sourcePkgPath) {
sourcePkgPath = await walkUp(PACKAGE_JSON, process.cwd());
}
if (!sourcePkgPath) {
throw new SyncMonorepoPackagesError('Could not find source package.json');
}
debug('found source package.json at %s', sourcePkgPath);
const sourcePkg = await readPackageJson(sourcePkgPath);
const sourcePkgFields = pick(sourcePkg, ...fields);
for await (const destPkgPath of findPackageJsons({
lernaJsonPath,
packages,
sourcePkgPath,
})) {
const destPkg = await readPackageJson(destPkgPath);
const destPkgFields = pick(destPkg, ...fields);
const patch = createPatch(destPkgFields, sourcePkgFields);
if (!patch.length) {
continue;
}
// applyPatch mutates newPkg in place
const newPkg = {...destPkg};
applyPatch(newPkg, patch);
const result = new PkgChangeResult(destPkgPath, patch, destPkg, newPkg);
if (!dryRun) {
await writePackageJson(destPkgPath, newPkg);
}
yield result;
}
};
/**
* Summarizes the results of a {@link syncPackageJsons} operation.
*
* @param results - Array of {@link PkgChangeResult} values
* @returns A {@link Summary} describing what happened
*/
export const summarizePackageChanges = (results) => {
if (results.length) {
return {
success: `Synced ${results.length} package.json ${pluralize('file', results.length)}`,
};
}
return {noop: 'No package.json changes needed; everything up-to-date!'};
};
//# sourceMappingURL=sync-package.js.map