11cb0ef41Sopenharmony_ciconst { createHook, executionAsyncId } = require('async_hooks') 21cb0ef41Sopenharmony_ciconst { EventEmitter } = require('events') 31cb0ef41Sopenharmony_ciconst { homedir, tmpdir } = require('os') 41cb0ef41Sopenharmony_ciconst { dirname, join } = require('path') 51cb0ef41Sopenharmony_ciconst { mkdir, rm } = require('fs/promises') 61cb0ef41Sopenharmony_ciconst mockLogs = require('./mock-logs') 71cb0ef41Sopenharmony_ciconst pkg = require('../../package.json') 81cb0ef41Sopenharmony_ci 91cb0ef41Sopenharmony_ciconst chain = new Map() 101cb0ef41Sopenharmony_ciconst sandboxes = new Map() 111cb0ef41Sopenharmony_ci 121cb0ef41Sopenharmony_ci// keep a reference to the real process 131cb0ef41Sopenharmony_ciconst _process = process 141cb0ef41Sopenharmony_ci 151cb0ef41Sopenharmony_cicreateHook({ 161cb0ef41Sopenharmony_ci init: (asyncId, type, triggerAsyncId, resource) => { 171cb0ef41Sopenharmony_ci // track parentage of asyncIds 181cb0ef41Sopenharmony_ci chain.set(asyncId, triggerAsyncId) 191cb0ef41Sopenharmony_ci }, 201cb0ef41Sopenharmony_ci before: (asyncId) => { 211cb0ef41Sopenharmony_ci // find the nearest parent id that has a sandbox 221cb0ef41Sopenharmony_ci let parent = asyncId 231cb0ef41Sopenharmony_ci while (chain.has(parent) && !sandboxes.has(parent)) { 241cb0ef41Sopenharmony_ci parent = chain.get(parent) 251cb0ef41Sopenharmony_ci } 261cb0ef41Sopenharmony_ci 271cb0ef41Sopenharmony_ci process = sandboxes.has(parent) 281cb0ef41Sopenharmony_ci ? sandboxes.get(parent) 291cb0ef41Sopenharmony_ci : _process 301cb0ef41Sopenharmony_ci }, 311cb0ef41Sopenharmony_ci}).enable() 321cb0ef41Sopenharmony_ci 331cb0ef41Sopenharmony_ciconst _data = Symbol('sandbox.data') 341cb0ef41Sopenharmony_ciconst _dirs = Symbol('sandbox.dirs') 351cb0ef41Sopenharmony_ciconst _test = Symbol('sandbox.test') 361cb0ef41Sopenharmony_ciconst _mocks = Symbol('sandbox.mocks') 371cb0ef41Sopenharmony_ciconst _npm = Symbol('sandbox.npm') 381cb0ef41Sopenharmony_ciconst _parent = Symbol('sandbox.parent') 391cb0ef41Sopenharmony_ciconst _output = Symbol('sandbox.output') 401cb0ef41Sopenharmony_ciconst _proxy = Symbol('sandbox.proxy') 411cb0ef41Sopenharmony_ciconst _get = Symbol('sandbox.proxy.get') 421cb0ef41Sopenharmony_ciconst _set = Symbol('sandbox.proxy.set') 431cb0ef41Sopenharmony_ciconst _logs = Symbol('sandbox.logs') 441cb0ef41Sopenharmony_ci 451cb0ef41Sopenharmony_ci// we can't just replace these values everywhere because they're known to be 461cb0ef41Sopenharmony_ci// very short strings that could be present all over the place, so we only 471cb0ef41Sopenharmony_ci// replace them if they're located within quotes for now 481cb0ef41Sopenharmony_ciconst vagueRedactedDefaults = [ 491cb0ef41Sopenharmony_ci 'editor', 501cb0ef41Sopenharmony_ci 'shell', 511cb0ef41Sopenharmony_ci] 521cb0ef41Sopenharmony_ci 531cb0ef41Sopenharmony_ciconst normalize = (str) => str 541cb0ef41Sopenharmony_ci .replace(/\r\n/g, '\n') // normalize line endings (for ini) 551cb0ef41Sopenharmony_ci .replace(/[A-z]:\\/g, '\\') // turn windows roots to posix ones 561cb0ef41Sopenharmony_ci .replace(/\\+/g, '/') // replace \ with / 571cb0ef41Sopenharmony_ci 581cb0ef41Sopenharmony_ciclass Sandbox extends EventEmitter { 591cb0ef41Sopenharmony_ci constructor (test, options = {}) { 601cb0ef41Sopenharmony_ci super() 611cb0ef41Sopenharmony_ci 621cb0ef41Sopenharmony_ci this[_test] = test 631cb0ef41Sopenharmony_ci this[_mocks] = options.mocks || {} 641cb0ef41Sopenharmony_ci this[_data] = new Map() 651cb0ef41Sopenharmony_ci this[_output] = [] 661cb0ef41Sopenharmony_ci const tempDir = `${test.testdirName}-sandbox` 671cb0ef41Sopenharmony_ci this[_dirs] = { 681cb0ef41Sopenharmony_ci temp: tempDir, 691cb0ef41Sopenharmony_ci global: options.global || join(tempDir, 'global'), 701cb0ef41Sopenharmony_ci home: options.home || join(tempDir, 'home'), 711cb0ef41Sopenharmony_ci project: options.project || join(tempDir, 'project'), 721cb0ef41Sopenharmony_ci cache: options.cache || join(tempDir, 'cache'), 731cb0ef41Sopenharmony_ci } 741cb0ef41Sopenharmony_ci 751cb0ef41Sopenharmony_ci this[_proxy] = new Proxy(_process, { 761cb0ef41Sopenharmony_ci get: this[_get].bind(this), 771cb0ef41Sopenharmony_ci set: this[_set].bind(this), 781cb0ef41Sopenharmony_ci }) 791cb0ef41Sopenharmony_ci this[_proxy].env = { ...options.env } 801cb0ef41Sopenharmony_ci this[_proxy].argv = [] 811cb0ef41Sopenharmony_ci 821cb0ef41Sopenharmony_ci test.cleanSnapshot = this.cleanSnapshot.bind(this) 831cb0ef41Sopenharmony_ci test.afterEach(() => this.reset()) 841cb0ef41Sopenharmony_ci test.teardown(() => this.teardown()) 851cb0ef41Sopenharmony_ci } 861cb0ef41Sopenharmony_ci 871cb0ef41Sopenharmony_ci get config () { 881cb0ef41Sopenharmony_ci return this[_npm] && this[_npm].config 891cb0ef41Sopenharmony_ci } 901cb0ef41Sopenharmony_ci 911cb0ef41Sopenharmony_ci get logs () { 921cb0ef41Sopenharmony_ci return this[_logs] 931cb0ef41Sopenharmony_ci } 941cb0ef41Sopenharmony_ci 951cb0ef41Sopenharmony_ci get global () { 961cb0ef41Sopenharmony_ci return this[_dirs].global 971cb0ef41Sopenharmony_ci } 981cb0ef41Sopenharmony_ci 991cb0ef41Sopenharmony_ci get home () { 1001cb0ef41Sopenharmony_ci return this[_dirs].home 1011cb0ef41Sopenharmony_ci } 1021cb0ef41Sopenharmony_ci 1031cb0ef41Sopenharmony_ci get project () { 1041cb0ef41Sopenharmony_ci return this[_dirs].project 1051cb0ef41Sopenharmony_ci } 1061cb0ef41Sopenharmony_ci 1071cb0ef41Sopenharmony_ci get cache () { 1081cb0ef41Sopenharmony_ci return this[_dirs].cache 1091cb0ef41Sopenharmony_ci } 1101cb0ef41Sopenharmony_ci 1111cb0ef41Sopenharmony_ci get process () { 1121cb0ef41Sopenharmony_ci return this[_proxy] 1131cb0ef41Sopenharmony_ci } 1141cb0ef41Sopenharmony_ci 1151cb0ef41Sopenharmony_ci get output () { 1161cb0ef41Sopenharmony_ci return this[_output].map((line) => line.join(' ')).join('\n') 1171cb0ef41Sopenharmony_ci } 1181cb0ef41Sopenharmony_ci 1191cb0ef41Sopenharmony_ci cleanSnapshot (snapshot) { 1201cb0ef41Sopenharmony_ci let clean = normalize(snapshot) 1211cb0ef41Sopenharmony_ci 1221cb0ef41Sopenharmony_ci const viewer = _process.platform === 'win32' 1231cb0ef41Sopenharmony_ci ? /"browser"([^:]+|$)/g 1241cb0ef41Sopenharmony_ci : /"man"([^:]+|$)/g 1251cb0ef41Sopenharmony_ci 1261cb0ef41Sopenharmony_ci // the global prefix is platform dependent 1271cb0ef41Sopenharmony_ci const realGlobalPrefix = _process.platform === 'win32' 1281cb0ef41Sopenharmony_ci ? dirname(_process.execPath) 1291cb0ef41Sopenharmony_ci : dirname(dirname(_process.execPath)) 1301cb0ef41Sopenharmony_ci 1311cb0ef41Sopenharmony_ci const cache = _process.platform === 'win32' 1321cb0ef41Sopenharmony_ci ? /\{HOME\}\/npm-cache(\r?\n|"|\/|$)/g 1331cb0ef41Sopenharmony_ci : /\{HOME\}\/\.npm(\n|"|\/|$)/g 1341cb0ef41Sopenharmony_ci 1351cb0ef41Sopenharmony_ci // and finally replace some paths we know could be present 1361cb0ef41Sopenharmony_ci clean = clean 1371cb0ef41Sopenharmony_ci .replace(viewer, '"{VIEWER}"$1') 1381cb0ef41Sopenharmony_ci .split(normalize(this[_proxy].execPath)).join('{EXECPATH}') 1391cb0ef41Sopenharmony_ci .split(normalize(_process.execPath)).join('{REALEXECPATH}') 1401cb0ef41Sopenharmony_ci .split(normalize(this.global)).join('{GLOBALPREFIX}') 1411cb0ef41Sopenharmony_ci .split(normalize(realGlobalPrefix)).join('{REALGLOBALREFIX}') 1421cb0ef41Sopenharmony_ci .split(normalize(this.project)).join('{LOCALPREFIX}') 1431cb0ef41Sopenharmony_ci .split(normalize(this.home)).join('{HOME}') 1441cb0ef41Sopenharmony_ci .replace(cache, '{CACHE}$1') 1451cb0ef41Sopenharmony_ci .split(normalize(dirname(dirname(__dirname)))).join('{NPMDIR}') 1461cb0ef41Sopenharmony_ci .split(normalize(tmpdir())).join('{TMP}') 1471cb0ef41Sopenharmony_ci .split(normalize(homedir())).join('{REALHOME}') 1481cb0ef41Sopenharmony_ci .split(this[_proxy].platform).join('{PLATFORM}') 1491cb0ef41Sopenharmony_ci .split(this[_proxy].arch).join('{ARCH}') 1501cb0ef41Sopenharmony_ci .replace(new RegExp(process.version, 'g'), '{NODE-VERSION}') 1511cb0ef41Sopenharmony_ci .replace(new RegExp(pkg.version, 'g'), '{NPM-VERSION}') 1521cb0ef41Sopenharmony_ci 1531cb0ef41Sopenharmony_ci // We do the defaults after everything else so that they don't cause the 1541cb0ef41Sopenharmony_ci // other cleaners to miss values we would have clobbered here. For 1551cb0ef41Sopenharmony_ci // instance if execPath is /home/user/.nvm/versions/node/1.0.0/bin/node, 1561cb0ef41Sopenharmony_ci // and we replaced the node version first, the real execPath we're trying 1571cb0ef41Sopenharmony_ci // to replace would no longer be represented, and be missed. 1581cb0ef41Sopenharmony_ci if (this[_npm]) { 1591cb0ef41Sopenharmony_ci // replace vague default config values that are present within quotes 1601cb0ef41Sopenharmony_ci // with placeholders 1611cb0ef41Sopenharmony_ci for (const name of vagueRedactedDefaults) { 1621cb0ef41Sopenharmony_ci const value = this[_npm].config.defaults[name] 1631cb0ef41Sopenharmony_ci clean = clean.split(`"${normalize(value)}"`).join(`"{${name.toUpperCase()}}"`) 1641cb0ef41Sopenharmony_ci } 1651cb0ef41Sopenharmony_ci } 1661cb0ef41Sopenharmony_ci 1671cb0ef41Sopenharmony_ci return clean 1681cb0ef41Sopenharmony_ci } 1691cb0ef41Sopenharmony_ci 1701cb0ef41Sopenharmony_ci // test.afterEach hook 1711cb0ef41Sopenharmony_ci reset () { 1721cb0ef41Sopenharmony_ci this.removeAllListeners() 1731cb0ef41Sopenharmony_ci this[_parent] = undefined 1741cb0ef41Sopenharmony_ci this[_output] = [] 1751cb0ef41Sopenharmony_ci this[_data].clear() 1761cb0ef41Sopenharmony_ci this[_proxy].env = {} 1771cb0ef41Sopenharmony_ci this[_proxy].argv = [] 1781cb0ef41Sopenharmony_ci this[_npm] = undefined 1791cb0ef41Sopenharmony_ci } 1801cb0ef41Sopenharmony_ci 1811cb0ef41Sopenharmony_ci // test.teardown hook 1821cb0ef41Sopenharmony_ci teardown () { 1831cb0ef41Sopenharmony_ci if (this[_parent]) { 1841cb0ef41Sopenharmony_ci const sandboxProcess = sandboxes.get(this[_parent]) 1851cb0ef41Sopenharmony_ci sandboxProcess.removeAllListeners('log') 1861cb0ef41Sopenharmony_ci sandboxes.delete(this[_parent]) 1871cb0ef41Sopenharmony_ci } 1881cb0ef41Sopenharmony_ci if (this[_npm]) { 1891cb0ef41Sopenharmony_ci this[_npm].unload() 1901cb0ef41Sopenharmony_ci } 1911cb0ef41Sopenharmony_ci return rm(this[_dirs].temp, { recursive: true, force: true }).catch(() => null) 1921cb0ef41Sopenharmony_ci } 1931cb0ef41Sopenharmony_ci 1941cb0ef41Sopenharmony_ci // proxy get handler 1951cb0ef41Sopenharmony_ci [_get] (target, prop, receiver) { 1961cb0ef41Sopenharmony_ci if (this[_data].has(prop)) { 1971cb0ef41Sopenharmony_ci return this[_data].get(prop) 1981cb0ef41Sopenharmony_ci } 1991cb0ef41Sopenharmony_ci 2001cb0ef41Sopenharmony_ci if (this[prop] !== undefined) { 2011cb0ef41Sopenharmony_ci return Reflect.get(this, prop, this) 2021cb0ef41Sopenharmony_ci } 2031cb0ef41Sopenharmony_ci 2041cb0ef41Sopenharmony_ci return Reflect.get(target, prop, receiver) 2051cb0ef41Sopenharmony_ci } 2061cb0ef41Sopenharmony_ci 2071cb0ef41Sopenharmony_ci // proxy set handler 2081cb0ef41Sopenharmony_ci [_set] (target, prop, value) { 2091cb0ef41Sopenharmony_ci if (prop === 'env') { 2101cb0ef41Sopenharmony_ci value = { 2111cb0ef41Sopenharmony_ci ...value, 2121cb0ef41Sopenharmony_ci HOME: this.home, 2131cb0ef41Sopenharmony_ci } 2141cb0ef41Sopenharmony_ci } 2151cb0ef41Sopenharmony_ci 2161cb0ef41Sopenharmony_ci if (prop === 'argv') { 2171cb0ef41Sopenharmony_ci value = [ 2181cb0ef41Sopenharmony_ci process.execPath, 2191cb0ef41Sopenharmony_ci join(dirname(process.execPath), 'npm'), 2201cb0ef41Sopenharmony_ci ...value, 2211cb0ef41Sopenharmony_ci ] 2221cb0ef41Sopenharmony_ci } 2231cb0ef41Sopenharmony_ci 2241cb0ef41Sopenharmony_ci return this[_data].set(prop, value) 2251cb0ef41Sopenharmony_ci } 2261cb0ef41Sopenharmony_ci 2271cb0ef41Sopenharmony_ci async run (command, argv = []) { 2281cb0ef41Sopenharmony_ci await Promise.all([ 2291cb0ef41Sopenharmony_ci mkdir(this.project, { recursive: true }), 2301cb0ef41Sopenharmony_ci mkdir(this.home, { recursive: true }), 2311cb0ef41Sopenharmony_ci mkdir(this.global, { recursive: true }), 2321cb0ef41Sopenharmony_ci ]) 2331cb0ef41Sopenharmony_ci 2341cb0ef41Sopenharmony_ci // attach the sandbox process now, doing it after the promise above is 2351cb0ef41Sopenharmony_ci // necessary to make sure that only async calls spawned as part of this 2361cb0ef41Sopenharmony_ci // call to run will receive the sandbox. if we attach it too early, we 2371cb0ef41Sopenharmony_ci // end up interfering with tap 2381cb0ef41Sopenharmony_ci this[_parent] = executionAsyncId() 2391cb0ef41Sopenharmony_ci this[_data].set('_asyncId', this[_parent]) 2401cb0ef41Sopenharmony_ci sandboxes.set(this[_parent], this[_proxy]) 2411cb0ef41Sopenharmony_ci process = this[_proxy] 2421cb0ef41Sopenharmony_ci 2431cb0ef41Sopenharmony_ci this[_proxy].argv = [ 2441cb0ef41Sopenharmony_ci '--prefix', this.project, 2451cb0ef41Sopenharmony_ci '--userconfig', join(this.home, '.npmrc'), 2461cb0ef41Sopenharmony_ci '--globalconfig', join(this.global, 'npmrc'), 2471cb0ef41Sopenharmony_ci '--cache', this.cache, 2481cb0ef41Sopenharmony_ci command, 2491cb0ef41Sopenharmony_ci ...argv, 2501cb0ef41Sopenharmony_ci ] 2511cb0ef41Sopenharmony_ci 2521cb0ef41Sopenharmony_ci const mockedLogs = mockLogs(this[_mocks]) 2531cb0ef41Sopenharmony_ci this[_logs] = mockedLogs.logs 2541cb0ef41Sopenharmony_ci const definitions = this[_test].mock('@npmcli/config/lib/definitions') 2551cb0ef41Sopenharmony_ci const Npm = this[_test].mock('../../lib/npm.js', { 2561cb0ef41Sopenharmony_ci '@npmcli/config/lib/definitions': definitions, 2571cb0ef41Sopenharmony_ci '../../lib/utils/update-notifier.js': async () => {}, 2581cb0ef41Sopenharmony_ci ...this[_mocks], 2591cb0ef41Sopenharmony_ci ...mockedLogs.logMocks, 2601cb0ef41Sopenharmony_ci }) 2611cb0ef41Sopenharmony_ci this.process.on('log', (l, ...args) => { 2621cb0ef41Sopenharmony_ci if (l !== 'pause' && l !== 'resume') { 2631cb0ef41Sopenharmony_ci this[_logs].push([l, ...args]) 2641cb0ef41Sopenharmony_ci } 2651cb0ef41Sopenharmony_ci }) 2661cb0ef41Sopenharmony_ci 2671cb0ef41Sopenharmony_ci this[_npm] = new Npm() 2681cb0ef41Sopenharmony_ci this[_npm].output = (...args) => this[_output].push(args) 2691cb0ef41Sopenharmony_ci await this[_npm].load() 2701cb0ef41Sopenharmony_ci 2711cb0ef41Sopenharmony_ci const cmd = this[_npm].argv.shift() 2721cb0ef41Sopenharmony_ci return this[_npm].exec(cmd, this[_npm].argv) 2731cb0ef41Sopenharmony_ci } 2741cb0ef41Sopenharmony_ci 2751cb0ef41Sopenharmony_ci async complete (command, argv, partial) { 2761cb0ef41Sopenharmony_ci if (!Array.isArray(argv)) { 2771cb0ef41Sopenharmony_ci partial = argv 2781cb0ef41Sopenharmony_ci argv = [] 2791cb0ef41Sopenharmony_ci } 2801cb0ef41Sopenharmony_ci 2811cb0ef41Sopenharmony_ci await Promise.all([ 2821cb0ef41Sopenharmony_ci mkdir(this.project, { recursive: true }), 2831cb0ef41Sopenharmony_ci mkdir(this.home, { recursive: true }), 2841cb0ef41Sopenharmony_ci mkdir(this.global, { recursive: true }), 2851cb0ef41Sopenharmony_ci ]) 2861cb0ef41Sopenharmony_ci 2871cb0ef41Sopenharmony_ci // attach the sandbox process now, doing it after the promise above is 2881cb0ef41Sopenharmony_ci // necessary to make sure that only async calls spawned as part of this 2891cb0ef41Sopenharmony_ci // call to run will receive the sandbox. if we attach it too early, we 2901cb0ef41Sopenharmony_ci // end up interfering with tap 2911cb0ef41Sopenharmony_ci this[_parent] = executionAsyncId() 2921cb0ef41Sopenharmony_ci this[_data].set('_asyncId', this[_parent]) 2931cb0ef41Sopenharmony_ci sandboxes.set(this[_parent], this[_proxy]) 2941cb0ef41Sopenharmony_ci process = this[_proxy] 2951cb0ef41Sopenharmony_ci 2961cb0ef41Sopenharmony_ci this[_proxy].argv = [ 2971cb0ef41Sopenharmony_ci '--prefix', this.project, 2981cb0ef41Sopenharmony_ci '--userconfig', join(this.home, '.npmrc'), 2991cb0ef41Sopenharmony_ci '--globalconfig', join(this.global, 'npmrc'), 3001cb0ef41Sopenharmony_ci '--cache', this.cache, 3011cb0ef41Sopenharmony_ci command, 3021cb0ef41Sopenharmony_ci ...argv, 3031cb0ef41Sopenharmony_ci ] 3041cb0ef41Sopenharmony_ci 3051cb0ef41Sopenharmony_ci const mockedLogs = mockLogs(this[_mocks]) 3061cb0ef41Sopenharmony_ci this[_logs] = mockedLogs.logs 3071cb0ef41Sopenharmony_ci const definitions = this[_test].mock('@npmcli/config/lib/definitions') 3081cb0ef41Sopenharmony_ci const Npm = this[_test].mock('../../lib/npm.js', { 3091cb0ef41Sopenharmony_ci '@npmcli/config/lib/definitions': definitions, 3101cb0ef41Sopenharmony_ci '../../lib/utils/update-notifier.js': async () => {}, 3111cb0ef41Sopenharmony_ci ...this[_mocks], 3121cb0ef41Sopenharmony_ci ...mockedLogs.logMocks, 3131cb0ef41Sopenharmony_ci }) 3141cb0ef41Sopenharmony_ci this.process.on('log', (l, ...args) => { 3151cb0ef41Sopenharmony_ci if (l !== 'pause' && l !== 'resume') { 3161cb0ef41Sopenharmony_ci this[_logs].push([l, ...args]) 3171cb0ef41Sopenharmony_ci } 3181cb0ef41Sopenharmony_ci }) 3191cb0ef41Sopenharmony_ci 3201cb0ef41Sopenharmony_ci this[_npm] = new Npm() 3211cb0ef41Sopenharmony_ci this[_npm].output = (...args) => this[_output].push(args) 3221cb0ef41Sopenharmony_ci await this[_npm].load() 3231cb0ef41Sopenharmony_ci 3241cb0ef41Sopenharmony_ci const Cmd = Npm.cmd(command) 3251cb0ef41Sopenharmony_ci return Cmd.completion({ 3261cb0ef41Sopenharmony_ci partialWord: partial, 3271cb0ef41Sopenharmony_ci conf: { 3281cb0ef41Sopenharmony_ci argv: { 3291cb0ef41Sopenharmony_ci remain: ['npm', command, ...argv], 3301cb0ef41Sopenharmony_ci }, 3311cb0ef41Sopenharmony_ci }, 3321cb0ef41Sopenharmony_ci }) 3331cb0ef41Sopenharmony_ci } 3341cb0ef41Sopenharmony_ci} 3351cb0ef41Sopenharmony_ci 3361cb0ef41Sopenharmony_cimodule.exports = Sandbox 337