index.js (7515B)
1 const path = require('path'); 2 const childProcess = require('child_process'); 3 const {promises: fs, constants: fsConstants} = require('fs'); 4 const isWsl = require('is-wsl'); 5 const isDocker = require('is-docker'); 6 const defineLazyProperty = require('define-lazy-prop'); 7 8 // Path to included `xdg-open`. 9 const localXdgOpenPath = path.join(__dirname, 'xdg-open'); 10 11 const {platform, arch} = process; 12 13 /** 14 Get the mount point for fixed drives in WSL. 15 16 @inner 17 @returns {string} The mount point. 18 */ 19 const getWslDrivesMountPoint = (() => { 20 // Default value for "root" param 21 // according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config 22 const defaultMountPoint = '/mnt/'; 23 24 let mountPoint; 25 26 return async function () { 27 if (mountPoint) { 28 // Return memoized mount point value 29 return mountPoint; 30 } 31 32 const configFilePath = '/etc/wsl.conf'; 33 34 let isConfigFileExists = false; 35 try { 36 await fs.access(configFilePath, fsConstants.F_OK); 37 isConfigFileExists = true; 38 } catch {} 39 40 if (!isConfigFileExists) { 41 return defaultMountPoint; 42 } 43 44 const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'}); 45 const configMountPoint = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/g.exec(configContent); 46 47 if (!configMountPoint) { 48 return defaultMountPoint; 49 } 50 51 mountPoint = configMountPoint.groups.mountPoint.trim(); 52 mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`; 53 54 return mountPoint; 55 }; 56 })(); 57 58 const pTryEach = async (array, mapper) => { 59 let latestError; 60 61 for (const item of array) { 62 try { 63 return await mapper(item); // eslint-disable-line no-await-in-loop 64 } catch (error) { 65 latestError = error; 66 } 67 } 68 69 throw latestError; 70 }; 71 72 const baseOpen = async options => { 73 options = { 74 wait: false, 75 background: false, 76 newInstance: false, 77 allowNonzeroExitCode: false, 78 ...options 79 }; 80 81 if (Array.isArray(options.app)) { 82 return pTryEach(options.app, singleApp => baseOpen({ 83 ...options, 84 app: singleApp 85 })); 86 } 87 88 let {name: app, arguments: appArguments = []} = options.app || {}; 89 appArguments = [...appArguments]; 90 91 if (Array.isArray(app)) { 92 return pTryEach(app, appName => baseOpen({ 93 ...options, 94 app: { 95 name: appName, 96 arguments: appArguments 97 } 98 })); 99 } 100 101 let command; 102 const cliArguments = []; 103 const childProcessOptions = {}; 104 105 if (platform === 'darwin') { 106 command = 'open'; 107 108 if (options.wait) { 109 cliArguments.push('--wait-apps'); 110 } 111 112 if (options.background) { 113 cliArguments.push('--background'); 114 } 115 116 if (options.newInstance) { 117 cliArguments.push('--new'); 118 } 119 120 if (app) { 121 cliArguments.push('-a', app); 122 } 123 } else if (platform === 'win32' || (isWsl && !isDocker())) { 124 const mountPoint = await getWslDrivesMountPoint(); 125 126 command = isWsl ? 127 `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` : 128 `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`; 129 130 cliArguments.push( 131 '-NoProfile', 132 '-NonInteractive', 133 '–ExecutionPolicy', 134 'Bypass', 135 '-EncodedCommand' 136 ); 137 138 if (!isWsl) { 139 childProcessOptions.windowsVerbatimArguments = true; 140 } 141 142 const encodedArguments = ['Start']; 143 144 if (options.wait) { 145 encodedArguments.push('-Wait'); 146 } 147 148 if (app) { 149 // Double quote with double quotes to ensure the inner quotes are passed through. 150 // Inner quotes are delimited for PowerShell interpretation with backticks. 151 encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList'); 152 if (options.target) { 153 appArguments.unshift(options.target); 154 } 155 } else if (options.target) { 156 encodedArguments.push(`"${options.target}"`); 157 } 158 159 if (appArguments.length > 0) { 160 appArguments = appArguments.map(arg => `"\`"${arg}\`""`); 161 encodedArguments.push(appArguments.join(',')); 162 } 163 164 // Using Base64-encoded command, accepted by PowerShell, to allow special characters. 165 options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); 166 } else { 167 if (app) { 168 command = app; 169 } else { 170 // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. 171 const isBundled = !__dirname || __dirname === '/'; 172 173 // Check if local `xdg-open` exists and is executable. 174 let exeLocalXdgOpen = false; 175 try { 176 await fs.access(localXdgOpenPath, fsConstants.X_OK); 177 exeLocalXdgOpen = true; 178 } catch {} 179 180 const useSystemXdgOpen = process.versions.electron || 181 platform === 'android' || isBundled || !exeLocalXdgOpen; 182 command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath; 183 } 184 185 if (appArguments.length > 0) { 186 cliArguments.push(...appArguments); 187 } 188 189 if (!options.wait) { 190 // `xdg-open` will block the process unless stdio is ignored 191 // and it's detached from the parent even if it's unref'd. 192 childProcessOptions.stdio = 'ignore'; 193 childProcessOptions.detached = true; 194 } 195 } 196 197 if (options.target) { 198 cliArguments.push(options.target); 199 } 200 201 if (platform === 'darwin' && appArguments.length > 0) { 202 cliArguments.push('--args', ...appArguments); 203 } 204 205 const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions); 206 207 if (options.wait) { 208 return new Promise((resolve, reject) => { 209 subprocess.once('error', reject); 210 211 subprocess.once('close', exitCode => { 212 if (options.allowNonzeroExitCode && exitCode > 0) { 213 reject(new Error(`Exited with code ${exitCode}`)); 214 return; 215 } 216 217 resolve(subprocess); 218 }); 219 }); 220 } 221 222 subprocess.unref(); 223 224 return subprocess; 225 }; 226 227 const open = (target, options) => { 228 if (typeof target !== 'string') { 229 throw new TypeError('Expected a `target`'); 230 } 231 232 return baseOpen({ 233 ...options, 234 target 235 }); 236 }; 237 238 const openApp = (name, options) => { 239 if (typeof name !== 'string') { 240 throw new TypeError('Expected a `name`'); 241 } 242 243 const {arguments: appArguments = []} = options || {}; 244 if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) { 245 throw new TypeError('Expected `appArguments` as Array type'); 246 } 247 248 return baseOpen({ 249 ...options, 250 app: { 251 name, 252 arguments: appArguments 253 } 254 }); 255 }; 256 257 function detectArchBinary(binary) { 258 if (typeof binary === 'string' || Array.isArray(binary)) { 259 return binary; 260 } 261 262 const {[arch]: archBinary} = binary; 263 264 if (!archBinary) { 265 throw new Error(`${arch} is not supported`); 266 } 267 268 return archBinary; 269 } 270 271 function detectPlatformBinary({[platform]: platformBinary}, {wsl}) { 272 if (wsl && isWsl) { 273 return detectArchBinary(wsl); 274 } 275 276 if (!platformBinary) { 277 throw new Error(`${platform} is not supported`); 278 } 279 280 return detectArchBinary(platformBinary); 281 } 282 283 const apps = {}; 284 285 defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({ 286 darwin: 'google chrome', 287 win32: 'chrome', 288 linux: ['google-chrome', 'google-chrome-stable', 'chromium'] 289 }, { 290 wsl: { 291 ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', 292 x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'] 293 } 294 })); 295 296 defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({ 297 darwin: 'firefox', 298 win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', 299 linux: 'firefox' 300 }, { 301 wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe' 302 })); 303 304 defineLazyProperty(apps, 'edge', () => detectPlatformBinary({ 305 darwin: 'microsoft edge', 306 win32: 'msedge', 307 linux: ['microsoft-edge', 'microsoft-edge-dev'] 308 }, { 309 wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' 310 })); 311 312 open.apps = apps; 313 open.openApp = openApp; 314 315 module.exports = open;