120 lines
2.9 KiB
JavaScript
120 lines
2.9 KiB
JavaScript
|
import {Buffer} from 'node:buffer';
|
||
|
import {ChildProcess} from 'node:child_process';
|
||
|
|
||
|
const normalizeArgs = (file, args = []) => {
|
||
|
if (!Array.isArray(args)) {
|
||
|
return [file];
|
||
|
}
|
||
|
|
||
|
return [file, ...args];
|
||
|
};
|
||
|
|
||
|
const NO_ESCAPE_REGEXP = /^[\w.-]+$/;
|
||
|
|
||
|
const escapeArg = arg => {
|
||
|
if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) {
|
||
|
return arg;
|
||
|
}
|
||
|
|
||
|
return `"${arg.replaceAll('"', '\\"')}"`;
|
||
|
};
|
||
|
|
||
|
export const joinCommand = (file, args) => normalizeArgs(file, args).join(' ');
|
||
|
|
||
|
export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' ');
|
||
|
|
||
|
const SPACES_REGEXP = / +/g;
|
||
|
|
||
|
// Handle `execaCommand()`
|
||
|
export const parseCommand = command => {
|
||
|
const tokens = [];
|
||
|
for (const token of command.trim().split(SPACES_REGEXP)) {
|
||
|
// Allow spaces to be escaped by a backslash if not meant as a delimiter
|
||
|
const previousToken = tokens.at(-1);
|
||
|
if (previousToken && previousToken.endsWith('\\')) {
|
||
|
// Merge previous token with current one
|
||
|
tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`;
|
||
|
} else {
|
||
|
tokens.push(token);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return tokens;
|
||
|
};
|
||
|
|
||
|
const parseExpression = expression => {
|
||
|
const typeOfExpression = typeof expression;
|
||
|
|
||
|
if (typeOfExpression === 'string') {
|
||
|
return expression;
|
||
|
}
|
||
|
|
||
|
if (typeOfExpression === 'number') {
|
||
|
return String(expression);
|
||
|
}
|
||
|
|
||
|
if (
|
||
|
typeOfExpression === 'object'
|
||
|
&& expression !== null
|
||
|
&& !(expression instanceof ChildProcess)
|
||
|
&& 'stdout' in expression
|
||
|
) {
|
||
|
const typeOfStdout = typeof expression.stdout;
|
||
|
|
||
|
if (typeOfStdout === 'string') {
|
||
|
return expression.stdout;
|
||
|
}
|
||
|
|
||
|
if (Buffer.isBuffer(expression.stdout)) {
|
||
|
return expression.stdout.toString();
|
||
|
}
|
||
|
|
||
|
throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`);
|
||
|
}
|
||
|
|
||
|
throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`);
|
||
|
};
|
||
|
|
||
|
const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0
|
||
|
? [...tokens, ...nextTokens]
|
||
|
: [
|
||
|
...tokens.slice(0, -1),
|
||
|
`${tokens.at(-1)}${nextTokens[0]}`,
|
||
|
...nextTokens.slice(1),
|
||
|
];
|
||
|
|
||
|
const parseTemplate = ({templates, expressions, tokens, index, template}) => {
|
||
|
const templateString = template ?? templates.raw[index];
|
||
|
const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean);
|
||
|
const newTokens = concatTokens(
|
||
|
tokens,
|
||
|
templateTokens,
|
||
|
templateString.startsWith(' '),
|
||
|
);
|
||
|
|
||
|
if (index === expressions.length) {
|
||
|
return newTokens;
|
||
|
}
|
||
|
|
||
|
const expression = expressions[index];
|
||
|
const expressionTokens = Array.isArray(expression)
|
||
|
? expression.map(expression => parseExpression(expression))
|
||
|
: [parseExpression(expression)];
|
||
|
return concatTokens(
|
||
|
newTokens,
|
||
|
expressionTokens,
|
||
|
templateString.endsWith(' '),
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export const parseTemplates = (templates, expressions) => {
|
||
|
let tokens = [];
|
||
|
|
||
|
for (const [index, template] of templates.entries()) {
|
||
|
tokens = parseTemplate({templates, expressions, tokens, index, template});
|
||
|
}
|
||
|
|
||
|
return tokens;
|
||
|
};
|
||
|
|