-
-
Notifications
You must be signed in to change notification settings - Fork 107
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce new browser finder, including custom order and Firefox support
- Loading branch information
Showing
17 changed files
with
587 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) : '', | ||
}) | ||
} |
Oops, something went wrong.