ci(aio): use custom package.json to run with local distributables (#19511)

Closes #19388

PR Close #19511
This commit is contained in:
Peter Bacon Darwin 2017-10-06 10:48:18 +01:00 committed by Tobias Bosch
parent 9fe6363575
commit d1a00459a8
6 changed files with 495 additions and 546 deletions

View File

@ -4,14 +4,11 @@ const path = require('canonical-path');
const shelljs = require('shelljs');
const yargs = require('yargs');
const ngPackagesInstaller = require('../ng-packages-installer');
const SHARED_PATH = path.resolve(__dirname, 'shared');
const SHARED_NODE_MODULES_PATH = path.resolve(SHARED_PATH, 'node_modules');
const BOILERPLATE_BASE_PATH = path.resolve(SHARED_PATH, 'boilerplate');
const BOILERPLATE_COMMON_BASE_PATH = path.resolve(BOILERPLATE_BASE_PATH, 'common');
const EXAMPLES_BASE_PATH = path.resolve(__dirname, '../../content/examples');
const TESTING_BASE_PATH = path.resolve(EXAMPLES_BASE_PATH, 'testing');
const BOILERPLATE_PATHS = {
cli: [
@ -99,13 +96,9 @@ class ExampleBoilerPlate {
}
installNodeModules(basePath, useLocal) {
shelljs.exec('yarn', {cwd: basePath});
if (useLocal) {
ngPackagesInstaller.overwritePackages(basePath);
} else {
ngPackagesInstaller.restorePackages(basePath);
}
const tool = 'node tools/ng-packages-installer';
const command = useLocal ? 'overwrite' : 'restore';
shelljs.exec([tool, command, basePath, '--debug'].join(' '));
}
getFoldersContaining(basePath, filename, ignore) {

View File

@ -3,7 +3,6 @@ const fs = require('fs-extra');
const glob = require('glob');
const shelljs = require('shelljs');
const ngPackagesInstaller = require('../ng-packages-installer');
const exampleBoilerPlate = require('./example-boilerplate');
describe('example-boilerplate tool', () => {
@ -99,31 +98,22 @@ describe('example-boilerplate tool', () => {
describe('installNodeModules', () => {
beforeEach(() => {
spyOn(shelljs, 'exec');
spyOn(ngPackagesInstaller, 'overwritePackages');
spyOn(ngPackagesInstaller, 'restorePackages');
});
it('should run `yarn` in the base path', () => {
exampleBoilerPlate.installNodeModules('some/base/path');
expect(shelljs.exec).toHaveBeenCalledWith('yarn', { cwd: 'some/base/path' });
});
it('should overwrite the Angular packages if `useLocal` is true', () => {
ngPackagesInstaller.overwritePackages.and.callFake(() => expect(shelljs.exec).toHaveBeenCalled());
exampleBoilerPlate.installNodeModules('some/base/path', true);
expect(ngPackagesInstaller.overwritePackages).toHaveBeenCalledWith('some/base/path');
expect(ngPackagesInstaller.restorePackages).not.toHaveBeenCalled();
expect(shelljs.exec).toHaveBeenCalledWith('node tools/ng-packages-installer overwrite some/base/path --debug');
expect(shelljs.exec.calls.count()).toEqual(1);
});
it('should restore the Angular packages if `useLocal` is not true', () => {
exampleBoilerPlate.installNodeModules('some/base/path1');
expect(ngPackagesInstaller.restorePackages).toHaveBeenCalledWith('some/base/path1');
expect(shelljs.exec).toHaveBeenCalledWith('node tools/ng-packages-installer restore some/base/path1 --debug');
exampleBoilerPlate.installNodeModules('some/base/path2', false);
expect(ngPackagesInstaller.restorePackages).toHaveBeenCalledWith('some/base/path2');
expect(shelljs.exec).toHaveBeenCalledWith('node tools/ng-packages-installer restore some/base/path2 --debug');
expect(ngPackagesInstaller.overwritePackages).not.toHaveBeenCalled();
expect(shelljs.exec.calls.count()).toEqual(2);
});
});

View File

@ -1,235 +0,0 @@
#!/bin/env node
'use strict';
// Imports
const chalk = require('chalk');
const fs = require('fs-extra');
const path = require('canonical-path');
const shelljs = require('shelljs');
const yargs = require('yargs');
// Config
shelljs.set('-e');
// Constants
const ROOT_DIR = path.resolve(__dirname, '../..');
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');
const PACKAGES_DIST_DIR = path.join(ROOT_DIR, 'dist/packages-dist');
const NG_LOCAL_FILENAME = '.ng-local';
// Classes
class NgPackagesInstaller {
constructor() {
// Properties - Protected
/**
* A sorted list of Angular package names.
* (Detected as directories in '/packages/' that contain a top-level 'package.json' file.)
*/
this.ngPackages = shelljs.
find(PACKAGES_DIR).
map(path => path.slice(PACKAGES_DIR.length + 1)).
filter(path => /^[^/]+\/package.json$/.test(path)).
map(path => path.slice(0, -13)).
sort();
}
// Methods - Public
/**
* Check whether the Angular packages installed in the specified `rootDir`'s 'node_modules/' come from npm and print a
* warning if not.
* @param {string} rootDir - The root directory whose npm dependencies will be checked.
*/
checkPackages(rootDir) {
rootDir = path.resolve(rootDir);
const localPackages = this._findLocalPackages(rootDir);
if (localPackages.length) {
const relativeScriptPath = path.relative('.', __filename.replace(/\.js$/, ''));
const relativeRootDir = path.relative('.', rootDir) || '.';
const restoreCmd = `node ${relativeScriptPath} restore ${relativeRootDir}`;
// Log a warning.
console.warn(chalk.yellow([
'',
'!'.repeat(110),
'!!!',
'!!! WARNING',
'!!!',
`!!! The following packages have been overwritten in '${rootDir}/node_modules/' with the locally built ones:`,
'!!!',
...localPackages.map(pkg => `!!! - @angular/${pkg}`),
'!!!',
'!!! To restore the packages run:',
'!!!',
`!!! ${restoreCmd}`,
'!!!',
'!'.repeat(110),
'',
].join('\n')));
}
}
/**
* Overwrite the Angular packages installed in the specified `rootDir`'s 'node_modules/' with the locally built ones.
* @param {string} rootDir - The root directory whose npm dependencies will be overwritten.
*/
overwritePackages(rootDir) {
rootDir = path.resolve(rootDir);
const nodeModulesDir = path.join(rootDir, 'node_modules');
this.ngPackages.forEach(packageName => this._overwritePackage(packageName, nodeModulesDir));
}
/**
* Ensure that the Angular packages installed in the specified `rootDir`'s 'node_modules/' come from npm.
* (If necessary, re-install the Angular packages using `yarn`.)
* @param {string} rootDir - The root directory whose npm dependencies will be restored.
*/
restorePackages(rootDir) {
rootDir = path.resolve(rootDir);
const localPackages = this._findLocalPackages(rootDir);
if (localPackages.length) {
this._reinstallOverwrittenNodeModules(rootDir);
}
}
// Methods - Protected
/**
* Find and return all Angular packages installed in the specified `rootDir`'s 'node_modules/' that have been
* overwritten with the locally built ones.
* @param {string} rootDir - The root directory whose npm dependencies will be checked.
* @return {string[]} - A list of overwritten package names.
*/
_findLocalPackages(rootDir) {
const nodeModulesDir = path.join(rootDir, 'node_modules');
const localPackages = this.ngPackages.filter(packageName => this._isLocalPackage(packageName, nodeModulesDir));
this._log(`Local packages found: ${localPackages.join(', ') || '-'}`);
return localPackages;
}
/**
* Check whether an installed Angular package from `nodeModulesDir` has been overwritten with a
* locally built package.
* @param {string} packageName - The name of the package to check.
* @param {string} nodeModulesDir - The target `node_modules/` directory.
* @return {boolean} - True if the package has been overwritten or false otherwise.
*/
_isLocalPackage(packageName, nodeModulesDir) {
const targetPackageDir = path.join(nodeModulesDir, '@angular', packageName);
const localFlagFile = path.join(targetPackageDir, NG_LOCAL_FILENAME);
const isLocal = fs.existsSync(localFlagFile);
this._log(`Checking package '${packageName}' (${targetPackageDir})... local: ${isLocal}`);
return isLocal;
}
/**
* Log a message if the `debug` property is set to true.
* @param {string} message - The message to be logged.
*/
_log(message) {
if (this.debug) {
const indent = ' ';
console.info(`${indent}[${NgPackagesInstaller.name}]: ${message.split('\n').join(`\n${indent}`)}`);
}
}
/**
* Parse and validate the input and invoke the appropriate command.
*/
_main() {
const preCommand = argv => {
this.debug = argv.debug;
const availablePackages = this.ngPackages.map(pkg => `\n - @angular/${pkg}`).join('') || '-';
this._log(`Available Angular packages: ${availablePackages}`);
};
yargs.
usage('$0 <cmd> <args>').
command(
'check <projectDir> [--debug]',
'Check whether the Angular packages installed as dependencies of `projectDir` come from npm and print a ' +
'warning if not.',
{
debug: {describe: 'Print debug information.'}
},
argv => {
preCommand(argv);
this.checkPackages(argv.projectDir);
}).
command(
'overwrite <projectDir> [--debug]',
'Overwrite the Angular packages installed as dependencies of `projectDir` with the locally built ones.',
{
debug: {describe: 'Print debug information.'}
},
argv => {
preCommand(argv);
this.overwritePackages(argv.projectDir);
}).
command(
'restore <projectDir> [--debug]',
'Ensure that the Angular packages installed as dependencies of `projectDir` come from npm.',
{
debug: {describe: 'Print debug information.'}
},
argv => {
preCommand(argv);
this.restorePackages(argv.projectDir);
}).
demandCommand(1, 'Please supply a command from the list above.').
strict().
argv;
}
/**
* Remove an installed Angular package from `nodeModulesDir` and replace it with the locally built
* one. Mark the package by adding an `.ng-local` file in the target directory.
* @param {string} packageName - The name of the package to overwrite.
* @param {string} nodeModulesDir - The target `node_modules/` directory.
*/
_overwritePackage(packageName, nodeModulesDir) {
const sourcePackageDir = path.join(PACKAGES_DIST_DIR, packageName);
const targetPackageDir = path.join(nodeModulesDir, '@angular', packageName);
const localFlagFile = path.join(targetPackageDir, NG_LOCAL_FILENAME);
this._log(`Overwriting package '${packageName}' (${sourcePackageDir} --> ${targetPackageDir})...`);
if (fs.existsSync(targetPackageDir)) {
shelljs.rm('-rf', targetPackageDir);
fs.copySync(sourcePackageDir, targetPackageDir);
fs.writeFileSync(localFlagFile, '');
} else {
this._log(' Nothing to overwrite - the package is not installed...');
}
}
/**
* Re-install overwritten npm dependencies using `yarn`. Removes the `.yarn-integrity` file to ensure `yarn` detects
* the overwritten packages.
* @param {string} rootDir - The root directory whose npm dependencies will be re-installed.
*/
_reinstallOverwrittenNodeModules(rootDir) {
const installCmd = 'yarn install --check-files';
this._log(`Running '${installCmd}' in '${rootDir}'...`);
shelljs.exec(installCmd, {cwd: rootDir});
}
}
// Exports
module.exports = new NgPackagesInstaller();
// Run
if (require.main === module) {
// This file was run directly; run the main function.
module.exports._main();
}

View File

@ -1,286 +0,0 @@
'use strict';
const fs = require('fs-extra');
const path = require('canonical-path');
const shelljs = require('shelljs');
const installer = require('./ng-packages-installer');
describe('NgPackagesInstaller', () => {
const ngPackages = installer.ngPackages;
const rootDir = 'root/dir';
const absoluteRootDir = path.resolve(rootDir);
const nodeModulesDir = `${absoluteRootDir}/node_modules`;
beforeEach(() => {
spyOn(fs, 'copySync');
spyOn(fs, 'existsSync');
spyOn(fs, 'writeFileSync');
spyOn(shelljs, 'exec');
spyOn(shelljs, 'rm');
});
// Properties
describe('.ngPackages', () => {
it('should include all package names', () => {
// For example...
expect(installer.ngPackages).toContain('common');
expect(installer.ngPackages).toContain('core');
expect(installer.ngPackages).toContain('router');
expect(installer.ngPackages).toContain('upgrade');
expect(installer.ngPackages).not.toContain('static');
expect(installer.ngPackages).not.toContain('upgrade/static');
});
it('should correspond to package directories with top-level \'package.json\' files', () => {
fs.existsSync.and.callThrough();
const packagesDir = path.resolve(__dirname, '../../packages');
installer.ngPackages.forEach(packageName => {
const packageJson = `${packagesDir}/${packageName}/package.json`;
expect(fs.existsSync(packageJson)).toBe(true);
});
});
});
// Methods
describe('checkPackages()', () => {
beforeEach(() => {
spyOn(console, 'warn');
spyOn(installer, '_findLocalPackages');
});
it('should check whether there are any local Angular packages in the target directory', () => {
installer._findLocalPackages.and.returnValue([]);
installer.checkPackages(rootDir);
expect(installer._findLocalPackages).toHaveBeenCalledWith(absoluteRootDir);
});
it('should not print a warning if all Angular packages come from npm', () => {
installer._findLocalPackages.and.returnValue([]);
installer.checkPackages(rootDir);
expect(console.warn).not.toHaveBeenCalled();
});
describe('when there are local Angular packages', () => {
beforeEach(() => {
installer._findLocalPackages.and.returnValue(['common', 'router']);
});
it('should print a warning', () => {
installer.checkPackages(rootDir);
expect(console.warn).toHaveBeenCalled();
expect(console.warn.calls.mostRecent().args[0]).toContain('WARNING');
});
it('should list the local (i.e. overwritten) packages', () => {
installer.checkPackages(rootDir);
const warning = console.warn.calls.mostRecent().args[0];
expect(warning).toContain('@angular/common');
expect(warning).toContain('@angular/router');
expect(warning).not.toContain('@angular/core');
expect(warning).not.toContain('@angular/upgrade');
});
it('should mention the command to restore the Angular packages', () => {
// When run for the current working directory...
const dir1 = '.';
const restoreCmdRe1 = RegExp('\\bnode .*?ng-packages-installer restore \\.');
installer.checkPackages(dir1);
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 restore .*?${path.normalize(rootDir)}\\b`);
installer.checkPackages(dir2);
expect(console.warn.calls.argsFor(1)[0]).toMatch(restoreCmdRe2);
});
});
});
describe('overwritePackages()', () => {
beforeEach(() => {
spyOn(installer, '_overwritePackage');
});
it('should override the Angular packages in the target directory with the locally built ones', () => {
installer.overwritePackages(rootDir);
expect(installer._overwritePackage).toHaveBeenCalledTimes(ngPackages.length);
ngPackages.forEach(packageName =>
expect(installer._overwritePackage).toHaveBeenCalledWith(packageName, nodeModulesDir));
});
});
describe('restorePackages()', () => {
beforeEach(() => {
spyOn(installer, '_findLocalPackages');
spyOn(installer, '_reinstallOverwrittenNodeModules');
});
it('should check whether there are any local Angular packages in the target directory first', () => {
installer._findLocalPackages.and.callFake(() => {
expect(installer._reinstallOverwrittenNodeModules).not.toHaveBeenCalled();
return [];
});
installer.restorePackages(rootDir);
expect(installer._findLocalPackages).toHaveBeenCalledWith(absoluteRootDir);
});
it('should re-install dependencies from npm afterwards (if necessary)', () => {
// No local packages.
installer._findLocalPackages.and.returnValue([]);
installer.restorePackages(rootDir);
expect(installer._reinstallOverwrittenNodeModules).not.toHaveBeenCalled();
// All local packages.
installer._reinstallOverwrittenNodeModules.calls.reset();
installer._findLocalPackages.and.returnValue(ngPackages);
installer.restorePackages(rootDir);
expect(installer._reinstallOverwrittenNodeModules).toHaveBeenCalledWith(absoluteRootDir);
// Some local packages.
installer._reinstallOverwrittenNodeModules.calls.reset();
installer._findLocalPackages.and.returnValue(['common', 'core', 'router', 'upgrade']);
installer.restorePackages(rootDir);
expect(installer._reinstallOverwrittenNodeModules).toHaveBeenCalledWith(absoluteRootDir);
});
});
describe('_findLocalPackages()', () => {
beforeEach(() => {
spyOn(installer, '_isLocalPackage');
});
it('should check all Angular packages', () => {
installer._findLocalPackages(absoluteRootDir);
ngPackages.forEach(packageName =>
expect(installer._isLocalPackage).toHaveBeenCalledWith(packageName, nodeModulesDir));
});
it('should return an empty list if all Angular packages come from npm', () => {
installer._isLocalPackage.and.returnValue(false);
expect(installer._findLocalPackages(rootDir)).toEqual([]);
});
it('should return a list of all local (i.e. overwritten) Angular packages', () => {
const localPackages = ['common', 'core', 'router', 'upgrade'];
installer._isLocalPackage.and.callFake(packageName => localPackages.includes(packageName));
expect(installer._findLocalPackages(rootDir)).toEqual(localPackages);
});
});
describe('_isLocalPackage()', () => {
it('should check whether the specified package is local/overwritten', () => {
const targetPackageDir = `${rootDir}/@angular/somePackage`;
const localFlagFile = `${targetPackageDir}/.ng-local`;
installer._isLocalPackage('somePackage', rootDir);
expect(fs.existsSync).toHaveBeenCalledWith(localFlagFile);
});
it('should return whether the specified package was local', () => {
fs.existsSync.and.returnValues(true, false);
expect(installer._isLocalPackage('somePackage', rootDir)).toBe(true);
expect(installer._isLocalPackage('somePackage', rootDir)).toBe(false);
});
});
describe('_log()', () => {
beforeEach(() => {
spyOn(console, 'info');
});
afterEach(() => {
installer.debug = false;
});
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('_overwritePackage()', () => {
beforeEach(() => {
fs.existsSync.and.returnValue(true);
});
it('should check whether the Angular package is installed', () => {
const targetPackageDir = `${rootDir}/@angular/somePackage`;
installer._overwritePackage('somePackage', rootDir);
expect(fs.existsSync).toHaveBeenCalledWith(targetPackageDir);
});
it('should remove the original package from the target directory', () => {
const targetPackageDir = `${rootDir}/@angular/somePackage`;
shelljs.rm.and.callFake(() => expect(fs.existsSync).toHaveBeenCalled());
installer._overwritePackage('somePackage', rootDir);
expect(shelljs.rm).toHaveBeenCalledWith('-rf', targetPackageDir);
});
it('should copy the source package directory to the target directory', () => {
const sourcePackageDir = path.resolve(__dirname, '../../dist/packages-dist/somePackage');
const targetPackageDir = `${rootDir}/@angular/somePackage`;
fs.copySync.and.callFake(() => expect(shelljs.rm).toHaveBeenCalled());
installer._overwritePackage('somePackage', rootDir);
expect(fs.copySync).toHaveBeenCalledWith(sourcePackageDir, targetPackageDir);
});
it('should add an empty `.ng-local` file to the target directory', () => {
const targetPackageDir = `${rootDir}/@angular/somePackage`;
const localFlagFile = `${targetPackageDir}/.ng-local`;
fs.writeFileSync.and.callFake(() => expect(fs.copySync).toHaveBeenCalled());
installer._overwritePackage('somePackage', rootDir);
expect(fs.writeFileSync).toHaveBeenCalledWith(localFlagFile, '');
});
it('should do nothing if the Angular package is not installed', () => {
fs.existsSync.and.returnValue(false);
installer._overwritePackage('somePackage', rootDir);
expect(shelljs.rm).not.toHaveBeenCalled();
expect(fs.copySync).not.toHaveBeenCalled();
expect(fs.writeFileSync).not.toHaveBeenCalled();
});
});
describe('_reinstallOverwrittenNodeModules()', () => {
it('should run `yarn install --check-files` in the specified directory', () => {
installer._reinstallOverwrittenNodeModules(rootDir);
expect(shelljs.exec).toHaveBeenCalledWith('yarn install --check-files', {cwd: rootDir});
});
});
});

View 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();
}

View 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');
});
});
});
});