UNPKG

sync-monorepo-packages

Version:

Synchronize files and metadata across packages in a monorepo

170 lines (169 loc) 5.44 kB
import createDebug from 'debug'; import {constants} from 'node:fs'; import {copyFile, mkdir, stat} from 'node:fs/promises'; import {dirname, join, relative, resolve} from 'node:path'; import {SyncMonorepoPackagesError} from './error.js'; import { findDirectoriesByGlobs, findLernaConfig, findWorkspaces, } from './find-package.js'; import {FileCopyResult} from './model.js'; const debug = createDebug('sync-monorepo-packages:sync-file'); const pluralize = (word, count) => (count === 1 ? word : `${word}s`); /** * Synchronizes one or more source files to all packages in a monorepo. Yields a * {@link FileCopyResult} for each (source, destination) pair attempted. * * @example * * ```ts * for await (const result of syncFile(['README.md'], {dryRun: true})) { * console.log(result.toString()); * } * ``` * * @param files - Source file paths or globs (relative to `process.cwd()`) * @param opts - Sync options */ export const syncFile = async function* ( files = [], { cwd = process.cwd(), dryRun = false, force = false, lerna: lernaJsonPath, packages = [], } = {}, ) { debug( 'syncFile called with force: %s, packages: %O, files: %O', force, packages, files, ); if (!files.length) { throw new SyncMonorepoPackagesError('No files to sync!'); } const {glob} = await import('node:fs/promises'); // Expand file globs relative to process.cwd() const resolvedFiles = []; for (const filePattern of files) { const matched = []; for await (const entry of glob(filePattern, {cwd: process.cwd()})) { matched.push(entry); } if (!matched.length) { throw new SyncMonorepoPackagesError( `Could not find any files matching glob "${filePattern}"`, ); } resolvedFiles.push(...matched); } // Find package directories let packageDirs; let packagesCwd; if (packages.length) { packageDirs = packages; packagesCwd = cwd; } else { const lernaInfo = await findLernaConfig({cwd, lernaJsonPath}); const lernaPackageDirs = []; if (lernaInfo?.lernaConfig.packages?.length) { for await (const dir of findDirectoriesByGlobs( lernaInfo.lernaConfig.packages, lernaInfo.lernaRoot, )) { lernaPackageDirs.push(dir); } } const workspaces = await findWorkspaces(cwd); const workspaceDirs = []; for await (const dir of findDirectoriesByGlobs(workspaces, cwd)) { workspaceDirs.push(dir); } packageDirs = [...new Set([...lernaPackageDirs, ...workspaceDirs])]; packagesCwd = cwd; } for (const srcFilePath of resolvedFiles) { for (const packageDir of packageDirs) { const absPackageDir = resolve(packagesCwd, packageDir); const absCwd = resolve(process.cwd(), packagesCwd); const srcRelativeToCwd = relative(absCwd, srcFilePath); const absDest = join(absPackageDir, srcRelativeToCwd); const destPath = relative(process.cwd(), absDest); const copyInfo = new FileCopyResult(srcFilePath, destPath); debug( 'attempting to copy %s to %s (overwrite: %s)', copyInfo.from, copyInfo.to, force, ); if (dryRun) { if (!force) { try { await stat(destPath); // Destination exists; simulate the EEXIST failure. yield copyInfo.withError( new SyncMonorepoPackagesError( `Refusing to overwrite existing file ${destPath}; use --force to overwrite`, ), ); } catch (err) { if (err.code === 'ENOENT') { yield copyInfo.withSuccess(); } else { throw err; } } } else { yield copyInfo.withSuccess(); } } else { try { await mkdir(dirname(destPath), {recursive: true}); await copyFile( srcFilePath, destPath, force ? 0 : constants.COPYFILE_EXCL, ); yield copyInfo.withSuccess(); } catch (err) { if (err.code === 'EEXIST') { yield copyInfo.withError( new SyncMonorepoPackagesError( `Refusing to overwrite existing file ${destPath}; use --force to overwrite`, ), ); } else { throw err; } } } } } }; /** * Summarizes the results of one or more {@link syncFile} operations. * * @param results - Array of {@link FileCopyResult} values * @returns A {@link Summary} object describing what happened */ export const summarizeFileCopies = (results) => { const successes = results.filter((r) => r.success); const failures = results.filter((r) => r.err); if (!successes.length && !failures.length) { return {noop: 'No files copied.'}; } const summary = {}; if (successes.length) { const sources = new Set(successes.map((r) => r.from)); summary.success = `Copied ${sources.size} ${pluralize('file', sources.size)} to ${successes.length} ${pluralize('package', successes.length)}`; } if (failures.length) { const sources = new Set(failures.map((r) => r.from)); summary.fail = `Failed to copy ${sources.size} ${pluralize('file', sources.size)} to ${failures.length} ${pluralize('package', failures.length)}; use --verbose for details`; } return summary; }; //# sourceMappingURL=sync-file.js.map