UNPKG

nx

Version:

Smart, Fast and Extensible Build System

485 lines • 20.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.filterUsingGlobPatterns = exports.expandNamedInput = exports.splitInputsIntoSelfAndDependencies = exports.Hasher = void 0; const tslib_1 = require("tslib"); const child_process_1 = require("child_process"); const minimatch = require("minimatch"); const typescript_1 = require("../utils/typescript"); const hashing_impl_1 = require("./hashing-impl"); const fileutils_1 = require("../utils/fileutils"); const path_1 = require("../utils/path"); /** * The default hasher used by executors. */ class Hasher { constructor(projectGraph, nxJson, options, hashing = undefined) { var _a, _b; this.projectGraph = projectGraph; this.nxJson = nxJson; this.options = options; if (!hashing) { this.hashing = hashing_impl_1.defaultHashing; } else { // this is only used for testing this.hashing = hashing; } const legacyRuntimeInputs = (this.options && this.options.runtimeCacheInputs ? this.options.runtimeCacheInputs : []).map((r) => ({ runtime: r })); const legacyFilesetInputs = [ ...Object.keys((_a = this.nxJson.implicitDependencies) !== null && _a !== void 0 ? _a : {}), 'nx.json', //TODO: vsavkin move the special cases into explicit ts support 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', // ignore files will change the set of inputs to the hasher '.gitignore', '.nxignore', ].map((d) => ({ fileset: `{workspaceRoot}/${d}` })); this.taskHasher = new TaskHasher(nxJson, legacyRuntimeInputs, legacyFilesetInputs, this.projectGraph, this.readTsConfig(), this.hashing, { selectivelyHashTsConfig: (_b = this.options.selectivelyHashTsConfig) !== null && _b !== void 0 ? _b : false }); } hashTask(task) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const res = yield this.taskHasher.hashTask(task, [task.target.project]); const command = this.hashCommand(task); return { value: this.hashArray([res.value, command]), details: { command, nodes: res.details, implicitDeps: {}, runtime: {}, }, }; }); } hashDependsOnOtherTasks(task) { return false; } /** * @deprecated use hashTask instead */ hashTaskWithDepsAndContext(task) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return this.hashTask(task); }); } /** * @deprecated hashTask will hash runtime inputs and global files */ hashContext() { return tslib_1.__awaiter(this, void 0, void 0, function* () { return { implicitDeps: '', runtime: '', }; }); } hashCommand(task) { var _a, _b, _c; const overrides = Object.assign({}, task.overrides); delete overrides['__overrides_unparsed__']; const sortedOverrides = {}; for (let k of Object.keys(overrides).sort()) { sortedOverrides[k] = overrides[k]; } return this.hashing.hashArray([ (_a = task.target.project) !== null && _a !== void 0 ? _a : '', (_b = task.target.target) !== null && _b !== void 0 ? _b : '', (_c = task.target.configuration) !== null && _c !== void 0 ? _c : '', JSON.stringify(sortedOverrides), ]); } /** * @deprecated use hashTask */ hashSource(task) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const hash = yield this.taskHasher.hashTask(task, [task.target.project]); return hash.details[`${task.target.project}:$filesets`]; }); } hashArray(values) { return this.hashing.hashArray(values); } hashFile(path) { return this.hashing.hashFile(path); } readTsConfig() { var _a; var _b; try { const res = (0, fileutils_1.readJsonFile)((0, typescript_1.getRootTsConfigFileName)()); (_a = (_b = res.compilerOptions).paths) !== null && _a !== void 0 ? _a : (_b.paths = {}); return res; } catch (_c) { return { compilerOptions: { paths: {} }, }; } } } exports.Hasher = Hasher; Hasher.version = '3.0'; const DEFAULT_INPUTS = [ { projects: 'self', fileset: '{projectRoot}/**/*', }, { projects: 'dependencies', input: 'default', }, ]; class TaskHasher { constructor(nxJson, legacyRuntimeInputs, legacyFilesetInputs, projectGraph, tsConfigJson, hashing, options) { this.nxJson = nxJson; this.legacyRuntimeInputs = legacyRuntimeInputs; this.legacyFilesetInputs = legacyFilesetInputs; this.projectGraph = projectGraph; this.tsConfigJson = tsConfigJson; this.hashing = hashing; this.options = options; this.filesetHashes = {}; this.runtimeHashes = {}; } hashTask(task, visited) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return Promise.resolve().then(() => tslib_1.__awaiter(this, void 0, void 0, function* () { const projectNode = this.projectGraph.nodes[task.target.project]; if (!projectNode) { return this.hashExternalDependency(task.target.project); } const namedInputs = Object.assign(Object.assign({ default: [{ fileset: '{projectRoot}/**/*' }] }, this.nxJson.namedInputs), projectNode.data.namedInputs); const targetData = projectNode.data.targets[task.target.target]; const targetDefaults = (this.nxJson.targetDefaults || {})[task.target.target]; const { selfInputs, depsInputs } = splitInputsIntoSelfAndDependencies(targetData.inputs || (targetDefaults === null || targetDefaults === void 0 ? void 0 : targetDefaults.inputs) || DEFAULT_INPUTS, namedInputs); return this.hashSelfAndDepsInputs(task.target.project, 'default', selfInputs, depsInputs, visited); })); }); } hashNamedInput(projectName, namedInput, visited) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const projectNode = this.projectGraph.nodes[projectName]; if (!projectNode) { return this.hashExternalDependency(projectName); } const namedInputs = Object.assign(Object.assign({ default: [{ fileset: '{projectRoot}/**/*' }] }, this.nxJson.namedInputs), projectNode.data.namedInputs); const selfInputs = expandNamedInput(namedInput, namedInputs); const depsInputs = [{ input: namedInput }]; return this.hashSelfAndDepsInputs(projectName, namedInput, selfInputs, depsInputs, visited); }); } hashSelfAndDepsInputs(projectName, namedInput, selfInputs, depsInputs, visited) { var _a; return tslib_1.__awaiter(this, void 0, void 0, function* () { const projectGraphDeps = (_a = this.projectGraph.dependencies[projectName]) !== null && _a !== void 0 ? _a : []; const self = yield this.hashSelfInputs(projectName, namedInput, selfInputs); const deps = yield this.hashDepsInputs(depsInputs, projectGraphDeps, visited); let details = {}; for (const s of self) { details = Object.assign(Object.assign({}, details), s.details); } for (const s of deps) { details = Object.assign(Object.assign({}, details), s.details); } const value = this.hashing.hashArray([ ...self.map((d) => d.value), ...deps.map((d) => d.value), ]); return { value, details }; }); } hashDepsInputs(inputs, projectGraphDeps, visited) { return tslib_1.__awaiter(this, void 0, void 0, function* () { return (yield Promise.all(inputs.map((input) => tslib_1.__awaiter(this, void 0, void 0, function* () { return yield Promise.all(projectGraphDeps.map((d) => tslib_1.__awaiter(this, void 0, void 0, function* () { if (visited.indexOf(d.target) > -1) { return null; } else { visited.push(d.target); return yield this.hashNamedInput(d.target, input.input || 'default', visited); } }))); })))) .flat() .filter((r) => !!r); }); } hashExternalDependency(projectName) { var _a; const n = this.projectGraph.externalNodes[projectName]; const version = (_a = n === null || n === void 0 ? void 0 : n.data) === null || _a === void 0 ? void 0 : _a.version; let hash; if (version) { hash = this.hashing.hashArray([version]); } else { // unknown dependency // this may occur if a file has a dependency to a npm package // which is not directly registestered in package.json // but only indirectly through dependencies of registered // npm packages // when it is at a later stage registered in package.json // the cache project graph will not know this module but // the new project graph will know it // The actual checksum added here is of no importance as // the version is unknown and may only change when some // other change occurs in package.json and/or package-lock.json hash = `__${projectName}__`; } return { value: hash, details: { [projectName]: version || hash, }, }; } hashSelfInputs(projectName, namedInput, inputs) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const filesets = inputs .filter((r) => !!r['fileset']) .map((r) => r['fileset']); const projectFilesets = []; const workspaceFilesets = []; let invalidFilesetNoPrefix = null; let invalidFilesetWorkspaceRootNegative = null; for (let f of filesets) { if (f.startsWith('{projectRoot}/') || f.startsWith('!{projectRoot}/')) { projectFilesets.push(f); } else if (f.startsWith('{workspaceRoot}/') || f.startsWith('!{workspaceRoot}/')) { workspaceFilesets.push(f); } else { invalidFilesetNoPrefix = f; } } if (invalidFilesetNoPrefix) { throw new Error([ `"${invalidFilesetNoPrefix}" is an invalid fileset.`, 'All filesets have to start with either {workspaceRoot} or {projectRoot}.', 'For instance: "!{projectRoot}/**/*.spec.ts" or "{workspaceRoot}/package.json".', `If "${invalidFilesetNoPrefix}" is a named input, make sure it is defined in, for instance, nx.json.`, ].join('\n')); } if (invalidFilesetWorkspaceRootNegative) { throw new Error([ `"${invalidFilesetWorkspaceRootNegative}" is an invalid fileset.`, 'It is not possible to negative filesets starting with {workspaceRoot}.', ].join('\n')); } const notFilesets = inputs.filter((r) => !r['fileset']); return Promise.all([ this.hashProjectFileset(projectName, namedInput, projectFilesets), ...[ ...workspaceFilesets, ...this.legacyFilesetInputs.map((r) => r.fileset), ].map((fileset) => this.hashRootFileset(fileset)), ...[...notFilesets, ...this.legacyRuntimeInputs].map((r) => r['runtime'] ? this.hashRuntime(r['runtime']) : this.hashEnv(r['env'])), ]); }); } hashRootFileset(fileset) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const mapKey = fileset; const withoutWorkspaceRoot = fileset.substring(16); if (!this.filesetHashes[mapKey]) { this.filesetHashes[mapKey] = new Promise((res) => tslib_1.__awaiter(this, void 0, void 0, function* () { const parts = []; if (fileset.indexOf('*') > -1) { this.projectGraph.allWorkspaceFiles .filter((f) => minimatch(f.file, withoutWorkspaceRoot)) .forEach((f) => { parts.push(f.hash); }); } else { const matchingFile = this.projectGraph.allWorkspaceFiles.find((t) => t.file === withoutWorkspaceRoot); if (matchingFile) { parts.push(matchingFile.hash); } } const value = this.hashing.hashArray(parts); res({ value, details: { [mapKey]: value }, }); })); } return this.filesetHashes[mapKey]; }); } hashProjectFileset(projectName, namedInput, filesetPatterns) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const mapKey = `${projectName}:$filesets:${namedInput}`; if (!this.filesetHashes[mapKey]) { this.filesetHashes[mapKey] = new Promise((res) => tslib_1.__awaiter(this, void 0, void 0, function* () { const p = this.projectGraph.nodes[projectName]; const filesetWithExpandedProjectRoot = filesetPatterns.map((f) => f.replace('{projectRoot}', p.data.root)); const filteredFiles = filterUsingGlobPatterns(p.data.root, p.data.files, filesetWithExpandedProjectRoot); const fileNames = filteredFiles.map((f) => f.file); const values = filteredFiles.map((f) => f.hash); let tsConfig; tsConfig = this.hashTsConfig(p); const value = this.hashing.hashArray([ ...fileNames, ...values, JSON.stringify(Object.assign(Object.assign({}, p.data), { files: undefined })), tsConfig, ]); res({ value, details: { [mapKey]: value }, }); })); } return this.filesetHashes[mapKey]; }); } hashRuntime(runtime) { return tslib_1.__awaiter(this, void 0, void 0, function* () { const mapKey = `runtime:${runtime}`; if (!this.runtimeHashes[mapKey]) { this.runtimeHashes[mapKey] = new Promise((res, rej) => { (0, child_process_1.exec)(runtime, (err, stdout, stderr) => { if (err) { rej(new Error(`Nx failed to execute {runtime: '${runtime}'}. ${err}.`)); } else { const value = `${stdout}${stderr}`.trim(); res({ details: { [`runtime:${runtime}`]: value }, value, }); } }); }); } return this.runtimeHashes[mapKey]; }); } hashEnv(envVarName) { var _a; return tslib_1.__awaiter(this, void 0, void 0, function* () { const value = this.hashing.hashArray([(_a = process.env[envVarName]) !== null && _a !== void 0 ? _a : '']); return { details: { [`env:${envVarName}`]: value }, value, }; }); } hashTsConfig(p) { if (this.options.selectivelyHashTsConfig) { return this.removeOtherProjectsPathRecords(p); } else { return JSON.stringify(this.tsConfigJson); } } removeOtherProjectsPathRecords(p) { var _a, _b; const _c = this.tsConfigJson.compilerOptions, { paths } = _c, compilerOptions = tslib_1.__rest(_c, ["paths"]); const rootPath = p.data.root.split('/'); rootPath.shift(); const pathAlias = (0, path_1.getImportPath)((_a = this.nxJson) === null || _a === void 0 ? void 0 : _a.npmScope, rootPath.join('/')); return JSON.stringify({ compilerOptions: Object.assign(Object.assign({}, compilerOptions), { paths: { [pathAlias]: (_b = paths[pathAlias]) !== null && _b !== void 0 ? _b : [], } }), }); } } function splitInputsIntoSelfAndDependencies(inputs, namedInputs) { const depsInputs = []; const selfInputs = []; for (const d of inputs) { if (typeof d === 'string') { if (d.startsWith('^')) { depsInputs.push({ input: d.substring(1) }); } else { selfInputs.push(d); } } else { if (d.projects === 'dependencies') { depsInputs.push(d); } else { selfInputs.push(d); } } } return { depsInputs, selfInputs: expandSelfInputs(selfInputs, namedInputs) }; } exports.splitInputsIntoSelfAndDependencies = splitInputsIntoSelfAndDependencies; function expandSelfInputs(inputs, namedInputs) { const expanded = []; for (const d of inputs) { if (typeof d === 'string') { if (d.startsWith('^')) throw new Error(`namedInputs definitions cannot start with ^`); if (namedInputs[d]) { expanded.push(...expandNamedInput(d, namedInputs)); } else { expanded.push({ fileset: d }); } } else { if (d.projects === 'dependencies') { throw new Error(`namedInputs definitions cannot contain any inputs with projects == 'dependencies'`); } if (d.fileset || d.env || d.runtime) { expanded.push(d); } else { expanded.push(...expandNamedInput(d.input, namedInputs)); } } } return expanded; } function expandNamedInput(input, namedInputs) { namedInputs || (namedInputs = {}); if (!namedInputs[input]) throw new Error(`Input '${input}' is not defined`); return expandSelfInputs(namedInputs[input], namedInputs); } exports.expandNamedInput = expandNamedInput; function filterUsingGlobPatterns(projectRoot, files, patterns) { const positive = []; const negative = []; for (const p of patterns) { if (p.startsWith('!')) { negative.push(p); } else { positive.push(p); } } if (positive.length === 0 && negative.length === 0) { return files; } return files.filter((f) => { let matchedPositive = false; if (positive.length === 0 || (positive.length === 1 && positive[0] === `${projectRoot}/**/*`)) { matchedPositive = true; } else { matchedPositive = positive.some((pattern) => minimatch(f.file, pattern)); } if (!matchedPositive) return false; return negative.every((pattern) => minimatch(f.file, pattern)); }); } exports.filterUsingGlobPatterns = filterUsingGlobPatterns; //# sourceMappingURL=hasher.js.map