Skip to content

Commit 74d6406

Browse files
committed
fix(@angular-devkit/schematics): filters out non-NPM dependencies
Fixes angular#13059. Non-NPM dependencies in package.json are now filtered out of `ng update`. Dependencies which are linked as tarball or Git references will now be skipped during the update process.
1 parent 924704c commit 74d6406

File tree

6 files changed

+86
-17
lines changed

6 files changed

+86
-17
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@
104104
"@types/loader-utils": "^1.1.3",
105105
"@types/minimist": "^1.2.0",
106106
"@types/node": "10.12.30",
107+
"@types/npm-package-arg": "^6.1.0",
107108
"@types/request": "^2.47.1",
108109
"@types/rimraf": "^2.0.2",
109110
"@types/semver": "^6.0.0",

packages/schematics/update/BUILD

+2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ ts_library(
2828
"//packages/angular_devkit/schematics",
2929
"//packages/angular_devkit/schematics:tasks",
3030
"@npm//@types/node",
31+
"@npm//@types/npm-package-arg",
3132
"@npm//@types/semver",
33+
"@npm//npm-package-arg",
3234
"@npm//rxjs",
3335
"@npm//semver",
3436
],

packages/schematics/update/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
"@angular-devkit/schematics": "0.0.0",
1616
"@yarnpkg/lockfile": "1.1.0",
1717
"ini": "1.3.5",
18+
"npm-package-arg": "^7.0.0",
1819
"pacote": "10.1.3",
20+
"rxjs": "6.5.3",
1921
"semver": "6.3.0",
20-
"semver-intersect": "1.4.0",
21-
"rxjs": "6.5.3"
22+
"semver-intersect": "1.4.0"
2223
}
2324
}

packages/schematics/update/update/index.ts

+33-13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
Tree,
1515
} from '@angular-devkit/schematics';
1616
import { NodePackageInstallTask, RunSchematicTask } from '@angular-devkit/schematics/tasks';
17+
import * as npa from 'npm-package-arg';
1718
import { Observable, from as observableFrom, of } from 'rxjs';
1819
import { map, mergeMap, reduce, switchMap } from 'rxjs/operators';
1920
import * as semver from 'semver';
@@ -788,7 +789,7 @@ function _addPeerDependencies(
788789
}
789790

790791

