jsii-pacmak
Version:
A code generation framework for jsii backend languages
277 lines • 10.9 kB
JavaScript
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
;