netlify-cli
Version:
Netlify command line tool
107 lines • 4.77 kB
JavaScript
import process from 'process';
import { Transform } from 'stream';
import { stripVTControlCharacters } from 'util';
import execa from 'execa';
import { stopSpinner } from '../lib/spinner.js';
import { chalk, log, NETLIFYDEVERR, NETLIFYDEVWARN } from './command-helpers.js';
import { processOnExit } from './dev.js';
const isErrnoException = (value) => value instanceof Error && Object.hasOwn(value, 'code');
const createStripAnsiControlCharsStream = () => new Transform({
transform(chunk, _encoding, callback) {
callback(null, stripVTControlCharacters(typeof chunk === 'string' ? chunk : chunk?.toString() ?? ''));
},
});
const cleanupWork = [];
let cleanupStarted = false;
const cleanupBeforeExit = async ({ exitCode } = {}) => {
// If cleanup has started, then wherever started it will be responsible for exiting
if (!cleanupStarted) {
cleanupStarted = true;
try {
await Promise.all(cleanupWork.map((cleanup) => cleanup()));
}
finally {
// eslint-disable-next-line n/no-process-exit
process.exit(exitCode);
}
}
};
export const runCommand = (command, options) => {
const { cwd, env = {}, spinner } = options;
const commandProcess = execa.command(command, {
preferLocal: true,
// we use reject=false to avoid rejecting synchronously when the command doesn't exist
reject: false,
env: {
// we want always colorful terminal outputs
FORCE_COLOR: 'true',
...env,
},
// windowsHide needs to be false for child process to terminate properly on Windows
windowsHide: false,
cwd,
});
// Ensure that an active spinner stays at the bottom of the commandline
// even though the actual framework command might be outputting stuff
if (spinner?.isSpinning) {
// The spinner is initially "started" in the usual sense (rendering frames on an interval).
// In this case, we want to manually control when to clear and when to render a frame, so we turn this off.
stopSpinner({ error: false, spinner });
}
const pipeDataWithSpinner = (writeStream, chunk) => {
if (spinner?.isSpinning) {
spinner.clear();
}
writeStream.write(chunk, () => {
spinner?.spin();
});
};
commandProcess.stdout
?.pipe(createStripAnsiControlCharsStream())
.on('data', pipeDataWithSpinner.bind(null, process.stdout));
commandProcess.stderr
?.pipe(createStripAnsiControlCharsStream())
.on('data', pipeDataWithSpinner.bind(null, process.stderr));
if (commandProcess.stdin != null) {
process.stdin.pipe(commandProcess.stdin);
}
// we can't try->await->catch since we don't want to block on the framework server which
// is a long running process
// eslint-disable-next-line @typescript-eslint/no-floating-promises
commandProcess.then(async () => {
const result = await commandProcess;
const [commandWithoutArgs] = command.split(' ');
if (result.failed && isNonExistingCommandError({ command: commandWithoutArgs, error: result })) {
log(`${NETLIFYDEVERR} Failed running command: ${command}. Please verify ${chalk.magenta(`'${commandWithoutArgs}'`)} exists`);
}
else {
const errorMessage = result.failed
? // @ts-expect-error FIXME(serhalp): We use `reject: false` which means the resolved value is either the resolved value
// or the rejected value, but the types aren't smart enough to know this.
`${NETLIFYDEVERR} ${result.shortMessage}`
: `${NETLIFYDEVWARN} "${command}" exited with code ${result.exitCode.toString()}`;
log(`${errorMessage}. Shutting down Netlify Dev server`);
}
await cleanupBeforeExit({ exitCode: 1 });
});
processOnExit(async () => {
await cleanupBeforeExit({});
});
return commandProcess;
};
const isNonExistingCommandError = ({ command, error: commandError }) => {
// `ENOENT` is only returned for non Windows systems
// See https://github.com/sindresorhus/execa/pull/447
if (isErrnoException(commandError) && commandError.code === 'ENOENT') {
return true;
}
// if the command is a package manager we let it report the error
if (['yarn', 'npm', 'pnpm'].includes(command)) {
return false;
}
// this only works on English versions of Windows
return (commandError instanceof Error &&
typeof commandError.message === 'string' &&
commandError.message.includes('is not recognized as an internal or external command'));
};
//# sourceMappingURL=shell.js.map