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