feat(docs-infra): support building the local Angular packages in `NgPackagesInstaller` (#31985)

Previously, when `NgPackagesInstaller` needed to install the local
Angular packages to a project, it assumed the `dist/packages-dist/`
would exist and contain up-to-date built packages. I.e. it was up to the
user to have built the packages first (by running the appropriate
script).
However, many people not familiar with the `aio/` infrastructure assumed
that `yarn build-local` or `yarn build-with-ivy` would take care of all
the necessary steps.

To avoid getting confusing errors (or worse yet, using outdated local
packages), `NgPackagesInstaller` now has an option to build the packages
before using them.
One caveat is that the build script does not currently work on Windows,
so a warning is printed instead, letting the user know they need to
(somehow) take care of it themselves.

NOTE 1: Since the build script is using bazel, running it should not be
        expensive if the directory is up-to-date (i.e. no changes have
        been made to source code after the last build).
NOTE 2: This commit adds support for `--build-packages`, but does not
        change the default behavior (not building the packages). It will
        be turned on in a subsequent commit.

PR Close #31985
This commit is contained in:
George Kalpakas 2019-08-03 17:37:19 +03:00 committed by Alex Rickabaugh
parent bc8eb8508b
commit 18aa173d39
2 changed files with 129 additions and 22 deletions

View File

@ -14,7 +14,8 @@ const LOCAL_MARKER_PATH = 'node_modules/_local_.json';
const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/; const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/;
const ANGULAR_ROOT_DIR = path.resolve(__dirname, '../../..'); const ANGULAR_ROOT_DIR = path.resolve(__dirname, '../../..');
const ANGULAR_DIST_PACKAGES = path.resolve(ANGULAR_ROOT_DIR, 'dist/packages-dist'); const ANGULAR_DIST_PACKAGES = path.join(ANGULAR_ROOT_DIR, 'dist/packages-dist');
const ANGULAR_DIST_PACKAGES_BUILD_CMD = path.join(ANGULAR_ROOT_DIR, 'scripts/build-packages-dist.sh');
/** /**
* A tool that can install Angular dependencies for a project from NPM or from the * A tool that can install Angular dependencies for a project from NPM or from the
@ -32,11 +33,14 @@ class NgPackagesInstaller {
* @param {object} options - a hash of options for the install: * @param {object} options - a hash of options for the install:
* * `debug` (`boolean`) - whether to display debug messages. * * `debug` (`boolean`) - whether to display debug messages.
* * `force` (`boolean`) - whether to force a local installation even if there is a local marker file. * * `force` (`boolean`) - whether to force a local installation even if there is a local marker file.
* * `buildPackages` (`boolean`) - whether to build the local Angular packages before using them.
* (NOTE: Building the packages is currently not supported on Windows, so a message is printed instead.)
* * `ignorePackages` (`string[]`) - a collection of names of packages that should not be copied over. * * `ignorePackages` (`string[]`) - a collection of names of packages that should not be copied over.
*/ */
constructor(projectDir, options = {}) { constructor(projectDir, options = {}) {
this.debug = options.debug; this.debug = options.debug;
this.force = options.force; this.force = options.force;
this.buildPackages = options.buildPackages;
this.ignorePackages = options.ignorePackages || []; this.ignorePackages = options.ignorePackages || [];
this.projectDir = path.resolve(projectDir); this.projectDir = path.resolve(projectDir);
this.localMarkerPath = path.resolve(this.projectDir, LOCAL_MARKER_PATH); this.localMarkerPath = path.resolve(this.projectDir, LOCAL_MARKER_PATH);
@ -158,6 +162,31 @@ class NgPackagesInstaller {
}); });
} }
/**
* Build the local Angular packages.
*
* NOTE:
* Building the packages is currently not supported on Windows, so a message is printed instead, prompting the user to
* do it themselves (e.g. using Windows Subsystem for Linux or a docker container).
*/
_buildDistPackages() {
const canBuild = process.platform !== 'win32';
if (canBuild) {
this._log(`Building the Angular packages with: ${ANGULAR_DIST_PACKAGES_BUILD_CMD}`);
shelljs.exec(ANGULAR_DIST_PACKAGES_BUILD_CMD);
} else {
this._warn([
'Automatically building the local Angular packages is currently not supported on Windows.',
`Please, ensure '${ANGULAR_DIST_PACKAGES}' exists and is up-to-date (e.g. by running ` +
`'${ANGULAR_DIST_PACKAGES_BUILD_CMD}' in Git Bash for Windows, Windows Subsystem for Linux or a Linux ` +
'docker container or VM).',
'',
'Proceeding anyway...',
].join('\n'));
}
}
_collectDependencies(dependencies, packages) { _collectDependencies(dependencies, packages) {
const peerDependencies = Object.create(null); const peerDependencies = Object.create(null);
const mergedDependencies = Object.assign(Object.create(null), dependencies); const mergedDependencies = Object.assign(Object.create(null), dependencies);
@ -190,6 +219,10 @@ class NgPackagesInstaller {
this._log(`Angular distributable directory: ${distDir}.`); this._log(`Angular distributable directory: ${distDir}.`);
if (this.buildPackages) {
this._buildDistPackages();
}
shelljs shelljs
.find(distDir) .find(distDir)
.map(filePath => filePath.slice(distDir.length + 1)) .map(filePath => filePath.slice(distDir.length + 1))
@ -251,17 +284,28 @@ class NgPackagesInstaller {
const restoreCmd = `node ${relativeScriptPath} restore ${absoluteProjectDir}`; const restoreCmd = `node ${relativeScriptPath} restore ${absoluteProjectDir}`;
// Log a warning. // Log a warning.
this._warn([
`The project at "${absoluteProjectDir}" is running against the local Angular build.`,
'',
'To restore the npm packages run:',
'',
` "${restoreCmd}"`,
].join('\n'));
}
/**
* Log a warning message do draw user's attention.
* @param {...string[]} messages - The messages to be logged.
*/
_warn(...messages) {
const lines = messages.join(' ').split('\n');
console.warn(chalk.yellow([ console.warn(chalk.yellow([
'', '',
'!'.repeat(110), '!'.repeat(110),
'!!!', '!!!',
'!!! WARNING', '!!! WARNING',
'!!!', '!!!',
`!!! The project at "${absoluteProjectDir}" is running against the local Angular build.`, ...lines.map(line => `!!! ${line}`),
'!!!',
'!!! To restore the npm packages run:',
'!!!',
`!!! "${restoreCmd}"`,
'!!!', '!!!',
'!'.repeat(110), '!'.repeat(110),
'', '',
@ -294,6 +338,7 @@ function main() {
.option('debug', { describe: 'Print additional debug information.', default: false }) .option('debug', { describe: 'Print additional debug information.', default: false })
.option('force', { describe: 'Force the command to execute even if not needed.', default: false }) .option('force', { describe: 'Force the command to execute even if not needed.', default: false })
.option('build-packages', { describe: 'Build the local Angular packages, before using them.', default: false })
.option('ignore-packages', { describe: 'List of Angular packages that should not be used in local mode.', default: [], array: true }) .option('ignore-packages', { describe: 'List of Angular packages that should not be used in local mode.', default: [], array: true })
.command('overwrite <projectDir> [--force] [--debug] [--ignore-packages package1 package2]', 'Install dependencies from the locally built Angular distributables.', () => {}, argv => { .command('overwrite <projectDir> [--force] [--debug] [--ignore-packages package1 package2]', 'Install dependencies from the locally built Angular distributables.', () => {}, argv => {

View File

@ -8,13 +8,14 @@ const shelljs = require('shelljs');
const NgPackagesInstaller = require('./index'); const NgPackagesInstaller = require('./index');
describe('NgPackagesInstaller', () => { describe('NgPackagesInstaller', () => {
const rootDir = 'root/dir'; const projectDir = 'root/dir';
const absoluteRootDir = path.resolve(rootDir); const absoluteProjectDir = path.resolve(projectDir);
const nodeModulesDir = path.resolve(absoluteRootDir, 'node_modules'); const nodeModulesDir = path.resolve(absoluteProjectDir, 'node_modules');
const packageJsonPath = path.resolve(absoluteRootDir, 'package.json'); const packageJsonPath = path.resolve(absoluteProjectDir, 'package.json');
const yarnLockPath = path.resolve(absoluteRootDir, 'yarn.lock'); const yarnLockPath = path.resolve(absoluteProjectDir, 'yarn.lock');
const packagesDir = path.resolve(path.resolve(__dirname, '../../../dist/packages-dist')); const ngRootDir = path.resolve(__dirname, '../../..');
const toolsDir = path.resolve(path.resolve(__dirname, '../../../dist/tools/@angular')); const packagesDir = path.join(ngRootDir, 'dist/packages-dist');
const toolsDir = path.join(ngRootDir, 'dist/tools/@angular');
let installer; let installer;
beforeEach(() => { beforeEach(() => {
@ -25,7 +26,7 @@ describe('NgPackagesInstaller', () => {
spyOn(shelljs, 'rm'); spyOn(shelljs, 'rm');
spyOn(console, 'log'); spyOn(console, 'log');
spyOn(console, 'warn'); spyOn(console, 'warn');
installer = new NgPackagesInstaller(rootDir); installer = new NgPackagesInstaller(projectDir);
}); });
describe('checkDependencies()', () => { describe('checkDependencies()', () => {
@ -36,14 +37,14 @@ describe('NgPackagesInstaller', () => {
it('should not print a warning if there is no _local_.json file', () => { it('should not print a warning if there is no _local_.json file', () => {
fs.existsSync.and.returnValue(false); fs.existsSync.and.returnValue(false);
installer.checkDependencies(); installer.checkDependencies();
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json')); expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(projectDir, 'node_modules/_local_.json'));
expect(installer._printWarning).not.toHaveBeenCalled(); expect(installer._printWarning).not.toHaveBeenCalled();
}); });
it('should print a warning if there is a _local_.json file', () => { it('should print a warning if there is a _local_.json file', () => {
fs.existsSync.and.returnValue(true); fs.existsSync.and.returnValue(true);
installer.checkDependencies(); installer.checkDependencies();
expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(rootDir, 'node_modules/_local_.json')); expect(fs.existsSync).toHaveBeenCalledWith(path.resolve(projectDir, 'node_modules/_local_.json'));
expect(installer._printWarning).toHaveBeenCalled(); expect(installer._printWarning).toHaveBeenCalled();
}); });
}); });
@ -242,7 +243,68 @@ describe('NgPackagesInstaller', () => {
}); });
}); });
describe('_buildDistPackages()', () => {
// Call `_buildDistPackages()` with a mock `process.platform` value.
const buildDistPackagesOnPlatform = platform => {
const originalDescriptor = Object.getOwnPropertyDescriptor(process, 'platform');
Object.defineProperty(process, 'platform', {...originalDescriptor, value: platform});
installer._buildDistPackages();
Object.defineProperty(process, 'platform', originalDescriptor);
};
it('should build the local packages, when not on Windows', () => {
const buildScript = path.join(ngRootDir, 'scripts/build-packages-dist.sh');
buildDistPackagesOnPlatform('linux');
expect(shelljs.exec).toHaveBeenCalledWith(buildScript);
shelljs.exec.calls.reset();
buildDistPackagesOnPlatform('darwin');
expect(shelljs.exec).toHaveBeenCalledWith(buildScript);
shelljs.exec.calls.reset();
buildDistPackagesOnPlatform('anythingButWindows :(');
expect(shelljs.exec).toHaveBeenCalledWith(buildScript);
// Ensure that the script does actually exist (e.g. it was not renamed/moved).
fs.existsSync.and.callThrough();
expect(fs.existsSync(buildScript)).toBe(true);
});
it('should print a warning, when on Windows', () => {
buildDistPackagesOnPlatform('win32');
const warning = console.warn.calls.argsFor(0)[0];
expect(shelljs.exec).not.toHaveBeenCalled();
expect(warning).toContain(
'Automatically building the local Angular packages is currently not supported on Windows.');
expect(warning).toContain('Git Bash for Windows');
expect(warning).toContain('Windows Subsystem for Linux');
expect(warning).toContain('Linux docker container or VM');
});
});
describe('_getDistPackages()', () => { describe('_getDistPackages()', () => {
beforeEach(() => spyOn(NgPackagesInstaller.prototype, '_buildDistPackages'));
it('should not build the local packages by default', () => {
installer._getDistPackages();
expect(installer._buildDistPackages).not.toHaveBeenCalled();
});
it('should build the local packages, if `buildPackages` is true', () => {
installer = new NgPackagesInstaller(projectDir, {buildPackages: true});
installer._getDistPackages();
expect(installer._buildDistPackages).toHaveBeenCalledTimes(1);
});
it('should not build the local packages by default', () => {
installer._getDistPackages();
expect(installer._buildDistPackages).not.toHaveBeenCalled();
});
it('should include top level Angular packages', () => { it('should include top level Angular packages', () => {
const ngPackages = installer._getDistPackages(); const ngPackages = installer._getDistPackages();
const expectedValue = jasmine.objectContaining({ const expectedValue = jasmine.objectContaining({
@ -269,7 +331,7 @@ describe('NgPackagesInstaller', () => {
}); });
it('should not include packages that have been ignored', () => { it('should not include packages that have been ignored', () => {
installer = new NgPackagesInstaller(rootDir, { ignorePackages: ['@angular/router'] }); installer = new NgPackagesInstaller(projectDir, { ignorePackages: ['@angular/router'] });
const ngPackages = installer._getDistPackages(); const ngPackages = installer._getDistPackages();
expect(ngPackages['@angular/common']).toBeDefined(); expect(ngPackages['@angular/common']).toBeDefined();
@ -283,9 +345,9 @@ describe('NgPackagesInstaller', () => {
}); });
it('should assign the debug property from the options', () => { it('should assign the debug property from the options', () => {
installer = new NgPackagesInstaller(rootDir, { debug: true }); installer = new NgPackagesInstaller(projectDir, { debug: true });
expect(installer.debug).toBe(true); expect(installer.debug).toBe(true);
installer = new NgPackagesInstaller(rootDir, { }); installer = new NgPackagesInstaller(projectDir, { });
expect(installer.debug).toBe(undefined); expect(installer.debug).toBe(undefined);
}); });
@ -350,7 +412,7 @@ describe('NgPackagesInstaller', () => {
expect(console.warn.calls.argsFor(0)[0]).toMatch(restoreCmdRe1); expect(console.warn.calls.argsFor(0)[0]).toMatch(restoreCmdRe1);
// When run for a different directory... // When run for a different directory...
const dir2 = rootDir; const dir2 = projectDir;
const restoreCmdRe2 = RegExp(`\\bnode .*?ng-packages-installer/index restore .*?${path.resolve(dir1)}\\b`); const restoreCmdRe2 = RegExp(`\\bnode .*?ng-packages-installer/index restore .*?${path.resolve(dir1)}\\b`);
installer = new NgPackagesInstaller(dir2); installer = new NgPackagesInstaller(dir2);
installer._printWarning(''); installer._printWarning('');
@ -361,14 +423,14 @@ describe('NgPackagesInstaller', () => {
describe('_installDeps()', () => { describe('_installDeps()', () => {
it('should run yarn install with the given options', () => { it('should run yarn install with the given options', () => {
installer._installDeps('option-1', 'option-2'); installer._installDeps('option-1', 'option-2');
expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteRootDir }); expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteProjectDir });
}); });
}); });
describe('local marker helpers', () => { describe('local marker helpers', () => {
let installer; let installer;
beforeEach(() => { beforeEach(() => {
installer = new NgPackagesInstaller(rootDir); installer = new NgPackagesInstaller(projectDir);
}); });
describe('_checkLocalMarker', () => { describe('_checkLocalMarker', () => {