index.js 8.8 KB

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