Skip to content

Commit

Permalink
Merge pull request #595 from marp-team/prepare-new-browser-finder
Browse files Browse the repository at this point in the history
Internal: Prepare new browser manager interface and recreated finder
  • Loading branch information
yhatt committed Sep 23, 2024
2 parents e622942 + 5d3f750 commit c727e9c
Show file tree
Hide file tree
Showing 40 changed files with 1,889 additions and 104 deletions.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-url": "^8.0.2",
"@tsconfig/node14": "^14.1.2",
"@tsconfig/node20": "^20.1.4",
"@tsconfig/recommended": "^1.0.7",
"@types/cheerio": "^0.22.35",
"@types/dom-view-transitions": "^1.0.5",
"@types/express": "^4.17.21",
Expand All @@ -88,6 +89,7 @@
"@types/node": "~16.18.108",
"@types/pug": "^2.0.10",
"@types/supertest": "^6.0.2",
"@types/which": "^3.0.4",
"@types/ws": "^8.5.12",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.5.0",
Expand All @@ -100,6 +102,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 Expand Up @@ -147,6 +150,7 @@
"typed-emitter": "^2.1.0",
"typescript": "^5.6.2",
"vhtml": "^2.2.0",
"which": "^4.0.0",
"wrap-ansi": "^9.0.0",
"yauzl": "^3.1.3",
"zip-stream": "^6.0.1"
Expand Down
28 changes: 28 additions & 0 deletions src/browser/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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 {
static readonly kind: BrowserKind
static readonly protocol: BrowserProtocol

// ---

purpose: BrowserPurpose

constructor(opts: BrowserOptions) {
this.purpose = opts.purpose
}

get kind() {
return (this.constructor as typeof Browser).kind
}

get protocol() {
return (this.constructor as typeof Browser).protocol
}
}
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 {
static readonly kind = 'chrome' as const
static readonly 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 {
static readonly kind = 'chrome' as const
static readonly 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 {
static readonly kind = 'firefox' as const
static readonly 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 (await 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
})
}
48 changes: 48 additions & 0 deletions src/browser/finders/chrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 { findExecutableBinary, getPlatform } 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 = await (async () => {
switch (platform) {
case 'darwin':
return darwinFast()
case 'linux':
return linux()[0]
case 'win32':
return win32()[0]
case 'wsl1':
return wsl()[0]
}
return await fallback()
})()

if (installation) return chrome(installation)

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

const fallback = async () =>
await findExecutableBinary([
'google-chrome-stable',
'google-chrome',
'chrome', // FreeBSD Chromium
'chromium-browser',
'chromium',
])
94 changes: 94 additions & 0 deletions src/browser/finders/edge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 { findExecutable, getPlatform } from './utils'

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

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

const platform = await getPlatform()
const installation = await (async () => {
switch (platform) {
case 'darwin':
return await edgeFinderDarwin()
case 'linux':
return await edgeFinderLinux()
case 'win32':
return await edgeFinderWin32()
case 'wsl1':
return await edgeFinderWSL1()
}
return undefined
})()

if (installation) return edge(installation)

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

const edgeFinderDarwin = async () =>
await 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 = async () =>
await findExecutable([
'/opt/microsoft/msedge-canary/msedge',
'/opt/microsoft/msedge-dev/msedge',
'/opt/microsoft/msedge-beta/msedge',
'/opt/microsoft/msedge/msedge',
])

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

const suffixes = [
['Microsoft', 'Edge SxS', 'Application', 'msedge.exe'],
['Microsoft', 'Edge Dev', 'Application', 'msedge.exe'],
['Microsoft', 'Edge Beta', 'Application', 'msedge.exe'],
['Microsoft', 'Edge', 'Application', 'msedge.exe'],
]

for (const suffix of suffixes) {
for (const prefix of [localAppData, programFiles, programFilesX86]) {
if (prefix) paths.push(join(prefix, ...suffix))
}
}

return await findExecutable(paths)
}

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

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

0 comments on commit c727e9c

Please sign in to comment.