wireit
Version:
Upgrade your npm scripts to make them smarter and more efficient
164 lines • 6.6 kB
JavaScript
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'crypto';
import { createReadStream } from './util/fs.js';
import { glob } from './util/glob.js';
import { scriptReferenceToString } from './config.js';
/**
* The fingerprint of a script. Converts lazily between string and data object
* forms.
*/
export class Fingerprint {
static fromString(string) {
const fingerprint = new Fingerprint();
fingerprint.
return fingerprint;
}
/**
* Generate the fingerprint data object for a script based on its current
* configuration, input files, and the fingerprints of its dependencies.
*/
static async compute(script, dependencyFingerprints) {
let allDependenciesAreFullyTracked = true;
const filteredDependencyFingerprints = [];
for (const [dep, depFingerprint] of dependencyFingerprints) {
if (!dep.cascade) {
// cascade: false means the fingerprint of the dependency isn't
// directly inherited.
continue;
}
if (!depFingerprint.data.fullyTracked) {
allDependenciesAreFullyTracked = false;
}
filteredDependencyFingerprints.push([
scriptReferenceToString(dep.config),
depFingerprint.hash,
]);
}
let fileHashes;
if (script.files?.values.length) {
const files = await glob(script.files.values, {
cwd: script.packageDir,
followSymlinks: true,
// TODO(aomarks) This means that empty directories are not reflected in
// the fingerprint, however an empty directory could modify the behavior
// of a script. We should probably include empty directories; we'll just
// need special handling when we compute the fingerprint, because there
// is no hash we can compute.
includeDirectories: false,
// We must expand directories here, because we need the complete
// explicit list of files to hash.
expandDirectories: true,
throwIfOutsideCwd: false,
});
// TODO(aomarks) Instead of reading and hashing every input file on every
// build, use inode/mtime/ctime/size metadata (which is much faster to
// read) as a heuristic to detect files that have likely changed, and
// otherwise re-use cached hashes that we store in e.g.
// ".wireit/<script>/hashes".
const erroredFilePaths = [];
fileHashes = await Promise.all(files.map(async (file) => {
const absolutePath = file.path;
const hash = createHash('sha256');
try {
const stream = await createReadStream(absolutePath);
for await (const chunk of stream) {
hash.update(chunk);
}
}
catch (error) {
// It's possible for a file to be deleted between the
// time it is globbed and the time it is fingerprinted.
const { code } = error;
if (code !== /* does not exist */ 'ENOENT') {
throw error;
}
erroredFilePaths.push(absolutePath);
}
return [file.path, hash.digest('hex')];
}));
if (erroredFilePaths.length > 0) {
return {
ok: false,
error: {
type: 'failure',
reason: 'input-file-deleted-unexpectedly',
script: script,
filePaths: erroredFilePaths,
},
};
}
}
else {
fileHashes = [];
}
const fullyTracked =
// If any any dependency is not fully tracked, then we can't be either,
// because we can't know if there was an undeclared input that this script
// depends on.
allDependenciesAreFullyTracked &&
// A no-command script. Doesn't ever do anything itsef, so always fully
// tracked.
(script.command === undefined ||
// A service. Fully tracked if we know its inputs. Can't produce output.
(script.service !== undefined && script.files !== undefined) ||
// A standard script. Fully tracked if we know both its inputs and
// outputs.
(script.files !== undefined && script.output !== undefined));
const fingerprint = new Fingerprint();
// Note: The order of all fields is important so that we can do fast string
// comparison.
const data = {
fullyTracked,
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
command: script.command?.value,
extraArgs: script.extraArgs ?? [],
clean: script.clean,
files: Object.fromEntries(fileHashes.sort(([aFile], [bFile]) => aFile.localeCompare(bFile))),
output: script.output?.values ?? [],
dependencies: Object.fromEntries(filteredDependencyFingerprints.sort(([aRef], [bRef]) => aRef.localeCompare(bRef))),
service: script.service === undefined
? undefined
: {
readyWhen: {
lineMatches: script.service.readyWhen.lineMatches?.toString(),
},
},
env: script.env,
};
fingerprint.
return { ok: true, value: fingerprint };
}
get string() {
if (this.
this.
}
return this.
}
get data() {
if (this.
this.
}
return this.
}
get hash() {
if (this.
this.
.update(this.string)
.digest('hex');
}
return this.
}
equal(other) {
return this.string === other.string;
}
}
//# sourceMappingURL=fingerprint.js.map