UNPKG

monorepo-next

Version:

Detach monorepo packages from normal linking

247 lines (202 loc) 6.71 kB
'use strict'; const buildDAG = require('./build-dag'); const { git, getLinesFromOutput, isCommitAncestorOf, getCommonAncestor, getCommitSinceLastRelease, } = require('./git'); const { collectPackages } = require('./build-dep-graph'); const { isSubDir } = require('./path'); const { getChangedReleasableFiles } = require('./releasable'); const Set = require('superset'); const { loadPackageConfig } = require('./config'); const path = require('path'); async function getPackageChangedFiles({ fromCommit, toCommit, packageCwd, // I had a feeling it took more time to spawn git in a loop // than the benefit you get by getting a git diff per-package, // especially when you are using a `fromCommit` instead of version tags. // In a test of mine, this brought `buildChangeGraph` // down to 25 seconds from 39 seconds. shouldRunPerPackage = true, shouldExcludeDeleted, options, }) { // Be careful you don't accidentally use `...` instead of `..`. // `...` finds the merge-base and uses that instead of `fromCommit`. // https://stackoverflow.com/a/60496462 // // I tried using ls-tree instead of diff when it is a new package (fromCommit is first commit in repo), // but it took the same amount of time. let committedChanges = await git(['diff', '--name-status', `${fromCommit}..${toCommit}`, ...shouldRunPerPackage ? [packageCwd] : []], options); committedChanges = getLinesFromOutput(committedChanges).reduce((committedChanges, line) => { let isDeleted = line[0] === 'D'; let isRename = line[0] === 'R'; let shouldExclude = shouldExcludeDeleted && (isDeleted || isRename); line = line.split('\t'); if (!shouldExclude) { committedChanges.add(line[1]); } if (isRename) { // renames are denoted by `R[01 - 100] <old filename> <new filename>` so we need to grab <new filename> as well` committedChanges.add(line[2]); } return committedChanges; }, new Set()); let dirtyChanges = await git(['status', '--porcelain', '--untracked-files', ...shouldRunPerPackage ? [packageCwd] : []], options); dirtyChanges = getLinesFromOutput(dirtyChanges).reduce((dirtyChanges, line) => { let isDeleted = line[1] === 'D'; let shouldExclude = shouldExcludeDeleted && isDeleted; line = line.substr(3); // if filename has space like `sample index.js`, if its modified and uncommited, that file will have double quotes in git status // example: '"packages/package-a/sample index.js"'. We need to strip `"` for that reason. line = line.replaceAll('"', ''); if (shouldExclude) { committedChanges.delete(line); } else { dirtyChanges.add(line); } return dirtyChanges; }, new Set()); let changedFiles = committedChanges.union(dirtyChanges); if (!shouldRunPerPackage) { let packageChangedFiles = new Set(); let relativePackageCwd = path.relative(options.cwd, packageCwd); for (let changedFile of changedFiles) { if (!changedFile.startsWith(relativePackageCwd)) { continue; } packageChangedFiles.add(changedFile); } changedFiles = packageChangedFiles; } return Array.from(changedFiles); } function crawlDag(dag, packagesWithChanges) { for (let group of dag.node.dependents) { if (packagesWithChanges[group.node.packageName]) { continue; } packagesWithChanges[group.node.packageName] = { changedFiles: [], changedReleasableFiles: [], dag: group, }; if (group.dependencyType !== 'devDependencies') { crawlDag(group, packagesWithChanges); } } } async function buildChangeGraph({ workspaceMeta, shouldOnlyIncludeReleasable, shouldExcludeDevChanges, shouldExcludeDeleted, fromCommit, fromCommitIfNewer, toCommit = 'HEAD', sinceBranch, cached, }) { let packagesWithChanges = {}; let sinceBranchCommit; let packagePaths = Object.values(workspaceMeta.packages).map(({ cwd }) => { return path.relative(workspaceMeta.cwd, cwd); }); for (let _package of collectPackages(workspaceMeta)) { if (!_package.packageName || !_package.version) { continue; } let nextConfig = await loadPackageConfig(_package.cwd); if (!nextConfig.shouldBumpVersion) { continue; } let _fromCommit; if (fromCommit) { _fromCommit = fromCommit; } else if (sinceBranch) { if (!sinceBranchCommit) { sinceBranchCommit = await getCommonAncestor(toCommit, sinceBranch, { cwd: workspaceMeta.cwd, cached, }); } _fromCommit = sinceBranchCommit; } else { _fromCommit = await getCommitSinceLastRelease(_package, { cwd: workspaceMeta.cwd, cached, }); } if (fromCommitIfNewer) { let [ isNewerThanTagCommit, isInSameBranch, ] = await Promise.all([ isCommitAncestorOf(_fromCommit, fromCommitIfNewer, { cwd: workspaceMeta.cwd, cached, }), isCommitAncestorOf(fromCommitIfNewer, toCommit, { cwd: workspaceMeta.cwd, cached, }), ]); if (isNewerThanTagCommit && isInSameBranch) { _fromCommit = fromCommitIfNewer; } } let changedFiles = await getPackageChangedFiles({ fromCommit: _fromCommit, toCommit, packageCwd: _package.cwd, shouldRunPerPackage: false, shouldExcludeDeleted, options: { cwd: workspaceMeta.cwd, cached, }, }); let newFiles = changedFiles; // remove package changes from the workspace root's changed files if (_package.cwd === workspaceMeta.cwd) { newFiles = newFiles.filter(file => { return !packagePaths.some((packagePath) => { return isSubDir(packagePath, file); }); }); } if (!newFiles.length) { continue; } let changedReleasableFiles = await getChangedReleasableFiles({ changedFiles: newFiles, packageCwd: _package.cwd, workspacesCwd: workspaceMeta.cwd, shouldExcludeDevChanges, fromCommit: _fromCommit, nextConfig, }); if (shouldOnlyIncludeReleasable && !changedReleasableFiles.length) { continue; } let dag = buildDAG(workspaceMeta, _package.packageName); packagesWithChanges[_package.packageName] = { changedFiles: newFiles, changedReleasableFiles, dag, }; } for (let { dag, changedReleasableFiles } of Object.values(packagesWithChanges)) { if (!changedReleasableFiles.length) { continue; } crawlDag(dag, packagesWithChanges); } return Object.values(packagesWithChanges); } module.exports = buildChangeGraph;