angular-cn/aio/tools/ng-packages-installer/index.spec.js

534 lines
22 KiB
JavaScript
Raw Normal View History

'use strict';
const fs = require('fs-extra');
const lockfile = require('@yarnpkg/lockfile');
const path = require('canonical-path');
const shelljs = require('shelljs');
const NgPackagesInstaller = require('./index');
describe('NgPackagesInstaller', () => {
const projectDir = 'root/dir';
const absoluteProjectDir = path.resolve(projectDir);
const nodeModulesDir = path.resolve(absoluteProjectDir, 'node_modules');
const packageJsonPath = path.resolve(absoluteProjectDir, 'package.json');
const yarnLockPath = path.resolve(absoluteProjectDir, 'yarn.lock');
const ngRootDir = path.resolve(__dirname, '../../..');
const packagesDir = path.join(ngRootDir, 'dist/packages-dist');
const toolsDir = path.join(ngRootDir, 'dist/tools/@angular');
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(projectDir);
});
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(projectDir, '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(projectDir, 'node_modules/_local_.json'));
expect(installer._printWarning).toHaveBeenCalled();
});
});
describe('installLocalDependencies()', () => {
const copyJsonObj = obj => JSON.parse(JSON.stringify(obj));
let dummyNgPackages, dummyPackage, dummyPackageJson, expectedModifiedPackage, expectedModifiedPackageJson;
beforeEach(() => {
spyOn(installer, '_checkLocalMarker');
spyOn(installer, '_installDeps');
spyOn(installer, '_setLocalMarker');
spyOn(installer, '_parseLockfile').and.returnValue({
'rxjs@^6.3.0': {version: '6.3.3'},
'zone.js@^0.8.26': {version: '0.8.27'}
});
// These are the packages that are "found" in the dist directory
dummyNgPackages = {
'@angular/core': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/core/package.json`,
config: {
peerDependencies: {
'rxjs': '^6.4.0',
'some-package': '5.0.1',
'zone.js': '~0.8.26'
}
}
},
'@angular/common': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/common/package.json`,
config: { peerDependencies: { '@angular/core': '4.4.4-1ab23cd4' } }
},
'@angular/compiler': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/compiler/package.json`,
config: { peerDependencies: { '@angular/common': '4.4.4-1ab23cd4' } }
},
'@angular/compiler-cli': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/compiler-cli/package.json`,
config: {
dependencies: { '@angular/tsc-wrapped': '4.4.4-1ab23cd4' },
peerDependencies: { typescript: '^2.4.2', '@angular/compiler': '4.4.4-1ab23cd4' }
}
},
'@angular/tsc-wrapped': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/tsc-wrapped/package.json`,
config: {
devDependencies: { '@angular/common': '4.4.4-1ab23cd4' },
peerDependencies: { tsickle: '^1.4.0' }
}
}
};
spyOn(installer, '_getDistPackages').and.callFake(() => copyJsonObj(dummyNgPackages));
// This is the package.json in the "test" folder
dummyPackage = {
dependencies: {
'@angular/core': '4.4.1',
'@angular/common': '4.4.1',
rxjs: '^6.3.0'
},
devDependencies: {
'@angular/compiler-cli': '4.4.1',
'zone.js': '^0.8.26'
}
};
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 updated or added as
// (dev)dependencies (unless the current version in lockfile satisfies semver).
//
// For example, `zone.js@0.8.27` (from lockfile) satisfies `zone.js@~0.8.26` (from
// `@angular/core`), thus `zone.js: ^0.8.26` (from original `package.json`) is retained.
// In contrast, `rxjs@6.3.3` (from lockfile) does not satisfy `rxjs@^6.4.0 (from
// `@angular/core`), thus `rxjs: ^6.3.0` (from original `package.json`) is replaced with
// `rxjs: ^6.4.0` (from `@angular/core`).
expectedModifiedPackage = {
dependencies: {
'@angular/core': `file:${packagesDir}/core`,
'@angular/common': `file:${packagesDir}/common`,
'rxjs': '^6.4.0'
},
devDependencies: {
'@angular/compiler-cli': `file:${toolsDir}/compiler-cli`,
'zone.js': '^0.8.26',
'some-package': '5.0.1',
typescript: '^2.4.2'
},
__angular: { local: true }
};
expectedModifiedPackageJson = JSON.stringify(expectedModifiedPackage, null, 2);
});
describe('when there is a local package marker', () => {
beforeEach(() => installer._checkLocalMarker.and.returnValue(true));
it('should not continue processing', () => {
installer.installLocalDependencies();
expect(installer._checkLocalMarker).toHaveBeenCalled();
expect(installer._getDistPackages).not.toHaveBeenCalled();
});
it('should continue processing (without checking for local marker) if `force` is true', () => {
installer.force = true;
installer.installLocalDependencies();
expect(installer._checkLocalMarker).not.toHaveBeenCalled();
expect(installer._getDistPackages).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}`));
installer._installDeps.and.callFake((...args) => log.push(`installDeps: ${args.join(' ')}`));
installer._checkLocalMarker.and.returnValue(false);
installer.installLocalDependencies();
});
it('should parse the lockfile and get the dist packages', () => {
expect(installer._checkLocalMarker).toHaveBeenCalled();
expect(installer._parseLockfile).toHaveBeenCalledWith(yarnLockPath);
expect(installer._getDistPackages).toHaveBeenCalled();
});
it('should temporarily overwrite the package.json files of local Angular packages', () => {
const pkgJsonFor = pkgName => dummyNgPackages[`@angular/${pkgName}`].packageJsonPath;
const pkgConfigFor = pkgName => copyJsonObj(dummyNgPackages[`@angular/${pkgName}`].config);
const overwriteConfigFor = (pkgName, newProps) => Object.assign(pkgConfigFor(pkgName), newProps);
const stringifyConfig = config => JSON.stringify(config, null, 2);
const allArgs = fs.writeFileSync.calls.allArgs();
const firstFiveArgs = allArgs.slice(0, 5);
const lastFiveArgs = allArgs.slice(-5);
expect(firstFiveArgs).toEqual([
[pkgJsonFor('core'), stringifyConfig(overwriteConfigFor('core', {private: true}))],
[pkgJsonFor('common'), stringifyConfig(overwriteConfigFor('common', {private: true}))],
[pkgJsonFor('compiler'), stringifyConfig(overwriteConfigFor('compiler', {private: true}))],
[pkgJsonFor('compiler-cli'), stringifyConfig(overwriteConfigFor('compiler-cli', {
private: true,
dependencies: { '@angular/tsc-wrapped': `file:${toolsDir}/tsc-wrapped` }
}))],
[pkgJsonFor('tsc-wrapped'), stringifyConfig(overwriteConfigFor('tsc-wrapped', {
private: true,
devDependencies: { '@angular/common': `file:${packagesDir}/common` }
}))],
]);
expect(lastFiveArgs).toEqual(['core', 'common', 'compiler', 'compiler-cli', 'tsc-wrapped']
.map(pkgName => [pkgJsonFor(pkgName), stringifyConfig(pkgConfigFor(pkgName))]));
});
it('should load the package.json', () => {
expect(fs.readFileSync).toHaveBeenCalledWith(packageJsonPath, 'utf8');
});
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: --pure-lockfile --check-files`,
`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` in the specified directory, with the correct options', () => {
spyOn(installer, '_installDeps');
installer.restoreNpmDependencies();
expect(installer._installDeps).toHaveBeenCalledWith('--frozen-lockfile', '--check-files');
});
});
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.js');
const buildCmd = `"${process.execPath}" "${buildScript}"`;
buildDistPackagesOnPlatform('linux');
expect(shelljs.exec).toHaveBeenCalledWith(buildCmd);
shelljs.exec.calls.reset();
buildDistPackagesOnPlatform('darwin');
expect(shelljs.exec).toHaveBeenCalledWith(buildCmd);
shelljs.exec.calls.reset();
buildDistPackagesOnPlatform('anythingButWindows :(');
expect(shelljs.exec).toHaveBeenCalledWith(buildCmd);
// 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()', () => {
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', () => {
const ngPackages = installer._getDistPackages();
const expectedValue = jasmine.objectContaining({
parentDir: jasmine.any(String),
packageJsonPath: jasmine.any(String),
config: jasmine.any(Object),
});
// For example...
expect(ngPackages['@angular/common']).toEqual(expectedValue);
expect(ngPackages['@angular/core']).toEqual(expectedValue);
expect(ngPackages['@angular/router']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
});
it('should store each package\'s parent directory', () => {
const ngPackages = installer._getDistPackages();
// For example...
expect(ngPackages['@angular/core'].parentDir).toBe(packagesDir);
expect(ngPackages['@angular/router'].parentDir).toBeDefined(toolsDir);
});
it('should not include packages that have been ignored', () => {
installer = new NgPackagesInstaller(projectDir, { ignorePackages: ['@angular/router'] });
const ngPackages = installer._getDistPackages();
expect(ngPackages['@angular/common']).toBeDefined();
expect(ngPackages['@angular/router']).toBeUndefined();
});
});
describe('_log()', () => {
beforeEach(() => {
spyOn(console, 'info');
});
it('should assign the debug property from the options', () => {
installer = new NgPackagesInstaller(projectDir, { debug: true });
expect(installer.debug).toBe(true);
installer = new NgPackagesInstaller(projectDir, { });
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');
});
});
fix(docs-infra): avoid version mismatch error when using local Angular packages (#34213) The local Angular packages used to build `aio/` when running `yarn setup-local`/`yarn build-local` (and related commands), are built by bazel. Bazel, determines the version to use for these locally built packages based on the latest tag for a commit on the current branch. (This works as expected during the release, because the packages are built on the correct branch with up-to-date tags.) During local development, however, this often leads to generating older versions than what the current `@angular/cli` version is compatible with, if the user has not fetched the latest tags from `angular/angular` or the branch has not been rebased recently. Previously, the above (common) situation would result in a build error by the CLI. (Note that this would be a false error, because in this case the version set by bazel would not reflect the actual version of the local packages.) The solution would be for the user to fetch the latest tags from `angular/angular`, rebase their branch and run a bazel build again (ensuring that it would actually build anew and not emit cached artifacts). This was cumbersome and most people didn't even know about it. This commit avoids this error and the associated pain-points by overwriting the versions of the installed local packages with fake versions based on the ones in the lockfile, which are guaranteed to be compatible with the currently used CLI version. Fixes #34208 PR Close #34213
2019-12-03 08:38:09 -05:00
describe('_overwritePackageVersion()', () => {
it('should do nothing if the specified package is not a dependency', () => {
const pkgConfig = {name: '@scope/missing', version: 'local-version'};
const lockFile = {
[`${pkgConfig.name}@project-range`]: {version: 'project-version'},
};
let projectConfig;
// No `dependencies`/`devDependencies` at all.
projectConfig = {};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('local-version');
// Not listed in `dependencies`/`devDependencies`.
projectConfig = {
dependencies: {otherPackage: 'foo'},
devDependencies: {yetAnotherPackage: 'bar'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('local-version');
});
it('should do nothing if the specified package cannot be found in the lockfile', () => {
const pkgConfig = {name: '@scope/missing', version: 'local-version'};
const projectConfig = {
dependencies: {[pkgConfig.name]: 'project-range'},
};
let lockFile;
// Package missing from lockfile.
lockFile = {
'otherPackage@someRange': {version: 'some-version'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('local-version');
// Package present in lockfile, but for a different version range.
lockFile = {
[`${pkgConfig.name}@other-range`]: {version: 'project-version'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('local-version');
});
it('should overwrite the package version if it is a dependency and found in the lockfile', () => {
const pkgConfig = {name: '@scope/found', version: 'local-version'};
const lockFile = {
[`${pkgConfig.name}@project-range-prod`]: {version: 'project-version-prod'},
[`${pkgConfig.name}@project-range-dev`]: {version: 'project-version-dev'},
};
let projectConfig;
// Package in `dependencies`.
projectConfig = {
dependencies: {[pkgConfig.name]: 'project-range-prod'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('project-version-prod+locally-overwritten-by-ngPackagesInstaller');
// // Package in `devDependencies`.
projectConfig = {
devDependencies: {[pkgConfig.name]: 'project-range-dev'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('project-version-dev+locally-overwritten-by-ngPackagesInstaller');
// // Package in both `dependencies` and `devDependencies` (the former takes precedence).
projectConfig = {
devDependencies: {[pkgConfig.name]: 'project-range-dev'},
dependencies: {[pkgConfig.name]: 'project-range-prod'},
};
installer._overwritePackageVersion(pkgConfig.name, pkgConfig, projectConfig, lockFile);
expect(pkgConfig.version).toBe('project-version-prod+locally-overwritten-by-ngPackagesInstaller');
});
});
describe('_parseLockfile()', () => {
let originalLockfileParseDescriptor;
beforeEach(() => {
// Workaround for `lockfile.parse()` being non-writable.
let parse = lockfile.parse;
originalLockfileParseDescriptor = Object.getOwnPropertyDescriptor(lockfile, 'parse');
Object.defineProperty(lockfile, 'parse', {
get() { return parse; },
set(newParse) { parse = newParse; },
});
fs.readFileSync.and.returnValue('mock content');
spyOn(lockfile, 'parse').and.returnValue({type: 'success', object: {foo: {version: 'bar'}}});
});
afterEach(() => Object.defineProperty(lockfile, 'parse', originalLockfileParseDescriptor));
it('should parse the specified lockfile', () => {
installer._parseLockfile('/foo/bar/yarn.lock');
expect(fs.readFileSync).toHaveBeenCalledWith('/foo/bar/yarn.lock', 'utf8');
expect(lockfile.parse).toHaveBeenCalledWith('mock content');
});
it('should throw if parsing the lockfile fails', () => {
lockfile.parse.and.returnValue({type: 'not success'});
expect(() => installer._parseLockfile('/foo/bar/yarn.lock')).toThrowError(
'[NgPackagesInstaller]: Error parsing lockfile \'/foo/bar/yarn.lock\' (result type: not success).');
});
it('should return the parsed lockfile content as an object', () => {
const parsed = installer._parseLockfile('/foo/bar/yarn.lock');
expect(parsed).toEqual({foo: {version: '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 = projectDir;
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: absoluteProjectDir });
});
});
describe('local marker helpers', () => {
let installer;
beforeEach(() => {
installer = new NgPackagesInstaller(projectDir);
});
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');
});
});
});
});