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}`));
							 | 
						||
| 
								 | 
							
								}
							 |