Skip to content

Commit

Permalink
Introduce new browser finder, including custom order and Firefox support
Browse files Browse the repository at this point in the history
  • Loading branch information
yhatt committed Sep 21, 2024
1 parent f824afe commit d07ee91
Show file tree
Hide file tree
Showing 17 changed files with 587 additions and 12 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"chrome-launcher": "^1.1.2",
"css.escape": "^1.5.1",
"cssnano": "^7.0.6",
"debug": "^4.3.7",
"eslint": "^9.10.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-exports": "^1.0.0-beta.5",
Expand Down
17 changes: 17 additions & 0 deletions src/browser/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type BrowserKind = 'chrome' | 'firefox'
export type BrowserProtocol = 'webdriver-bidi' | 'cdp'
export type BrowserPurpose = 'convert' | 'preview'

export interface BrowserOptions {
purpose: BrowserPurpose
}

export abstract class Browser {
abstract kind: BrowserKind
abstract protocol: BrowserProtocol
purpose: BrowserPurpose

constructor(opts: BrowserOptions) {
this.purpose = opts.purpose
}
}
6 changes: 6 additions & 0 deletions src/browser/browsers/chrome-cdp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Browser } from '../browser'

export class ChromeCdpBrowser extends Browser {
kind = 'chrome' as const
protocol = 'cdp' as const
}
6 changes: 6 additions & 0 deletions src/browser/browsers/chrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Browser } from '../browser'

export class ChromeBrowser extends Browser {
kind = 'chrome' as const
protocol = 'webdriver-bidi' as const
}
6 changes: 6 additions & 0 deletions src/browser/browsers/firefox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Browser } from '../browser'

export class FirefoxBrowser extends Browser {
kind = 'firefox' as const
protocol = 'webdriver-bidi' as const
}
104 changes: 104 additions & 0 deletions src/browser/finder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { CLIError, CLIErrorCode } from '../error'
import { debugBrowserFinder } from '../utils/debug'
import type { Browser } from './browser'
import { chromeFinder as chrome } from './finders/chrome'
import { edgeFinder as edge } from './finders/edge'
import { firefoxFinder as firefox } from './finders/firefox'
import { isExecutable, normalizeDarwinAppPath } from './finders/utils'

export interface BrowserFinderResult {
path: string
acceptedBrowsers: (typeof Browser)[]
}

export interface BrowserFinderOptions {
preferredPath?: string
}

export type BrowserFinder = (
opts: BrowserFinderOptions
) => Promise<BrowserFinderResult>

const finderMap = { chrome, edge, firefox } as const

export const autoFinders = ['chrome', 'edge', 'firefox'] as const

export const findBrowser = async (
finders: readonly (keyof typeof finderMap)[] = autoFinders,
opts: BrowserFinderOptions = {}
) => {
const finderCount = finders.length
const normalizedOpts = {
preferredPath: await (async () => {
if (opts.preferredPath) {
const normalized = await normalizeDarwinAppPath(opts.preferredPath)
if (isExecutable(normalized)) return normalized
}
return undefined
})(),
}

if (finderCount === 0) {
debugBrowserFinder('No browser finder specified.')

if (normalizedOpts.preferredPath) {
debugBrowserFinder(
'Use preferred path as Chrome: %s',
normalizedOpts.preferredPath
)

return await chrome(normalizedOpts)
}

throw new CLIError(
'No suitable browser found.',
CLIErrorCode.NOT_FOUND_BROWSER
)
}

debugBrowserFinder(
`Start finding browser from ${finders.join(', ')} (%o)`,
normalizedOpts
)

return new Promise<BrowserFinderResult>((res, rej) => {
const results = Array<BrowserFinderResult>(finderCount)
const resolved = Array<boolean | undefined>(finderCount)

finders.forEach((finderName, index) => {
const finder = finderMap[finderName]

finder(normalizedOpts)
.then((ret) => {
debugBrowserFinder(`Found ${finderName}: %o`, ret)
results[index] = ret
resolved[index] = true
})
.catch((e) => {
debugBrowserFinder(`Finder ${finderName} was failed: %o`, e)
resolved[index] = false
})
.finally(() => {
let target: number | undefined

for (let i = finderCount - 1; i >= 0; i -= 1) {
if (resolved[i] !== false) target = i
}

if (target === undefined) {
rej(
new CLIError(
`No suitable browser found. Please ensure one of the following browsers is installed: ${finders.join(', ')}`,
CLIErrorCode.NOT_FOUND_BROWSER
)
)
} else if (resolved[target]) {
res(results[target])
}
})
})
}).then((result) => {
debugBrowserFinder('Use browser: %o', result)
return result
})
}
58 changes: 58 additions & 0 deletions src/browser/finders/chrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
darwinFast,
linux,
win32,
wsl,
} from 'chrome-launcher/dist/chrome-finder'
import { error, CLIErrorCode } from '../../error'
import { ChromeBrowser } from '../browsers/chrome'
import { ChromeCdpBrowser } from '../browsers/chrome-cdp'
import type { BrowserFinder, BrowserFinderResult } from '../finder'
import { getPlatform, isExecutable, which } from './utils'

