Created
February 28, 2025 14:09
-
-
Save dmitry-stepanenko/af297def0340f091a05cd6be21a0532f to your computer and use it in GitHub Desktop.
nx-run-touched
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { execSync } from 'child_process'; | |
import { output } from '@nx/devkit'; | |
import yargs from 'yargs'; | |
import { hideBin } from 'yargs/helpers'; | |
import { | |
ensureHeadAndBaseAreSet, | |
findClosestProjectByFilePath, | |
getFilesUsingBaseAndHead, | |
getUncommittedFiles, | |
getUntrackedFiles, | |
} from './utils.js'; | |
// Can be used as `node nx-run-touched.js -t lint` to run only those projects that were modified between revisions | |
const argv = yargs(hideBin(process.argv)) | |
.option('parallel', { | |
alias: 'p', | |
type: 'number', | |
description: 'Max number of parallel processes [default is 3].', | |
}) | |
.option('nxBail', { | |
type: 'boolean', | |
description: 'Stop execution after the first failed task', | |
}) | |
.option('exclude', { | |
type: 'string', | |
description: 'Exclude certain projects from being processed.', | |
}) | |
.option('skipNxCache', { | |
type: 'boolean', | |
description: | |
'Rerun the tasks even when the results are available in the cache.', | |
}) | |
.option('configuration', { | |
alias: 'c', | |
type: 'string', | |
description: | |
'This is the configuration to use when performing tasks on projects.', | |
}) | |
.option('base', { | |
describe: 'Base of the current branch (usually master).', | |
type: 'string', | |
}) | |
.option('head', { | |
describe: 'Latest commit of the current branch (usually HEAD).', | |
type: 'string', | |
}) | |
.option('target', { | |
alias: 't', | |
type: 'string', | |
description: 'Specify the target task', | |
demandOption: true, | |
}) | |
.help().argv; | |
ensureHeadAndBaseAreSet(argv); | |
const optionalArgs = [ | |
argv.nxBail ? '--nxBail' : '', | |
argv.skipNxCache ? '--skipNxCache' : '', | |
argv.parallel && '--parallel=' + argv.parallel, | |
argv.exclude && '--exclude=' + argv.exclude, | |
argv.configuration && '--configuration=' + argv.configuration, | |
]; | |
const touchedFiles = Array.from( | |
new Set([ | |
...getFilesUsingBaseAndHead(argv.base, argv.head), | |
...getUncommittedFiles(), | |
...getUntrackedFiles(), | |
]) | |
); | |
const projects = [ | |
...new Set( | |
touchedFiles.map((f) => findClosestProjectByFilePath(f)).filter(Boolean) | |
), | |
]; | |
if (!projects.length) { | |
output.warn({ title: 'No projects to run' }); | |
process.exit(0); | |
} | |
execSync( | |
`npx nx run-many --target=${argv.target} --base=${argv.base} --head=${argv.head} --projects=${projects.join(',')} ${optionalArgs.join(' ')}`.trim(), | |
{ stdio: ['inherit', 'inherit', 'inherit'] } | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { execSync } from 'child_process'; | |
import { existsSync, readFileSync } from 'fs'; | |
import { dirname, join, resolve } from 'path'; | |
import { output, workspaceRoot } from '@nx/devkit'; | |
// mostly taken from https://github.com/nrwl/nx/blob/master/packages/nx/src/utils/command-line-utils.ts | |
const TEN_MEGABYTES = 1024 * 10000; | |
const knownProjectsByPath = new Map(); | |
export function findClosestProjectByFilePath(filePath) { | |
filePath = resolve(workspaceRoot, filePath); | |
let currentDir = dirname(filePath); | |
while (currentDir && currentDir.startsWith(workspaceRoot)) { | |
const projectJsonPath = join(currentDir, 'project.json'); | |
if (knownProjectsByPath.has(projectJsonPath)) { | |
return knownProjectsByPath.get(projectJsonPath); | |
} | |
if (existsSync(projectJsonPath)) { | |
const projectName = JSON.parse( | |
readFileSync(projectJsonPath, 'utf-8') | |
).name; | |
knownProjectsByPath.set(projectJsonPath, projectName); | |
return projectName; | |
} | |
const parentDir = dirname(currentDir); | |
if (parentDir === currentDir) break; // Stop if reached root | |
currentDir = parentDir; | |
} | |
return null; | |
} | |
export function ensureHeadAndBaseAreSet(args) { | |
args.head ??= 'HEAD'; | |
if (!args.base) { | |
args.base = getBaseRef(); | |
output.note({ | |
title: `Affected criteria defaulted to --base=${output.bold( | |
`${args.base}` | |
)} --head=${output.bold(args.head)}`, | |
}); | |
} | |
if (!args.base) { | |
throw new Error('Failed to resolve "base" ref'); | |
} | |
args.base = getMergeBase(args.base, args.head); | |
} | |
export function getBaseRef() { | |
// we don't really need to be flexible here, just return our base | |
return 'master'; | |
} | |
export function getMergeBase(base, head = 'HEAD') { | |
try { | |
return execSync(`git merge-base "${base}" "${head}"`, { | |
maxBuffer: TEN_MEGABYTES, | |
cwd: workspaceRoot, | |
stdio: 'pipe', | |
windowsHide: false, | |
}) | |
.toString() | |
.trim(); | |
} catch { | |
try { | |
return execSync(`git merge-base --fork-point "${base}" "${head}"`, { | |
maxBuffer: TEN_MEGABYTES, | |
cwd: workspaceRoot, | |
stdio: 'pipe', | |
windowsHide: false, | |
}) | |
.toString() | |
.trim(); | |
} catch { | |
return base; | |
} | |
} | |
} | |
export function getFilesUsingBaseAndHead(base, head) { | |
return parseGitOutput( | |
`git diff --name-only --no-renames --relative "${base}" "${head}"` | |
); | |
} | |
export function getUncommittedFiles() { | |
return parseGitOutput(`git diff --name-only --no-renames --relative HEAD .`); | |
} | |
export function getUntrackedFiles() { | |
return parseGitOutput(`git ls-files --others --exclude-standard`); | |
} | |
export function parseGitOutput(command) { | |
return execSync(command, { | |
maxBuffer: TEN_MEGABYTES, | |
cwd: workspaceRoot, | |
windowsHide: false, | |
}) | |
.toString('utf-8') | |
.split('\n') | |
.map((a) => a.trim()) | |
.filter((a) => a.length > 0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment