|
1 | 1 | import type { InstallOptions } from '@puppeteer/browsers'
|
2 | 2 | import type { ResolvedUserConfig, UnlighthouseTabs, UserConfig } from './types'
|
3 | 3 | import { Buffer } from 'node:buffer'
|
4 |
| -import { existsSync } from 'node:fs' |
| 4 | +import { existsSync, readFileSync } from 'node:fs' |
5 | 5 | import { homedir } from 'node:os'
|
6 |
| -import { join, resolve } from 'node:path' |
7 |
| -import { computeExecutablePath, install } from '@puppeteer/browsers' |
| 6 | +import path, { join, resolve } from 'node:path' |
| 7 | +import { computeExecutablePath, detectBrowserPlatform, install } from '@puppeteer/browsers' |
8 | 8 | import { Launcher } from 'chrome-launcher'
|
9 | 9 | import { createDefu, defu } from 'defu'
|
10 | 10 | import { pathExists } from 'fs-extra'
|
11 | 11 | import { pick } from 'lodash-es'
|
12 | 12 | import { resolve as resolveModule } from 'mlly'
|
13 |
| -import puppeteer from 'puppeteer-core' |
| 13 | +import puppeteer, { launch } from 'puppeteer-core' |
14 | 14 | import { PUPPETEER_REVISIONS } from 'puppeteer-core/lib/cjs/puppeteer/revisions.js'
|
15 | 15 | import { defaultConfig } from './constants'
|
16 | 16 | import { useLogger } from './logger'
|
@@ -185,28 +185,41 @@ export const resolveUserConfig: (userConfig: UserConfig) => Promise<ResolvedUser
|
185 | 185 | // we'll try and resolve their local chrome
|
186 | 186 | const chromePath = Launcher.getFirstInstallation()
|
187 | 187 | if (chromePath) {
|
188 |
| - logger.info(`Using system chrome located at: \`${chromePath}\`.`) |
| 188 | + logger.info(`Using system Chrome located at: \`${chromePath}\`.`) |
189 | 189 | // set default to puppeteer core
|
190 | 190 | config.puppeteerClusterOptions.puppeteer = puppeteer
|
191 | 191 | // point to our pre-installed chrome version
|
192 | 192 | config.puppeteerOptions.executablePath = chromePath
|
193 | 193 | foundChrome = true
|
194 | 194 | }
|
195 | 195 | }
|
| 196 | + if (foundChrome) { |
| 197 | + logger.debug('Testing system Chrome installation.') |
| 198 | + // mock the behavior of the custer so we can handle errors better |
| 199 | + const instance = await launch(config.puppeteerOptions).catch((e) => { |
| 200 | + logger.warn(`Failed to launch puppeteer instance using \`${config.puppeteerOptions?.executablePath}\`.`, e) |
| 201 | + foundChrome = false |
| 202 | + }) |
| 203 | + // let the cluster do the work |
| 204 | + if (instance) { |
| 205 | + await instance.close() |
| 206 | + } |
| 207 | + } |
196 | 208 | if (!foundChrome) {
|
197 | 209 | // if we can't find their local chrome, we just need to make sure they have puppeteer, this is a similar check
|
198 | 210 | // puppeteer-cluster will do, but we can provide a nicer error
|
199 | 211 | try {
|
200 | 212 | await resolveModule('puppeteer')
|
201 | 213 | foundChrome = true
|
202 |
| - logger.info('Using puppeteer dependency for chrome.') |
| 214 | + logger.info('Using puppeteer dependency for Chrome.') |
203 | 215 | }
|
204 | 216 | catch (e) {
|
205 | 217 | logger.debug('Puppeteer does not exist as a dependency.', e)
|
206 | 218 | }
|
207 | 219 | }
|
208 | 220 | if (config.chrome.useDownloadFallback && !foundChrome) {
|
209 | 221 | const browserOptions = {
|
| 222 | + installDeps: process.getuid?.() === 0, |
210 | 223 | cacheDir: config.chrome.downloadFallbackCacheDir,
|
211 | 224 | buildId: config.chrome.downloadFallbackVersion || PUPPETEER_REVISIONS.chrome,
|
212 | 225 | browser: 'chrome',
|
@@ -235,6 +248,37 @@ export const resolveUserConfig: (userConfig: UserConfig) => Promise<ResolvedUser
|
235 | 248 | if (!foundChrome)
|
236 | 249 | throw new Error('Failed to find chrome. Please ensure you have a valid chrome installed.')
|
237 | 250 |
|
| 251 | + // mock the behavior of the custer so we can handle errors better |
| 252 | + const instance = await launch(config.puppeteerOptions).catch((e) => { |
| 253 | + if (detectBrowserPlatform() === 'linux' && e.toString().includes('error while loading shared libraries')) { |
| 254 | + const depsPath = path.join( |
| 255 | + path.dirname(config.puppeteerOptions.executablePath), |
| 256 | + 'deb.deps', |
| 257 | + ) |
| 258 | + if (existsSync(depsPath)) { |
| 259 | + const data = readFileSync(depsPath, 'utf-8').trim().split('\n').map(d => `"${d}"`).join(',') |
| 260 | + logger.warn('Failed to start puppeteer, you may be missing dependencies.') |
| 261 | + logger.log('') |
| 262 | + const command = [ |
| 263 | + 'sudo', |
| 264 | + 'apt-get', |
| 265 | + 'satisfy', |
| 266 | + '-y', |
| 267 | + data, |
| 268 | + '--no-install-recommends', |
| 269 | + ].join(' ') |
| 270 | + // eslint-disable-next-line no-console |
| 271 | + console.log(`\x1B[96m%s\x1B[0m`, `Run the following command:\n${command}`) |
| 272 | + logger.log('') |
| 273 | + } |
| 274 | + } |
| 275 | + throw e |
| 276 | + }) |
| 277 | + // let the cluster do the work |
| 278 | + if (instance) { |
| 279 | + await instance.close() |
| 280 | + } |
| 281 | + |
238 | 282 | // resolve the output path
|
239 | 283 | config.outputPath = resolve(config.root!, config.outputPath!)
|
240 | 284 | return config as ResolvedUserConfig
|
|
0 commit comments