import process from 'node:process'; import {Buffer} from 'node:buffer'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import childProcess from 'node:child_process'; import fs from 'node:fs/promises'; import {constants as fsConstants} from 'node:fs'; // TODO: Move this to the above import when targeting Node.js 18. import isWsl from 'is-wsl'; import defineLazyProperty from 'define-lazy-prop'; import defaultBrowser from 'default-browser'; import isInsideContainer from 'is-inside-container'; // Path to included `xdg-open`. const __dirname = path.dirname(fileURLToPath(import.meta.url)); const localXdgOpenPath = path.join(__dirname, 'xdg-open'); const {platform, arch} = process; /** Get the mount point for fixed drives in WSL. @inner @returns {string} The mount point. */ const getWslDrivesMountPoint = (() => { // Default value for "root" param // according to https://docs.microsoft.com/en-us/windows/wsl/wsl-config const defaultMountPoint = '/mnt/'; let mountPoint; return async function () { if (mountPoint) { // Return memoized mount point value return mountPoint; } const configFilePath = '/etc/wsl.conf'; let isConfigFileExists = false; try { await fs.access(configFilePath, fsConstants.F_OK); isConfigFileExists = true; } catch {} if (!isConfigFileExists) { return defaultMountPoint; } const configContent = await fs.readFile(configFilePath, {encoding: 'utf8'}); const configMountPoint = /(?.*)/g.exec(configContent); if (!configMountPoint) { return defaultMountPoint; } mountPoint = configMountPoint.groups.mountPoint.trim(); mountPoint = mountPoint.endsWith('/') ? mountPoint : `${mountPoint}/`; return mountPoint; }; })(); const pTryEach = async (array, mapper) => { let latestError; for (const item of array) { try { return await mapper(item); // eslint-disable-line no-await-in-loop } catch (error) { latestError = error; } } throw latestError; }; const baseOpen = async options => { options = { wait: false, background: false, newInstance: false, allowNonzeroExitCode: false, ...options, }; if (Array.isArray(options.app)) { return pTryEach(options.app, singleApp => baseOpen({ ...options, app: singleApp, })); } let {name: app, arguments: appArguments = []} = options.app ?? {}; appArguments = [...appArguments]; if (Array.isArray(app)) { return pTryEach(app, appName => baseOpen({ ...options, app: { name: appName, arguments: appArguments, }, })); } if (app === 'browser' || app === 'browserPrivate') { // IDs from default-browser for macOS and windows are the same const ids = { 'com.google.chrome': 'chrome', 'google-chrome.desktop': 'chrome', 'org.mozilla.firefox': 'firefox', 'firefox.desktop': 'firefox', 'com.microsoft.msedge': 'edge', 'com.microsoft.edge': 'edge', 'microsoft-edge.desktop': 'edge', }; // Incognito flags for each browser in `apps`. const flags = { chrome: '--incognito', firefox: '--private-window', edge: '--inPrivate', }; const browser = await defaultBrowser(); if (browser.id in ids) { const browserName = ids[browser.id]; if (app === 'browserPrivate') { appArguments.push(flags[browserName]); } return baseOpen({ ...options, app: { name: apps[browserName], arguments: appArguments, }, }); } throw new Error(`${browser.name} is not supported as a default browser`); } let command; const cliArguments = []; const childProcessOptions = {}; if (platform === 'darwin') { command = 'open'; if (options.wait) { cliArguments.push('--wait-apps'); } if (options.background) { cliArguments.push('--background'); } if (options.newInstance) { cliArguments.push('--new'); } if (app) { cliArguments.push('-a', app); } } else if (platform === 'win32' || (isWsl && !isInsideContainer() && !app)) { const mountPoint = await getWslDrivesMountPoint(); command = isWsl ? `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe` : `${process.env.SYSTEMROOT}\\System32\\WindowsPowerShell\\v1.0\\powershell`; cliArguments.push( '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', ); if (!isWsl) { childProcessOptions.windowsVerbatimArguments = true; } const encodedArguments = ['Start']; if (options.wait) { encodedArguments.push('-Wait'); } if (app) { // Double quote with double quotes to ensure the inner quotes are passed through. // Inner quotes are delimited for PowerShell interpretation with backticks. encodedArguments.push(`"\`"${app}\`""`); if (options.target) { appArguments.push(options.target); } } else if (options.target) { encodedArguments.push(`"${options.target}"`); } if (appArguments.length > 0) { appArguments = appArguments.map(arg => `"\`"${arg}\`""`); encodedArguments.push('-ArgumentList', appArguments.join(',')); } // Using Base64-encoded command, accepted by PowerShell, to allow special characters. options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64'); } else { if (app) { command = app; } else { // When bundled by Webpack, there's no actual package file path and no local `xdg-open`. const isBundled = !__dirname || __dirname === '/'; // Check if local `xdg-open` exists and is executable. let exeLocalXdgOpen = false; try { await fs.access(localXdgOpenPath, fsConstants.X_OK); exeLocalXdgOpen = true; } catch {} const useSystemXdgOpen = process.versions.electron ?? (platform === 'android' || isBundled || !exeLocalXdgOpen); command = useSystemXdgOpen ? 'xdg-open' : localXdgOpenPath; } if (appArguments.length > 0) { cliArguments.push(...appArguments); } if (!options.wait) { // `xdg-open` will block the process unless stdio is ignored // and it's detached from the parent even if it's unref'd. childProcessOptions.stdio = 'ignore'; childProcessOptions.detached = true; } } if (options.target) { cliArguments.push(options.target); } if (platform === 'darwin' && appArguments.length > 0) { cliArguments.push('--args', ...appArguments); } const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions); if (options.wait) { return new Promise((resolve, reject) => { subprocess.once('error', reject); subprocess.once('close', exitCode => { if (!options.allowNonzeroExitCode && exitCode > 0) { reject(new Error(`Exited with code ${exitCode}`)); return; } resolve(subprocess); }); }); } subprocess.unref(); return subprocess; }; const open = (target, options) => { if (typeof target !== 'string') { throw new TypeError('Expected a `target`'); } return baseOpen({ ...options, target, }); }; export const openApp = (name, options) => { if (typeof name !== 'string') { throw new TypeError('Expected a `name`'); } const {arguments: appArguments = []} = options ?? {}; if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) { throw new TypeError('Expected `appArguments` as Array type'); } return baseOpen({ ...options, app: { name, arguments: appArguments, }, }); }; function detectArchBinary(binary) { if (typeof binary === 'string' || Array.isArray(binary)) { return binary; } const {[arch]: archBinary} = binary; if (!archBinary) { throw new Error(`${arch} is not supported`); } return archBinary; } function detectPlatformBinary({[platform]: platformBinary}, {wsl}) { if (wsl && isWsl) { return detectArchBinary(wsl); } if (!platformBinary) { throw new Error(`${platform} is not supported`); } return detectArchBinary(platformBinary); } export const apps = {}; defineLazyProperty(apps, 'chrome', () => detectPlatformBinary({ darwin: 'google chrome', win32: 'chrome', linux: ['google-chrome', 'google-chrome-stable', 'chromium'], }, { wsl: { ia32: '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe', x64: ['/mnt/c/Program Files/Google/Chrome/Application/chrome.exe', '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'], }, })); defineLazyProperty(apps, 'firefox', () => detectPlatformBinary({ darwin: 'firefox', win32: 'C:\\Program Files\\Mozilla Firefox\\firefox.exe', linux: 'firefox', }, { wsl: '/mnt/c/Program Files/Mozilla Firefox/firefox.exe', })); defineLazyProperty(apps, 'edge', () => detectPlatformBinary({ darwin: 'microsoft edge', win32: 'msedge', linux: ['microsoft-edge', 'microsoft-edge-dev'], }, { wsl: '/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', })); defineLazyProperty(apps, 'browser', () => 'browser'); defineLazyProperty(apps, 'browserPrivate', () => 'browserPrivate'); export default open;