Skip to content

Commit

Permalink
Decrease package size
Browse files Browse the repository at this point in the history
  • Loading branch information
ehmicky committed Sep 1, 2024
1 parent e55dbd2 commit 3817682
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 62 deletions.
26 changes: 11 additions & 15 deletions source/context.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import process from 'node:process';
import {stripVTControlCharacters} from 'node:util';

export const getContext = (previous, rawFile, rawArguments) => {
const start = previous.start ?? process.hrtime.bigint();
const command = [previous.command, getCommand(rawFile, rawArguments)].filter(Boolean).join(' | ');
return {start, command, state: {stdout: '', stderr: '', output: ''}};
};
export const getContext = ({start, command}, raw) => ({
start: start ?? process.hrtime.bigint(),
command: [
command,
raw.map(part => getCommandPart(stripVTControlCharacters(part))).join(' '),
].filter(Boolean).join(' | '),
state: {stdout: '', stderr: '', output: ''},
});

const getCommand = (rawFile, rawArguments) => [rawFile, ...rawArguments]
.map(part => getCommandPart(part))
.join(' ');

const getCommandPart = part => {
part = stripVTControlCharacters(part);
return /[^\w./-]/.test(part)
? `'${part.replaceAll('\'', '\'\\\'\'')}'`
: part;
};
const getCommandPart = part => /[^\w./-]/.test(part)
? `'${part.replaceAll('\'', '\'\\\'\'')}'`
: part;
8 changes: 4 additions & 4 deletions source/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import {handlePipe} from './pipe.js';
import {lineIterator, combineAsyncIterators} from './iterable.js';

