From d1a00459a86307d0b51ceee5a298fc720c69556b Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Fri, 6 Oct 2017 10:48:18 +0100 Subject: [PATCH] ci(aio): use custom package.json to run with local distributables (#19511) Closes #19388 PR Close #19511 --- aio/tools/examples/example-boilerplate.js | 13 +- .../examples/example-boilerplate.spec.js | 20 +- aio/tools/ng-packages-installer.js | 235 -------------- aio/tools/ng-packages-installer.spec.js | 286 ------------------ aio/tools/ng-packages-installer/index.js | 241 +++++++++++++++ aio/tools/ng-packages-installer/index.spec.js | 246 +++++++++++++++ 6 files changed, 495 insertions(+), 546 deletions(-) delete mode 100644 aio/tools/ng-packages-installer.js delete mode 100644 aio/tools/ng-packages-installer.spec.js create mode 100644 aio/tools/ng-packages-installer/index.js create mode 100644 aio/tools/ng-packages-installer/index.spec.js diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index aedb7f9de2..8ba07cc107 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -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) { diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js index cb17bfd05b..f9f2b5aa79 100644 --- a/aio/tools/examples/example-boilerplate.spec.js +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -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); }); }); diff --git a/aio/tools/ng-packages-installer.js b/aio/tools/ng-packages-installer.js deleted file mode 100644 index 1bcd29980e..0000000000 --- a/aio/tools/ng-packages-installer.js +++ /dev/null @@ -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 '). - command( - 'check [--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 [--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 [--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(); -} diff --git a/aio/tools/ng-packages-installer.spec.js b/aio/tools/ng-packages-installer.spec.js deleted file mode 100644 index 690a6144bb..0000000000 --- a/aio/tools/ng-packages-installer.spec.js +++ /dev/null @@ -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}); - }); - }); -}); diff --git a/aio/tools/ng-packages-installer/index.js b/aio/tools/ng-packages-installer/index.js new file mode 100644 index 0000000000..665500019d --- /dev/null +++ b/aio/tools/ng-packages-installer/index.js @@ -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 [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 [--force] [--debug]', 'Install dependencies from the locally built Angular distributables.', () => {}, argv => { + const installer = new NgPackagesInstaller(argv.projectDir, argv); + installer.installLocalDependencies(); + }) + .command('restore [--debug]', 'Install dependencies from the npm registry.', () => {}, argv => { + const installer = new NgPackagesInstaller(argv.projectDir, argv); + installer.restoreNpmDependencies(); + }) + .command('check [--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(); +} \ No newline at end of file diff --git a/aio/tools/ng-packages-installer/index.spec.js b/aio/tools/ng-packages-installer/index.spec.js new file mode 100644 index 0000000000..a910c034a8 --- /dev/null +++ b/aio/tools/ng-packages-installer/index.spec.js @@ -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'); + }); + }); + }); +});