123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- 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, {constants as fsConstants} from 'node:fs/promises';
- 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 = /(?<!#.*)root\s*=\s*(?<mountPoint>.*)/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 || process.env.windir || 'C:\\Windows'}\\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(argument => `"\`"${argument}\`""`);
- 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 (platform === 'darwin' && appArguments.length > 0) {
- cliArguments.push('--args', ...appArguments);
- }
- // This has to come after `--args`.
- if (options.target) {
- cliArguments.push(options.target);
- }
- 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' && !Array.isArray(name)) {
- throw new TypeError('Expected a valid `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;
|