firefox-location2 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,9 +11,15 @@
11
11
 
12
12
  <img alt="Firefox" align="right" src="https://cdn.jsdelivr.net/gh/extension-js/media@db5deb23fbfa85530f8146718812972998e13a4d/browser_logos/svg/firefox.svg" width="10.5%" />
13
13
 
14
- * By default checks only `stable`. Optionally can cascade to `esr` / `developer edition` / `nightly`.
15
- * Supports macOS / Windows / Linux
16
- * Works both as an ES module or CommonJS
14
+ - By default checks only `stable`. Optionally can cascade to `esr` / `developer edition` / `nightly`.
15
+ - Supports macOS / Windows / Linux
16
+ - Works both as an ES module or CommonJS
17
+
18
+ New in this version:
19
+
20
+ - Optional helper to throw with a friendly install guide when nothing is found
21
+ - CLI output is colorized (green on success, red on error)
22
+ - After you run `npx @puppeteer/browsers install firefox@stable` once, we auto-detect Firefox from Puppeteer's cache on all platforms (no env vars needed)
17
23
 
18
24
  ## Support table
19
25
 
@@ -162,15 +168,57 @@ Note: On Linux, the module first tries to resolve binaries on <code>$PATH</code>
162
168
  **Via Node.js (strict by default):**
163
169
 
164
170
  ```js
165
- import firefoxLocation from "firefox-location2";
171
+ import firefoxLocation from 'firefox-location2'
172
+ import {getFirefoxVersion} from 'firefox-location2'
166
173
 
167
174
  // Strict (Stable only)
168
- console.log(firefoxLocation());
175
+ console.log(firefoxLocation())
169
176
  // => "/Applications/Firefox.app/Contents/MacOS/firefox" or null
170
177
 
171
178
  // Enable fallback (Stable / ESR / Developer Edition / Nightly)
172
- console.log(firefoxLocation(true));
179
+ console.log(firefoxLocation(true))
173
180
  // => first found among Stable/ESR/Developer/Nightly or null
181
+
182
+ // Throw with a friendly, copy-pasteable guide when not found
183
+ import {locateFirefoxOrExplain, getInstallGuidance} from 'firefox-location2'
184
+ try {
185
+ const path = locateFirefoxOrExplain({allowFallback: true})
186
+ console.log(path)
187
+
188
+ // Cross-platform version (no exec by default)
189
+ const v = getFirefoxVersion(path)
190
+ console.log(v) // e.g. "130.0.1" or null
191
+
192
+ // Opt-in: allow executing the binary to fetch version on platforms without metadata (e.g. Linux)
193
+ const v2 = getFirefoxVersion(path, {allowExec: true})
194
+ console.log(v2)
195
+ } catch (e) {
196
+ console.error(String(e))
197
+ // Or print getInstallGuidance() explicitly
198
+ }
199
+ ```
200
+
201
+ **CommonJS:**
202
+
203
+ ```js
204
+ const api = require('firefox-location2')
205
+ const locateFirefox = api.default || api
206
+
207
+ // Strict (Stable only)
208
+ console.log(locateFirefox())
209
+
210
+ // With fallback enabled
211
+ console.log(locateFirefox(true))
212
+
213
+ // Helper that throws with guidance
214
+ try {
215
+ const p = (
216
+ api.locateFirefoxOrExplain || ((o) => locateFirefox(o?.allowFallback))
217
+ )({allowFallback: true})
218
+ console.log(p)
219
+ } catch (e) {
220
+ console.error(String(e))
221
+ }
174
222
  ```
175
223
 
176
224
  **Via CLI:**
@@ -181,8 +229,46 @@ npx firefox-location2
181
229
 
182
230
  npx firefox-location2 --fallback
183
231
  # Enable cascade (Stable / ESR / Developer / Nightly)
