80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
|
#!/usr/bin/env node
|
||
|
|
||
|
/**
|
||
|
* **Usage:**
|
||
|
* ```
|
||
|
* node check-depenencies <project-directory-path>
|
||
|
* ```
|
||
|
*
|
||
|
* Checks the non-local dependencies of the specified project and ensures that:
|
||
|
* - Exact versions (not version ranges) are specified in the project's `package.json`.
|
||
|
* This reduces the probability of installing a breaking version of a direct or transitive
|
||
|
* dependency, in case of an out-of-sync lockfile.
|
||
|
* - The project's lockfile (`yarn.lock`) is in-sync with `package.json` wrt these dependencies.
|
||
|
*
|
||
|
* If any of the above checks fails, the script will throw an error, otherwise it will complete
|
||
|
* successfully.
|
||
|
*
|
||
|
* **Context:**
|
||
|
* In order to keep integration tests on CI as determinitstic as possible, we need to ensure that
|
||
|
* the same dependencies (including transitive ones) are installed each time. One way to ensure that
|
||
|
* is using a lockfile (such as `yarn.lock`) to pin the dependencies to exact versions. This works
|
||
|
* as long as the lockfile itself is in-sync with the corresponding `package.json`, which specifies
|
||
|
* the dependencies.
|
||
|
*
|
||
|
* Ideally, we would run `yarn install` with the `--frozen-lockfile` option to verify that the
|
||
|
* lockfile is in-sync with `package.json`, but we cannot do that for integration projects, because
|
||
|
* we want to be able to install the locally built Angular packages). Therefore, we must manually
|
||
|
* esnure that the integration project lockfiles remain in-sync, which is error-prone.
|
||
|
*
|
||
|
* The checks performed by this script (although not full-proof) provide another line of defense
|
||
|
* against indeterminism caused by unpinned dependencies.
|
||
|
*/
|
||
|
'use strict';
|
||
|
|
||
|
const {parse: parseLockfile} = require('@yarnpkg/lockfile');
|
||
|
const {readFileSync} = require('fs');
|
||
|
const {resolve: resolvePath} = require('path');
|
||
|
|
||
|
const projectDir = resolvePath(process.argv[2]);
|
||
|
const pkgJsonPath = `${projectDir}/package.json`;
|
||
|
const lockfilePath = `${projectDir}/yarn.lock`;
|
||
|
|
||
|
console.log(`Checking dependencies for '${projectDir}'...`);
|
||
|
|
||
|
// Collect non-local dependencies (in `[name, version]` pairs).
|
||
|
// (Also ingore `git+https:` dependencies, because checking them is not straight-forward.)
|
||
|
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||
|
const deps = [
|
||
|
...Object.entries(pkgJson.dependencies || {}),
|
||
|
...Object.entries(pkgJson.devDependencies || {}),
|
||
|
].filter(([, version]) => !/^(?:file|git\+https):/.test(version));
|
||
|
|
||
|
// Check for dependencies with non-exact versions.
|
||
|
const nonExactDeps = deps.filter(([, version]) => !/^\d+\.\d+\.\d+(?:-\w+\.\d+)?$/.test(version));
|
||
|
|
||
|
if (nonExactDeps.length) {
|
||
|
throw new Error(
|
||
|
`The following dependencies in '${projectDir}' are not pinned to exact versions (of the ` +
|
||
|
'format X.Y.Z[-<pre-release-identifier>]):' +
|
||
|
nonExactDeps.map(([name, version]) => `\n ${name}: ${version}`));
|
||
|
}
|
||
|
|
||
|
// Check for dependencies that are not in-sync between `package.json` and the lockfile.
|
||
|
const {object: parsedLockfile} = parseLockfile(readFileSync(lockfilePath, 'utf8'));
|
||
|
const outOfSyncDeps = deps
|
||
|
.map(([depName, pkgJsonVersion]) => [
|
||
|
depName,
|
||
|
pkgJsonVersion,
|
||
|
(parsedLockfile[`${depName}@${pkgJsonVersion}`] || {}).version,
|
||
|
])
|
||
|
.filter(([, pkgJsonVersion, lockfileVersion]) => pkgJsonVersion !== lockfileVersion);
|
||
|
|
||
|
if (outOfSyncDeps.length) {
|
||
|
throw new Error(
|
||
|
`The following dependencies in '${projectDir}' are out-of-sync between 'package.json' and ` +
|
||
|
'the lockfile:' +
|
||
|
outOfSyncDeps.map(([name, pkgJsonVersion, lockfileVersion]) =>
|
||
|
`\n ${name}: ${pkgJsonVersion} vs ${lockfileVersion}`));
|
||
|
}
|