export default function nanoSpawn(first, second = [], third = {}) {
const [rawFile, previous] = Array.isArray(first) ? first : [first, {}];
const [rawArguments, options] = Array.isArray(second) ? [second, third] : [[], second];
const [file, previous] = Array.isArray(first) ? first : [first, {}];
const [commandArguments, options] = Array.isArray(second) ? [second, third] : [[], second];

const context = getContext(previous, rawFile, rawArguments);
const context = getContext(previous, [file, ...commandArguments]);
const spawnOptions = getOptions(options);
const nodeChildProcess = spawnSubprocess(rawFile, rawArguments, spawnOptions, context);
const nodeChildProcess = spawnSubprocess(file, commandArguments, spawnOptions, context);
const resultPromise = getResult(nodeChildProcess, spawnOptions, context);
Object.assign(resultPromise, {nodeChildProcess});
const finalPromise = previous.resultPromise === undefined ? resultPromise : handlePipe(previous, resultPromise);
Expand Down
3 changes: 1 addition & 2 deletions source/iterable.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ export const lineIterator = async function * (resultPromise, {state}, streamName
state.isIterating = true;

try {
const instance = await resultPromise.nodeChildProcess;
const stream = instance[streamName];
const {[streamName]: stream} = await resultPromise.nodeChildProcess;
if (!stream) {
return;
}
Expand Down
6 changes: 2 additions & 4 deletions source/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ export const getOptions = ({
}) => {
const cwd = cwdOption instanceof URL ? fileURLToPath(cwdOption) : path.resolve(cwdOption);
const env = envOption === undefined ? undefined : {...process.env, ...envOption};
const [stdioOption, input] = stdio[0]?.string === undefined
? [stdio]
: [['pipe', ...stdio.slice(1)], stdio[0].string];
const input = stdio[0]?.string;
return {
...options,
stdio: stdioOption,
input,
stdio: input === undefined ? stdio : ['pipe', ...stdio.slice(1)],
env: preferLocal ? addLocalPath(env ?? process.env, cwd) : env,
cwd,
};
Expand Down
34 changes: 15 additions & 19 deletions source/result.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import {once, on} from 'node:events';
import process from 'node:process';

export const getResult = async (nodeChildProcess, options, context) => {
export const getResult = async (nodeChildProcess, {input}, context) => {
const instance = await nodeChildProcess;
useInput(instance, options);
if (input !== undefined) {
instance.stdin.end(input);
}

const onClose = once(instance, 'close');

try {
await Promise.race([onClose, ...onStreamErrors(instance)]);
await Promise.race([
onClose,
...instance.stdio.filter(Boolean).map(stream => onStreamError(stream)),
]);
checkFailure(context, getErrorOutput(instance));
return getOutputs(context);
} catch (error) {
Expand All @@ -16,25 +22,15 @@ export const getResult = async (nodeChildProcess, options, context) => {
}
};

const useInput = (instance, {input}) => {
if (input !== undefined) {
instance.stdin.end(input);
}
};

const onStreamErrors = ({stdio}) => stdio.filter(Boolean).map(stream => onStreamError(stream));

const onStreamError = async stream => {
for await (const [error] of on(stream, 'error')) {
if (!IGNORED_CODES.has(error?.code)) {
// Ignore errors that are due to closing errors when the subprocesses exit normally, or due to piping
if (!['ERR_STREAM_PREMATURE_CLOSE', 'EPIPE'].includes(error?.code)) {
throw error;
}
}
};

// Ignore errors that are due to closing errors when the subprocesses exit normally, or due to piping
const IGNORED_CODES = new Set(['ERR_STREAM_PREMATURE_CLOSE', 'EPIPE']);

const checkFailure = ({command}, {exitCode, signalName}) => {
if (signalName !== undefined) {
throw new Error(`Command was terminated with ${signalName}: ${command}`);
Expand All @@ -57,7 +53,7 @@ const getErrorInstance = (error, {command}) => error?.message.startsWith('Comman

const getErrorOutput = ({exitCode, signalCode}) => ({
// `exitCode` can be a negative number (`errno`) when the `error` event is emitted on the `instance`
...(exitCode === null || exitCode < 1 ? {} : {exitCode}),
...(exitCode < 1 ? {} : {exitCode}),
...(signalCode === null ? {} : {signalName: signalCode}),
});

Expand All @@ -69,6 +65,6 @@ const getOutputs = ({state: {stdout, stderr, output}, command, start}) => ({
durationMs: Number(process.hrtime.bigint() - start) / 1e6,
});

const getOutput = input => input?.at(-1) === '\n'
? input.slice(0, input.at(-2) === '\r' ? -2 : -1)
: input;
const getOutput = output => output.at(-1) === '\n'
? output.slice(0, output.at(-2) === '\r' ? -2 : -1)
: output;
22 changes: 10 additions & 12 deletions source/spawn.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ import process from 'node:process';
import {getForcedShell, escapeArguments} from './windows.js';
import {getResultError} from './result.js';

export const spawnSubprocess = async (rawFile, rawArguments, options, context) => {
export const spawnSubprocess = async (file, commandArguments, options, context) => {
try {
const [file, commandArguments] = handleNode(rawFile, rawArguments);
// When running `node`, keep the current Node version and CLI flags.
// Not applied with file paths to `.../node` since those indicate a clear intent to use a specific Node version.
// Does not work with shebangs, but those don't work cross-platform anyway.
[file, commandArguments] = ['node', 'node.exe'].includes(file.toLowerCase())
? [process.execPath, [...process.execArgv.filter(flag => !flag.startsWith('--inspect')), ...commandArguments]]
: [file, commandArguments];

const forcedShell = await getForcedShell(file, options);
const instance = spawn(...escapeArguments(file, commandArguments, forcedShell), {
...options,
Expand All @@ -27,13 +33,6 @@ export const spawnSubprocess = async (rawFile, rawArguments, options, context) =
}
};

// When running `node`, keep the current Node version and CLI flags.
// Not applied with file paths to `.../node` since those indicate a clear intent to use a specific Node version.
// Does not work with shebangs, but those don't work cross-platform anyway.
const handleNode = (rawFile, rawArguments) => rawFile.toLowerCase().replace(/\.exe$/, '') === 'node'
? [process.execPath, [...process.execArgv.filter(flag => !flag.startsWith('--inspect')), ...rawArguments]]
: [rawFile, rawArguments];

const bufferOutput = (stream, {state}, streamName) => {
if (!stream) {
return;
Expand All @@ -46,8 +45,7 @@ const bufferOutput = (stream, {state}, streamName) => {

state.isIterating = false;
stream.on('data', chunk => {
for (const outputName of [streamName, 'output']) {
state[outputName] += chunk;
}
state[streamName] += chunk;
state.output += chunk;
});
};
9 changes: 3 additions & 6 deletions source/windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,9 @@ export const escapeArguments = (file, commandArguments, forcedShell) => forcedSh

// `cmd.exe` escaping for arguments.
// Taken from https://github.com/moxystudio/node-cross-spawn
const escapeArgument = argument => {
const escapedArgument = argument
.replaceAll(/(\\*)"/g, '$1$1\\"')
.replace(/(\\*)$/, '$1$1');
return escapeFile(escapeFile(`"${escapedArgument}"`));
};
const escapeArgument = argument => escapeFile(escapeFile(`"${argument
.replaceAll(/(\\*)"/g, '$1$1\\"')
.replace(/(\\*)$/, '$1$1')}"`));

// `cmd.exe` escaping for file and arguments.
const escapeFile = file => file.replaceAll(/([()\][%!^"`<>&|;, *?])/g, '^$1');

0 comments on commit 3817682

Please sign in to comment.