|
#!/usr/bin/env node |
|
|
|
const path = require('node:path'); |
|
const {Worker} = require('node:worker_threads'); |
|
|
|
const DEBUG = {parsing: false, timing: false}; |
|
const debugEnv = process.env.DEBUG; |
|
if (debugEnv) { |
|
for (const name in DEBUG) { |
|
DEBUG[name] = |
|
debugEnv === 'true' || debugEnv === '1' || debugEnv.includes(name); |
|
} |
|
} |
|
const readFile = (file) => require('node:fs/promises').readFile(file, 'utf-8'); |
|
const exists = (filename) => |
|
require('node:fs/promises') |
|
.access(filename) |
|
.then(() => true) |
|
.catch(() => false); |
|
|
|
const $env = (env) => (env ? {...process.env, ...env} : process.env); |
|
|
|
let markId = 0; |
|
function measure(name, parent = '', depth = 0) { |
|
const start = `${parent}:${name}:${++markId}`; |
|
performance.mark(start); |
|
function end() { |
|
performance.measure(name, { |
|
start, |
|
detail: {depth, parent}, |
|
}); |
|
} |
|
end.submeasure = (sub) => measure(sub, start, depth + 1); |
|
return end; |
|
} |
|
|
|
const color = (code, end) => (str) => `\x1B[${code}m${str}\x1B[${end}m`; |
|
const bold = color(1, 22); |
|
const dim = color(2, 22); |
|
const red = color(31, 39); |
|
const panic = (message) => { |
|
console.log(red(bold('Error: ') + message)); |
|
process.exit(1); |
|
}; |
|
|
|
function json5(json) { |
|
try { |
|
return JSON.parse(json); |
|
} catch (_2) { |
|
return (0, eval)(`(${json})`); |
|
} |
|
} |
|
|
|
const endPkgParse = measure('parse package.json'); |
|
let pkg; |
|
const pkgPromise = readFile('package.json').then((json) => { |
|
pkg = json5(json); |
|
endPkgParse(); |
|
return pkg; |
|
}); |
|
|
|
const INITIAL = 0; |
|
const COMMAND = 1; |
|
const ARGS = 2; |
|
|
|
function parseShell(script) { |
|
script += ' '; |
|
const tokenizer = /(if|then|fi|&&|\|\||[ ;"'`()=])/g; |
|
const expectedOps = { |
|
')': '(', |
|
fi: 'if', |
|
}; |
|
const stack = []; |
|
const logic = []; |
|
let context = INITIAL; |
|
let token; |
|
let index = 0; |
|
let lastOp; |
|
let lastEnv; |
|
let buf = ''; |
|
let command = {name: '', args: [], env: {}}; |
|
const root = []; |
|
let seq = root; |
|
const commit = () => { |
|
if (context === INITIAL) { |
|
if (lastEnv) { |
|
if (!command) seq.push((command = {name: '', args: [], env: {}})); |
|
command.env[lastEnv] = buf; |
|
lastEnv = undefined; |
|
} else if (buf) { |
|
if (!command) seq.push((command = {name: '', args: [], env: {}})); |
|
command.name = buf; |
|
context = ARGS; |
|
} |
|
} else if (context === ARGS) { |
|
// unquote |
|
if (buf[0] === "'" && buf.at(-1) === "'") { |
|
buf = buf.slice(1, -1); |
|
} else if (buf[0] === '"' && buf.at(-1) === '"') { |
|
buf = buf.slice(1, -1).replaceAll('\\"', '"'); |
|
} |
|
command.args.push(buf); |
|
} |
|
buf = ''; |
|
}; |
|
while ((token = tokenizer.exec(script))) { |
|
let op = token[0]; |
|
buf += script.substring(index, token.index); |
|
index = tokenizer.lastIndex; |
|
switch (op) { |
|
case '&&': |
|
case '||': |
|
if (lastOp) continue; |
|
seq.push({type: op}); |
|
case ';': |
|
if (lastOp) continue; |
|
commit(); |
|
command = null; |
|
context = INITIAL; |
|
break; |
|
case ' ': |
|
if (!buf) continue; |
|
if (lastOp) { |
|
if (buf) buf += ' '; |
|
continue; |
|
} |
|
commit(); |
|
break; |
|
case '=': |
|
if (context === INITIAL && !lastOp) { |
|
lastEnv = buf; |
|
buf = ''; |
|
} else { |
|
buf += op; |
|
} |
|
break; |
|
case 'if': |
|
if (context !== INITIAL) { |
|
buf += op; |
|
continue; |
|
} |
|
logic.push({ |
|
type: 'conditional', |
|
test: [], |
|
consequent: [], |
|
alternate: [], |
|
}); |
|
seq = logic.at(-1)?.test; |
|
command = null; |
|
context = INITIAL; |
|
break; |
|
case 'then': |
|
if (context !== INITIAL) { |
|
buf += op; |
|
continue; |
|
} |
|
seq = logic.at(-1)?.consequent; |
|
command = null; |
|
context = INITIAL; |
|
break; |
|
case 'else': |
|
if (context !== INITIAL) { |
|
buf += op; |
|
continue; |
|
} |
|
seq = logic.at(-1)?.alternate; |
|
command = null; |
|
context = INITIAL; |
|
break; |
|
case 'fi': |
|
if (context !== INITIAL) { |
|
buf += op; |
|
continue; |
|
} |
|
seq = root; |
|
if (logic.length) seq.push(logic.pop()); |
|
break; |
|
case '(': |
|
case ')': |
|
case '"': |
|
case `'`: |
|
case '`': |
|
buf += op; |
|
default: |
|
if (op in expectedOps) { |
|
const start = expectedOps[op]; |
|
if (lastOp !== start) { |
|
const line = `${script}\n${'-'.repeat(token.index)}^`; |
|
panic(`unexpected "${op}"\n${dim(line)}`); |
|
} |
|
op = start; |
|
} |
|
if (lastOp === op) { |
|
stack.pop(); |
|
lastOp = stack.at(-1); |
|
} else { |
|
stack.push(op); |
|
lastOp = op; |
|
} |
|
break; |
|
} |
|
} |
|
if (command && seq.at(-1) !== command) seq.push(command); |
|
return seq; |
|
} |
|
|
|
async function run(seq, env = {}) { |
|
let prev = true; |
|
for (const node of seq) { |
|
switch (node.type) { |
|
case '&&': |
|
if (!prev) return; |
|
break; |
|
case '||': |
|
if (prev) return prev; |
|
break; |
|
case 'conditional': |
|
if (await run(node.test, env)) { |
|
prev = await run(node.consequent, env); |
|
} else if (node.alternate) { |
|
prev = await run(node.alternate, env); |
|
} |
|
break; |
|
default: |
|
if (node.name === '$npm_execpath' || node.name === 'npm') { |
|
let script = node.args.shift(); |
|
if (script === 'run') script = node.args.shift(); |
|
prev = await runScript(script, node.args, {...env, ...node.env}); |
|
} else if (node.name === 'node') { |
|
const script = node.args.shift(); |
|
prev = await runNode(script, node.args, {...env, ...node.env}); |
|
} else if (node.name === 'test') { |
|
const a1 = node.args[0].replace( |
|
/$([a-z0-9_]+)/g, |
|
(_, c) => process.env[c] ?? _, |
|
); |
|
const a2 = node.args[2].replace( |
|
/$([a-z0-9_]+)/g, |
|
(_, c) => process.env[c] ?? _, |
|
); |
|
if (node.args[1] === '=') prev = a1 === a2; |
|
else if (node.args[1] === '!=') prev = a1 !== a2; |
|
} else if (node.name) { |
|
// panic(`Unknown command ${node.name} (${node.args})`); |
|
prev = await runBin(node.name, node.args, node.env); |
|
} else { |
|
panic(`Unknown node ${JSON.stringify(node)}`); |
|
} |
|
} |
|
} |
|
return prev; |
|
} |
|
|
|
const workerShim = ` |
|
const {parentPort} = require('node:worker_threads'); |
|
const {dirname, basename, sep} = require('path'); |
|
parentPort.once('message', ([mod, filename, args, env, compiledCode]) => { |
|
process.stdout.isTTY = process.stdin.isTTY = process.stderr.isTTY = true; |
|
process.argv = [process.argv[0], filename].concat(args); |
|
module.id = basename(filename); |
|
module.filename = __filename = filename; |
|
module.path = __dirname = dirname(__filename); |
|
Object.assign(process.env, env); |
|
parentPort.postMessage(0); |
|
const exit = process.exit.bind(process); |
|
process.exit = (exitCode) => { |
|
process.stdout.end(); |
|
process.setUncaughtExceptionCaptureCallback(() => {}); |
|
setTimeout(exit, 0, exitCode); |
|
}; |
|
if (mod && !compiledCode) return import(filename); |
|
require = require('node:module').createRequire(require.resolve(filename)); |
|
require('vm').runInThisContext(compiledCode || require('fs').readFileSync(filename, 'utf-8'), {filename}); |
|
}); |
|
`; |
|
const workerPool = { |
|
workers: [], |
|
get: () => workerPool.workers.pop() ?? workerPool.create(), |
|
create: () => new Worker(workerShim, {eval: true}), |
|
}; |
|
workerPool.workers.push(workerPool.create()); |
|
|
|
const pkgCache = new Map(); |
|
function getPkg(filename) { |
|
const index = filename.lastIndexOf('/node_modules/'); |
|
if (index === -1) return pkgPromise; |
|
const pkgName = filename.slice(index + 14).match(/^(@[^/]+\/)?[^/]+/g)[0]; |
|
const pkgFile = `${filename.slice(0, index + 14)}${pkgName}/package.json`; |
|
const cached = pkgCache.get(pkgFile); |
|
if (cached) return cached; |
|
const prom = readFile(pkgFile).then(json5); |
|
pkgCache.set(pkgFile, prom); |
|
return prom; |
|
} |
|
|
|
async function runNode(script, args, env, compiledCode) { |
|
if ( |
|
process.env.ESBUILD === 'force' && |
|
compiledCode === undefined && |
|
!script.endsWith('.built.mjs') |
|
) { |
|
return runTs(script, args, env); |
|
} |
|
const scriptId = `node(${script} ${args.join(' ')})`; |
|
const done = measure(scriptId); |
|
const filename = path.resolve(script); |
|
|
|
const isModule = |
|
filename.endsWith('.mjs') || |
|
((await getPkg(filename)).type === 'module' && !filename.endsWith('.cjs')); |
|
|
|
return new Promise((resolve) => { |
|
const end = done.submeasure('worker start'); |
|
const worker = workerPool.get(); |
|
worker.postMessage([isModule, filename, args, $env(env), compiledCode]); |
|
worker.once('message', done.submeasure('worker init')); |
|
worker.once('exit', done); |
|
worker.once('exit', resolve); |
|
end(); |
|
}); |
|
} |
|
|
|
async function runTs(script, args, env) { |
|
const end = measure(`esbuild(${script})`); |
|
const {build} = require('esbuild'); |
|
const outfile = script.endsWith('.mjs') |
|
? script.replace(/([^/]+)\.mjs$/, '.$1.built.mjs') |
|
: undefined; |
|
if (!outfile && !/\.[a-z]+$/.test(script)) { |
|
const [ext, index] = await Promise.all([ |
|
exists(`${script}.ts`), |
|
exists(`${script}/index.ts`), |
|
]); |
|
if (ext) script += '.ts'; |
|
else if (index) script += '/index.ts'; |
|
} |
|
const result = await build({ |
|
bundle: true, |
|
entryPoints: [script], |
|
target: 'node20', |
|
platform: 'node', |
|
format: outfile ? 'esm' : 'cjs', |
|
packages: 'external', |
|
write: Boolean(outfile), |
|
outfile, |
|
treeShaking: true, |
|
minifyIdentifiers: true, |
|
minifySyntax: true, |
|
nodePaths: [], |
|
}); |
|
if (result.errors.length) { |
|
panic(`TS build of ${script} failed:\n${result.errors.join('\n')}`); |
|
} |
|
|
|
if (outfile) { |
|
end(); |
|
try { |
|
return await runNode(outfile, args, env); |
|
} finally { |
|
require('node:fs').unlink(outfile, Object); |
|
} |
|
} |
|
let code = result.outputFiles[0].text; |
|
|
|
// move top-level requires to callsites so they only get evaluated when surrounding code is executed |
|
const idents = {}; |
|
code = code.replace( |
|
/(?:^var (import_[a-zA-Z0-9_$]+) = ((?:__toESM\()?require\("[^"]+"\)\)?);?$|\b(import_[a-zA-Z0-9_$]+)\b)/gm, |
|
(_, ident, req, ident2) => { |
|
if (ident2) return idents[ident2] ?? ident2; |
|
idents[ident] = req; |
|
return ''; |
|
}, |
|
); |
|
end(); |
|
// process.setSourceMapsEnabled(true); |
|
return runNode(script, args, env, code); |
|
} |
|
|
|
const BIN = { |
|
'ts-node': (args, env) => { |
|
let script; |
|
const remainingArgs = args.filter((arg) => { |
|
if (arg[0] === '-' || script) return true; |
|
script = arg; |
|
return false; |
|
}); |
|
return runTs(script, remainingArgs, env); |
|
}, |
|
}; |
|
|
|
async function runBin(bin, args, env) { |
|
if (Object.prototype.hasOwnProperty.call(BIN, bin)) { |
|
return BIN[bin](args, env); |
|
} |
|
let resolvedBin = bin; |
|
try { |
|
const done = measure(`resolve-bin(${bin} ${args.join(' ')})`); |
|
const code = await readFile(`node_modules/.bin/${bin}`); |
|
resolvedBin = `node_modules/.bin/${bin}`; |
|
// exec node "$basedir/../vite/bin/vite.js" "$@" |
|
const match = code.match(/exec +node +"([^"]+)" +"\$@"/)?.[1]; |
|
if (!match) throw Error(`Unknown format at node_modules/.bin/${bin}`); |
|
const binFile = path.resolve( |
|
match.replace('$basedir', './node_modules/.bin'), |
|
); |
|
const binRel = path.relative('.', binFile); |
|
done(); |
|
return runNode(binRel, args, env); |
|
} catch (err) { |
|
const done = measure(`run-bin(${bin} ${args.join(' ')})`); |
|
// console.error(`failed to run "${bin}" seamlessly:`, err); |
|
return new Promise((resolve) => { |
|
const {spawn} = require('node:child_process'); |
|
const proc = spawn(resolvedBin, args, {env: $env(env), stdio: 'inherit'}); |
|
// proc.on('error', reject); |
|
proc.once('exit', (exitCode) => { |
|
done(); |
|
resolve(exitCode); |
|
}); |
|
}); |
|
} |
|
} |
|
|
|
async function runScript(scriptName, args, env) { |
|
const script = (await pkgPromise).scripts[scriptName]; |
|
if (!script) { |
|
const bin = path.basename(scriptName); |
|
if (await exists(`node_modules/.bin/${bin}`)) { |
|
return runBin(bin, args, env); |
|
} |
|
panic(`Unknown script "${scriptName}".`); |
|
} |
|
let scriptWithArgs = script; |
|
for (const arg of args) scriptWithArgs += ` ${arg}`; |
|
const done = measure('parse'); |
|
const parsed = parseShell(scriptWithArgs); |
|
if (DEBUG.parsing) console.log('parsed: ', scriptWithArgs, parsed); |
|
done(); |
|
return run(parsed, env); |
|
} |
|
|
|
(async () => { |
|
const start = performance.now(); |
|
const cmd = path.basename(process.argv[1]); |
|
const runner = cmd === 'exec' ? runBin : runScript; |
|
const name = process.argv[2]; |
|
if (!name) panic('No script name provided.'); |
|
try { |
|
process.exitCode = await runner(name, process.argv.slice(3)); |
|
} finally { |
|
let worker; |
|
while ((worker = workerPool.workers?.pop())) worker.terminate(); |
|
if (DEBUG.timing) { |
|
console.log(bold(dim(`Done in ${(performance.now() - start) | 0}ms:`))); |
|
const dur = (ms) => (ms < 1 ? `${(ms * 1000) | 0}µs` : `${ms | 0}ms`); |
|
for (const entry of performance.getEntriesByType('measure')) { |
|
const pfx = entry.detail.depth ? ' '.repeat(entry.detail.depth) : ''; |
|
console.log(dim(`${pfx}⏱ ${dur(entry.duration)}: ${entry.name}`)); |
|
} |
|
} |
|
} |
|
})(); |