jsii-pacmak
Version:
A code generation framework for jsii backend languages
344 lines • 12.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scratch = exports.AllAttemptsFailed = void 0;
exports.findDependencyDirectory = findDependencyDirectory;
exports.isBuiltinModule = isBuiltinModule;
exports.findPackageJsonUp = findPackageJsonUp;
exports.findUp = findUp;
exports.retry = retry;
exports.shell = shell;
exports.subprocess = subprocess;
exports.slugify = slugify;
exports.setExtend = setExtend;
exports.filterAsync = filterAsync;
exports.wait = wait;
exports.flatten = flatten;
exports.zip = zip;
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;
}
/**
* 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);
}
/**
* 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);
});
}
/**
* 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;
}
}
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));
}
/**
* Spawns a shell with the provided commandline.
*
* 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).
*/
async function shell(commandLine, options) {
return handleSubprocess(options, () => {
logging.debug(commandLine, JSON.stringify(options));
return {
command: commandLine,
child: (0, child_process_1.spawn)(commandLine, {
...options,
shell: true,
env: { ...process.env, ...(options?.env ?? {}) },
stdio: ['ignore', 'pipe', 'pipe'],
}),
};
});
}
/**
* Spawns a subprocess with the provided command and arguments.
*
* The child process is always spawned using `shell: false`, 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).
*
* To make this work on Windows, if the binary happens to resolve to a batch file
* we run it through cmd.exe.
*
* @param binary the command to shell out to.
* @param args the arguments to provide to `cmd`
* @param options any options to pass to `spawn`
*/
async function subprocess(binary, args, options) {
if (os.platform() === 'win32') {
const resolved = resolveBinaryWindows(binary);
// Anything that's not an executable, run it through cmd.exe
if (!resolved.toLocaleLowerCase().endsWith('.exe')) {
binary = process.env.COMSPEC ?? 'cmd.exe';
args = ['/d', '/c', resolved, ...args];
}
}
return handleSubprocess(options, () => {
logging.debug(binary, args.join(' '), JSON.stringify(options ?? {}));
return {
command: `${binary} ${args.join(' ')}`.trim(),
child: (0, child_process_1.spawn)(binary, args, {
...options,
env: { ...process.env, ...(options?.env ?? {}) },
stdio: ['ignore', 'pipe', 'pipe'],
}),
};
});
}
async function handleSubprocess({ retry: retryOptions } = {}, doSpawn) {
async function spawn1() {
return new Promise((ok, ko) => {
const { command, child } = doSpawn();
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}`;
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(`${message}. ${retryInfo}.`);
}),
});
}
return spawn1();
}
/**
* Resolve a command to an executable on Windows
*/
function resolveBinaryWindows(command) {
const extensions = (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH').split(';');
const dirs = [
process.cwd(),
...(process.env.PATH?.split(path.delimiter) ?? []),
];
for (const dir of dirs) {
for (const ext of ['', ...extensions]) {
const candidate = path.resolve(dir, `${command}${ext}`);
if (fs.pathExistsSync(candidate)) {
return candidate;
}
}
}
throw new Error(`Unable to resolve command: ${command} in ${process.env.PATH}`);
}
/**
* Strip filesystem unsafe characters from a string
*/
function slugify(x) {
return x.replace(/[^a-zA-Z0-9_-]/g, '_');
}
/**
* Class that makes a temporary directory and holds on to an operation object
*/
class Scratch {
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()));
}
constructor(directory, object, fake) {
this.directory = directory;
this.object = object;
this.fake = fake;
}
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);
}
}
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);
}
async function wait(ms) {
return new Promise((ok) => setTimeout(ok, ms));
}
function flatten(xs) {
return Array.prototype.concat.call([], ...xs);
}
function zip(xs, ys) {
const ret = new Array();
for (let i = 0; i < xs.length; i++) {
ret.push([xs[i], ys[i]]);
}
return ret;
}
//# sourceMappingURL=util.js.map