index.js 9.1 KB

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