ci(aio): use custom package.json to run with local distributables (#19511)
Closes #19388 PR Close #19511
This commit is contained in:

committed by
Tobias Bosch

parent
9fe6363575
commit
d1a00459a8
241
aio/tools/ng-packages-installer/index.js
Normal file
241
aio/tools/ng-packages-installer/index.js
Normal file
@ -0,0 +1,241 @@
|
||||
'use strict';
|
||||
|
||||
const chalk = require('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
const yargs = require('yargs');
|
||||
|
||||
const PACKAGE_JSON = 'package.json';
|
||||
const LOCKFILE = 'yarn.lock';
|
||||
const LOCAL_MARKER_PATH = 'node_modules/_local_.json';
|
||||
const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/;
|
||||
|
||||
const ANGULAR_ROOT_DIR = path.resolve(__dirname, '../../..');
|
||||
const ANGULAR_DIST_PACKAGES = path.resolve(ANGULAR_ROOT_DIR, 'dist/packages-dist');
|
||||
|
||||
/**
|
||||
* A tool that can install Angular dependencies for a project from NPM or from the
|
||||
* locally built distributables.
|
||||
*
|
||||
* This tool is used to change dependencies of the `aio` application and the example
|
||||
* applications.
|
||||
*/
|
||||
class NgPackagesInstaller {
|
||||
|
||||
/**
|
||||
* Create a new installer for a project in the specified directory.
|
||||
*
|
||||
* @param {string} projectDir - the path to the directory containing the project.
|
||||
* @param {object} options - a hash of options for the install
|
||||
* * `debug` (`boolean`) - whether to display debug messages.
|
||||
* * `force` (`boolean`) - whether to force a local installation
|
||||
* even if there is a local marker file.
|
||||
* * `ignorePackages` (`string[]`) - a collection of names of packages
|
||||
* that should not be copied over.
|
||||
*/
|
||||
constructor(projectDir, options = {}) {
|
||||
this.debug = options.debug;
|
||||
this.force = options.force;
|
||||
this.projectDir = path.resolve(projectDir);
|
||||
this.localMarkerPath = path.resolve(this.projectDir, LOCAL_MARKER_PATH);
|
||||
|
||||
this._log('Project directory:', this.projectDir);
|
||||
}
|
||||
|
||||
// Public methods
|
||||
|
||||
/**
|
||||
* Check whether the dependencies have been overridden with locally built
|
||||
* Angular packages. This is done by checking for the `_local_.json` marker file.
|
||||
* This will emit a warning to the console if the dependencies have been overridden.
|
||||
*/
|
||||
checkDependencies() {
|
||||
if (this._checkLocalMarker()) {
|
||||
this._printWarning();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Install locally built Angular dependencies, overriding the dependencies in the package.json
|
||||
* This will also write a "marker" file (`_local_.json`), which contains the overridden package.json
|
||||
* contents and acts as an indicator that dependencies have been overridden.
|
||||
*/
|
||||
installLocalDependencies() {
|
||||
if (this._checkLocalMarker() !== true || this.force) {
|
||||
const pathToPackageConfig = path.resolve(this.projectDir, PACKAGE_JSON);
|
||||
const packages = this._getDistPackages();
|
||||
const packageConfigFile = fs.readFileSync(pathToPackageConfig);
|
||||
const packageConfig = JSON.parse(packageConfigFile);
|
||||
|
||||
const [dependencies, peers] = this._collectDependencies(packageConfig.dependencies || {}, packages);
|
||||
const [devDependencies, devPeers] = this._collectDependencies(packageConfig.devDependencies || {}, packages);
|
||||
|
||||
this._assignPeerDependencies(peers, dependencies, devDependencies);
|
||||
this._assignPeerDependencies(devPeers, dependencies, devDependencies);
|
||||
|
||||
const localPackageConfig = Object.assign(Object.create(null), packageConfig, { dependencies, devDependencies });
|
||||
localPackageConfig.__angular = { local: true };
|
||||
const localPackageConfigJson = JSON.stringify(localPackageConfig, null, 2);
|
||||
|
||||
try {
|
||||
this._log(`Writing temporary local ${PACKAGE_JSON} to ${pathToPackageConfig}`);
|
||||
fs.writeFileSync(pathToPackageConfig, localPackageConfigJson);
|
||||
this._installDeps('--no-lockfile', '--check-files');
|
||||
this._setLocalMarker(localPackageConfigJson);
|
||||
} finally {
|
||||
this._log(`Restoring original ${PACKAGE_JSON} to ${pathToPackageConfig}`);
|
||||
fs.writeFileSync(pathToPackageConfig, packageConfigFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstall the original package.json depdendencies
|
||||
* Yarn will also delete the local marker file for us.
|
||||
*/
|
||||
restoreNpmDependencies() {
|
||||
this._installDeps('--check-files');
|
||||
}
|
||||
|
||||
// Protected helpers
|
||||
|
||||
_assignPeerDependencies(peerDependencies, dependencies, devDependencies) {
|
||||
Object.keys(peerDependencies).forEach(key => {
|
||||
// If there is already an equivalent dependency then override it - otherwise assign/override the devDependency
|
||||
if (dependencies[key]) {
|
||||
this._log(`Overriding dependency with peerDependency: ${key}: ${peerDependencies[key]}`);
|
||||
dependencies[key] = peerDependencies[key];
|
||||
} else {
|
||||
this._log(`${devDependencies[key] ? 'Overriding' : 'Assigning'} devDependency with peerDependency: ${key}: ${peerDependencies[key]}`);
|
||||
devDependencies[key] = peerDependencies[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_collectDependencies(dependencies, packages) {
|
||||
const peerDependencies = Object.create(null);
|
||||
const mergedDependencies = Object.assign(Object.create(null), dependencies);
|
||||
|
||||
Object.keys(dependencies).forEach(key => {
|
||||
const sourcePackage = packages[key];
|
||||
if (sourcePackage) {
|
||||
// point the core Angular packages at the distributable folder
|
||||
mergedDependencies[key] = `file:${ANGULAR_DIST_PACKAGES}/${key.replace('@angular/', '')}`;
|
||||
this._log(`Overriding dependency with local package: ${key}: ${mergedDependencies[key]}`);
|
||||
// grab peer dependencies
|
||||
Object.keys(sourcePackage.peerDependencies || {})
|
||||
// ignore peerDependencies which are already core Angular packages
|
||||
.filter(key => !packages[key])
|
||||
.forEach(key => peerDependencies[key] = sourcePackage.peerDependencies[key]);
|
||||
}
|
||||
});
|
||||
return [mergedDependencies, peerDependencies];
|
||||
}
|
||||
|
||||
/**
|
||||
* A hash of Angular package configs.
|
||||
* (Detected as directories in '/packages/' that contain a top-level 'package.json' file.)
|
||||
*/
|
||||
_getDistPackages() {
|
||||
const packageConfigs = Object.create(null);
|
||||
this._log(`Angular distributable directory: ${ANGULAR_DIST_PACKAGES}.`);
|
||||
shelljs
|
||||
.find(ANGULAR_DIST_PACKAGES)
|
||||
.map(filePath => filePath.slice(ANGULAR_DIST_PACKAGES.length + 1))
|
||||
.filter(filePath => PACKAGE_JSON_REGEX.test(filePath))
|
||||
.forEach(packagePath => {
|
||||
const packageConfig = require(path.resolve(ANGULAR_DIST_PACKAGES, packagePath));
|
||||
const packageName = `@angular/${packagePath.slice(0, -PACKAGE_JSON.length -1)}`;
|
||||
packageConfigs[packageName] = packageConfig;
|
||||
});
|
||||
this._log('Found the following Angular distributables:', Object.keys(packageConfigs).map(key => `\n - ${key}`));
|
||||
return packageConfigs;
|
||||
}
|
||||
|
||||
_installDeps(...options) {
|
||||
const command = 'yarn install ' + options.join(' ');
|
||||
this._log('Installing dependencies with:', command);
|
||||
shelljs.exec(command, {cwd: this.projectDir});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message if the `debug` property is set to true.
|
||||
* @param {...string[]} messages - The messages to be logged.
|
||||
*/
|
||||
_log(...messages) {
|
||||
if (this.debug) {
|
||||
const header = ` [${NgPackagesInstaller.name}]: `;
|
||||
const indent = ' '.repeat(header.length);
|
||||
const message = messages.join(' ');
|
||||
console.info(`${header}${message.split('\n').join(`\n${indent}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
_printWarning() {
|
||||
const relativeScriptPath = path.relative('.', __filename.replace(/\.js$/, ''));
|
||||
const absoluteProjectDir = path.resolve(this.projectDir);
|
||||
const restoreCmd = `node ${relativeScriptPath} restore ${absoluteProjectDir}`;
|
||||
|
||||
// Log a warning.
|
||||
console.warn(chalk.yellow([
|
||||
'',
|
||||
'!'.repeat(110),
|
||||
'!!!',
|
||||
'!!! WARNING',
|
||||
'!!!',
|
||||
`!!! The project at "${absoluteProjectDir}" is running against the local Angular build.`,
|
||||
'!!!',
|
||||
'!!! To restore the npm packages run:',
|
||||
'!!!',
|
||||
`!!! "${restoreCmd}"`,
|
||||
'!!!',
|
||||
'!'.repeat(110),
|
||||
'',
|
||||
].join('\n')));
|
||||
}
|
||||
|
||||
// Local marker helpers
|
||||
|
||||
_checkLocalMarker() {
|
||||
this._log('Checking for local marker at', this.localMarkerPath);
|
||||
return fs.existsSync(this.localMarkerPath);
|
||||
}
|
||||
|
||||
_setLocalMarker(contents) {
|
||||
this._log('Writing local marker file to', this.localMarkerPath);
|
||||
fs.writeFileSync(this.localMarkerPath, contents);
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
shelljs.set('-e');
|
||||
|
||||
yargs
|
||||
.usage('$0 <cmd> [args]')
|
||||
|
||||
.option('debug', { describe: 'Print additional debug information.', default: false })
|
||||
.option('force', { describe: 'Force the command to execute even if not needed.', default: false })
|
||||
|
||||
.command('overwrite <projectDir> [--force] [--debug]', 'Install dependencies from the locally built Angular distributables.', () => {}, argv => {
|
||||
const installer = new NgPackagesInstaller(argv.projectDir, argv);
|
||||
installer.installLocalDependencies();
|
||||
})
|
||||
.command('restore <projectDir> [--debug]', 'Install dependencies from the npm registry.', () => {}, argv => {
|
||||
const installer = new NgPackagesInstaller(argv.projectDir, argv);
|
||||
installer.restoreNpmDependencies();
|
||||
})
|
||||
.command('check <projectDir> [--debug]', 'Check that dependencies came from npm. Otherwise display a warning message.', () => {}, argv => {
|
||||
const installer = new NgPackagesInstaller(argv.projectDir, argv);
|
||||
installer.checkDependencies();
|
||||
})
|
||||
.demandCommand(1, 'Please supply a command from the list above.')
|
||||
.strict()
|
||||
.wrap(yargs.terminalWidth())
|
||||
.argv;
|
||||
}
|
||||
|
||||
module.exports = NgPackagesInstaller;
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
246
aio/tools/ng-packages-installer/index.spec.js
Normal file
246
aio/tools/ng-packages-installer/index.spec.js
Normal file
@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('canonical-path');
|
||||
const shelljs = require('shelljs');
|
||||
|
||||
const NgPackagesInstaller = require('./index');
|
||||
|
||||
describe('NgPackagesInstaller', () => {
|
||||
const rootDir = 'root/dir';
|
||||
const absoluteRootDir = path.resolve(rootDir);
|
||||
const nodeModulesDir = path.resolve(absoluteRootDir, 'node_modules');
|
||||
const packageJsonPath = path.resolve(absoluteRootDir, 'package.json');
|
||||
const packagesDir = path.resolve(path.resolve(__dirname, '../../../dist/packages-dist'));
|
||||
let installer;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(fs, 'existsSync');
|
||||
spyOn(fs, 'readFileSync');
|
||||
spyOn(fs, 'writeFileSync');
|
||||
spyOn(shelljs, 'exec');
|
||||
spyOn(shelljs, 'rm');
|
||||
spyOn(console, 'log');
|
||||
spyOn(console, 'warn');
|
||||
installer = new NgPackagesInstaller(rootDir);
|
||||
});
|
||||
|
||||
describe('checkDependencies()', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(installer, '_printWarning');
|
||||
});
|
||||
|
||||
it('should not print a warning if there is no _local_.json file', () => {
|
||||
fs.existsSync.and.returnValue(false);
|
||||
installer.checkDependencies();
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json'));
|
||||
expect(installer._printWarning).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should print a warning if there is a _local_.json file', () => {
|
||||
fs.existsSync.and.returnValue(true);
|
||||
installer.checkDependencies();
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json'));
|
||||
expect(installer._printWarning).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('installLocalDependencies()', () => {
|
||||
let dummyNgPackages, dummyPackage, dummyPackageJson, expectedModifiedPackage, expectedModifiedPackageJson;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(installer, '_checkLocalMarker');
|
||||
|
||||
// These are the packages that are "found" in the dist directory
|
||||
dummyNgPackages = {
|
||||
'@angular/core': { peerDependencies: { rxjs: '5.0.1' } },
|
||||
'@angular/common': { peerDependencies: { '@angular/core': '4.4.1' } },
|
||||
'@angular/compiler': { },
|
||||
'@angular/compiler-cli': { peerDependencies: { typescript: '^2.4.2', '@angular/compiler': '4.3.2' } }
|
||||
};
|
||||
spyOn(installer, '_getDistPackages').and.returnValue(dummyNgPackages);
|
||||
|
||||
// This is the package.json in the "test" folder
|
||||
dummyPackage = {
|
||||
dependencies: {
|
||||
'@angular/core': '4.4.1',
|
||||
'@angular/common': '4.4.1'
|
||||
},
|
||||
devDependencies: {
|
||||
'@angular/compiler-cli': '4.4.1'
|
||||
}
|
||||
};
|
||||
dummyPackageJson = JSON.stringify(dummyPackage);
|
||||
fs.readFileSync.and.returnValue(dummyPackageJson);
|
||||
|
||||
// This is the package.json that is temporarily written to the "test" folder
|
||||
// Note that the Angular (dev)dependencies have been modified to use a "file:" path
|
||||
// And that the peerDependencies from `dummyNgPackages` have been added as (dev)dependencies.
|
||||
expectedModifiedPackage = {
|
||||
dependencies: {
|
||||
'@angular/core': `file:${packagesDir}/core`,
|
||||
'@angular/common': `file:${packagesDir}/common`
|
||||
},
|
||||
devDependencies: {
|
||||
'@angular/compiler-cli': `file:${packagesDir}/compiler-cli`,
|
||||
rxjs: '5.0.1',
|
||||
typescript: '^2.4.2'
|
||||
},
|
||||
__angular: { local: true }
|
||||
};
|
||||
expectedModifiedPackageJson = JSON.stringify(expectedModifiedPackage, null, 2);
|
||||
});
|
||||
|
||||
describe('when there is a local package marker', () => {
|
||||
it('should not continue processing', () => {
|
||||
installer._checkLocalMarker.and.returnValue(true);
|
||||
installer.installLocalDependencies();
|
||||
expect(installer._checkLocalMarker).toHaveBeenCalled();
|
||||
expect(installer._getDistPackages).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is no local package marker', () => {
|
||||
let log;
|
||||
|
||||
beforeEach(() => {
|
||||
log = [];
|
||||
fs.writeFileSync.and.callFake((filePath, contents) => filePath === packageJsonPath && log.push(`writeFile: ${contents}`));
|
||||
spyOn(installer, '_installDeps').and.callFake(() => log.push('installDeps:'));
|
||||
spyOn(installer, '_setLocalMarker');
|
||||
installer._checkLocalMarker.and.returnValue(false);
|
||||
installer.installLocalDependencies();
|
||||
});
|
||||
|
||||
it('should get the dist packages', () => {
|
||||
expect(installer._checkLocalMarker).toHaveBeenCalled();
|
||||
expect(installer._getDistPackages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should load the package.json', () => {
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith(packageJsonPath);
|
||||
});
|
||||
|
||||
it('should overwrite package.json with modified config', () => {
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(packageJsonPath, expectedModifiedPackageJson);
|
||||
});
|
||||
|
||||
it('should restore original package.json', () => {
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(packageJsonPath, dummyPackageJson);
|
||||
});
|
||||
|
||||
it('should overwrite package.json, then install deps, then restore original package.json', () => {
|
||||
expect(log).toEqual([
|
||||
`writeFile: ${expectedModifiedPackageJson}`,
|
||||
`installDeps:`,
|
||||
`writeFile: ${dummyPackageJson}`
|
||||
]);
|
||||
});
|
||||
|
||||
it('should set the local marker file with the contents of the modified package.json', () => {
|
||||
expect(installer._setLocalMarker).toHaveBeenCalledWith(expectedModifiedPackageJson);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('restoreNpmDependencies()', () => {
|
||||
it('should run `yarn install --check-files` in the specified directory', () => {
|
||||
spyOn(installer, '_installDeps');
|
||||
installer.restoreNpmDependencies();
|
||||
expect(installer._installDeps).toHaveBeenCalledWith('--check-files');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getDistPackages', () => {
|
||||
it('should include top level Angular packages', () => {
|
||||
const ngPackages = installer._getDistPackages();
|
||||
|
||||
// For example...
|
||||
expect(ngPackages['@angular/common']).toBeDefined();
|
||||
expect(ngPackages['@angular/core']).toBeDefined();
|
||||
expect(ngPackages['@angular/router']).toBeDefined();
|
||||
expect(ngPackages['@angular/upgrade']).toBeDefined();
|
||||
|
||||
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_log()', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(console, 'info');
|
||||
});
|
||||
|
||||
it('should assign the debug property from the options', () => {
|
||||
installer = new NgPackagesInstaller(rootDir, { debug: true });
|
||||
expect(installer.debug).toBe(true);
|
||||
installer = new NgPackagesInstaller(rootDir, { });
|
||||
expect(installer.debug).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should log a message to the console if the `debug` property is true', () => {
|
||||
installer._log('foo');
|
||||
expect(console.info).not.toHaveBeenCalled();
|
||||
|
||||
installer.debug = true;
|
||||
installer._log('bar');
|
||||
expect(console.info).toHaveBeenCalledWith(' [NgPackagesInstaller]: bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_printWarning', () => {
|
||||
it('should mention the message passed in the warning', () => {
|
||||
installer._printWarning();
|
||||
expect(console.warn.calls.argsFor(0)[0]).toContain('is running against the local Angular build');
|
||||
});
|
||||
|
||||
it('should mention the command to restore the Angular packages in any warning', () => {
|
||||
// When run for the current working directory...
|
||||
const dir1 = '.';
|
||||
const restoreCmdRe1 = RegExp('\\bnode .*?ng-packages-installer/index restore ' + path.resolve(dir1));
|
||||
installer = new NgPackagesInstaller(dir1);
|
||||
installer._printWarning('');
|
||||
expect(console.warn.calls.argsFor(0)[0]).toMatch(restoreCmdRe1);
|
||||
|
||||
// When run for a different directory...
|
||||
const dir2 = rootDir;
|
||||
const restoreCmdRe2 = RegExp(`\\bnode .*?ng-packages-installer/index restore .*?${path.resolve(dir1)}\\b`);
|
||||
installer = new NgPackagesInstaller(dir2);
|
||||
installer._printWarning('');
|
||||
expect(console.warn.calls.argsFor(1)[0]).toMatch(restoreCmdRe2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_installDeps', () => {
|
||||
it('should run yarn install with the given options', () => {
|
||||
installer._installDeps('option-1', 'option-2');
|
||||
expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteRootDir });
|
||||
});
|
||||
});
|
||||
|
||||
describe('local marker helpers', () => {
|
||||
let installer;
|
||||
beforeEach(() => {
|
||||
installer = new NgPackagesInstaller(rootDir);
|
||||
});
|
||||
|
||||
describe('_checkLocalMarker', () => {
|
||||
it ('should return true if the local marker file exists', () => {
|
||||
fs.existsSync.and.returnValue(true);
|
||||
expect(installer._checkLocalMarker()).toEqual(true);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'));
|
||||
fs.existsSync.calls.reset();
|
||||
|
||||
fs.existsSync.and.returnValue(false);
|
||||
expect(installer._checkLocalMarker()).toEqual(false);
|
||||
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_setLocalMarker', () => {
|
||||
it('should create a local marker file', () => {
|
||||
installer._setLocalMarker('test contents');
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(path.resolve(nodeModulesDir, '_local_.json'), 'test contents');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user