UNPKG

10.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.flatten = exports.wait = exports.filterAsync = exports.setExtend = exports.Scratch = exports.slugify = exports.shell = exports.retry = exports.AllAttemptsFailed = exports.findUp = exports.findPackageJsonUp = exports.isBuiltinModule = exports.findDependencyDirectory = void 0;
4const child_process_1 = require("child_process");
5const fs = require("fs-extra");
6const os = require("os");
7const path = require("path");
8const logging = require("./logging");
9/**
10 * Find the directory that contains a given dependency, identified by its 'package.json', from a starting search directory
11 *
12 * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all
13 * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236)
14 */
15async function findDependencyDirectory(dependencyName, searchStart) {
16 // Explicitly do not use 'require("dep/package.json")' because that will fail if the
17 // package does not export that particular file.
18 const entryPoint = require.resolve(dependencyName, {
19 paths: [searchStart],
20 });
21 // Search up from the given directory, looking for a package.json that matches
22 // the dependency name (so we don't accidentally find stray 'package.jsons').
23 const depPkgJsonPath = await findPackageJsonUp(dependencyName, path.dirname(entryPoint));
24 if (!depPkgJsonPath) {
25 throw new Error(`Could not find dependency '${dependencyName}' from '${searchStart}'`);
26 }
27 return depPkgJsonPath;
28}
29exports.findDependencyDirectory = findDependencyDirectory;
30/**
31 * Whether the given dependency is a built-in
32 *
33 * Some dependencies that occur in `package.json` are also built-ins in modern Node
34 * versions (most egregious example: 'punycode'). Detect those and filter them out.
35 */
36function isBuiltinModule(depName) {
37 // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
38 const { builtinModules } = require('module');
39 return (builtinModules ?? []).includes(depName);
40}
41exports.isBuiltinModule = isBuiltinModule;
42/**
43 * Find the package.json for a given package upwards from the given directory
44 *
45 * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all
46 * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236)
47 */
48async function findPackageJsonUp(packageName, directory) {
49 return findUp(directory, async (dir) => {
50 const pjFile = path.join(dir, 'package.json');
51 return ((await fs.pathExists(pjFile)) &&
52 (await fs.readJson(pjFile)).name === packageName);
53 });
54}
55exports.findPackageJsonUp = findPackageJsonUp;
56/**
57 * Find a directory up the tree from a starting directory matching a condition
58 *
59 * Will return `undefined` if no directory matches
60 *
61 * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all
62 * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236)
63 */
64async function findUp(directory, pred) {
65 // eslint-disable-next-line no-constant-condition
66 while (true) {
67 // eslint-disable-next-line no-await-in-loop
68 if (await pred(directory)) {
69 return directory;
70 }
71 const parent = path.dirname(directory);
72 if (parent === directory) {
73 return undefined;
74 }
75 directory = parent;
76 }
77}
78exports.findUp = findUp;
79class AllAttemptsFailed extends Error {
80 constructor(callback, errors) {
81 super(`All attempts failed. Last error: ${errors[errors.length - 1].message}`);
82 this.callback = callback;
83 this.errors = errors;
84 }
85}
86exports.AllAttemptsFailed = AllAttemptsFailed;
87/**
88 * Adds back-off and retry logic around the provided callback.
89 *
90 * @param cb the callback which is to be retried.
91 * @param opts the backoff-and-retry configuration
92 *
93 * @returns the result of `cb`
94 */
95async function retry(cb, opts = {}, waiter = wait) {
96 let attemptsLeft = opts.maxAttempts ?? 5;
97 let backoffMs = opts.backoffBaseMilliseconds ?? 150;
98 const backoffMult = opts.backoffMultiplier ?? 2;
99 // Check for incorrect usage
100 if (attemptsLeft <= 0) {
101 throw new Error('maxTries must be > 0');
102 }
103 if (backoffMs <= 0) {
104 throw new Error('backoffBaseMilliseconds must be > 0');
105 }
106 if (backoffMult <= 1) {
107 throw new Error('backoffMultiplier must be > 1');
108 }
109 const errors = new Array();
110 while (attemptsLeft > 0) {
111 attemptsLeft--;
112 try {
113 // eslint-disable-next-line no-await-in-loop
114 return await cb();
115 }
116 catch (error) {
117 errors.push(error);
118 if (opts.onFailedAttempt != null) {
119 opts.onFailedAttempt(error, attemptsLeft, backoffMs);
120 }
121 }
122 if (attemptsLeft > 0) {
123 // eslint-disable-next-line no-await-in-loop
124 await waiter(backoffMs).then(() => (backoffMs *= backoffMult));
125 }
126 }
127 return Promise.reject(new AllAttemptsFailed(cb, errors));
128}
129exports.retry = retry;
130/**
131 * Spawns a child process with the provided command and arguments. The child
132 * process is always spawned using `shell: true`, and the contents of
133 * `process.env` is used as the initial value of the `env` spawn option (values
134 * provided in `options.env` can override those).
135 *
136 * @param cmd the command to shell out to.
137 * @param args the arguments to provide to `cmd`
138 * @param options any options to pass to `spawn`
139 */
140async function shell(cmd, args, { retry: retryOptions, ...options } = {}) {
141 async function spawn1() {
142 logging.debug(cmd, args.join(' '), JSON.stringify(options));
143 return new Promise((ok, ko) => {
144 const child = (0, child_process_1.spawn)(cmd, args, {
145 ...options,
146 shell: true,
147 env: { ...process.env, ...(options.env ?? {}) },
148 stdio: ['ignore', 'pipe', 'pipe'],
149 });
150 const stdout = new Array();
151 const stderr = new Array();
152 child.stdout.on('data', (chunk) => {
153 if (logging.level.valueOf() >= logging.LEVEL_SILLY) {
154 process.stderr.write(chunk); // notice - we emit all build output to stderr
155 }
156 stdout.push(Buffer.from(chunk));
157 });
158 child.stderr.on('data', (chunk) => {
159 if (logging.level.valueOf() >= logging.LEVEL_SILLY) {
160 process.stderr.write(chunk);
161 }
162 stderr.push(Buffer.from(chunk));
163 });
164 child.once('error', ko);
165 // Must use CLOSE instead of EXIT; EXIT may fire while there is still data in the
166 // I/O pipes, which we will miss if we return at that point.
167 child.once('close', (code, signal) => {
168 const out = Buffer.concat(stdout).toString('utf-8');
169 if (code === 0) {
170 return ok(out);
171 }
172 const err = Buffer.concat(stderr).toString('utf-8');
173 const reason = signal != null ? `signal ${signal}` : `status ${code}`;
174 const command = `${cmd} ${args.join(' ')}`;
175 return ko(new Error([
176 `Command (${command}) failed with ${reason}:`,
177 // STDERR first, the erro message could be truncated in logs.
178 prefix(err, '#STDERR> '),
179 prefix(out, '#STDOUT> '),
180 ].join('\n')));
181 function prefix(text, add) {
182 return text
183 .split('\n')
184 .map((line) => `${add}${line}`)
185 .join('\n');
186 }
187 });
188 });
189 }
190 if (retryOptions != null) {
191 return retry(spawn1, {
192 ...retryOptions,
193 onFailedAttempt: retryOptions.onFailedAttempt ??
194 ((error, attemptsLeft, backoffMs) => {
195 const message = error.message ?? error;
196 const retryInfo = attemptsLeft > 0
197 ? `Waiting ${backoffMs} ms before retrying (${attemptsLeft} attempts left)`
198 : 'No attempts left';
199 logging.info(`Command "${cmd} ${args.join(' ')}" failed with ${message}. ${retryInfo}.`);
200 }),
201 });
202 }
203 return spawn1();
204}
205exports.shell = shell;
206/**
207 * Strip filesystem unsafe characters from a string
208 */
209function slugify(x) {
210 return x.replace(/[^a-zA-Z0-9_-]/g, '_');
211}
212exports.slugify = slugify;
213/**
214 * Class that makes a temporary directory and holds on to an operation object
215 */
216class Scratch {
217 constructor(directory, object, fake) {
218 this.directory = directory;
219 this.object = object;
220 this.fake = fake;
221 }
222 static async make(factory) {
223 const tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-pack'));
224 return new Scratch(tmpdir, await factory(tmpdir), false);
225 }
226 static fake(directory, object) {
227 return new Scratch(directory, object, true);
228 }
229 static async cleanupAll(tempDirs) {
230 await Promise.all(tempDirs.map((t) => t.cleanup()));
231 }
232 async cleanup() {
233 if (!this.fake) {
234 try {
235 await fs.remove(this.directory);
236 }
237 catch (e) {
238 if (e.code === 'EBUSY') {
239 // This occasionally happens on Windows if we try to clean up too
240 // quickly after we're done... Could be because some AV software is
241 // still running in the background.
242 // Wait 1s and retry once!
243 await new Promise((ok) => setTimeout(ok, 1000));
244 try {
245 await fs.remove(this.directory);
246 }
247 catch (e2) {
248 logging.warn(`Unable to clean up ${this.directory}: ${e2}`);
249 }
250 return;
251 }
252 logging.warn(`Unable to clean up ${this.directory}: ${e}`);
253 }
254 }
255 }
256}
257exports.Scratch = Scratch;
258function setExtend(xs, els) {
259 for (const el of els) {
260 xs.add(el);
261 }
262}
263exports.setExtend = setExtend;
264async function filterAsync(xs, pred) {
265 const mapped = await Promise.all(xs.map(async (x) => ({ x, pred: await pred(x) })));
266 return mapped.filter(({ pred }) => pred).map(({ x }) => x);
267}
268exports.filterAsync = filterAsync;
269async function wait(ms) {
270 return new Promise((ok) => setTimeout(ok, ms));
271}
272exports.wait = wait;
273function flatten(xs) {
274 return Array.prototype.concat.call([], ...xs);
275}
276exports.flatten = flatten;
277//# sourceMappingURL=util.js.map
\No newline at end of file