Skip to content

Commit baa5375

Browse files
committed
refactor(@schematics/update): cleanup/optimize npm fetch
1 parent 248d531 commit baa5375

File tree

3 files changed

+244
-272
lines changed

3 files changed

+244
-272
lines changed

packages/schematics/update/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"dependencies": {
1313
"@angular-devkit/core": "0.0.0",
1414
"@angular-devkit/schematics": "0.0.0",
15-
"npm-registry-client": "8.6.0",
15+
"pacote": "9.1.0",
1616
"semver": "5.5.1",
1717
"semver-intersect": "1.4.0",
1818
"rxjs": "6.3.3"

packages/schematics/update/update/npm.ts

+59-250
Original file line numberDiff line numberDiff line change
@@ -6,142 +6,68 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import { logging } from '@angular-devkit/core';
9-
import { exec } from 'child_process';
109
import { readFileSync } from 'fs';
11-
import { Observable, ReplaySubject, concat, of } from 'rxjs';
12-
import {
13-
catchError,
14-
concatMap,
15-
defaultIfEmpty,
16-
filter,
17-
first,
18-
map,
19-
shareReplay,
20-
toArray,
21-
} from 'rxjs/operators';
22-
import * as url from 'url';
10+
import { Observable, from } from 'rxjs';
11+
import { shareReplay } from 'rxjs/operators';
2312
import { NpmRepositoryPackageJson } from './npm-package-json';
2413

25-
const RegistryClient = require('npm-registry-client');
14+
const pacote = require('pacote');
2615

2716
const npmPackageJsonCache = new Map<string, Observable<NpmRepositoryPackageJson>>();
28-
const npmConfigOptionCache = new Map<string, Observable<string | undefined>>();
2917

30-
31-
function _readNpmRc(): Observable<{ [key: string]: string }> {
32-
return new Observable<{ [key: string]: string }>(subject => {
33-
// TODO: have a way to read options without using fs directly.
34-
const path = require('path');
35-
const fs = require('fs');
36-
const perProjectNpmrc = path.resolve('.npmrc');
37-
38-
let npmrc = '';
39-
40-
if (fs.existsSync(perProjectNpmrc)) {
41-
npmrc = fs.readFileSync(perProjectNpmrc).toString('utf-8');
42-
} else {
43-
if (process.platform === 'win32') {
44-
if (process.env.LOCALAPPDATA) {
45-
npmrc = fs.readFileSync(path.join(process.env.LOCALAPPDATA, '.npmrc')).toString('utf-8');
46-
}
47-
} else {
48-
if (process.env.HOME) {
49-
npmrc = fs.readFileSync(path.join(process.env.HOME, '.npmrc')).toString('utf-8');
50-
}
51-
}
52-
}
53-
54-
const allOptionsArr = npmrc.split(/\r?\n/).map(x => x.trim());
55-
const allOptions: { [key: string]: string } = {};
56-
57-
allOptionsArr.forEach(x => {
58-
const [key, ...value] = x.split('=');
59-
allOptions[key.trim()] = value.join('=').trim();
60-
});
61-
62-
subject.next(allOptions);
63-
subject.complete();
64-
}).pipe(
65-
catchError(() => of({})),
66-
shareReplay(),
67-
);
18+
let npmrc: { [key: string]: string };
19+
try {
20+
npmrc = _readNpmRc();
21+
} catch {
22+
npmrc = {};
6823
}
6924

7025

71-
function getOptionFromNpmRc(option: string): Observable<string | undefined> {
72-
return _readNpmRc().pipe(
73-
map(options => options[option]),
74-
);
75-
}
26+
function _readNpmRc(): { [key: string]: string } {
27+
// TODO: have a way to read options without using fs directly.
28+
const path = require('path');
29+
const fs = require('fs');
30+
const perProjectNpmrc = path.resolve('.npmrc');
7631

77-
function getOptionFromNpmCli(option: string): Observable<string | undefined> {
78-
return new Observable<string | undefined>(subject => {
79-
exec(`npm get ${option}`, (error, data) => {
80-
if (error) {
81-
throw error;
82-
} else {
83-
data = data.trim();
84-
if (!data || data === 'undefined' || data === 'null') {
85-
subject.next();
86-
} else {
87-
subject.next(data);
88-
}
89-
}
32+
const configs: string[] = [];
9033

91-
subject.complete();
92-
});
93-
}).pipe(
94-
catchError(() => of(undefined)),
95-
shareReplay(),
96-
);
97-
}
98-
99-
function getNpmConfigOption(
100-
option: string,
101-
scope?: string,
102-
tryWithoutScope?: boolean,
103-
): Observable<string | undefined> {
104-
if (scope && tryWithoutScope) {
105-
return concat(
106-
getNpmConfigOption(option, scope),
107-
getNpmConfigOption(option),
108-
).pipe(
109-
filter(result => !!result),
110-
defaultIfEmpty(),
111-
first(),
112-
);
34+
if (process.platform === 'win32') {
35+
if (process.env.LOCALAPPDATA) {
36+
configs.push(fs.readFileSync(path.join(process.env.LOCALAPPDATA, '.npmrc'), 'utf8'));
37+
}
38+
} else {
39+
if (process.env.HOME) {
40+
configs.push(fs.readFileSync(path.join(process.env.HOME, '.npmrc'), 'utf8'));
41+
}
11342
}
11443

115-
const fullOption = `${scope ? scope + ':' : ''}${option}`;
116-
117-
let value = npmConfigOptionCache.get(fullOption);
118-
if (value) {
119-
return value;
44+
if (fs.existsSync(perProjectNpmrc)) {
45+
configs.push(fs.readFileSync(perProjectNpmrc, 'utf8'));
12046
}
12147

122-
value = option.startsWith('_')
123-
? getOptionFromNpmRc(fullOption)
124-
: getOptionFromNpmCli(fullOption);
125-
126-
npmConfigOptionCache.set(fullOption, value);
127-
128-
return value;
129-
}
130-
131-
function getNpmClientSslOptions(strictSsl?: string, cafile?: string) {
132-
const sslOptions: { strict?: boolean, ca?: Buffer } = {};
48+
const allOptions: { [key: string]: string } = {};
49+
for (const config of configs) {
50+
const allOptionsArr = config.split(/\r?\n/).map(x => x.trim());
13351

134-
if (strictSsl === 'false') {
135-
sslOptions.strict = false;
136-
} else if (strictSsl === 'true') {
137-
sslOptions.strict = true;
138-
}
52+
allOptionsArr.forEach(x => {
53+
const [key, ...value] = x.split('=');
54+
const fullValue = value.join('=').trim();
55+
if (key && fullValue && fullValue !== 'null') {
56+
allOptions[key.trim()] = fullValue;
57+
}
58+
});
13959

140-
if (cafile) {
141-
sslOptions.ca = readFileSync(cafile);
60+
if (allOptions.cafile) {
61+
const cafile = allOptions.cafile;
62+
delete allOptions.cafile;
63+
try {
64+
allOptions.ca = readFileSync(cafile, 'utf8');
65+
allOptions.ca = allOptions.ca.replace(/\r?\n/, '\\n');
66+
} catch { }
67+
}
14268
}
14369

144-
return sslOptions;
70+
return allOptions;
14571
}
14672

14773
/**
@@ -155,141 +81,24 @@ function getNpmClientSslOptions(strictSsl?: string, cafile?: string) {
15581
export function getNpmPackageJson(
15682
packageName: string,
15783
registryUrl: string | undefined,
158-
logger: logging.LoggerApi,
84+
_logger: logging.LoggerApi,
15985
): Observable<Partial<NpmRepositoryPackageJson>> {
160-
const scope = packageName.startsWith('@') ? packageName.split('/')[0] : undefined;
161-
162-
return (
163-
registryUrl ? of(registryUrl) : getNpmConfigOption('registry', scope, true)
164-
).pipe(
165-
map(partialUrl => {
166-
if (!partialUrl) {
167-
partialUrl = 'https://registry.npmjs.org/';
168-
}
169-
const partial = url.parse(partialUrl);
170-
let fullUrl = new url.URL(`http://${partial.host}/${packageName.replace(/\//g, '%2F')}`);
171-
try {
172-
const registry = new url.URL(partialUrl);
173-
registry.pathname = (registry.pathname || '')
174-
.replace(/\/?$/, '/' + packageName.replace(/\//g, '%2F'));
175-
fullUrl = new url.URL(url.format(registry));
176-
} catch {}
177-
178-
logger.debug(
179-
`Getting package.json from '${packageName}' (url: ${JSON.stringify(fullUrl)})...`,
180-
);
181-
182-
return fullUrl;
183-
}),
184-
concatMap(fullUrl => {
185-
let maybeRequest = npmPackageJsonCache.get(fullUrl.toString());
186-
if (maybeRequest) {
187-
return maybeRequest;
188-
}
189-
190-
const registryKey = `//${fullUrl.host}/`;
191-
192-
return concat(
193-
getNpmConfigOption('proxy'),
194-
getNpmConfigOption('https-proxy'),
195-
getNpmConfigOption('strict-ssl'),
196-
getNpmConfigOption('cafile'),
197-
getNpmConfigOption('_auth'),
198-
getNpmConfigOption('_authToken', registryKey),
199-
getNpmConfigOption('username', registryKey, true),
200-
getNpmConfigOption('password', registryKey, true),
201-
getNpmConfigOption('email', registryKey, true),
202-
getNpmConfigOption('always-auth', registryKey, true),
203-
).pipe(
204-
toArray(),
205-
concatMap(options => {
206-
const [
207-
http,
208-
https,
209-
strictSsl,
210-
cafile,
211-
token,
212-
authToken,
213-
username,
214-
password,
215-
email,
216-
alwaysAuth,
217-
] = options;
218-
219-
const subject = new ReplaySubject<NpmRepositoryPackageJson>(1);
220-
221-
const sslOptions = getNpmClientSslOptions(strictSsl, cafile);
222-
223-
const auth: {
224-
token?: string,
225-
alwaysAuth?: boolean;
226-
username?: string;
227-
password?: string;
228-
email?: string;
229-
} = {};
230-
231-
if (alwaysAuth !== undefined) {
232-
auth.alwaysAuth = alwaysAuth === 'false' ? false : !!alwaysAuth;
233-
}
234-
235-
if (email) {
236-
auth.email = email;
237-
}
238-
239-
if (authToken) {
240-
auth.token = authToken;
241-
} else if (token) {
242-
try {
243-
// attempt to parse "username:password" from base64 token
244-
// to enable Artifactory / Nexus-like repositories support
245-
const delimiter = ':';
246-
const parsedToken = Buffer.from(token, 'base64').toString('ascii');
247-
const [extractedUsername, ...passwordArr] = parsedToken.split(delimiter);
248-
const extractedPassword = passwordArr.join(delimiter);
249-
250-
if (extractedUsername && extractedPassword) {
251-
auth.username = extractedUsername;
252-
auth.password = extractedPassword;
253-
} else {
254-
throw new Error('Unable to extract username and password from _auth token');
255-
}
256-
} catch (ex) {
257-
auth.token = token;
258-
}
259-
} else if (username) {
260-
auth.username = username;
261-
auth.password = password;
262-
}
263-
264-
const client = new RegistryClient({
265-
proxy: { http, https },
266-
ssl: sslOptions,
267-
});
268-
client.log.level = 'silent';
269-
const params = {
270-
timeout: 30000,
271-
auth,
272-
};
273-
274-
client.get(
275-
fullUrl.toString(),
276-
params,
277-
(error: object, data: NpmRepositoryPackageJson) => {
278-
if (error) {
279-
subject.error(error);
280-
}
281-
282-
subject.next(data);
283-
subject.complete();
284-
});
285-
286-
maybeRequest = subject.asObservable();
287-
npmPackageJsonCache.set(fullUrl.toString(), maybeRequest);
86+
const cachedResponse = npmPackageJsonCache.get(packageName);
87+
if (cachedResponse) {
88+
return cachedResponse;
89+
}
28890

289-
return maybeRequest;
290-
}),
291-
);
292-
}),
91+
const resultPromise = pacote.packument(
92+
packageName,
93+
{
94+
'full-metadata': true,
95+
...npmrc,
96+
registry: registryUrl,
97+
},
29398
);
29499

100+
const response = from<NpmRepositoryPackageJson>(resultPromise).pipe(shareReplay());
101+
npmPackageJsonCache.set(packageName, response);
102+
103+
return response;
295104
}

0 commit comments

Comments
 (0)