316 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const path = require('path');
 | ||
| const childProcess = require('child_process');
 | ||
| const {promises: fs, constants: fsConstants} = require('fs');
 | ||
| const isWsl = require('is-wsl');
 | ||
| const isDocker = require('is-docker');
 | ||
| const defineLazyProperty = require('define-lazy-prop');
 | ||
| 
 | ||
| // Path to included `xdg-open`.
 | ||
| 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
 | ||
| 			}
 | ||
| 		}));
 | ||
| 	}
 | ||
| 
 | ||
| 	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 && !isDocker())) {
 | ||
| 		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}\`""`, '-ArgumentList');
 | ||
| 			if (options.target) {
 | ||
| 				appArguments.unshift(options.target);
 | ||
| 			}
 | ||
| 		} else if (options.target) {
 | ||
| 			encodedArguments.push(`"${options.target}"`);
 | ||
| 		}
 | ||
| 
 | ||
| 		if (appArguments.length > 0) {
 | ||
| 			appArguments = appArguments.map(arg => `"\`"${arg}\`""`);
 | ||
| 			encodedArguments.push(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
 | ||
| 	});
 | ||
| };
 | ||
| 
 | ||
| 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);
 | ||
| }
 | ||
| 
 | ||
| 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'
 | ||
| }));
 | ||
| 
 | ||
| open.apps = apps;
 | ||
| open.openApp = openApp;
 | ||
| 
 | ||
| module.exports = open;
 |