jazelle/utils/find-changed-targets.js (158 lines of code) (raw):
// @flow
const {getManifest} = require('./get-manifest.js');
const {exec} = require('./node-helpers.js');
const {bazel} = require('./binary-paths.js');
const {getDownstreams} = require('../utils/get-downstreams.js');
const {exists, read} = require('../utils/node-helpers.js');
/*::
export type FindChangedTargetsArgs = {
root: string,
files?: string,
format?: string,
};
export type FindChangedTargets = (FindChangedTargetsArgs) => Promise<Array<string>>;
*/
const findChangedTargets /*: FindChangedTargets */ = async ({
root,
files,
format = 'targets',
}) => {
let {targets} = await findChangedBazelTargets({root, files});
if (format === 'dirs') {
targets = [
...targets.reduce((set, target) => {
// convert from target to dir path
set.add(target.slice(2, target.indexOf(':')));
return set;
}, new Set()),
];
}
return targets;
};
const scan = async (root, lines) => {
const result = await Promise.all(
lines.map(async file => [file, await exists(`${root}/${file}`)])
);
return [
result.filter(r => !r[1]).map(r => r[0]),
result.filter(r => r[1]).map(r => r[0]),
];
};
const batches = (q, size) => {
const result = [];
const copy = [...q];
while (copy.length) {
result.push(copy.splice(0, size));
}
return result;
};
const findChangedBazelTargets = async ({root, files}) => {
const bazelignore = await read(`${root}/.bazelignore`, 'utf8').catch(
() => ''
);
const ignored = bazelignore
.split('\n')
.filter(Boolean)
.filter(line => !line.endsWith('node_modules'))
.map(line => line.trim());
// if no file, fallback to reading from stdin (fd=0)
const data = await read(files || 0, 'utf8').catch(() => '');
const lines = data
.split('\n')
.filter(Boolean)
.map(line => line.trim())
.filter(line => !ignored.find(i => line.startsWith(i)));
const invalid = lines.find(line => line.includes(' '));
if (invalid) throw new Error(`File path cannot contain spaces: ${invalid}`);
const {projects, workspace} = await getManifest({root});
const opts = {cwd: root, maxBuffer: 1e8};
if (workspace === 'sandbox') {
if (lines.includes('WORKSPACE') || lines.includes('.bazelversion')) {
const cmd = `${bazel} query 'kind(".*_test rule", "...")'`;
const result = await exec(cmd, opts);
const unfiltered = result.split('\n').filter(Boolean);
const targets = unfiltered.filter(target => {
const path = target.replace(/\/\/(.+?):.+/, '$1');
return projects.includes(path);
});
return {workspace, targets};
} else {
/*
Separate files into two categories: files that exist and files that have been deleted
For files that have been deleted, try to recover some other file in the package
*/
const [missing, exists] = await scan(root, lines);
const recoveredMissing = await batch(root, missing, async file => {
const find = `${bazel} query "${file}"`;
const result = await exec(find, opts).catch(async e => {
// if file doesn't exist, find which package it would've belong to, and find another file in the same package
// doing so is sufficient, because we just want to find out which targets have changed
// - in the case the file was deleted but a package still exists, pkg will refer to the package
// - in the case the package itself was deleted, pkg will refer to the root package (which will typically yield no targets in a typical Jazelle setup)
const regex = /not declared in package '(.*?)'/;
const [, pkg = ''] = e.message.match(regex) || [];
if (pkg === '') return '';
const cmd = `${bazel} query 'kind("source file", //${pkg}:*)' | head -n 1`;
return exec(cmd).catch(() => '');
});
return result;
});
const queryables = [...exists, ...recoveredMissing];
const unfiltered = await batch(
root,
batches(queryables, 1000), // batching required, else E2BIG errors
async q => {
const innerQuery = q.join(' union ');
const cmd = `${bazel} query 'let graph = kind(".*_test rule", rdeps("...", ${innerQuery})) in $graph except filter("node_modules", $graph)' --output label`;
return exec(cmd, opts);
}
);
const targets = unfiltered.filter(target => {
const path = target.replace(/\/\/(.+?):.+/, '$1');
return projects.includes(path);
});
return {workspace, targets};
}
} else {
const allProjects = await Promise.all([
...projects.map(async dir => {
const meta = JSON.parse(
await read(`${root}/${dir}/package.json`, 'utf8')
);
return {dir, meta, depth: 1};
}),
]);
if (lines.includes('WORKSPACE')) {
const targets = [];
for (const project of projects) {
targets.push(
`//${project}:test`,
`//${project}:lint`,
`//${project}:flow`
);
}
return {workspace, targets};
} else {
const set = new Set();
if (lines.length > 0) {
for (const project of projects) {
for (const line of lines) {
if (line.startsWith(project)) set.add(project);
}
}
}
// Add to the changeSet all downstream packages that have a dependency
const changeSet = new Set(set);
for (const target of set) {
const dep = allProjects.find(project => project.dir === target);
if (dep) {
const downstreamDeps = getDownstreams(allProjects, dep);
for (const downstreamDep of downstreamDeps) {
changeSet.add(downstreamDep.dir);
}
}
}
const targets = [];
for (const project of changeSet) {
targets.push(
`//${project}:test`,
`//${project}:lint`,
`//${project}:flow`
);
}
return {workspace, targets};
}
}
};
async function batch(root, items, fn) {
const stdouts = await Promise.all(items.map(fn));
return [
...new Set(
stdouts
.map(r => r.trim())
.join('\n')
.split('\n')
),
].filter(Boolean);
}
module.exports = {findChangedTargets};