command.js (3071B)
1 import {Buffer} from 'node:buffer'; 2 import {ChildProcess} from 'node:child_process'; 3 4 const normalizeArgs = (file, args = []) => { 5 if (!Array.isArray(args)) { 6 return [file]; 7 } 8 9 return [file, ...args]; 10 }; 11 12 const NO_ESCAPE_REGEXP = /^[\w.-]+$/; 13 const DOUBLE_QUOTES_REGEXP = /"/g; 14 15 const escapeArg = arg => { 16 if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) { 17 return arg; 18 } 19 20 return `"${arg.replace(DOUBLE_QUOTES_REGEXP, '\\"')}"`; 21 }; 22 23 export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); 24 25 export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); 26 27 const SPACES_REGEXP = / +/g; 28 29 // Handle `execaCommand()` 30 export const parseCommand = command => { 31 const tokens = []; 32 for (const token of command.trim().split(SPACES_REGEXP)) { 33 // Allow spaces to be escaped by a backslash if not meant as a delimiter 34 const previousToken = tokens[tokens.length - 1]; 35 if (previousToken && previousToken.endsWith('\\')) { 36 // Merge previous token with current one 37 tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`; 38 } else { 39 tokens.push(token); 40 } 41 } 42 43 return tokens; 44 }; 45 46 const parseExpression = expression => { 47 const typeOfExpression = typeof expression; 48 49 if (typeOfExpression === 'string') { 50 return expression; 51 } 52 53 if (typeOfExpression === 'number') { 54 return String(expression); 55 } 56 57 if ( 58 typeOfExpression === 'object' 59 && expression !== null 60 && !(expression instanceof ChildProcess) 61 && 'stdout' in expression 62 ) { 63 const typeOfStdout = typeof expression.stdout; 64 65 if (typeOfStdout === 'string') { 66 return expression.stdout; 67 } 68 69 if (Buffer.isBuffer(expression.stdout)) { 70 return expression.stdout.toString(); 71 } 72 73 throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`); 74 } 75 76 throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); 77 }; 78 79 const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 80 ? [...tokens, ...nextTokens] 81 : [ 82 ...tokens.slice(0, -1), 83 `${tokens[tokens.length - 1]}${nextTokens[0]}`, 84 ...nextTokens.slice(1), 85 ]; 86 87 const parseTemplate = ({templates, expressions, tokens, index, template}) => { 88 const templateString = template ?? templates.raw[index]; 89 const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); 90 const newTokens = concatTokens( 91 tokens, 92 templateTokens, 93 templateString.startsWith(' '), 94 ); 95 96 if (index === expressions.length) { 97 return newTokens; 98 } 99 100 const expression = expressions[index]; 101 const expressionTokens = Array.isArray(expression) 102 ? expression.map(expression => parseExpression(expression)) 103 : [parseExpression(expression)]; 104 return concatTokens( 105 newTokens, 106 expressionTokens, 107 templateString.endsWith(' '), 108 ); 109 }; 110 111 export const parseTemplates = (templates, expressions) => { 112 let tokens = []; 113 114 for (const [index, template] of templates.entries()) { 115 tokens = parseTemplate({templates, expressions, tokens, index, template}); 116 } 117 118 return tokens; 119 }; 120