deployphp/node_modules/zx/build/core.js
2023-01-10 16:49:41 +01:00

362 lines
12 KiB
JavaScript

// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import assert from 'node:assert';
import { spawn } from 'node:child_process';
import { AsyncLocalStorage, createHook } from 'node:async_hooks';
import { inspect } from 'node:util';
import chalk from 'chalk';
import which from 'which';
import { errnoMessage, exitCodeInfo, formatCmd, noop, parseDuration, psTree, quote, quotePowerShell, } from './util.js';
const processCwd = Symbol('processCwd');
const storage = new AsyncLocalStorage();
const hook = createHook({
init: syncCwd,
before: syncCwd,
promiseResolve: syncCwd,
after: syncCwd,
destroy: syncCwd,
});
hook.enable();
export const defaults = {
[processCwd]: process.cwd(),
verbose: true,
env: process.env,
shell: true,
prefix: '',
quote: () => {
throw new Error('No quote function is defined: https://ï.at/no-quote-func');
},
spawn,
log,
};
try {
defaults.shell = which.sync('bash');
defaults.prefix = 'set -euo pipefail;';
defaults.quote = quote;
}
catch (err) {
if (process.platform == 'win32') {
defaults.shell = which.sync('powershell.exe');
defaults.quote = quotePowerShell;
}
}
function getStore() {
return storage.getStore() || defaults;
}
export const $ = new Proxy(function (pieces, ...args) {
const from = new Error().stack.split(/^\s*at\s/m)[2].trim();
if (pieces.some((p) => p == undefined)) {
throw new Error(`Malformed command at ${from}`);
}
let resolve, reject;
const promise = new ProcessPromise((...args) => ([resolve, reject] = args));
let cmd = pieces[0], i = 0;
while (i < args.length) {
let s;
if (Array.isArray(args[i])) {
s = args[i].map((x) => $.quote(substitute(x))).join(' ');
}
else {
s = $.quote(substitute(args[i]));
}
cmd += s + pieces[++i];
}
promise._bind(cmd, from, resolve, reject, getStore());
// Postpone run to allow promise configuration.
setImmediate(() => promise.isHalted || promise.run());
return promise;
}, {
set(_, key, value) {
const target = key in Function.prototype ? _ : getStore();
Reflect.set(target, key, value);
return true;
},
get(_, key) {
const target = key in Function.prototype ? _ : getStore();
return Reflect.get(target, key);
},
});
function substitute(arg) {
if (arg?.stdout) {
return arg.stdout.replace(/\n$/, '');
}
return `${arg}`;
}
export class ProcessPromise extends Promise {
constructor() {
super(...arguments);
this._command = '';
this._from = '';
this._resolve = noop;
this._reject = noop;
this._snapshot = getStore();
this._stdio = ['inherit', 'pipe', 'pipe'];
this._nothrow = false;
this._quiet = false;
this._resolved = false;
this._halted = false;
this._piped = false;
this._prerun = noop;
this._postrun = noop;
}
_bind(cmd, from, resolve, reject, options) {
this._command = cmd;
this._from = from;
this._resolve = resolve;
this._reject = reject;
this._snapshot = { ...options };
}
run() {
const $ = this._snapshot;
if (this.child)
return this; // The _run() can be called from a few places.
this._prerun(); // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
$.log({
kind: 'cmd',
cmd: this._command,
verbose: $.verbose && !this._quiet,
});
this.child = $.spawn($.prefix + this._command, {
cwd: $.cwd ?? $[processCwd],
shell: typeof $.shell === 'string' ? $.shell : true,
stdio: this._stdio,
windowsHide: true,
env: $.env,
});
this.child.on('close', (code, signal) => {
let message = `exit code: ${code}`;
if (code != 0 || signal != null) {
message = `${stderr || '\n'} at ${this._from}`;
message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`;
if (signal != null) {
message += `\n signal: ${signal}`;
}
}
let output = new ProcessOutput(code, signal, stdout, stderr, combined, message);
if (code === 0 || this._nothrow) {
this._resolve(output);
}
else {
this._reject(output);
}
this._resolved = true;
});
this.child.on('error', (err) => {
const message = `${err.message}\n` +
` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
` code: ${err.code}\n` +
` at ${this._from}`;
this._reject(new ProcessOutput(null, null, stdout, stderr, combined, message));
this._resolved = true;
});
let stdout = '', stderr = '', combined = '';
let onStdout = (data) => {
$.log({ kind: 'stdout', data, verbose: $.verbose && !this._quiet });
stdout += data;
combined += data;
};
let onStderr = (data) => {
$.log({ kind: 'stderr', data, verbose: $.verbose && !this._quiet });
stderr += data;
combined += data;
};
if (!this._piped)
this.child.stdout?.on('data', onStdout); // If process is piped, don't collect or print output.
this.child.stderr?.on('data', onStderr); // Stderr should be printed regardless of piping.
this._postrun(); // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
if (this._timeout && this._timeoutSignal) {
const t = setTimeout(() => this.kill(this._timeoutSignal), this._timeout);
this.finally(() => clearTimeout(t)).catch(noop);
}
return this;
}
get stdin() {
this.stdio('pipe');
this.run();
assert(this.child);
if (this.child.stdin == null)
throw new Error('The stdin of subprocess is null.');
return this.child.stdin;
}
get stdout() {
this.run();
assert(this.child);
if (this.child.stdout == null)
throw new Error('The stdout of subprocess is null.');
return this.child.stdout;
}
get stderr() {
this.run();
assert(this.child);
if (this.child.stderr == null)
throw new Error('The stderr of subprocess is null.');
return this.child.stderr;
}
get exitCode() {
return this.then((p) => p.exitCode, (p) => p.exitCode);
}
then(onfulfilled, onrejected) {
if (this.isHalted && !this.child) {
throw new Error('The process is halted!');
}
return super.then(onfulfilled, onrejected);
}
catch(onrejected) {
return super.catch(onrejected);
}
pipe(dest) {
if (typeof dest == 'string')
throw new Error('The pipe() method does not take strings. Forgot $?');
if (this._resolved) {
if (dest instanceof ProcessPromise)
dest.stdin.end(); // In case of piped stdin, we may want to close stdin of dest as well.
throw new Error("The pipe() method shouldn't be called after promise is already resolved!");
}
this._piped = true;
if (dest instanceof ProcessPromise) {
dest.stdio('pipe');
dest._prerun = this.run.bind(this);
dest._postrun = () => {
if (!dest.child)
throw new Error('Access to stdin of pipe destination without creation a subprocess.');
this.stdout.pipe(dest.stdin);
};
return dest;
}
else {
this._postrun = () => this.stdout.pipe(dest);
return this;
}
}
async kill(signal = 'SIGTERM') {
if (!this.child)
throw new Error('Trying to kill a process without creating one.');
if (!this.child.pid)
throw new Error('The process pid is undefined.');
let children = await psTree(this.child.pid);
for (const p of children) {
try {
process.kill(+p.PID, signal);
}
catch (e) { }
}
try {
process.kill(this.child.pid, signal);
}
catch (e) { }
}
stdio(stdin, stdout = 'pipe', stderr = 'pipe') {
this._stdio = [stdin, stdout, stderr];
return this;
}
nothrow() {
this._nothrow = true;
return this;
}
quiet() {
this._quiet = true;
return this;
}
timeout(d, signal = 'SIGTERM') {
this._timeout = parseDuration(d);
this._timeoutSignal = signal;
return this;
}
halt() {
this._halted = true;
return this;
}
get isHalted() {
return this._halted;
}
}
export class ProcessOutput extends Error {
constructor(code, signal, stdout, stderr, combined, message) {
super(message);
this._code = code;
this._signal = signal;
this._stdout = stdout;
this._stderr = stderr;
this._combined = combined;
}
toString() {
return this._combined;
}
get stdout() {
return this._stdout;
}
get stderr() {
return this._stderr;
}
get exitCode() {
return this._code;
}
get signal() {
return this._signal;
}
[inspect.custom]() {
let stringify = (s, c) => s.length === 0 ? "''" : c(inspect(s));
return `ProcessOutput {
stdout: ${stringify(this.stdout, chalk.green)},
stderr: ${stringify(this.stderr, chalk.red)},
signal: ${inspect(this.signal)},
exitCode: ${(this.exitCode === 0 ? chalk.green : chalk.red)(this.exitCode)}${exitCodeInfo(this.exitCode)
? chalk.grey(' (' + exitCodeInfo(this.exitCode) + ')')
: ''}
}`;
}
}
export function within(callback) {
return storage.run({ ...getStore() }, callback);
}
function syncCwd() {
if ($[processCwd] != process.cwd())
process.chdir($[processCwd]);
}
export function cd(dir) {
$.log({ kind: 'cd', dir });
process.chdir(dir);
$[processCwd] = process.cwd();
}
export function log(entry) {
switch (entry.kind) {
case 'cmd':
if (!entry.verbose)
return;
process.stderr.write(formatCmd(entry.cmd));
break;
case 'stdout':
case 'stderr':
if (!entry.verbose)
return;
process.stderr.write(entry.data);
break;
case 'cd':
if (!$.verbose)
return;
process.stderr.write('$ ' + chalk.greenBright('cd') + ` ${entry.dir}\n`);
break;
case 'fetch':
if (!$.verbose)
return;
const init = entry.init ? ' ' + inspect(entry.init) : '';
process.stderr.write('$ ' + chalk.greenBright('fetch') + ` ${entry.url}${init}\n`);
break;
case 'retry':
if (!$.verbose)
return;
process.stderr.write(entry.error + '\n');
}
}