232
+
233
+ # Output is colorized when printed to a TTY
234
+
235
+ # Respect Puppeteer cache (after you install once):
236
+ npx @puppeteer/browsers install firefox@stable
237
+ npx firefox-location2
184
238
  ```
185
239
 
240
+ ### Environment overrides
241
+
242
+ If this environment variable is set and points to an existing binary, it takes precedence:
243
+
244
+ - `FIREFOX_BINARY`
245
+
246
+ Exit behavior:
247
+
248
+ - Prints the resolved path on success
249
+ - Exits with code 1 and prints a guidance message if nothing suitable is found
250
+
251
+ Notes:
252
+
253
+ - Output is colorized when printed to a TTY (green success, red error)
254
+ - After you run `npx @puppeteer/browsers install firefox@stable` once, we auto-detect Firefox from Puppeteer's cache on all platforms. No env vars needed.
255
+
256
+ ## API
257
+
258
+ - `default export locateFirefox(allowFallback?: boolean): string | null`
259
+ - Returns the first existing path among the selected channels or `null`.
260
+ - When `allowFallback` is `true`, checks Stable → ESR → Developer → Nightly.
261
+
262
+ - `locateFirefoxOrExplain(options?: boolean | { allowFallback?: boolean }): string`
263
+ - Returns a path if found, otherwise throws an `Error` with a friendly installation guide.
264
+ - Path resolution never executes the browser.
265
+
266
+ - `getFirefoxVersion(bin: string, opts?: { allowExec?: boolean }): string | null`
267
+ - Cross-platform version resolver that does not execute the browser by default.
268
+ - Windows: reads PE file metadata via PowerShell (no GUI spawn).
269
+ - macOS: reads `Info.plist` (no GUI spawn).
270
+ - Linux/other: returns `null` unless `allowExec` is `true`, then tries `--version`.
271
+
186
272
  ## Planned enhancements
187
273
 
188
274
  - Flatpak detection on Linux: detect installed Flatpak app <code>org.mozilla.firefox</code> and expose the appropriate invocation.
@@ -192,12 +278,12 @@ npx firefox-location2 --fallback
192
278
 
193
279
  ## Related projects
194
280
 
195
- * [brave-location](https://github.com/cezaraugusto/brave-location)
196
- * [chrome-location2](https://github.com/cezaraugusto/chrome-location2)
197
- * [edge-location](https://github.com/cezaraugusto/edge-location)
198
- * [opera-location2](https://github.com/cezaraugusto/opera-location2)
199
- * [vivaldi-location2](https://github.com/cezaraugusto/vivaldi-location2)
200
- * [yandex-location2](https://github.com/cezaraugusto/yandex-location2)
281
+ - [brave-location](https://github.com/cezaraugusto/brave-location)
282
+ - [chrome-location2](https://github.com/cezaraugusto/chrome-location2)
283
+ - [edge-location](https://github.com/cezaraugusto/edge-location)
284
+ - [opera-location2](https://github.com/cezaraugusto/opera-location2)
285
+ - [vivaldi-location2](https://github.com/cezaraugusto/vivaldi-location2)
286
+ - [yandex-location2](https://github.com/cezaraugusto/yandex-location2)
201
287
 
202
288
  ## License
203
289
 
package/bin.js CHANGED
@@ -1,9 +1,42 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const locateFirefox =
4
- require('./dist/index.cjs').default || require('./dist/index.cjs');
3
+ import locateFirefox, {
4
+ locateFirefoxOrExplain,
5
+ getInstallGuidance,
6
+ getFirefoxVersion,
7
+ } from './dist/index.js';
5
8
 
6
9
  const argv = process.argv.slice(2);
7
10
  const allowFallback = argv.includes('--fallback') || argv.includes('-f');
11
+ const printBrowserVersion =
12
+ argv.includes('--firefox-version') || argv.includes('--browser-version');
13
+ const allowExec = argv.includes('--allow-exec');
8
14
 
9
- console.log(locateFirefox(allowFallback));
15
+ try {
16
+ const location =
17
+ (typeof locateFirefoxOrExplain === 'function' &&
18
+ locateFirefoxOrExplain({ allowFallback })) ||
19
+ (typeof locateFirefox === 'function' && locateFirefox(allowFallback)) ||
20
+ null;
21
+
22
+ if (!location)
23
+ throw new Error(
24
+ (typeof getInstallGuidance === 'function' && getInstallGuidance()) ||
25
+ 'No suitable Firefox binary found.',
26
+ );
27
+
28
+ if (printBrowserVersion && typeof getFirefoxVersion === 'function') {
29
+ const v = getFirefoxVersion(location, { allowExec });
30
+ if (!v) {
31
+ console.log('');
32
+ process.exit(2);
33
+ }
34
+ console.log(String(v));
35
+ process.exit(0);
36
+ }
37
+
38
+ console.log(String(location));
39
+ } catch (e) {
40
+ console.error(String(e));
41
+ process.exit(1);
42
+ }
package/dist/index.cjs CHANGED
@@ -33,7 +33,10 @@ var __webpack_require__ = {};
33
33
  var __webpack_exports__ = {};
34
34
  __webpack_require__.r(__webpack_exports__);
35
35
  __webpack_require__.d(__webpack_exports__, {
36
- default: ()=>locateFirefox
36
+ default: ()=>locateFirefox,
37
+ getFirefoxVersion: ()=>getFirefoxVersion,
38
+ getInstallGuidance: ()=>getInstallGuidance,
39
+ locateFirefoxOrExplain: ()=>locateFirefoxOrExplain
37
40
  });
38
41
  const external_fs_namespaceObject = require("fs");
39
42
  var external_fs_default = /*#__PURE__*/ __webpack_require__.n(external_fs_namespaceObject);
@@ -43,6 +46,79 @@ const external_os_namespaceObject = require("os");
43
46
  var external_os_default = /*#__PURE__*/ __webpack_require__.n(external_os_namespaceObject);
44
47
  const external_which_namespaceObject = require("which");
45
48
  var external_which_default = /*#__PURE__*/ __webpack_require__.n(external_which_namespaceObject);
49
+ const external_child_process_namespaceObject = require("child_process");
50
+ const external_node_fs_namespaceObject = require("node:fs");
51
+ var external_node_fs_default = /*#__PURE__*/ __webpack_require__.n(external_node_fs_namespaceObject);
52
+ const external_node_path_namespaceObject = require("node:path");
53
+ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
54
+ function resolveFromPuppeteerCache(deps) {
55
+ const f = (null == deps ? void 0 : deps.fs) ?? external_node_fs_default();
56
+ const env = (null == deps ? void 0 : deps.env) ?? process.env;
57
+ const platform = (null == deps ? void 0 : deps.platform) ?? process.platform;
58
+ try {
59
+ if ('darwin' === platform) {
60
+ const home = (null == deps ? void 0 : deps.homeDir) ?? env.HOME ?? '';
61
+ if (!home) return null;
62
+ const base = external_node_path_default().join(home, 'Library', 'Caches', 'puppeteer', 'firefox');
63
+ const dirs = listDirs(f, base).filter((d)=>d.startsWith('mac-') || d.startsWith('mac_arm-'));
64
+ const candidates = [];
65
+ for (const d of dirs){
66
+ candidates.push(external_node_path_default().join(base, d, 'Firefox.app', 'Contents', 'MacOS', 'firefox'));
67
+ candidates.push(external_node_path_default().join(base, d, 'Firefox Nightly.app', 'Contents', 'MacOS', 'firefox'));
68
+ }
69
+ return firstExisting(f, candidates);
70
+ }
71
+ if ('win32' === platform) {
72
+ const lad = (null == deps ? void 0 : deps.localAppData) ?? env.LOCALAPPDATA;
73
+ if (!lad) return null;
74
+ const base = external_node_path_default().join(lad, 'puppeteer', 'firefox');
75
+ const dirs = listDirs(f, base);
76
+ const preferred = [
77
+ ...dirs.filter((d)=>d.startsWith('win64-')),
78
+ ...dirs.filter((d)=>d.startsWith('win32-'))
79
+ ];
80
+ const candidates = [];
81
+ for (const d of preferred){
82
+ candidates.push(external_node_path_default().join(base, d, 'firefox.exe'));
83
+ candidates.push(external_node_path_default().join(base, d, 'firefox', 'firefox.exe'));
84
+ }
85
+ return firstExisting(f, candidates);
86
+ }
87
+ const xdg = env.XDG_CACHE_HOME;
88
+ const home = (null == deps ? void 0 : deps.homeDir) ?? env.HOME ?? '';
89
+ const cacheBase = xdg || (home ? external_node_path_default().join(home, '.cache') : void 0);
90
+ if (!cacheBase) return null;
91
+ const base = external_node_path_default().join(cacheBase, 'puppeteer', 'firefox');
92
+ const dirs = listDirs(f, base).filter((d)=>d.startsWith('linux-'));
93
+ const candidates = [];
94
+ for (const d of dirs){
95
+ candidates.push(external_node_path_default().join(base, d, 'firefox'));
96
+ candidates.push(external_node_path_default().join(base, d, 'firefox', 'firefox'));
97
+ }
98
+ return firstExisting(f, candidates);
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ function listDirs(f, dir) {
104
+ try {
105
+ return f.readdirSync(dir, {
106
+ withFileTypes: true
107
+ }).filter((e)=>{
108
+ if (!e) return false;
109
+ const v = e.isDirectory;
110
+ return 'function' == typeof v ? v.call(e) : Boolean(v);
111
+ }).map((e)=>e.name || String(e));
112
+ } catch {
113
+ return [];
114
+ }
115
+ }
116
+ function firstExisting(f, candidates) {
117
+ for (const c of candidates)try {
118
+ if (c && f.existsSync(c)) return c;
119
+ } catch {}
120
+ return null;
121
+ }
46
122
  function locateFirefox(allowFallbackOrDeps, depsMaybe) {
47
123
  const isBoolean = 'boolean' == typeof allowFallbackOrDeps;
48
124
  const allowFallback = isBoolean ? allowFallbackOrDeps : false;
@@ -53,10 +129,13 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
53
129
  const p = (null == deps ? void 0 : deps.path) ?? external_path_default();
54
130
  const env = (null == deps ? void 0 : deps.env) ?? process.env;
55
131
  const platform = (null == deps ? void 0 : deps.platform) ?? process.platform;
132
+ const override = env.FIREFOX_BINARY;
133
+ if (override && f.existsSync(override)) return override;
56
134
  const osx = 'darwin' === platform;
57
135
  const win = 'win32' === platform;
58
136
  const other = !osx && !win;
59
137
  if (other) {
138
+ var _process_env, _process_env1;
60
139
  const stable = [
61
140
  'firefox'
62
141
  ];
@@ -89,9 +168,20 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
89
168
  ];
90
169
  for (const linuxPath of linuxPaths)if (f.existsSync(linuxPath)) return linuxPath;
91
170
  }
171
+ if (!deps) {
172
+ const viaCache = resolveFromPuppeteerCache();
173
+ if (viaCache) return viaCache;
174
+ }
175
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env = process.env) ? void 0 : _process_env.VITEST) || void 0 !== (null == (_process_env1 = process.env) ? void 0 : _process_env1.JEST_WORKER_ID);
176
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
177
+ if (allowFallback && !deps && !skipCliProbe) {
178
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
179
+ if (viaCLI) return viaCLI;
180
+ }
92
181
  return null;
93
182
  }
94
183
  if (osx) {
184
+ var _process_env2, _process_env3;
95
185
  const appsAll = [
96
186
  {
97
187
  app: 'Firefox.app',
@@ -121,9 +211,20 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
121
211
  const userPath = `${userBase}/${app}/Contents/MacOS/${exec}`;
122
212
  if (f.existsSync(userPath)) return userPath;
123
213
  }
214
+ if (!deps) {
215
+ const viaCache = resolveFromPuppeteerCache();
216
+ if (viaCache) return viaCache;
217
+ }
218
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env2 = process.env) ? void 0 : _process_env2.VITEST) || void 0 !== (null == (_process_env3 = process.env) ? void 0 : _process_env3.JEST_WORKER_ID);
219
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
220
+ if (allowFallback && !deps && !skipCliProbe) {
221
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
222
+ if (viaCLI) return viaCLI;
223
+ }
124
224
  return null;
125
225
  }
126
226
  {
227
+ var _process_env4, _process_env5;
127
228
  const prefixes = [
128
229
  env.LOCALAPPDATA,
129
230
  env.PROGRAMFILES,
@@ -154,12 +255,167 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
154
255
  ];
155
256
  const defaultPaths = allowFallback ? defaultPathsAll : defaultPathsAll.slice(0, 2);
156
257
  for (const defaultPath of defaultPaths)if (f.existsSync(defaultPath)) return defaultPath;
258
+ if (!deps) {
259
+ const viaCache = resolveFromPuppeteerCache();
260
+ if (viaCache) return viaCache;
261
+ }
262
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env4 = process.env) ? void 0 : _process_env4.VITEST) || void 0 !== (null == (_process_env5 = process.env) ? void 0 : _process_env5.JEST_WORKER_ID);
263
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
264
+ if (allowFallback && !deps && !skipCliProbe) {
265
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
266
+ if (viaCLI) return viaCLI;
267
+ }
268
+ return null;
269
+ }
270
+ }
271
+ function getInstallGuidance() {
272
+ return "We couldn't find a Firefox browser on this machine.\n\nHere's the fastest way to get set up:\n\n1) Install Firefox via Puppeteer Browsers (recommended for CI/dev)\n npx @puppeteer/browsers install firefox@stable\n\nThen re-run your command — we will detect it automatically.\n\nAlternatively, install Firefox using your OS package manager and re-run.";
273
+ }
274
+ function locateFirefoxOrExplain(options) {
275
+ const allowFallback = 'boolean' == typeof options ? options : Boolean(null == options ? void 0 : options.allowFallback);
276
+ const found = locateFirefox(allowFallback) || locateFirefox(true);
277
+ if ('string' == typeof found && found) return found;
278
+ throw new Error(getInstallGuidance());
279
+ }
280
+ function getFirefoxVersion(bin, opts) {
281
+ if ('win32' === process.platform) {
282
+ try {
283
+ const psPath = bin.replace(/'/g, "''");
284
+ const pv = (0, external_child_process_namespaceObject.execFileSync)('powershell.exe', [
285
+ '-NoProfile',
286
+ '-Command',
287
+ `(Get-Item -LiteralPath '${psPath}').VersionInfo.ProductVersion`
288
+ ], {
289
+ encoding: 'utf8',
290
+ stdio: [
291
+ 'ignore',
292
+ 'pipe',
293
+ 'ignore'
294
+ ]
295
+ }).trim();
296
+ return normalizeVersion(pv);
297
+ } catch {}
298
+ if (null == opts ? void 0 : opts.allowExec) {
299
+ const v = tryExec(bin, [
300
+ '--version'
301
+ ]);
302
+ return normalizeVersion(v);
303
+ }
304
+ return null;
305
+ }
306
+ if ('darwin' === process.platform) {
307
+ try {
308
+ const contentsDir = external_path_default().dirname(external_path_default().dirname(bin));
309
+ const infoPlist = external_path_default().join(contentsDir, 'Info.plist');
310
+ if (external_fs_default().existsSync(infoPlist)) {
311
+ const xml = external_fs_default().readFileSync(infoPlist, 'utf8');
312
+ const v = parsePlistString(xml, 'CFBundleShortVersionString') || parsePlistString(xml, 'CFBundleVersion') || '';
313
+ return normalizeVersion(v);
314
+ }
315
+ } catch {}
316
+ if (null == opts ? void 0 : opts.allowExec) {
317
+ const v = tryExec(bin, [
318
+ '--version'
319
+ ]);
320
+ return normalizeVersion(v);
321
+ }
157
322
  return null;
158
323
  }
324
+ if (null == opts ? void 0 : opts.allowExec) {
325
+ const v = tryExec(bin, [
326
+ '--version'
327
+ ]);
328
+ return normalizeVersion(v);
329
+ }
330
+ return null;
331
+ }
332
+ function normalizeVersion(s) {
333
+ if (!s) return null;
334
+ const m = String(s).match(/(\d+(?:\.\d+){1,3})/);
335
+ return m ? m[1] : null;
336
+ }
337
+ function parsePlistString(xml, key) {
338
+ const re = new RegExp(`<key>${key}<\\/key>\\s*<string>([^<]+)<\\/string>`);
339
+ const m = xml.match(re);
340
+ return m ? m[1].trim() : null;
341
+ }
342
+ function tryExec(bin, args) {
343
+ try {
344
+ return (0, external_child_process_namespaceObject.execFileSync)(bin, args, {
345
+ encoding: 'utf8',
346
+ stdio: [
347
+ 'ignore',
348
+ 'pipe',
349
+ 'ignore'
350
+ ]
351
+ }).trim();
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+ function resolveFromPuppeteerBrowsersCLI() {
357
+ try {
358
+ const attempts = [
359
+ {
360
+ cmd: 'npx',
361
+ args: [
362
+ '-y',
363
+ '@puppeteer/browsers',
364
+ 'path',
365
+ 'firefox@stable'
366
+ ]
367
+ },
368
+ {
369
+ cmd: 'pnpm',
370
+ args: [
371
+ 'dlx',
372
+ '@puppeteer/browsers',
373
+ 'path',
374
+ 'firefox@stable'
375
+ ]
376
+ },
377
+ {
378
+ cmd: 'yarn',
379
+ args: [
380
+ 'dlx',
381
+ '@puppeteer/browsers',
382
+ 'path',
383
+ 'firefox@stable'
384
+ ]
385
+ },
386
+ {
387
+ cmd: 'bunx',
388
+ args: [
389
+ '@puppeteer/browsers',
390
+ 'path',
391
+ 'firefox@stable'
392
+ ]
393
+ }
394
+ ];
395
+ for (const { cmd, args } of attempts)try {
396
+ const out = (0, external_child_process_namespaceObject.execFileSync)(cmd, args, {
397
+ encoding: 'utf8',
398
+ stdio: [
399
+ 'ignore',
400
+ 'pipe',
401
+ 'ignore'
402
+ ],
403
+ timeout: 2000
404
+ }).trim();
405
+ if (out && external_fs_default().existsSync(out)) return out;
406
+ } catch {}
407
+ } catch {}
408
+ return null;
159
409
  }
160
410
  exports["default"] = __webpack_exports__["default"];
411
+ exports.getFirefoxVersion = __webpack_exports__.getFirefoxVersion;
412
+ exports.getInstallGuidance = __webpack_exports__.getInstallGuidance;
413
+ exports.locateFirefoxOrExplain = __webpack_exports__.locateFirefoxOrExplain;
161
414
  for(var __webpack_i__ in __webpack_exports__)if (-1 === [
162
- "default"
415
+ "default",
416
+ "getFirefoxVersion",
417
+ "getInstallGuidance",
418
+ "locateFirefoxOrExplain"
163
419
  ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
164
420
  Object.defineProperty(exports, '__esModule', {
165
421
  value: true
package/dist/index.d.ts CHANGED
@@ -17,3 +17,17 @@ export type Deps = {
17
17
  platform?: NodeJS.Platform;
18
18
  };
19
19
  export default function locateFirefox(allowFallbackOrDeps?: boolean | Deps, depsMaybe?: Deps): string | null;
20
+ export declare function getInstallGuidance(): string;
21
+ export declare function locateFirefoxOrExplain(options?: boolean | {
22
+ allowFallback?: boolean;
23
+ }): string;
24
+ /**
25
+ * Cross-platform Firefox version resolver.
26
+ * - Never executes the browser by default.
27
+ * - On Windows: reads PE metadata via PowerShell.
28
+ * - On macOS: reads Info.plist next to the binary.
29
+ * - On Linux/others: returns null unless opts.allowExec is true, then tries --version.
30
+ */
31
+ export declare function getFirefoxVersion(bin: string, opts?: {
32
+ allowExec?: boolean;
33
+ }): string | null;
package/dist/index.js CHANGED
@@ -2,6 +2,77 @@ import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs";
2
2
  import * as __WEBPACK_EXTERNAL_MODULE_path__ from "path";
3
3
  import * as __WEBPACK_EXTERNAL_MODULE_os__ from "os";
4
4
  import * as __WEBPACK_EXTERNAL_MODULE_which__ from "which";
5
+ import * as __WEBPACK_EXTERNAL_MODULE_child_process__ from "child_process";
6
+ import * as __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__ from "node:fs";
7
+ import * as __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__ from "node:path";
8
+ function resolveFromPuppeteerCache(deps) {
9
+ const f = (null == deps ? void 0 : deps.fs) ?? __WEBPACK_EXTERNAL_MODULE_node_fs_5ea92f0c__["default"];
10
+ const env = (null == deps ? void 0 : deps.env) ?? process.env;
11
+ const platform = (null == deps ? void 0 : deps.platform) ?? process.platform;
12
+ try {
13
+ if ('darwin' === platform) {
14
+ const home = (null == deps ? void 0 : deps.homeDir) ?? env.HOME ?? '';
15
+ if (!home) return null;
16
+ const base = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(home, 'Library', 'Caches', 'puppeteer', 'firefox');
17
+ const dirs = listDirs(f, base).filter((d)=>d.startsWith('mac-') || d.startsWith('mac_arm-'));
18
+ const candidates = [];
19
+ for (const d of dirs){
20
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'Firefox.app', 'Contents', 'MacOS', 'firefox'));
21
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'Firefox Nightly.app', 'Contents', 'MacOS', 'firefox'));
22
+ }
23
+ return firstExisting(f, candidates);
24
+ }
25
+ if ('win32' === platform) {
26
+ const lad = (null == deps ? void 0 : deps.localAppData) ?? env.LOCALAPPDATA;
27
+ if (!lad) return null;
28
+ const base = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(lad, 'puppeteer', 'firefox');
29
+ const dirs = listDirs(f, base);
30
+ const preferred = [
31
+ ...dirs.filter((d)=>d.startsWith('win64-')),
32
+ ...dirs.filter((d)=>d.startsWith('win32-'))
33
+ ];
34
+ const candidates = [];
35
+ for (const d of preferred){
36
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'firefox.exe'));
37
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'firefox', 'firefox.exe'));
38
+ }
39
+ return firstExisting(f, candidates);
40
+ }
41
+ const xdg = env.XDG_CACHE_HOME;
42
+ const home = (null == deps ? void 0 : deps.homeDir) ?? env.HOME ?? '';
43
+ const cacheBase = xdg || (home ? __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(home, '.cache') : void 0);
44
+ if (!cacheBase) return null;
45
+ const base = __WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(cacheBase, 'puppeteer', 'firefox');
46
+ const dirs = listDirs(f, base).filter((d)=>d.startsWith('linux-'));
47
+ const candidates = [];
48
+ for (const d of dirs){
49
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'firefox'));
50
+ candidates.push(__WEBPACK_EXTERNAL_MODULE_node_path_c5b9b54f__["default"].join(base, d, 'firefox', 'firefox'));
51
+ }
52
+ return firstExisting(f, candidates);
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ function listDirs(f, dir) {
58
+ try {
59
+ return f.readdirSync(dir, {
60
+ withFileTypes: true
61
+ }).filter((e)=>{
62
+ if (!e) return false;
63
+ const v = e.isDirectory;
64
+ return 'function' == typeof v ? v.call(e) : Boolean(v);
65
+ }).map((e)=>e.name || String(e));
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+ function firstExisting(f, candidates) {
71
+ for (const c of candidates)try {
72
+ if (c && f.existsSync(c)) return c;
73
+ } catch {}
74
+ return null;
75
+ }
5
76
  function locateFirefox(allowFallbackOrDeps, depsMaybe) {
6
77
  const isBoolean = 'boolean' == typeof allowFallbackOrDeps;
7
78
  const allowFallback = isBoolean ? allowFallbackOrDeps : false;
@@ -12,10 +83,13 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
12
83
  const p = (null == deps ? void 0 : deps.path) ?? __WEBPACK_EXTERNAL_MODULE_path__["default"];
13
84
  const env = (null == deps ? void 0 : deps.env) ?? process.env;
14
85
  const platform = (null == deps ? void 0 : deps.platform) ?? process.platform;
86
+ const override = env.FIREFOX_BINARY;
87
+ if (override && f.existsSync(override)) return override;
15
88
  const osx = 'darwin' === platform;
16
89
  const win = 'win32' === platform;
17
90
  const other = !osx && !win;
18
91
  if (other) {
92
+ var _process_env, _process_env1;
19
93
  const stable = [
20
94
  'firefox'
21
95
  ];
@@ -48,9 +122,20 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
48
122
  ];
49
123
  for (const linuxPath of linuxPaths)if (f.existsSync(linuxPath)) return linuxPath;
50
124
  }
125
+ if (!deps) {
126
+ const viaCache = resolveFromPuppeteerCache();
127
+ if (viaCache) return viaCache;
128
+ }
129
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env = process.env) ? void 0 : _process_env.VITEST) || void 0 !== (null == (_process_env1 = process.env) ? void 0 : _process_env1.JEST_WORKER_ID);
130
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
131
+ if (allowFallback && !deps && !skipCliProbe) {
132
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
133
+ if (viaCLI) return viaCLI;
134
+ }
51
135
  return null;
52
136
  }
53
137
  if (osx) {
138
+ var _process_env2, _process_env3;
54
139
  const appsAll = [
55
140
  {
56
141
  app: 'Firefox.app',
@@ -80,9 +165,20 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
80
165
  const userPath = `${userBase}/${app}/Contents/MacOS/${exec}`;
81
166
  if (f.existsSync(userPath)) return userPath;
82
167
  }
168
+ if (!deps) {
169
+ const viaCache = resolveFromPuppeteerCache();
170
+ if (viaCache) return viaCache;
171
+ }
172
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env2 = process.env) ? void 0 : _process_env2.VITEST) || void 0 !== (null == (_process_env3 = process.env) ? void 0 : _process_env3.JEST_WORKER_ID);
173
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
174
+ if (allowFallback && !deps && !skipCliProbe) {
175
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
176
+ if (viaCLI) return viaCLI;
177
+ }
83
178
  return null;
84
179
  }
85
180
  {
181
+ var _process_env4, _process_env5;
86
182
  const prefixes = [
87
183
  env.LOCALAPPDATA,
88
184
  env.PROGRAMFILES,
@@ -113,7 +209,156 @@ function locateFirefox(allowFallbackOrDeps, depsMaybe) {
113
209
  ];
114
210
  const defaultPaths = allowFallback ? defaultPathsAll : defaultPathsAll.slice(0, 2);
115
211
  for (const defaultPath of defaultPaths)if (f.existsSync(defaultPath)) return defaultPath;
212
+ if (!deps) {
213
+ const viaCache = resolveFromPuppeteerCache();
214
+ if (viaCache) return viaCache;
215
+ }
216
+ const isTestEnv = 'test' === process.env.NODE_ENV || void 0 !== (null == (_process_env4 = process.env) ? void 0 : _process_env4.VITEST) || void 0 !== (null == (_process_env5 = process.env) ? void 0 : _process_env5.JEST_WORKER_ID);
217
+ const skipCliProbe = isTestEnv && 'darwin' === process.platform;
218
+ if (allowFallback && !deps && !skipCliProbe) {
219
+ const viaCLI = resolveFromPuppeteerBrowsersCLI();
220
+ if (viaCLI) return viaCLI;
221
+ }
222
+ return null;
223
+ }
224
+ }
225
+ function getInstallGuidance() {
226
+ return "We couldn't find a Firefox browser on this machine.\n\nHere's the fastest way to get set up:\n\n1) Install Firefox via Puppeteer Browsers (recommended for CI/dev)\n npx @puppeteer/browsers install firefox@stable\n\nThen re-run your command — we will detect it automatically.\n\nAlternatively, install Firefox using your OS package manager and re-run.";
227
+ }
228
+ function locateFirefoxOrExplain(options) {
229
+ const allowFallback = 'boolean' == typeof options ? options : Boolean(null == options ? void 0 : options.allowFallback);
230
+ const found = locateFirefox(allowFallback) || locateFirefox(true);
231
+ if ('string' == typeof found && found) return found;
232
+ throw new Error(getInstallGuidance());
233
+ }
234
+ function getFirefoxVersion(bin, opts) {
235
+ if ('win32' === process.platform) {
236
+ try {
237
+ const psPath = bin.replace(/'/g, "''");
238
+ const pv = (0, __WEBPACK_EXTERNAL_MODULE_child_process__.execFileSync)('powershell.exe', [
239
+ '-NoProfile',
240
+ '-Command',
241
+ `(Get-Item -LiteralPath '${psPath}').VersionInfo.ProductVersion`
242
+ ], {
243
+ encoding: 'utf8',
244
+ stdio: [
245
+ 'ignore',
246
+ 'pipe',
247
+ 'ignore'
248
+ ]
249
+ }).trim();
250
+ return normalizeVersion(pv);
251
+ } catch {}
252
+ if (null == opts ? void 0 : opts.allowExec) {
253
+ const v = tryExec(bin, [
254
+ '--version'
255
+ ]);
256
+ return normalizeVersion(v);
257
+ }
258
+ return null;
259
+ }
260
+ if ('darwin' === process.platform) {
261
+ try {
262
+ const contentsDir = __WEBPACK_EXTERNAL_MODULE_path__["default"].dirname(__WEBPACK_EXTERNAL_MODULE_path__["default"].dirname(bin));
263
+ const infoPlist = __WEBPACK_EXTERNAL_MODULE_path__["default"].join(contentsDir, 'Info.plist');
264
+ if (__WEBPACK_EXTERNAL_MODULE_fs__["default"].existsSync(infoPlist)) {
265
+ const xml = __WEBPACK_EXTERNAL_MODULE_fs__["default"].readFileSync(infoPlist, 'utf8');
266
+ const v = parsePlistString(xml, 'CFBundleShortVersionString') || parsePlistString(xml, 'CFBundleVersion') || '';
267
+ return normalizeVersion(v);
268
+ }
269
+ } catch {}
270
+ if (null == opts ? void 0 : opts.allowExec) {
271
+ const v = tryExec(bin, [
272
+ '--version'
273
+ ]);
274
+ return normalizeVersion(v);
275
+ }
116
276
  return null;
117
277
  }
278
+ if (null == opts ? void 0 : opts.allowExec) {
279
+ const v = tryExec(bin, [
280
+ '--version'
281
+ ]);
282
+ return normalizeVersion(v);
283
+ }
284
+ return null;
285
+ }
286
+ function normalizeVersion(s) {
287
+ if (!s) return null;
288
+ const m = String(s).match(/(\d+(?:\.\d+){1,3})/);
289
+ return m ? m[1] : null;
290
+ }
291
+ function parsePlistString(xml, key) {
292
+ const re = new RegExp(`<key>${key}<\\/key>\\s*<string>([^<]+)<\\/string>`);
293
+ const m = xml.match(re);
294
+ return m ? m[1].trim() : null;
295
+ }
296
+ function tryExec(bin, args) {
297
+ try {
298
+ return (0, __WEBPACK_EXTERNAL_MODULE_child_process__.execFileSync)(bin, args, {
299
+ encoding: 'utf8',
300
+ stdio: [
301
+ 'ignore',
302
+ 'pipe',
303
+ 'ignore'
304
+ ]
305
+ }).trim();
306
+ } catch {
307
+ return null;
308
+ }
309
+ }
310
+ function resolveFromPuppeteerBrowsersCLI() {
311
+ try {
312
+ const attempts = [
313
+ {
314
+ cmd: 'npx',
315
+ args: [
316
+ '-y',
317
+ '@puppeteer/browsers',
318
+ 'path',
319
+ 'firefox@stable'
320
+ ]
321
+ },
322
+ {
323
+ cmd: 'pnpm',
324
+ args: [
325
+ 'dlx',
326
+ '@puppeteer/browsers',
327
+ 'path',
328
+ 'firefox@stable'
329
+ ]
330
+ },
331
+ {
332
+ cmd: 'yarn',
333
+ args: [
334
+ 'dlx',
335
+ '@puppeteer/browsers',
336
+ 'path',
337
+ 'firefox@stable'
338
+ ]
339
+ },
340
+ {
341
+ cmd: 'bunx',
342
+ args: [
343
+ '@puppeteer/browsers',
344
+ 'path',
345
+ 'firefox@stable'
346
+ ]
347
+ }
348
+ ];
349
+ for (const { cmd, args } of attempts)try {
350
+ const out = (0, __WEBPACK_EXTERNAL_MODULE_child_process__.execFileSync)(cmd, args, {
351
+ encoding: 'utf8',
352
+ stdio: [
353
+ 'ignore',
354
+ 'pipe',
355
+ 'ignore'
356
+ ],
357
+ timeout: 2000
358
+ }).trim();
359
+ if (out && __WEBPACK_EXTERNAL_MODULE_fs__["default"].existsSync(out)) return out;
360
+ } catch {}
361
+ } catch {}
362
+ return null;
118
363
  }
119
- export { locateFirefox as default };
364
+ export { locateFirefox as default, getFirefoxVersion, getInstallGuidance, locateFirefoxOrExplain };
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+ type FsLike = Pick<typeof fs, 'existsSync' | 'readdirSync'>;
3
+ type EnvLike = NodeJS.ProcessEnv;
4
+ export declare function resolveFromPuppeteerCache(deps?: {
5
+ fs?: FsLike;
6
+ env?: EnvLike;
7
+ platform?: NodeJS.Platform;
8
+ homeDir?: string;
9
+ localAppData?: string;
10
+ }): string | null;
11
+ export {};
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/cezaraugusto/firefox-location2.git"
6
6
  },
7
7
  "name": "firefox-location2",
8
- "version": "2.0.1",
8
+ "version": "2.1.0",
9
9
  "description": "Approximates the current location of the Firefox browser across platforms.",
10
10
  "homepage": "https://www.npmjs.com/package/firefox-location2",
11
11
  "type": "module",
@@ -26,13 +26,17 @@
26
26
  },
27
27
  "scripts": {
28
28
  "build": "rslib build",
29
- "check": "biome check --write",
29
+ "check": "pnpm dlx @biomejs/biome@1.9.4 check --write",
30
30
  "dev": "rslib build --watch",
31
- "format": "biome format --write",
32
- "test": "vitest run",
31
+ "format": "pnpm dlx @biomejs/biome@1.9.4 format --write",
32
+ "test": "vitest run --pool vmForks --poolOptions.vmForks.singleFork",
33
33
  "prepublishOnly": "npm run build",
34
34
  "publish:provenance": "np patch --no-tests --any-branch --yolo --message 'release: %s' --provenance"
35
35
  },
36
+ "engines": {
37
+ "node": ">=18 <23"
38
+ },
39
+ "engineStrict": true,
36
40
  "publishConfig": {
37
41
  "provenance": true
38
42
  },