791-
function _getAllDependencies(tree: Tree): Map<string, VersionRange> {
792+
function _getAllDependencies(tree: Tree): Array<readonly [string, VersionRange]> {
792793
const packageJsonContent = tree.read('/package.json');
793794
if (!packageJsonContent) {
794795
throw new SchematicsException('Could not find a package.json. Are you in a Node project?');
@@ -801,11 +802,11 @@ function _getAllDependencies(tree: Tree): Map<string, VersionRange> {
801802
throw new SchematicsException('package.json could not be parsed: ' + e.message);
802803
}
803804

804-
return new Map<string, VersionRange>([
805-
...Object.entries(packageJson.peerDependencies || {}),
806-
...Object.entries(packageJson.devDependencies || {}),
807-
...Object.entries(packageJson.dependencies || {}),
808-
] as [string, VersionRange][]);
805+
return [
806+
...Object.entries(packageJson.peerDependencies || {}) as Array<[string, VersionRange]>,
807+
...Object.entries(packageJson.devDependencies || {}) as Array<[string, VersionRange]>,
808+
...Object.entries(packageJson.dependencies || {}) as Array<[string, VersionRange]>,
809+
];
809810
}
810811

811812
function _formatVersion(version: string | undefined) {
@@ -826,6 +827,16 @@ function _formatVersion(version: string | undefined) {
826827
return version;
827828
}
828829

830+
/**
831+
* Returns whether or not the given package specifier (the value string in a
832+
* `package.json` dependency) is hosted in the NPM registry.
833+
* @throws When the specifier cannot be parsed.
834+
*/
835+
function isPkgFromRegistry(name: string, specifier: string): boolean {
836+
const result = npa.resolve(name, specifier);
837+
838+
return !!result.registry;
839+
}
829840

830841
export default function(options: UpdateSchema): Rule {
831842
if (!options.packages) {
@@ -847,14 +858,23 @@ export default function(options: UpdateSchema): Rule {
847858

848859
options.from = _formatVersion(options.from);
849860
options.to = _formatVersion(options.to);
861+
const usingYarn = options.packageManager === 'yarn';
850862

851863
return (tree: Tree, context: SchematicContext) => {
852864
const logger = context.logger;
853-
const allDependencies = _getAllDependencies(tree);
854-
const packages = _buildPackageList(options, allDependencies, logger);
855-
const usingYarn = options.packageManager === 'yarn';
865+
const npmDeps = new Map(_getAllDependencies(tree).filter(([name, specifier]) => {
866+
try {
867+
return isPkgFromRegistry(name, specifier);
868+
} catch {
869+
// Abort on failure because package.json is malformed.
870+
throw new SchematicsException(
871+
`Failed to parse dependency "${name}" with specifier "${specifier}"`
872+
+ ` from package.json. Is the specifier malformed?`);
873+
}
874+
}));
875+
const packages = _buildPackageList(options, npmDeps, logger);
856876

857-
return observableFrom([...allDependencies.keys()]).pipe(
877+
return observableFrom(npmDeps.keys()).pipe(
858878
// Grab all package.json from the npm repository. This requires a lot of HTTP calls so we
859879
// try to parallelize as many as possible.
860880
mergeMap(depName => getNpmPackageJson(
@@ -899,8 +919,8 @@ export default function(options: UpdateSchema): Rule {
899919
do {
900920
lastPackagesSize = packages.size;
901921
npmPackageJsonMap.forEach((npmPackageJson) => {
902-
_addPackageGroup(tree, packages, allDependencies, npmPackageJson, logger);
903-
_addPeerDependencies(tree, packages, allDependencies, npmPackageJson, npmPackageJsonMap, logger);
922+
_addPackageGroup(tree, packages, npmDeps, npmPackageJson, logger);
923+
_addPeerDependencies(tree, packages, npmDeps, npmPackageJson, npmPackageJsonMap, logger);
904924
});
905925
} while (packages.size > lastPackagesSize);
906926

@@ -909,7 +929,7 @@ export default function(options: UpdateSchema): Rule {
909929
npmPackageJsonMap.forEach((npmPackageJson) => {
910930
packageInfoMap.set(
911931
npmPackageJson.name,
912-
_buildPackageInfo(tree, packages, allDependencies, npmPackageJson, logger),
932+
_buildPackageInfo(tree, packages, npmDeps, npmPackageJson, logger),
913933
);
914934
});
915935

packages/schematics/update/update/index_spec.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
// tslint:disable:no-big-function
99

1010
import { normalize, virtualFs } from '@angular-devkit/core';
11-
import { HostTree } from '@angular-devkit/schematics';
11+
import { HostTree, SchematicsException } from '@angular-devkit/schematics';
1212
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
13-
import { map } from 'rxjs/operators';
13+
import { EMPTY } from 'rxjs';
14+
import { catchError, map } from 'rxjs/operators';
1415
import * as semver from 'semver';
1516
import { angularMajorCompatGuarantee } from './index';
1617

@@ -75,6 +76,45 @@ describe('@schematics/update', () => {
7576
).toPromise().then(done, done.fail);
7677
}, 45000);
7778

79+
it('ignores dependencies not hosted on the NPM registry', done => {
80+
const tree = new UnitTestTree(new HostTree(new virtualFs.test.TestHost({
81+
'/package.json': `{
82+
"name": "blah",
83+
"dependencies": {
84+
"@angular-devkit-tests/update-base": "file:update-base-1.0.0.tgz"
85+
}
86+
}`,
87+
})));
88+
89+
schematicRunner.runSchematicAsync('update', { all: true }, tree).pipe(
90+
map(t => {
91+
const packageJson = JSON.parse(t.readContent('/package.json'));
92+
expect(packageJson['dependencies']['@angular-devkit-tests/update-base'])
93+
.toBe('file:update-base-1.0.0.tgz');
94+
}),
95+
).toPromise().then(done, done.fail);
96+
}, 45000);
97+
98+
it('emits SchematicsException when given an invalid dependency', done => {
99+
const tree = new UnitTestTree(new HostTree(new virtualFs.test.TestHost({
100+
'/package.json': `{
101+
"name": "blah",
102+
"dependencies": {
103+
"//": "'abc://' is not a valid protocol for NPM.",
104+
"@angular-devkit-tests/update-base": "abc://foo.tgz"
105+
}
106+
}`,
107+
})));
108+
109+
schematicRunner.runSchematicAsync('update', { all: true }, tree).pipe(
110+
catchError(err => {
111+
expect(err instanceof SchematicsException).toBe(true);
112+
113+
return EMPTY;
114+
}),
115+
).toPromise().then(done, done.fail);
116+
});
117+
78118
it('respects existing tilde and caret ranges', done => {
79119
// Add ranges.
80120
const content = virtualFs.fileBufferToString(host.sync.read(normalize('/package.json')));

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,11 @@
11661166
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
11671167
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
11681168

1169+
"@types/npm-package-arg@^6.1.0":
1170+
version "6.1.0"
1171+
resolved "https://registry.yarnpkg.com/@types/npm-package-arg/-/npm-package-arg-6.1.0.tgz#88bdfce72f6a3d5fa1053c8d44d655e7850642e4"
1172+
integrity sha512-vbt5fb0y1svMhu++1lwtKmZL76d0uPChFlw7kEzyUmTwfmpHRcFb8i0R8ElT69q/L+QLgK2hgECivIAvaEDwag==
1173+
11691174
"@types/[email protected]", "@types/progress@^2.0.3":
11701175
version "2.0.3"
11711176
resolved "https://registry.yarnpkg.com/@types/progress/-/progress-2.0.3.tgz#7ccbd9c6d4d601319126c469e73b5bb90dfc8ccc"

0 commit comments

Comments
 (0)