Skip to content

Commit b57ee3f

Browse files
authored
fix: on-demand chromium for dev / prerender (#150)
1 parent 5fde697 commit b57ee3f

File tree

10 files changed

+117
-98
lines changed

10 files changed

+117
-98
lines changed

src/build/ensureChromium.ts

-21
This file was deleted.

src/compatibility.ts

+3
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ export function applyNitroPresetCompatibility(nitroConfig: NitroConfig, options:
136136
let binding = options.compatibility?.[key]
137137
if (typeof binding === 'undefined')
138138
binding = compatibility[key]
139+
// TODO avoid breaking changes, remove this in v4
140+
if (key === 'chromium' && binding === 'node')
141+
binding = 'playwright'
139142
// @ts-expect-error untyped
140143
resolvedCompatibility[key] = binding
141144
return {

src/module.ts

+29-33
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ import {
1616
} from '@nuxt/kit'
1717
import type { SatoriOptions } from 'satori'
1818
import { installNuxtSiteConfig } from 'nuxt-site-config-kit'
19-
import { isCI, isDevelopment } from 'std-env'
19+
import { isDevelopment } from 'std-env'
2020
import { hash } from 'ohash'
2121
import { relative } from 'pathe'
2222
import type { ResvgRenderOptions } from '@resvg/resvg-js'
2323
import type { SharpOptions } from 'sharp'
2424
import { defu } from 'defu'
25-
import { Launcher } from 'chrome-launcher'
2625
import { readPackageJSON } from 'pkg-types'
2726
import type {
2827
CompatibilityFlagEnvOverrides,
@@ -31,6 +30,7 @@ import type {
3130
OgImageComponent,
3231
OgImageOptions,
3332
OgImageRuntimeConfig,
33+
RuntimeCompatibilitySchema,
3434
} from './runtime/types'
3535
import {
3636
ensureDependencies,
@@ -43,9 +43,8 @@ import { setupDevHandler } from './build/dev'
4343
import { setupGenerateHandler } from './build/generate'
4444
import { setupPrerenderHandler } from './build/prerender'
4545
import { setupBuildHandler } from './build/build'
46-
import { ensureChromium } from './build/ensureChromium'
46+
import { checkLocalChrome, checkPlaywrightDependency, isUndefinedOrTruthy } from './util'
4747
import { normaliseFontInput } from './runtime/utils.pure'
48-
import { isUndefinedOrTruthy } from './util'
4948

5049
export interface ModuleOptions {
5150
/**
@@ -201,38 +200,35 @@ export default defineNuxtModule<ModuleOptions>({
201200
})
202201
}
203202

204-
// we can check if we have chrome and disable chromium if not
205-
let hasChromeLocally = false
206-
try {
207-
hasChromeLocally = !!Launcher.getFirstInstallation()
203+
// in dev and prerender we rely on local chrome or playwright dependency
204+
// for runtime we need playwright dependency
205+
const hasChromeLocally = checkLocalChrome()
206+
const hasPlaywrightDependency = await checkPlaywrightDependency()
207+
const chromeCompatibilityFlags = {
208+
prerender: config.compatibility?.prerender?.chromium,
209+
dev: config.compatibility?.dev?.chromium,
210+
runtime: config.compatibility?.runtime?.chromium,
208211
}
209-
catch {}
210-
const isUndefinedOrTruthy = (v?: any) => typeof v === 'undefined' || v !== false
211-
if (isUndefinedOrTruthy(config.compatibility?.prerender?.chromium) && isUndefinedOrTruthy(config.compatibility?.runtime?.chromium)) {
212-
if (isCI)
213-
await ensureChromium(logger)
214-
215-
const hasPlaywrightDependency = !!(await tryResolveModule('playwright'))
216-
if (hasChromeLocally) {
217-
// we have chrome locally so we can enable chromium in dev
218-
config.compatibility = defu(config.compatibility, <CompatibilityFlagEnvOverrides>{
219-
runtime: { chromium: false },
220-
dev: { chromium: 'node' },
221-
prerender: { chromium: 'node' },
222-
})
223-
}
224-
else if (hasPlaywrightDependency && targetCompatibility.chromium) {
225-
// need to disable chromium in all environments
226-
config.compatibility = defu(config.compatibility, <CompatibilityFlagEnvOverrides>{
227-
runtime: { chromium: 'node' },
228-
dev: { chromium: 'node' },
229-
prerender: { chromium: 'node' },
230-
})
231-
}
212+
const chromiumBinding: Record<string, RuntimeCompatibilitySchema['chromium'] | null> = {
213+
dev: null,
214+
prerender: null,
215+
runtime: null,
232216
}
233-
else if (!hasChromeLocally && nuxt.options.dev && config.compatibility?.dev?.chromium === 'node') {
234-
await ensureChromium(logger)
217+
if (nuxt.options.dev) {
218+
if (isUndefinedOrTruthy(chromeCompatibilityFlags.dev))
219+
chromiumBinding.dev = hasChromeLocally ? 'chrome-launcher' : hasPlaywrightDependency ? 'playwright' : 'on-demand'
235220
}
221+
else {
222+
if (isUndefinedOrTruthy(chromeCompatibilityFlags.prerender))
223+
chromiumBinding.prerender = hasChromeLocally ? 'chrome-launcher' : hasPlaywrightDependency ? 'playwright' : 'on-demand'
224+
if (isUndefinedOrTruthy(chromeCompatibilityFlags.runtime))
225+
chromiumBinding.runtime = hasPlaywrightDependency ? 'playwright' : null
226+
}
227+
config.compatibility = defu(config.compatibility, <CompatibilityFlagEnvOverrides>{
228+
runtime: { chromium: chromiumBinding.runtime },
229+
dev: { chromium: chromiumBinding.dev },
230+
prerender: { chromium: chromiumBinding.prerender },
231+
})
236232

237233
// let's check we can access resvg
238234
await import('@resvg/resvg-js')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { Browser } from 'playwright-core'
2+
import playwrightCore from 'playwright-core'
3+
import { Launcher } from 'chrome-launcher'
4+
5+
const chromePath = Launcher.getFirstInstallation()
6+
7+
export async function createBrowser(): Promise<Browser | void> {
8+
return playwrightCore.chromium.launch({
9+
headless: true,
10+
executablePath: chromePath,
11+
})
12+
}

src/runtime/core/bindings/chromium/node.ts

-42
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import type { Browser } from 'playwright-core'
2+
import { execa } from 'execa'
3+
import terminate from 'terminate'
4+
import { createConsola } from 'consola'
5+
import playwrightCore from 'playwright-core'
6+
7+
let installedChromium = false
8+
let installChromiumPromise: Promise<void>
9+
10+
export async function createBrowser(): Promise<Browser | void> {
11+
if (installChromiumPromise)
12+
await installChromiumPromise
13+
if (!installedChromium) {
14+
installChromiumPromise = new Promise<void>((_resolve) => {
15+
const logger = createConsola().withTag('Nuxt OG Image')
16+
// avoid problems by installing playwright
17+
logger.info('Installing Chromium install for og:image generation...')
18+
const installChromeProcess = execa('npx', ['playwright', 'install', 'chromium'], {
19+
stdio: 'inherit',
20+
})
21+
22+
installChromeProcess.stderr?.pipe(process.stderr)
23+
new Promise((resolve) => {
24+
installChromeProcess.on('exit', (e) => {
25+
if (e !== 0)
26+
logger.error('Failed to install Playwright dependency for og:image generation. Trying anyway...')
27+
resolve(true)
28+
})
29+
}).then(() => {
30+
installChromeProcess.pid && terminate(installChromeProcess.pid)
31+
logger.info('Installed Chromium install for og:image generation.')
32+
_resolve()
33+
})
34+
}).then(() => {
35+
installedChromium = true
36+
})
37+
}
38+
39+
return await playwrightCore.chromium.launch({
40+
headless: true,
41+
})
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Browser } from 'playwright-core'
2+
import playwright from 'playwright'
3+
4+
export async function createBrowser(): Promise<Browser | void> {
5+
return await playwright.chromium.launch({
6+
headless: true,
7+
})
8+
}

src/runtime/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export interface ResolvedFontConfig extends FontConfig { cacheKey: string, data?
129129
export type InputFontConfig = (`${string}:${number}` | string | FontConfig)
130130

131131
export interface RuntimeCompatibilitySchema {
132-
chromium: 'node' | false
132+
chromium: 'chrome-launcher' | 'on-demand' | 'playwright' | false
133133
['css-inline']: 'node' | 'wasm' | 'wasm-fs' | false
134134
resvg: 'node' | 'wasm' | 'wasm-fs' | false
135135
satori: 'node' | 'wasm' | 'wasm-fs' | false

src/util.ts

+21
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1+
import { Launcher } from 'chrome-launcher'
2+
import { tryResolveModule } from '@nuxt/kit'
3+
import { isCI } from 'std-env'
4+
15
export const isUndefinedOrTruthy = (v?: any) => typeof v === 'undefined' || v !== false
6+
7+
export function checkLocalChrome() {
8+
// quick path for CI
9+
if (isCI)
10+
return false
11+
12+
let hasChromeLocally = false
13+
try {
14+
hasChromeLocally = !!Launcher.getFirstInstallation()
15+
}
16+
catch {}
17+
return hasChromeLocally
18+
}
19+
20+
export async function checkPlaywrightDependency() {
21+
return !!(await tryResolveModule('playwright'))
22+
}

test/integration/endpoints/debug.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ describe('debug', () => {
1717
expect(debug).toMatchInlineSnapshot(`
1818
{
1919
"compatibility": {
20-
"chromium": false,
20+
"chromium": "playwright",
2121
"css-inline": "node",
2222
"resvg": "node",
2323
"satori": "node",

0 commit comments

Comments
 (0)