const chrome = (path: string): BrowserFinderResult => ({
path,
acceptedBrowsers: [ChromeBrowser, ChromeCdpBrowser],
})

export const chromeFinder: BrowserFinder = async ({ preferredPath } = {}) => {
if (preferredPath) return chrome(preferredPath)

const platform = await getPlatform()
const installation = (() => {
switch (platform) {
case 'darwin':
return darwinFast()
case 'linux':
return linux()[0]
case 'win32':
return win32()[0]
// CI cannot test against WSL environment
/* c8 ignore start */
case 'wsl1':
return wsl()[0]
}
return fallback()
/* c8 ignore stop */
})()

if (installation) return chrome(installation)

error('Chrome browser could not be found.', CLIErrorCode.NOT_FOUND_BROWSER)
}

const fallbackExecutableNames = [
'google-chrome-stable',
'google-chrome',
'chrome', // FreeBSD Chromium
'chromium-browser',
'chromium',
] as const

const fallback = () => {
for (const executableName of fallbackExecutableNames) {
const executablePath = which(executableName)
if (executablePath && isExecutable(executablePath)) return executablePath
}
return undefined
}
95 changes: 95 additions & 0 deletions src/browser/finders/edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import path from 'node:path'
import { error, CLIErrorCode } from '../../error'
import {
resolveWSLPathToGuestSync,
resolveWindowsEnvSync,
} from '../../utils/wsl'
import { ChromeBrowser } from '../browsers/chrome'
import { ChromeCdpBrowser } from '../browsers/chrome-cdp'
import type { BrowserFinder, BrowserFinderResult } from '../finder'
import { getPlatform, isExecutable } from './utils'

const edge = (path: string): BrowserFinderResult => ({
path,
acceptedBrowsers: [ChromeBrowser, ChromeCdpBrowser],
})

const findExecutable = (paths: string[]): string | undefined =>
paths.find((p) => isExecutable(p))

export const edgeFinder: BrowserFinder = async ({ preferredPath } = {}) => {
if (preferredPath) return edge(preferredPath)

const platform = await getPlatform()
const installation = (() => {
switch (platform) {
case 'darwin':
return edgeFinderDarwin()
case 'linux':
return edgeFinderLinux()
case 'win32':
return edgeFinderWin32()
// CI cannot test against WSL environment
/* c8 ignore start */
case 'wsl1':
return edgeFinderWSL1()
}
return undefined
/* c8 ignore stop */
})()

if (installation) return edge(installation)

error('Edge browser could not be found.', CLIErrorCode.NOT_FOUND_BROWSER)
}

const edgeFinderDarwin = () =>
findExecutable([
'/Applications/Microsoft Edge Canary.app/Contents/MacOS/Microsoft Edge Canary',
'/Applications/Microsoft Edge Dev.app/Contents/MacOS/Microsoft Edge Dev',
'/Applications/Microsoft Edge Beta.app/Contents/MacOS/Microsoft Edge Beta',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
])

const edgeFinderLinux = () =>
findExecutable([
'/opt/microsoft/msedge-canary/msedge',
'/opt/microsoft/msedge-dev/msedge',
'/opt/microsoft/msedge-beta/msedge',
'/opt/microsoft/msedge/msedge',
])

const edgeFinderWin32 = ({
programFiles = process.env.PROGRAMFILES,
programFilesX86 = process.env['PROGRAMFILES(X86)'],
localAppData = process.env.LOCALAPPDATA,
}: {
programFiles?: string
programFilesX86?: string
localAppData?: string
} = {}): string | undefined => {
const paths: string[] = []

for (const prefix of [programFiles, programFilesX86, localAppData]) {
if (!prefix) continue

paths.push(
path.join(prefix, 'Microsoft', 'Edge SxS', 'Application', 'msedge.exe'),
path.join(prefix, 'Microsoft', 'Edge Dev', 'Application', 'msedge.exe'),
path.join(prefix, 'Microsoft', 'Edge Beta', 'Application', 'msedge.exe'),
path.join(prefix, 'Microsoft', 'Edge', 'Application', 'msedge.exe')
)
}

return findExecutable(paths)
}

const edgeFinderWSL1 = () => {
const localAppData = resolveWindowsEnvSync('LOCALAPPDATA')

return edgeFinderWin32({
programFiles: '/mnt/c/Program Files',
programFilesX86: '/mnt/c/Program Files (x86)',
localAppData: localAppData ? resolveWSLPathToGuestSync(localAppData) : '',
})
}
Loading

0 comments on commit d07ee91

Please sign in to comment.