-
-
Notifications
You must be signed in to change notification settings - Fork 109
/
puppeteer.ts
186 lines (158 loc) · 5.89 KB
/
puppeteer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import fs from 'fs'
import os from 'os'
import path from 'path'
import { launch } from 'puppeteer-core'
import macDockIcon from '../assets/mac-dock-icon.png'
import { warn } from '../cli'
import { CLIErrorCode, error, isError } from '../error'
import { findChromeInstallation } from './chrome-finder'
import { isInsideContainer } from './container'
import { findEdgeInstallation } from './edge-finder'
import { isWSL, resolveWindowsEnv } from './wsl'
let executablePath: string | undefined | false = false
let wslTmp: string | undefined
export const enableHeadless = (): true | 'new' =>
process.env.PUPPETEER_HEADLESS_MODE?.toLowerCase() === 'new' ? 'new' : true
const isShebang = (path: string) => {
let fd: number | null = null
try {
fd = fs.openSync(path, 'r')
const shebangBuffer = Buffer.alloc(2)
fs.readSync(fd, shebangBuffer, 0, 2, 0)
if (shebangBuffer[0] === 0x23 && shebangBuffer[1] === 0x21) return true
} catch (e: unknown) {
// no ops
} finally {
if (fd !== null) fs.closeSync(fd)
}
return false
}
const isSnapBrowser = (executablePath: string | undefined) => {
if (process.platform === 'linux' && executablePath) {
// Snapd binary
if (executablePath.startsWith('/snap/')) return true
// Check the content of shebang script (for alias script installed by apt)
if (isShebang(executablePath)) {
const scriptContent = fs.readFileSync(executablePath, 'utf8')
if (scriptContent.includes('/snap/')) return true
}
}
return false
}
export const generatePuppeteerDataDirPath = async (
name: string,
{ wslHost }: { wslHost?: boolean } = {}
): Promise<string> => {
const dataDir = await (async () => {
if ((await isWSL()) && wslHost) {
// In WSL environment, Marp CLI may use Chrome on Windows. If Chrome has
// located in host OS (Windows), we have to specify Windows path.
if (wslTmp === undefined) wslTmp = await resolveWindowsEnv('TMP')
if (wslTmp !== undefined) return path.win32.resolve(wslTmp, name)
}
return path.resolve(os.tmpdir(), name)
})()
// Ensure the data directory is created
try {
await fs.promises.mkdir(dataDir, { recursive: true })
} catch (e: unknown) {
if (isError(e) && e.code !== 'EEXIST') throw e
}
return dataDir
}
export const generatePuppeteerLaunchArgs = async () => {
const args = new Set<string>(['--export-tagged-pdf', '--test-type'])
// Docker environment and WSL environment always need to disable sandbox
if (process.env.CHROME_NO_SANDBOX || isInsideContainer() || (await isWSL()))
args.add('--no-sandbox')
args.add('--enable-blink-features=ViewTransition')
// LayoutNG Printing
if (process.env.CHROME_LAYOUTNG_PRINTING)
args.add(
'--enable-blink-features=LayoutNGPrinting,LayoutNGTableFragmentation'
)
// Resolve Chrome path to execute
if (executablePath === false) {
let findChromeError: Error | undefined
try {
executablePath = await findChromeInstallation()
} catch (e: unknown) {
if (isError(e)) findChromeError = e
}
if (!executablePath) {
// Find Edge as fallback (Edge has pre-installed to almost Windows)
executablePath = await findEdgeInstallation()
if (!executablePath) {
if (findChromeError) warn(findChromeError.message)
// https://github.com/marp-team/marp-cli/issues/475
// https://github.com/GoogleChrome/chrome-launcher/issues/278
const chromiumResolvable = process.platform === 'linux'
error(
`You have to install Google Chrome${
chromiumResolvable ? ', Chromium,' : ''
} or Microsoft Edge to convert slide deck with current options.`,
CLIErrorCode.NOT_FOUND_CHROMIUM
)
}
}
}
return {
executablePath,
args: [...args],
pipe: !(isWSL() || isSnapBrowser(executablePath)),
headless: enableHeadless(),
// Workaround to avoid force-extensions policy for Chrome enterprise (SET CHROME_ENABLE_EXTENSIONS=1)
// https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-windows
//
// @see https://github.com/marp-team/marp-cli/issues/231
ignoreDefaultArgs: process.env.CHROME_ENABLE_EXTENSIONS
? ['--disable-extensions']
: undefined,
}
}
export const launchPuppeteer = async (
...[options]: Parameters<typeof launch>
) => {
try {
const browser = await launch(options)
// Set Marp icon asynchrnously (only for macOS)
/* c8 ignore start */
browser
?.target()
.createCDPSession()
.then((session) => {
session
.send('Browser.setDockTile', { image: macDockIcon.slice(22) })
.catch(() => {
// No ops
})
})
/* c8 ignore stop */
return browser
} catch (e: unknown) {
if (isError(e)) {
// Retry to launch Chromium with WebSocket connection instead of pipe if failed to connect to Chromium
// https://github.com/puppeteer/puppeteer/issues/6258
if (options?.pipe && e.message.includes('Target.setDiscoverTargets')) {
return await launch({ ...options, pipe: false })
}
// Warning when tried to spawn the snap chromium within the snapd container
// (e.g. Terminal in VS Code installed by snap + chromium installed by apt)
// It would be resolved by https://github.com/snapcore/snapd/pull/10029 but there is no progress :(
if (
options?.executablePath &&
isSnapBrowser(options.executablePath) &&
/^need to run as root or suid$/im.test(e.message)
) {
error(
'Marp CLI has detected trying to spawn Chromium browser installed by snap, from the confined environment like another snap app. At least either of Chrome/Chromium or the shell environment must be non snap app.',
CLIErrorCode.CANNOT_SPAWN_SNAP_CHROMIUM
)
}
}
throw e
}
}
export const resetExecutablePath = () => {
executablePath = false
}