diff --git a/aio/README.md b/aio/README.md index dd2c51ab13..bf2e18a827 100644 --- a/aio/README.md +++ b/aio/README.md @@ -13,7 +13,11 @@ You should run all these tasks from the `angular/aio` folder. Here are the most important tasks you might need to use: * `yarn` - install all the dependencies. -* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs. +* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs. +* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate. + +* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc). +* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`. * `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary. * `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console. diff --git a/aio/package.json b/aio/package.json index 8e79c001d5..14bf46def3 100644 --- a/aio/package.json +++ b/aio/package.json @@ -8,31 +8,38 @@ "scripts": { "ng": "yarn check-env && ng", "start": "yarn check-env && ng serve", - "prebuild": "yarn check-env && yarn setup", - "build": "ng build --target=production --environment=stable -sm --build-optimizer", - "postbuild": "yarn sw-manifest && yarn sw-copy", + "prebuild": "yarn setup", + "build": "yarn ~~build", + "prebuild-local": "yarn setup-local", + "build-local": "yarn ~~build", "lint": "yarn check-env && yarn docs-lint && ng lint && yarn example-lint", "test": "yarn check-env && ng test", "pree2e": "yarn check-env && yarn ~~update-webdriver", "e2e": "ng e2e --no-webdriver-update", - "setup": "yarn && yarn build-ie-polyfills && yarn boilerplate:remove && yarn boilerplate:add && yarn generate-plunkers && yarn generate-zips && yarn docs", - "pretest-pwa-score-local": "yarn build", - "test-pwa-score-local": "concurrently --kill-others --success first \"http-server dist -p 4200 --silent\" \"yarn test-pwa-score -- http://localhost:4200 90\"", + "presetup": "yarn ~~check-env && yarn install && yarn boilerplate:remove", + "setup": "node tools/ng-packages-installer restore . && yarn boilerplate:add", + "postsetup": "yarn build-ie-polyfills && yarn generate-plunkers && yarn generate-zips && yarn docs", + "presetup-local": "yarn presetup", + "setup-local": "node tools/ng-packages-installer overwrite . && yarn boilerplate:add -- --local", + "postsetup-local": "yarn postsetup", + "pretest-pwa-score-localhost": "yarn build", + "test-pwa-score-localhost": "concurrently --kill-others --success first \"http-server dist -p 4200 --silent\" \"yarn test-pwa-score -- http://localhost:4200 90\"", "test-pwa-score": "node scripts/test-pwa-score", - "example-e2e": "node ./tools/examples/run-example-e2e", + "example-e2e": "yarn check-ng-packages -- tools/examples/shared && node ./tools/examples/run-example-e2e", "example-lint": "tslint -c \"content/examples/tslint.json\" \"content/examples/**/*.ts\" -e \"content/examples/styleguide/**/*.avoid.ts\"", "deploy-preview": "scripts/deploy-preview.sh", "deploy-production": "scripts/deploy-to-firebase.sh", - "check-env": "node scripts/check-environment", + "check-ng-packages": "node tools/ng-packages-installer check", + "check-env": "yarn ~~check-env", + "postcheck-env": "yarn check-ng-packages -- .", "payload-size": "scripts/payload.sh", "predocs": "rimraf src/generated/{docs,*.json}", "docs": "dgeni ./tools/transforms/angular.io-package", "docs-watch": "node tools/transforms/authors-package/watchr.js", "docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms", "docs-test": "node tools/transforms/test.js", - "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test", + "tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer.spec.js", "serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"", - "~~update-webdriver": "webdriver-manager update --standalone false --gecko false", "boilerplate:add": "node ./tools/examples/example-boilerplate add", "boilerplate:remove": "node ./tools/examples/example-boilerplate remove", "boilerplate:test": "node tools/examples/test.js", @@ -41,7 +48,10 @@ "sw-manifest": "ngu-sw-manifest --dist dist --in ngsw-manifest.json --out dist/ngsw-manifest.json", "sw-copy": "cp node_modules/@angular/service-worker/bundles/worker-basic.min.js dist/", "postinstall": "uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map", - "build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js" + "build-ie-polyfills": "node node_modules/webpack/bin/webpack.js -p src/ie-polyfills.js src/generated/ie-polyfills.min.js", + "~~check-env": "node scripts/check-environment", + "~~build": "ng build --target=production --environment=stable -sm --build-optimizer && yarn sw-manifest && yarn sw-copy", + "~~update-webdriver": "webdriver-manager update --standalone false --gecko false" }, "engines": { "node": ">=6.9.5 <7.0.0", @@ -78,6 +88,7 @@ "@types/node": "~6.0.60", "archiver": "^1.3.0", "canonical-path": "^0.0.2", + "chalk": "^2.1.0", "codelyzer": "~2.0.0", "concurrently": "^3.4.0", "cross-spawn": "^5.1.0", diff --git a/aio/tools/examples/example-boilerplate.js b/aio/tools/examples/example-boilerplate.js index 73911342c1..aedb7f9de2 100644 --- a/aio/tools/examples/example-boilerplate.js +++ b/aio/tools/examples/example-boilerplate.js @@ -4,6 +4,8 @@ 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'); @@ -46,23 +48,6 @@ const BOILERPLATE_PATHS = { ] }; -const ANGULAR_DIST_PATH = path.resolve(__dirname, '../../../dist'); -const ANGULAR_PACKAGES_PATH = path.resolve(ANGULAR_DIST_PATH, 'packages-dist'); -const ANGULAR_PACKAGES = [ - 'animations', - 'common', - 'compiler', - 'compiler-cli', - 'core', - 'forms', - 'http', - 'platform-browser', - 'platform-browser-dynamic', - 'platform-server', - 'router', - 'upgrade', -]; - const EXAMPLE_CONFIG_FILENAME = 'example-config.json'; class ExampleBoilerPlate { @@ -72,13 +57,8 @@ class ExampleBoilerPlate { * @param useLocal if true then overwrite the Angular library files with locally built ones */ add(useLocal) { - // first install the shared node_modules - this.installNodeModules(SHARED_PATH); - - // Replace the Angular packages with those from the dist folder, if necessary - if (useLocal) { - ANGULAR_PACKAGES.forEach(packageName => this.overridePackage(ANGULAR_PACKAGES_PATH, packageName)); - } + // Install the shared `node_modules/` (if necessary overwrite Angular packages from npm with local ones). + this.installNodeModules(SHARED_PATH, useLocal); // Get all the examples folders, indicated by those that contain a `example-config.json` file const exampleFolders = this.getFoldersContaining(EXAMPLES_BASE_PATH, EXAMPLE_CONFIG_FILENAME, 'node_modules'); @@ -118,15 +98,14 @@ class ExampleBoilerPlate { .argv; } - installNodeModules(basePath) { + installNodeModules(basePath, useLocal) { shelljs.exec('yarn', {cwd: basePath}); - } - overridePackage(basePath, packageName) { - const sourceFolder = path.resolve(basePath, packageName); - const destinationFolder = path.resolve(SHARED_NODE_MODULES_PATH, '@angular', packageName); - shelljs.rm('-rf', destinationFolder); - fs.copySync(sourceFolder, destinationFolder); + if (useLocal) { + ngPackagesInstaller.overwritePackages(basePath); + } else { + ngPackagesInstaller.restorePackages(basePath); + } } getFoldersContaining(basePath, filename, ignore) { diff --git a/aio/tools/examples/example-boilerplate.spec.js b/aio/tools/examples/example-boilerplate.spec.js index 53ae3dce48..cb17bfd05b 100644 --- a/aio/tools/examples/example-boilerplate.spec.js +++ b/aio/tools/examples/example-boilerplate.spec.js @@ -1,11 +1,15 @@ -const exampleBoilerPlate = require('./example-boilerplate'); -const shelljs = require('shelljs'); +const path = require('canonical-path'); const fs = require('fs-extra'); const glob = require('glob'); -const path = require('canonical-path'); +const shelljs = require('shelljs'); + +const ngPackagesInstaller = require('../ng-packages-installer'); +const exampleBoilerPlate = require('./example-boilerplate'); describe('example-boilerplate tool', () => { describe('add', () => { + const sharedDir = path.resolve(__dirname, 'shared'); + const sharedNodeModulesDir = path.resolve(sharedDir, 'node_modules'); const BPFiles = { cli: 18, systemjs: 7, @@ -14,41 +18,44 @@ describe('example-boilerplate tool', () => { const exampleFolders = ['a/b', 'c/d']; beforeEach(() => { - spyOn(exampleBoilerPlate, 'installNodeModules'); - spyOn(exampleBoilerPlate, 'overridePackage'); - spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders); spyOn(fs, 'ensureSymlinkSync'); spyOn(exampleBoilerPlate, 'copyFile'); + spyOn(exampleBoilerPlate, 'getFoldersContaining').and.returnValue(exampleFolders); + spyOn(exampleBoilerPlate, 'installNodeModules'); spyOn(exampleBoilerPlate, 'loadJsonFile').and.returnValue({}); }); - it('should install the node modules', () => { + it('should install the npm dependencies into `sharedDir` (and pass the `useLocal` argument through)', () => { exampleBoilerPlate.add(); - expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(path.resolve(__dirname, 'shared')); - }); + expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(sharedDir, undefined); + + exampleBoilerPlate.installNodeModules.calls.reset(); - it('should override the Angular node_modules with the locally built Angular packages if `useLocal` is true', () => { - const numberOfAngularPackages = 12; exampleBoilerPlate.add(true); - expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledTimes(numberOfAngularPackages); - // for example - expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'common'); - expect(exampleBoilerPlate.overridePackage).toHaveBeenCalledWith(path.resolve(__dirname, '../../../dist/packages-dist'), 'core'); + expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(sharedDir, true); + + exampleBoilerPlate.installNodeModules.calls.reset(); + + exampleBoilerPlate.add(false); + expect(exampleBoilerPlate.installNodeModules).toHaveBeenCalledWith(sharedDir, false); }); it('should process all the example folders', () => { + const examplesDir = path.resolve(__dirname, '../../content/examples'); exampleBoilerPlate.add(); - expect(exampleBoilerPlate.getFoldersContaining).toHaveBeenCalledWith(path.resolve(__dirname, '../../content/examples'), 'example-config.json', 'node_modules'); + expect(exampleBoilerPlate.getFoldersContaining) + .toHaveBeenCalledWith(examplesDir, 'example-config.json', 'node_modules'); }); it('should symlink the node_modules', () => { exampleBoilerPlate.add(); expect(fs.ensureSymlinkSync).toHaveBeenCalledTimes(exampleFolders.length); - expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('a/b/node_modules')); - expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/node_modules'), path.resolve('c/d/node_modules')); + expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(sharedNodeModulesDir, path.resolve('a/b/node_modules')); + expect(fs.ensureSymlinkSync).toHaveBeenCalledWith(sharedNodeModulesDir, path.resolve('c/d/node_modules')); }); it('should copy all the source boilerplate files for systemjs', () => { + const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); exampleBoilerPlate.loadJsonFile.and.callFake(filePath => filePath.indexOf('a/b') !== -1 ? { projectType: 'systemjs' } : {}) exampleBoilerPlate.add(); expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( @@ -57,19 +64,20 @@ describe('example-boilerplate tool', () => { (BPFiles.common * exampleFolders.length) ); // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/boilerplate/systemjs'), 'a/b', 'package.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/boilerplate/common'), 'a/b', 'src/styles.css'); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/systemjs`, 'a/b', 'package.json'); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'a/b', 'src/styles.css'); }); it('should copy all the source boilerplate files for cli', () => { + const boilerplateDir = path.resolve(sharedDir, 'boilerplate'); exampleBoilerPlate.add(); expect(exampleBoilerPlate.copyFile).toHaveBeenCalledTimes( (BPFiles.cli * exampleFolders.length) + (BPFiles.common * exampleFolders.length) ); // for example - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/boilerplate/cli'), 'a/b', 'package.json'); - expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(path.resolve(__dirname, 'shared/boilerplate/common'), 'c/d', 'src/styles.css'); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/cli`, 'a/b', 'package.json'); + expect(exampleBoilerPlate.copyFile).toHaveBeenCalledWith(`${boilerplateDir}/common`, 'c/d', 'src/styles.css'); }); it('should try to load the example config file', () => { @@ -89,27 +97,33 @@ describe('example-boilerplate tool', () => { }); describe('installNodeModules', () => { - it('should run `yarn` in the base path', () => { + 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' }); }); - }); - describe('overridePackage', () => { - beforeEach(() => { - spyOn(shelljs, 'rm'); - spyOn(fs, 'copySync'); + 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(); }); - it('should remove the original package from the shared node_modules folder', () => { - exampleBoilerPlate.overridePackage('base/path', 'somePackage'); - expect(shelljs.rm).toHaveBeenCalledWith('-rf', path.resolve(__dirname, 'shared/node_modules/@angular/somePackage')); - }); + it('should restore the Angular packages if `useLocal` is not true', () => { + exampleBoilerPlate.installNodeModules('some/base/path1'); + expect(ngPackagesInstaller.restorePackages).toHaveBeenCalledWith('some/base/path1'); - it('should copy the source folder to the shared node_modules folder', () => { - exampleBoilerPlate.overridePackage('base/path', 'somePackage'); - expect(fs.copySync).toHaveBeenCalledWith(path.resolve('base/path/somePackage'), path.resolve(__dirname, 'shared/node_modules/@angular/somePackage')); + exampleBoilerPlate.installNodeModules('some/base/path2', false); + expect(ngPackagesInstaller.restorePackages).toHaveBeenCalledWith('some/base/path2'); + + expect(ngPackagesInstaller.overwritePackages).not.toHaveBeenCalled(); }); }); diff --git a/aio/tools/ng-packages-installer.js b/aio/tools/ng-packages-installer.js new file mode 100644 index 0000000000..1bcd29980e --- /dev/null +++ b/aio/tools/ng-packages-installer.js @@ -0,0 +1,235 @@ +#!/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 new file mode 100644 index 0000000000..690a6144bb --- /dev/null +++ b/aio/tools/ng-packages-installer.spec.js @@ -0,0 +1,286 @@ +'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/yarn.lock b/aio/yarn.lock index 12ad34e9bf..614cbe3dfd 100644 --- a/aio/yarn.lock +++ b/aio/yarn.lock @@ -1085,7 +1085,7 @@ chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" dependencies: diff --git a/scripts/ci/test-aio.sh b/scripts/ci/test-aio.sh index 12374312f5..f3353d7e12 100755 --- a/scripts/ci/test-aio.sh +++ b/scripts/ci/test-aio.sh @@ -18,12 +18,6 @@ source ${thisDir}/_travis-fold.sh travisFoldEnd "test.aio.lint" - # Run unit tests for boilerplate tools - travisFoldStart "test.aio.boilerplate.unit" - yarn boilerplate:test - travisFoldEnd "test.aio.boilerplate.unit" - - # Run unit tests travisFoldStart "test.aio.unit" yarn test -- --single-run @@ -38,7 +32,7 @@ source ${thisDir}/_travis-fold.sh # Run PWA-score tests travisFoldStart "test.aio.pwaScore" - yarn test-pwa-score-local + yarn test-pwa-score-localhost travisFoldEnd "test.aio.pwaScore"