Skip to content

Commit a2d6751

Browse files
committed
refactor(@schematics/update): cleanup/optimize npm fetch
1 parent a53cf44 commit a2d6751

File tree

3 files changed

+283
-299
lines changed

3 files changed

+283
-299
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.1",
1616
"semver": "5.5.1",
1717
"semver-intersect": "1.4.0",
1818
"rxjs": "6.3.3"

packages/schematics/update/update/npm.ts

+59-253
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,144 +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('user-agent'),
199-
getNpmConfigOption('_authToken', registryKey),
200-
getNpmConfigOption('username', registryKey, true),
201-
getNpmConfigOption('password', registryKey, true),
202-
getNpmConfigOption('email', registryKey, true),
203-
getNpmConfigOption('always-auth', registryKey, true),
204-
).pipe(
205-
toArray(),
206-
concatMap(options => {
207-
const [
208-
http,
209-
https,
210-
strictSsl,
211-
cafile,
212-
token,
213-
userAgent,
214-
authToken,
215-
username,
216-
password,
217-
email,
218-
alwaysAuth,
219-
] = options;
220-
221-
const subject = new ReplaySubject<NpmRepositoryPackageJson>(1);
222-
223-
const sslOptions = getNpmClientSslOptions(strictSsl, cafile);
224-
225-
const auth: {
226-
token?: string,
227-
alwaysAuth?: boolean;
228-
username?: string;
229-
password?: string;
230-
email?: string;
231-
} = {};
232-
233-
if (alwaysAuth !== undefined) {
234-
auth.alwaysAuth = alwaysAuth === 'false' ? false : !!alwaysAuth;
235-
}
236-
237-
if (email) {
238-
auth.email = email;
239-
}
240-
241-
if (authToken) {
242-
auth.token = authToken;
243-
} else if (token) {
244-
try {
245-
// attempt to parse "username:password" from base64 token
246-
// to enable Artifactory / Nexus-like repositories support
247-
const delimiter = ':';
248-
const parsedToken = Buffer.from(token, 'base64').toString('ascii');
249-
const [extractedUsername, ...passwordArr] = parsedToken.split(delimiter);
250-
const extractedPassword = passwordArr.join(delimiter);
251-
252-
if (extractedUsername && extractedPassword) {
253-
auth.username = extractedUsername;
254-
auth.password = extractedPassword;
255-
} else {
256-
throw new Error('Unable to extract username and password from _auth token');
257-
}
258-
} catch (ex) {
259-
auth.token = token;
260-
}
261-
} else if (username) {
262-
auth.username = username;
263-
auth.password = password;
264-
}
265-
266-
const client = new RegistryClient({
267-
proxy: { http, https },
268-
ssl: sslOptions,
269-
...(userAgent && {userAgent: userAgent}),
270-
});
271-
client.log.level = 'silent';
272-
const params = {
273-
timeout: 30000,
274-
auth,
275-
};
276-
277-
client.get(
278-
fullUrl.toString(),
279-
params,
280-
(error: object, data: NpmRepositoryPackageJson) => {
281-
if (error) {
282-
subject.error(error);
283-
}
284-
285-
subject.next(data);
286-
subject.complete();
287-
});
288-
289-
maybeRequest = subject.asObservable();
290-
npmPackageJsonCache.set(fullUrl.toString(), maybeRequest);
86+
const cachedResponse = npmPackageJsonCache.get(packageName);
87+
if (cachedResponse) {
88+
return cachedResponse;
89+
}
29190

292-
return maybeRequest;
293-
}),
294-
);
295-
}),
91+
const resultPromise = pacote.packument(
92+
packageName,
93+
{
94+
'full-metadata': true,
95+
...npmrc,
96+
registry: registryUrl,
97+
},
29698
);
29799

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

0 commit comments

Comments
 (0)