diff --git a/aio/tools/ng-packages-installer/index.js b/aio/tools/ng-packages-installer/index.js index 2146dded56..c5e93da4e4 100644 --- a/aio/tools/ng-packages-installer/index.js +++ b/aio/tools/ng-packages-installer/index.js @@ -70,8 +70,12 @@ class NgPackagesInstaller { installLocalDependencies() { if (this.force || !this._checkLocalMarker()) { const pathToPackageConfig = path.resolve(this.projectDir, PACKAGE_JSON); + const packageConfigFile = fs.readFileSync(pathToPackageConfig, 'utf8'); + const packageConfig = JSON.parse(packageConfigFile); + const pathToLockfile = path.resolve(this.projectDir, YARN_LOCK); const parsedLockfile = this._parseLockfile(pathToLockfile); + const packages = this._getDistPackages(); try { @@ -96,12 +100,12 @@ class NgPackagesInstaller { }); }); + // Overwrite the package's version to avoid version mismatch errors with the CLI. + this._overwritePackageVersion(key, tmpConfig, packageConfig, parsedLockfile); + fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(tmpConfig, null, 2)); }); - const packageConfigFile = fs.readFileSync(pathToPackageConfig, 'utf8'); - const packageConfig = JSON.parse(packageConfigFile); - const [dependencies, peers] = this._collectDependencies(packageConfig.dependencies || {}, packages); const [devDependencies, devPeers] = this._collectDependencies(packageConfig.devDependencies || {}, packages); @@ -265,6 +269,35 @@ class NgPackagesInstaller { } } + /** + * Update a package's version with the fake version based on the package's original version in the projects's + * lockfile. + * + * **Background:** + * This helps avoid version mismatch errors with the CLI. + * Since the version set by bazel on the locally built packages is determined based on the latest tag for a commit on + * the current branch, it is often the case that this version is older than what the current `@angular/cli` version is + * compatible with (e.g. if the user has not fetched the latest tags from `angular/angular` or the branch has not been + * rebased recently. + * + * @param {string} packageName - The name of the package we are updating (e.g. `'@angular/core'`). + * @param {{[key: string]: any}} packageConfig - The package's parsed `package.json`. + * @param {{[key: string]: any}} projectConfig - The project's parsed `package.json`. + * @param {import('@yarnpkg/lockfile').LockFileObject} projectLockfile - The projects's parsed `yarn.lock`. + */ + _overwritePackageVersion(packageName, packageConfig, projectConfig, projectLockfile) { + const originalVersionRange = (projectConfig.dependencies || {})[packageName] || + (projectConfig.devDependencies || {})[packageName]; + const originalVersion = + (projectLockfile[`${packageName}@${originalVersionRange}`] || {}).version; + + if (originalVersion !== undefined) { + const newVersion = `${originalVersion}+locally-overwritten-by-ngPackagesInstaller`; + this._log(`Overwriting the version of '${packageName}': ${packageConfig.version} --> ${newVersion}`); + packageConfig.version = newVersion; + } + } + /** * Extract the value for a boolean cli argument/option. When passing an option multiple times, `yargs` parses it as an * array of boolean values. In that case, we only care about the last occurrence. diff --git a/aio/tools/ng-packages-installer/index.spec.js b/aio/tools/ng-packages-installer/index.spec.js index f858e5e005..7511e86200 100644 --- a/aio/tools/ng-packages-installer/index.spec.js +++ b/aio/tools/ng-packages-installer/index.spec.js @@ -362,6 +362,82 @@ describe('NgPackagesInstaller', () => { }); }); + 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;