UNPKG

jsii-pacmak

Version:

A code generation framework for jsii backend languages

277 lines 10.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.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; const child_process_1 = require("child_process"); const fs = require("fs-extra"); const os = require("os"); const path = require("path"); const logging = require("./logging"); /** * Find the directory that contains a given dependency, identified by its 'package.json', from a starting search directory * * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236) */ async function findDependencyDirectory(dependencyName, searchStart) { // Explicitly do not use 'require("dep/package.json")' because that will fail if the // package does not export that particular file. const entryPoint = require.resolve(dependencyName, { paths: [searchStart], }); // Search up from the given directory, looking for a package.json that matches // the dependency name (so we don't accidentally find stray 'package.jsons'). const depPkgJsonPath = await findPackageJsonUp(dependencyName, path.dirname(entryPoint)); if (!depPkgJsonPath) { throw new Error(`Could not find dependency '${dependencyName}' from '${searchStart}'`); } return depPkgJsonPath; } exports.findDependencyDirectory = findDependencyDirectory; /** * Whether the given dependency is a built-in * * Some dependencies that occur in `package.json` are also built-ins in modern Node * versions (most egregious example: 'punycode'). Detect those and filter them out. */ function isBuiltinModule(depName) { // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires const { builtinModules } = require('module'); return (builtinModules ?? []).includes(depName); } exports.isBuiltinModule = isBuiltinModule; /** * Find the package.json for a given package upwards from the given directory * * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236) */ async function findPackageJsonUp(packageName, directory) { return findUp(directory, async (dir) => { const pjFile = path.join(dir, 'package.json'); return ((await fs.pathExists(pjFile)) && (await fs.readJson(pjFile)).name === packageName); }); } exports.findPackageJsonUp = findPackageJsonUp; /** * Find a directory up the tree from a starting directory matching a condition * * Will return `undefined` if no directory matches * * (This code is duplicated among jsii/jsii-pacmak/jsii-reflect. Changes should be done in all * 3 locations, and we should unify these at some point: https://github.com/aws/jsii/issues/3236) */ async function findUp(directory, pred) { // eslint-disable-next-line no-constant-condition while (true) { // eslint-disable-next-line no-await-in-loop if (await pred(directory)) { return directory; } const parent = path.dirname(directory); if (parent === directory) { return undefined; } directory = parent; } } exports.findUp = findUp; class AllAttemptsFailed extends Error { constructor(callback, errors) { super(`All attempts failed. Last error: ${errors[errors.length - 1].message}`); this.callback = callback; this.errors = errors; } } exports.AllAttemptsFailed = AllAttemptsFailed; /** * Adds back-off and retry logic around the provided callback. * * @param cb the callback which is to be retried. * @param opts the backoff-and-retry configuration * * @returns the result of `cb` */ async function retry(cb, opts = {}, waiter = wait) { let attemptsLeft = opts.maxAttempts ?? 5; let backoffMs = opts.backoffBaseMilliseconds ?? 150; const backoffMult = opts.backoffMultiplier ?? 2; // Check for incorrect usage if (attemptsLeft <= 0) { throw new Error('maxTries must be > 0'); } if (backoffMs <= 0) { throw new Error('backoffBaseMilliseconds must be > 0'); } if (backoffMult <= 1) { throw new Error('backoffMultiplier must be > 1'); } const errors = new Array(); while (attemptsLeft > 0) { attemptsLeft--; try { // eslint-disable-next-line no-await-in-loop return await cb(); } catch (error) { errors.push(error); if (opts.onFailedAttempt != null) { opts.onFailedAttempt(error, attemptsLeft, backoffMs); } } if (attemptsLeft > 0) { // eslint-disable-next-line no-await-in-loop await waiter(backoffMs).then(() => (backoffMs *= backoffMult)); } } return Promise.reject(new AllAttemptsFailed(cb, errors)); } exports.retry = retry; /** * Spawns a child process with the provided command and arguments. The child * process is always spawned using `shell: true`, and the contents of * `process.env` is used as the initial value of the `env` spawn option (values * provided in `options.env` can override those). * * @param cmd the command to shell out to. * @param args the arguments to provide to `cmd` * @param options any options to pass to `spawn` */ async function shell(cmd, args, { retry: retryOptions, ...options } = {}) { async function spawn1() { logging.debug(cmd, args.join(' '), JSON.stringify(options)); return new Promise((ok, ko) => { const child = (0, child_process_1.spawn)(cmd, args, { ...options, shell: true, env: { ...process.env, ...(options.env ?? {}) }, stdio: ['ignore', 'pipe', 'pipe'], }); const stdout = new Array(); const stderr = new Array(); child.stdout.on('data', (chunk) => { if (logging.level.valueOf() >= logging.LEVEL_SILLY) { process.stderr.write(chunk); // notice - we emit all build output to stderr } stdout.push(Buffer.from(chunk)); }); child.stderr.on('data', (chunk) => { if (logging.level.valueOf() >= logging.LEVEL_SILLY) { process.stderr.write(chunk); } stderr.push(Buffer.from(chunk)); }); child.once('error', ko); // Must use CLOSE instead of EXIT; EXIT may fire while there is still data in the // I/O pipes, which we will miss if we return at that point. child.once('close', (code, signal) => { const out = Buffer.concat(stdout).toString('utf-8'); if (code === 0) { return ok(out); } const err = Buffer.concat(stderr).toString('utf-8'); const reason = signal != null ? `signal ${signal}` : `status ${code}`; const command = `${cmd} ${args.join(' ')}`; return ko(new Error([ `Command (${command}) failed with ${reason}:`, // STDERR first, the erro message could be truncated in logs. prefix(err, '#STDERR> '), prefix(out, '#STDOUT> '), ].join('\n'))); function prefix(text, add) { return text .split('\n') .map((line) => `${add}${line}`) .join('\n'); } }); }); } if (retryOptions != null) { return retry(spawn1, { ...retryOptions, onFailedAttempt: retryOptions.onFailedAttempt ?? ((error, attemptsLeft, backoffMs) => { const message = error.message ?? error; const retryInfo = attemptsLeft > 0 ? `Waiting ${backoffMs} ms before retrying (${attemptsLeft} attempts left)` : 'No attempts left'; logging.info(`Command "${cmd} ${args.join(' ')}" failed with ${message}. ${retryInfo}.`); }), }); } return spawn1(); } exports.shell = shell; /** * Strip filesystem unsafe characters from a string */ function slugify(x) { return x.replace(/[^a-zA-Z0-9_-]/g, '_'); } exports.slugify = slugify; /** * Class that makes a temporary directory and holds on to an operation object */ class Scratch { constructor(directory, object, fake) { this.directory = directory; this.object = object; this.fake = fake; } static async make(factory) { const tmpdir = await fs.mkdtemp(path.join(os.tmpdir(), 'npm-pack')); return new Scratch(tmpdir, await factory(tmpdir), false); } static fake(directory, object) { return new Scratch(directory, object, true); } static async cleanupAll(tempDirs) { await Promise.all(tempDirs.map((t) => t.cleanup())); } async cleanup() { if (!this.fake) { try { await fs.remove(this.directory); } catch (e) { if (e.code === 'EBUSY') { // This occasionally happens on Windows if we try to clean up too // quickly after we're done... Could be because some AV software is // still running in the background. // Wait 1s and retry once! await new Promise((ok) => setTimeout(ok, 1000)); try { await fs.remove(this.directory); } catch (e2) { logging.warn(`Unable to clean up ${this.directory}: ${e2}`); } return; } logging.warn(`Unable to clean up ${this.directory}: ${e}`); } } } } exports.Scratch = Scratch; function setExtend(xs, els) { for (const el of els) { xs.add(el); } } exports.setExtend = setExtend; async function filterAsync(xs, pred) { const mapped = await Promise.all(xs.map(async (x) => ({ x, pred: await pred(x) }))); return mapped.filter(({ pred }) => pred).map(({ x }) => x); } exports.filterAsync = filterAsync; async function wait(ms) { return new Promise((ok) => setTimeout(ok, ms)); } exports.wait = wait; function flatten(xs) { return Array.prototype.concat.call([], ...xs); } exports.flatten = flatten; //# sourceMappingURL=util.js.map