@jsenv/core 24.3.3 → 24.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/core",
3
- "version": "24.3.3",
3
+ "version": "24.4.0",
4
4
  "description": "Tool to develop, test and build js projects",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,11 +36,16 @@ export const executeTestPlan = async ({
36
36
  cooldownBetweenExecutions,
37
37
 
38
38
  maxExecutionsInParallel,
39
-
40
39
  completedExecutionLogAbbreviation = false,
41
40
  completedExecutionLogMerging = false,
42
41
  logSummary = true,
43
42
  updateProcessExitCode = true,
43
+ // stopAfterExecute: true to ensure runtime is stopped once executed
44
+ // because we have what we wants: execution is completed and
45
+ // we have associated coverage and capturedConsole
46
+ // passsing false means all node process and browsers launched stays opened
47
+ // (can eventually be used for debug)
48
+ stopAfterExecute = true,
44
49
 
45
50
  coverage = process.argv.includes("--cover") ||
46
51
  process.argv.includes("--coverage"),
@@ -145,6 +150,7 @@ export const executeTestPlan = async ({
145
150
 
146
151
  defaultMsAllocatedPerExecution,
147
152
  maxExecutionsInParallel,
153
+ stopAfterExecute,
148
154
  cooldownBetweenExecutions,
149
155
  completedExecutionLogMerging,
150
156
  completedExecutionLogAbbreviation,
@@ -0,0 +1,314 @@
1
+ // https://github.com/microsoft/playwright/blob/master/docs/api.md
2
+
3
+ import { createDetailedMessage } from "@jsenv/logger"
4
+ import {
5
+ Abort,
6
+ createCallbackListNotifiedOnce,
7
+ createCallbackList,
8
+ raceProcessTeardownEvents,
9
+ } from "@jsenv/abort"
10
+ import { memoize } from "@jsenv/filesystem"
11
+
12
+ import { trackPageToNotify } from "./trackPageToNotify.js"
13
+ import { executeHtmlFile } from "./executeHtmlFile.js"
14
+
15
+ export const createRuntimeFromPlaywright = ({
16
+ browserName,
17
+ browserVersion,
18
+ coveragePlaywrightAPIAvailable = false,
19
+ ignoreErrorHook = () => false,
20
+ transformErrorHook = (error) => error,
21
+ tab = false,
22
+ }) => {
23
+ const runtime = {
24
+ name: browserName,
25
+ version: browserVersion,
26
+ }
27
+
28
+ let browserAndContextPromise
29
+ runtime.launch = async ({
30
+ signal = new AbortController().signal,
31
+ executablePath,
32
+ browserServerLogLevel,
33
+
34
+ projectDirectoryUrl,
35
+ compileServerOrigin,
36
+ outDirectoryRelativeUrl,
37
+
38
+ collectPerformance,
39
+ measurePerformance,
40
+ collectCoverage,
41
+ coverageIgnorePredicate,
42
+ coverageForceIstanbul,
43
+
44
+ headless = true,
45
+ stopOnExit = true,
46
+ ignoreHTTPSErrors = true,
47
+ stopAfterAllExecutionCallbackList,
48
+ }) => {
49
+ const stopCallbackList = createCallbackListNotifiedOnce()
50
+ const stoppedCallbackList = createCallbackListNotifiedOnce()
51
+ const errorCallbackList = createCallbackList()
52
+ const outputCallbackList = createCallbackList()
53
+
54
+ const stop = memoize(async (reason) => {
55
+ await stopCallbackList.notify({ reason })
56
+ stoppedCallbackList.notify({ reason })
57
+ return { graceful: false }
58
+ })
59
+ const closeBrowser = async () => {
60
+ const { browser } = await browserAndContextPromise
61
+ browserAndContextPromise = null
62
+ await stopBrowser(browser)
63
+ }
64
+
65
+ if (
66
+ !browserAndContextPromise ||
67
+ !tab ||
68
+ !stopAfterAllExecutionCallbackList
69
+ ) {
70
+ browserAndContextPromise = (async () => {
71
+ const browser = await launchBrowserUsingPlaywright({
72
+ signal,
73
+ browserName,
74
+ stopOnExit,
75
+ playwrightOptions: {
76
+ headless,
77
+ executablePath,
78
+ },
79
+ })
80
+ const browserContext = await browser.newContext({ ignoreHTTPSErrors })
81
+ return { browser, browserContext }
82
+ })()
83
+
84
+ // when using chromium tab during multiple executions we reuse the chromium browser
85
+ // and only once all executions are done we close the browser
86
+ if (tab && stopAfterAllExecutionCallbackList) {
87
+ stopAfterAllExecutionCallbackList.add(async () => {
88
+ await closeBrowser()
89
+ })
90
+ } else {
91
+ stopCallbackList.add(async () => {
92
+ await closeBrowser()
93
+ })
94
+ }
95
+ }
96
+
97
+ const { browser, browserContext } = await browserAndContextPromise
98
+ // https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-disconnected
99
+ browser.on("disconnected", () => {
100
+ stop()
101
+ })
102
+
103
+ const page = await browserContext.newPage()
104
+ stoppedCallbackList.add(async () => {
105
+ try {
106
+ await page.close()
107
+ } catch (e) {
108
+ if (isTargetClosedError(e)) {
109
+ return
110
+ }
111
+ throw e
112
+ }
113
+ })
114
+ const stopTrackingToNotify = trackPageToNotify(page, {
115
+ onError: (error) => {
116
+ error = transformErrorHook(error)
117
+ if (!ignoreErrorHook(error)) {
118
+ errorCallbackList.notify(error)
119
+ }
120
+ },
121
+ onConsole: outputCallbackList.notify,
122
+ })
123
+ stoppedCallbackList.add(stopTrackingToNotify)
124
+
125
+ const execute = createExecuteHook({
126
+ page,
127
+ runtime,
128
+ browserServerLogLevel,
129
+
130
+ projectDirectoryUrl,
131
+ compileServerOrigin,
132
+ outDirectoryRelativeUrl,
133
+
134
+ collectPerformance,
135
+ measurePerformance,
136
+ collectCoverage,
137
+ coverageIgnorePredicate,
138
+ coverageForceIstanbul,
139
+
140
+ coveragePlaywrightAPIAvailable,
141
+ transformErrorHook,
142
+ })
143
+
144
+ return {
145
+ stopCallbackList,
146
+ stoppedCallbackList,
147
+ errorCallbackList,
148
+ outputCallbackList,
149
+ execute,
150
+ stop,
151
+ }
152
+ }
153
+ if (!tab) {
154
+ runtime.tab = createRuntimeFromPlaywright({
155
+ browserName,
156
+ browserVersion,
157
+ coveragePlaywrightAPIAvailable,
158
+ ignoreErrorHook,
159
+ transformErrorHook,
160
+ tab: true,
161
+ })
162
+ }
163
+ return runtime
164
+ }
165
+
166
+ const stopBrowser = async (browser) => {
167
+ const disconnected = browser.isConnected()
168
+ ? new Promise((resolve) => {
169
+ const disconnectedCallback = () => {
170
+ browser.removeListener("disconnected", disconnectedCallback)
171
+ resolve()
172
+ }
173
+ browser.on("disconnected", disconnectedCallback)
174
+ })
175
+ : Promise.resolve()
176
+
177
+ // for some reason without this 100ms timeout
178
+ // browser.close() never resolves (playwright does not like something)
179
+ await new Promise((resolve) => setTimeout(resolve, 100))
180
+
181
+ try {
182
+ await browser.close()
183
+ } catch (e) {
184
+ if (isTargetClosedError(e)) {
185
+ return
186
+ }
187
+ throw e
188
+ }
189
+ await disconnected
190
+ }
191
+
192
+ const launchBrowserUsingPlaywright = async ({
193
+ signal,
194
+ browserName,
195
+ stopOnExit,
196
+ playwrightOptions,
197
+ }) => {
198
+ const launchBrowserOperation = Abort.startOperation()
199
+ launchBrowserOperation.addAbortSignal(signal)
200
+ const playwright = await importPlaywright({ browserName })
201
+ if (stopOnExit) {
202
+ launchBrowserOperation.addAbortSource((abort) => {
203
+ return raceProcessTeardownEvents(
204
+ {
205
+ SIGHUP: true,
206
+ SIGTERM: true,
207
+ SIGINT: true,
208
+ beforeExit: true,
209
+ exit: true,
210
+ },
211
+ abort,
212
+ )
213
+ })
214
+ }
215
+
216
+ const browserClass = playwright[browserName]
217
+ try {
218
+ const browser = await browserClass.launch({
219
+ ...playwrightOptions,
220
+ // let's handle them to close properly browser + remove listener
221
+ // instead of relying on playwright to do so
222
+ handleSIGINT: false,
223
+ handleSIGTERM: false,
224
+ handleSIGHUP: false,
225
+ })
226
+ launchBrowserOperation.throwIfAborted()
227
+ return browser
228
+ } catch (e) {
229
+ if (launchBrowserOperation.signal.aborted && isTargetClosedError(e)) {
230
+ // rethrow the abort error
231
+ launchBrowserOperation.throwIfAborted()
232
+ }
233
+ throw e
234
+ } finally {
235
+ await launchBrowserOperation.end()
236
+ }
237
+ }
238
+
239
+ const importPlaywright = async ({ browserName }) => {
240
+ try {
241
+ const namespace = await import("playwright")
242
+ return namespace
243
+ } catch (e) {
244
+ if (e.code === "ERR_MODULE_NOT_FOUND") {
245
+ throw new Error(
246
+ createDetailedMessage(
247
+ `"playwright" not found. You need playwright in your dependencies when using "${browserName}Runtime"`,
248
+ {
249
+ suggestion: `npm install --save-dev playwright`,
250
+ },
251
+ ),
252
+ { cause: e },
253
+ )
254
+ }
255
+ throw e
256
+ }
257
+ }
258
+
259
+ const createExecuteHook = ({
260
+ page,
261
+ runtime,
262
+ projectDirectoryUrl,
263
+ compileServerOrigin,
264
+ compileServerId,
265
+ outDirectoryRelativeUrl,
266
+
267
+ collectPerformance,
268
+ measurePerformance,
269
+ collectCoverage,
270
+ coverageIgnorePredicate,
271
+ coverageForceIstanbul,
272
+
273
+ coveragePlaywrightAPIAvailable,
274
+ transformErrorHook,
275
+ }) => {
276
+ const execute = async ({ signal, fileRelativeUrl }) => {
277
+ const executeOperation = Abort.startOperation()
278
+ executeOperation.addAbortSignal(signal)
279
+ executeOperation.throwIfAborted()
280
+ const result = await executeHtmlFile(fileRelativeUrl, {
281
+ runtime,
282
+ executeOperation,
283
+
284
+ projectDirectoryUrl,
285
+ compileServerOrigin,
286
+ compileServerId,
287
+ outDirectoryRelativeUrl,
288
+
289
+ page,
290
+ measurePerformance,
291
+ collectPerformance,
292
+ collectCoverage,
293
+ coverageForceIstanbul,
294
+ coveragePlaywrightAPIAvailable,
295
+ coverageIgnorePredicate,
296
+ transformErrorHook,
297
+ })
298
+ return result
299
+ }
300
+ return execute
301
+ }
302
+
303
+ const isTargetClosedError = (error) => {
304
+ if (error.message.match(/Protocol error \(.*?\): Target closed/)) {
305
+ return true
306
+ }
307
+ if (error.message.match(/Protocol error \(.*?\): Browser.*?closed/)) {
308
+ return true
309
+ }
310
+ if (error.message.includes("browserContext.close: Browser closed")) {
311
+ return true
312
+ }
313
+ return false
314
+ }
@@ -11,7 +11,7 @@ import {
11
11
  normalizeStructuredMetaMap,
12
12
  urlToMeta,
13
13
  } from "@jsenv/filesystem"
14
- import { Abort } from "@jsenv/abort"
14
+ import { Abort, createCallbackListNotifiedOnce } from "@jsenv/abort"
15
15
 
16
16
  import { launchAndExecute } from "../executing/launchAndExecute.js"
17
17
  import { reportToCoverage } from "./coverage/reportToCoverage.js"
@@ -34,6 +34,7 @@ export const executeConcurrently = async (
34
34
  defaultMsAllocatedPerExecution = 30000,
35
35
  cooldownBetweenExecutions = 0,
36
36
  maxExecutionsInParallel = 1,
37
+ stopAfterExecute,
37
38
  completedExecutionLogMerging,
38
39
  completedExecutionLogAbbreviation,
39
40
 
@@ -137,6 +138,8 @@ export const executeConcurrently = async (
137
138
  let timedoutCount = 0
138
139
  let erroredCount = 0
139
140
  let completedCount = 0
141
+ const stopAfterAllExecutionCallbackList = createCallbackListNotifiedOnce()
142
+
140
143
  const executionsDone = await executeInParallel({
141
144
  multipleExecutionsOperation,
142
145
  maxExecutionsInParallel,
@@ -153,12 +156,7 @@ export const executeConcurrently = async (
153
156
  measurePerformance: false,
154
157
  collectPerformance: false,
155
158
  captureConsole: true,
156
- // stopAfterExecute: true to ensure runtime is stopped once executed
157
- // because we have what we wants: execution is completed and
158
- // we have associated coverage and capturedConsole
159
- // passsing false means all node process and browsers launched stays opened
160
- // (can eventually be used for debug)
161
- stopAfterExecute: true,
159
+ stopAfterExecute,
162
160
  stopAfterExecuteReason: "execution-done",
163
161
  allocatedMs: defaultMsAllocatedPerExecution,
164
162
  ...paramsFromStep,
@@ -212,6 +210,7 @@ export const executeConcurrently = async (
212
210
  collectCoverage: coverage,
213
211
  coverageIgnorePredicate,
214
212
  coverageForceIstanbul,
213
+ stopAfterAllExecutionCallbackList,
215
214
  ...executionParams.runtimeParams,
216
215
  },
217
216
  executeParams: {
@@ -283,6 +282,10 @@ export const executeConcurrently = async (
283
282
  },
284
283
  })
285
284
 
285
+ if (stopAfterExecute) {
286
+ stopAfterAllExecutionCallbackList.notify()
287
+ }
288
+
286
289
  const summaryCounts = reportToSummary(report)
287
290
 
288
291
  const summary = {
@@ -26,6 +26,7 @@ export const executePlan = async (
26
26
 
27
27
  defaultMsAllocatedPerExecution,
28
28
  maxExecutionsInParallel,
29
+ stopAfterExecute,
29
30
  cooldownBetweenExecutions,
30
31
  completedExecutionLogMerging,
31
32
  completedExecutionLogAbbreviation,
@@ -156,6 +157,7 @@ export const executePlan = async (
156
157
 
157
158
  defaultMsAllocatedPerExecution,
158
159
  maxExecutionsInParallel,
160
+ stopAfterExecute,
159
161
  cooldownBetweenExecutions,
160
162
  completedExecutionLogMerging,
161
163
  completedExecutionLogAbbreviation,
@@ -1,511 +1,43 @@
1
- // https://github.com/microsoft/playwright/blob/master/docs/api.md
2
-
3
- import { createDetailedMessage } from "@jsenv/logger"
4
- import {
5
- Abort,
6
- createCallbackListNotifiedOnce,
7
- createCallbackList,
8
- raceProcessTeardownEvents,
9
- } from "@jsenv/abort"
10
- import { memoize } from "@jsenv/filesystem"
11
-
12
- import { fetchUrl } from "./internal/fetchUrl.js"
13
- import { validateResponse } from "./internal/response_validation.js"
14
- import { trackPageToNotify } from "./internal/browser_launcher/trackPageToNotify.js"
15
- import { createSharing } from "./internal/browser_launcher/createSharing.js"
16
- import { executeHtmlFile } from "./internal/browser_launcher/executeHtmlFile.js"
1
+ import { createRuntimeFromPlaywright } from "@jsenv/core/src/internal/browser_launcher/from_playwright.js"
17
2
  import {
18
3
  PLAYWRIGHT_CHROMIUM_VERSION,
19
4
  PLAYWRIGHT_FIREFOX_VERSION,
20
5
  PLAYWRIGHT_WEBKIT_VERSION,
21
6
  } from "./playwright_browser_versions.js"
22
7
 
23
- const chromiumSharing = createSharing()
24
- export const chromiumRuntime = {
25
- name: "chromium",
26
- version: PLAYWRIGHT_CHROMIUM_VERSION,
27
- }
28
- chromiumRuntime.launch = async ({
29
- signal = new AbortController().signal,
30
- browserServerLogLevel,
31
- chromiumExecutablePath,
32
-
33
- projectDirectoryUrl,
34
- compileServerOrigin,
35
- compileServerId,
36
- outDirectoryRelativeUrl,
37
-
38
- collectPerformance,
39
- measurePerformance,
40
- collectCoverage,
41
- coverageIgnorePredicate,
42
- coverageForceIstanbul,
43
-
44
- headless = true,
45
- // about debug check https://github.com/microsoft/playwright/blob/master/docs/api.md#browsertypelaunchserveroptions
46
- debug = false,
47
- debugPort = 0,
48
- stopOnExit = true,
49
- share = false,
50
- }) => {
51
- const launchBrowserOperation = Abort.startOperation()
52
- launchBrowserOperation.addAbortSignal(signal)
53
-
54
- const sharingToken = share
55
- ? chromiumSharing.getSharingToken({
56
- chromiumExecutablePath,
57
- headless,
58
- debug,
59
- debugPort,
60
- })
61
- : chromiumSharing.getUniqueSharingToken()
62
- if (!sharingToken.isUsed()) {
63
- const { chromium } = await importPlaywright({ browserName: "chromium" })
64
- const launchOperation = launchBrowser("chromium", {
65
- browserClass: chromium,
66
- launchBrowserOperation,
67
- options: {
68
- headless,
69
- executablePath: chromiumExecutablePath,
70
- ...(debug ? { devtools: true } : {}),
71
- args: [
72
- // https://github.com/GoogleChrome/puppeteer/issues/1834
73
- // https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#tips
74
- // "--disable-dev-shm-usage",
75
- ...(debug ? [`--remote-debugging-port=${debugPort}`] : []),
76
- ],
77
- },
78
- stopOnExit,
79
- })
80
- sharingToken.setSharedValue(launchOperation)
81
- }
82
-
83
- const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
84
- launchBrowserOperation.addEndCallback(stopUsingBrowser)
85
- const browser = await browserPromise
86
-
87
- if (debug) {
88
- // https://github.com/puppeteer/puppeteer/blob/v2.0.0/docs/api.md#browserwsendpoint
89
- // https://chromedevtools.github.io/devtools-protocol/#how-do-i-access-the-browser-target
90
- const webSocketEndpoint = browser.wsEndpoint()
91
- const webSocketUrl = new URL(webSocketEndpoint)
92
- const browserEndpoint = `http://${webSocketUrl.host}/json/version`
93
- const browserResponse = await fetchUrl(browserEndpoint, {
94
- signal,
95
- ignoreHttpsError: true,
96
- })
97
- const { isValid, message, details } = await validateResponse(
98
- browserResponse,
99
- )
100
- if (!isValid) {
101
- throw new Error(createDetailedMessage(message, details))
8
+ export const chromiumRuntime = createRuntimeFromPlaywright({
9
+ browserName: "chromium",
10
+ browserVersion: PLAYWRIGHT_CHROMIUM_VERSION,
11
+ coveragePlaywrightAPIAvailable: true,
12
+ })
13
+ export const chromiumTabRuntime = chromiumRuntime.tab
14
+
15
+ export const firefoxRuntime = createRuntimeFromPlaywright({
16
+ browserName: "firefox",
17
+ browserVersion: PLAYWRIGHT_FIREFOX_VERSION,
18
+ })
19
+ export const firefoxTabRuntime = firefoxRuntime.tab
20
+
21
+ export const webkitRuntime = createRuntimeFromPlaywright({
22
+ browserName: "webkit",
23
+ browserVersion: PLAYWRIGHT_WEBKIT_VERSION,
24
+ ignoreErrorHook: (error) => {
25
+ // we catch error during execution but safari throw unhandled rejection
26
+ // in a non-deterministic way.
27
+ // I suppose it's due to some race condition to decide if the promise is catched or not
28
+ // for now we'll ignore unhandled rejection on wekbkit
29
+ if (error.name === "Unhandled Promise Rejection") {
30
+ return true
102
31
  }
103
-
104
- const browserResponseObject = JSON.parse(browserResponse.body)
105
- const { webSocketDebuggerUrl } = browserResponseObject
106
- console.log(`Debugger listening on ${webSocketDebuggerUrl}`)
107
- }
108
-
109
- const browserHooks = browserToRuntimeHooks(browser, {
110
- runtime: chromiumRuntime,
111
- browserServerLogLevel,
112
-
113
- projectDirectoryUrl,
114
- compileServerOrigin,
115
- compileServerId,
116
- outDirectoryRelativeUrl,
117
-
118
- collectPerformance,
119
- measurePerformance,
120
- collectCoverage,
121
- coverageIgnorePredicate,
122
- coverageForceIstanbul,
123
- coveragePlaywrightAPIAvailable: true,
124
- })
125
-
126
- return {
127
- browser,
128
- ...browserHooks,
129
- }
130
- }
131
- export const chromiumTabRuntime = {
132
- ...chromiumRuntime,
133
- launch: (params) =>
134
- chromiumRuntime.launch({
135
- shared: true,
136
- ...params,
137
- }),
138
- }
139
-
140
- const firefoxSharing = createSharing()
141
- export const firefoxRuntime = {
142
- name: "firefox",
143
- version: PLAYWRIGHT_FIREFOX_VERSION,
144
- }
145
- firefoxRuntime.launch = async ({
146
- signal = new AbortController().signal,
147
- firefoxExecutablePath,
148
- browserServerLogLevel,
149
-
150
- projectDirectoryUrl,
151
- compileServerOrigin,
152
- outDirectoryRelativeUrl,
153
-
154
- collectPerformance,
155
- measurePerformance,
156
- collectCoverage,
157
- coverageIgnorePredicate,
158
- coverageForceIstanbul,
159
-
160
- headless = true,
161
- stopOnExit = true,
162
- share = false,
163
- }) => {
164
- const launchBrowserOperation = Abort.startOperation()
165
- launchBrowserOperation.addAbortSignal(signal)
166
-
167
- const sharingToken = share
168
- ? firefoxSharing.getSharingToken({ firefoxExecutablePath, headless })
169
- : firefoxSharing.getUniqueSharingToken()
170
- if (!sharingToken.isUsed()) {
171
- const { firefox } = await importPlaywright({ browserName: "firefox" })
172
- const launchOperation = launchBrowser("firefox", {
173
- browserClass: firefox,
174
-
175
- launchBrowserOperation,
176
- options: {
177
- headless,
178
- executablePath: firefoxExecutablePath,
179
- },
180
- stopOnExit,
181
- })
182
- sharingToken.setSharedValue(launchOperation)
183
- }
184
-
185
- const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
186
- launchBrowserOperation.addEndCallback(stopUsingBrowser)
187
- const browser = await browserPromise
188
-
189
- const browserHooks = browserToRuntimeHooks(browser, {
190
- runtime: firefoxRuntime,
191
- launchBrowserOperation,
192
- browserServerLogLevel,
193
-
194
- projectDirectoryUrl,
195
- compileServerOrigin,
196
- outDirectoryRelativeUrl,
197
-
198
- collectPerformance,
199
- measurePerformance,
200
- collectCoverage,
201
- coverageIgnorePredicate,
202
- coverageForceIstanbul,
203
- })
204
-
205
- return {
206
- browser,
207
- ...browserHooks,
208
- }
209
- }
210
- export const firefoxTabRuntime = {
211
- ...firefoxRuntime,
212
- launch: (params) =>
213
- firefoxRuntime.launch({
214
- shared: true,
215
- ...params,
216
- }),
217
- }
218
-
219
- const webkitSharing = createSharing()
220
- export const webkitRuntime = {
221
- name: "webkit",
222
- version: PLAYWRIGHT_WEBKIT_VERSION,
223
- }
224
- webkitRuntime.launch = async ({
225
- signal = new AbortController().signal,
226
- browserServerLogLevel,
227
- webkitExecutablePath,
228
-
229
- projectDirectoryUrl,
230
- compileServerOrigin,
231
- outDirectoryRelativeUrl,
232
-
233
- collectPerformance,
234
- measurePerformance,
235
- collectCoverage,
236
- coverageIgnorePredicate,
237
- coverageForceIstanbul,
238
-
239
- headless = true,
240
- stopOnExit = true,
241
- share = false,
242
- }) => {
243
- const launchBrowserOperation = Abort.startOperation()
244
- launchBrowserOperation.addAbortSignal(signal)
245
-
246
- const sharingToken = share
247
- ? webkitSharing.getSharingToken({ webkitExecutablePath, headless })
248
- : webkitSharing.getUniqueSharingToken()
249
-
250
- if (!sharingToken.isUsed()) {
251
- const { webkit } = await await importPlaywright({ browserName: "webkit" })
252
- const launchOperation = launchBrowser("webkit", {
253
- browserClass: webkit,
254
- launchBrowserOperation,
255
- options: {
256
- headless,
257
- executablePath: webkitExecutablePath,
258
- },
259
- stopOnExit,
260
- })
261
- sharingToken.setSharedValue(launchOperation)
262
- }
263
-
264
- const [browserPromise, stopUsingBrowser] = sharingToken.useSharedValue()
265
- launchBrowserOperation.addEndCallback(stopUsingBrowser)
266
- const browser = await browserPromise
267
-
268
- const browserHooks = browserToRuntimeHooks(browser, {
269
- runtime: webkitRuntime,
270
- launchBrowserOperation,
271
- browserServerLogLevel,
272
-
273
- projectDirectoryUrl,
274
- compileServerOrigin,
275
- outDirectoryRelativeUrl,
276
-
277
- collectPerformance,
278
- measurePerformance,
279
- collectCoverage,
280
- coverageIgnorePredicate,
281
- coverageForceIstanbul,
282
- ignoreErrorHook: (error) => {
283
- // we catch error during execution but safari throw unhandled rejection
284
- // in a non-deterministic way.
285
- // I suppose it's due to some race condition to decide if the promise is catched or not
286
- // for now we'll ignore unhandled rejection on wekbkit
287
- if (error.name === "Unhandled Promise Rejection") {
288
- return true
289
- }
290
- return false
291
- },
292
- transformErrorHook: (error) => {
293
- // Force error stack to contain the error message
294
- // because it's not the case on webkit
295
- error.stack = `${error.message}
32
+ return false
33
+ },
34
+ transformErrorHook: (error) => {
35
+ // Force error stack to contain the error message
36
+ // because it's not the case on webkit
37
+ error.stack = `${error.message}
296
38
  at ${error.stack}`
297
39
 
298
- return error
299
- },
300
- })
301
-
302
- return {
303
- browser,
304
- ...browserHooks,
305
- }
306
- }
307
- export const webkitTabRuntime = {
308
- ...webkitRuntime,
309
- launch: (params) =>
310
- webkitRuntime.launch({
311
- shared: true,
312
- ...params,
313
- }),
314
- }
315
-
316
- const launchBrowser = async (
317
- browserName,
318
- { launchBrowserOperation, browserClass, options, stopOnExit },
319
- ) => {
320
- if (stopOnExit) {
321
- launchBrowserOperation.addAbortSource((abort) => {
322
- return raceProcessTeardownEvents(
323
- {
324
- SIGHUP: true,
325
- SIGTERM: true,
326
- SIGINT: true,
327
- beforeExit: true,
328
- exit: true,
329
- },
330
- abort,
331
- )
332
- })
333
- }
334
-
335
- try {
336
- const browser = await browserClass.launch({
337
- ...options,
338
- // let's handle them to close properly browser + remove listener
339
- // instead of relying on playwright to do so
340
- handleSIGINT: false,
341
- handleSIGTERM: false,
342
- handleSIGHUP: false,
343
- })
344
- launchBrowserOperation.throwIfAborted()
345
- return browser
346
- } catch (e) {
347
- if (launchBrowserOperation.signal.aborted && isTargetClosedError(e)) {
348
- // rethrow the abort error
349
- launchBrowserOperation.throwIfAborted()
350
- }
351
- throw e
352
- } finally {
353
- await launchBrowserOperation.end()
354
- }
355
- }
356
-
357
- const importPlaywright = async ({ browserName }) => {
358
- try {
359
- const namespace = await import("playwright")
360
- return namespace
361
- } catch (e) {
362
- if (e.code === "ERR_MODULE_NOT_FOUND") {
363
- throw new Error(
364
- createDetailedMessage(
365
- `"playwright" not found. You need playwright in your dependencies when using "${browserName}Runtime"`,
366
- {
367
- suggestion: `npm install --save-dev playwright`,
368
- },
369
- ),
370
- { cause: e },
371
- )
372
- }
373
- throw e
374
- }
375
- }
376
-
377
- const stopBrowser = async (browser) => {
378
- const disconnected = browser.isConnected()
379
- ? new Promise((resolve) => {
380
- const disconnectedCallback = () => {
381
- browser.removeListener("disconnected", disconnectedCallback)
382
- resolve()
383
- }
384
- browser.on("disconnected", disconnectedCallback)
385
- })
386
- : Promise.resolve()
387
-
388
- // for some reason without this 100ms timeout
389
- // browser.close() never resolves (playwright does not like something)
390
- await new Promise((resolve) => setTimeout(resolve, 100))
391
-
392
- await browser.close()
393
- await disconnected
394
- }
395
-
396
- const browserToRuntimeHooks = (
397
- browser,
398
- {
399
- runtime,
400
- projectDirectoryUrl,
401
- compileServerOrigin,
402
- compileServerId,
403
- outDirectoryRelativeUrl,
404
-
405
- collectPerformance,
406
- measurePerformance,
407
- collectCoverage,
408
- coverageIgnorePredicate,
409
- coverageForceIstanbul,
410
- coveragePlaywrightAPIAvailable = false,
411
- ignoreErrorHook = () => false,
412
- transformErrorHook = (error) => error,
40
+ return error
413
41
  },
414
- ) => {
415
- const stopCallbackList = createCallbackListNotifiedOnce()
416
- const stoppedCallbackList = createCallbackListNotifiedOnce()
417
- const stop = memoize(async (reason) => {
418
- await stopCallbackList.notify({ reason })
419
- stoppedCallbackList.notify({ reason })
420
- return { graceful: false }
421
- })
422
-
423
- // https://github.com/GoogleChrome/puppeteer/blob/v1.4.0/docs/api.md#event-disconnected
424
- browser.on("disconnected", () => {
425
- stop()
426
- })
427
-
428
- stopCallbackList.add(async () => {
429
- await stopBrowser(browser)
430
- })
431
-
432
- const errorCallbackList = createCallbackList()
433
-
434
- const outputCallbackList = createCallbackList()
435
-
436
- const execute = async ({
437
- signal,
438
- fileRelativeUrl,
439
- ignoreHTTPSErrors = true, // we mostly use self signed certificates during tests
440
- }) => {
441
- const executeOperation = Abort.startOperation()
442
- executeOperation.addAbortSignal(signal)
443
- executeOperation.throwIfAborted()
444
- // open a tab to execute to the file
445
- const browserContext = await browser.newContext({ ignoreHTTPSErrors })
446
- executeOperation.throwIfAborted()
447
- const page = await browserContext.newPage()
448
- executeOperation.addEndCallback(async () => {
449
- try {
450
- await browserContext.close()
451
- } catch (e) {
452
- if (isTargetClosedError(e)) {
453
- return
454
- }
455
- throw e
456
- }
457
- })
458
- // track tab error and console
459
- const stopTrackingToNotify = trackPageToNotify(page, {
460
- onError: (error) => {
461
- error = transformErrorHook(error)
462
- if (!ignoreErrorHook(error)) {
463
- errorCallbackList.notify(error)
464
- }
465
- },
466
- onConsole: outputCallbackList.notify,
467
- })
468
- stoppedCallbackList.add(stopTrackingToNotify)
469
-
470
- const result = await executeHtmlFile(fileRelativeUrl, {
471
- runtime,
472
- executeOperation,
473
-
474
- projectDirectoryUrl,
475
- compileServerOrigin,
476
- compileServerId,
477
- outDirectoryRelativeUrl,
478
-
479
- page,
480
- measurePerformance,
481
- collectPerformance,
482
- collectCoverage,
483
- coverageForceIstanbul,
484
- coveragePlaywrightAPIAvailable,
485
- coverageIgnorePredicate,
486
- transformErrorHook,
487
- })
488
- return result
489
- }
490
-
491
- return {
492
- stoppedCallbackList,
493
- errorCallbackList,
494
- outputCallbackList,
495
- execute,
496
- stop,
497
- }
498
- }
499
-
500
- const isTargetClosedError = (error) => {
501
- if (error.message.match(/Protocol error \(.*?\): Target closed/)) {
502
- return true
503
- }
504
- if (error.message.match(/Protocol error \(.*?\): Browser.*?closed/)) {
505
- return true
506
- }
507
- if (error.message.includes("browserContext.close: Browser closed")) {
508
- return true
509
- }
510
- return false
511
- }
42
+ })
43
+ export const webkitTabRuntime = webkitRuntime.tab
@@ -1,70 +0,0 @@
1
- export const createSharing = ({ argsToId = argsToIdFallback } = {}) => {
2
- const tokenMap = {}
3
-
4
- const getSharingToken = (...args) => {
5
- const id = argsToId(args)
6
-
7
- if (id in tokenMap) {
8
- return tokenMap[id]
9
- }
10
-
11
- const sharingToken = createSharingToken({
12
- unusedCallback: () => {
13
- delete tokenMap[id]
14
- },
15
- })
16
- tokenMap[id] = sharingToken
17
- return sharingToken
18
- }
19
-
20
- const getUniqueSharingToken = () => {
21
- return createSharingToken()
22
- }
23
-
24
- return { getSharingToken, getUniqueSharingToken }
25
- }
26
-
27
- const createSharingToken = ({ unusedCallback = () => {} } = {}) => {
28
- let useCount = 0
29
- let sharedValue
30
- let cleanup
31
- const sharingToken = {
32
- isUsed: () => useCount > 0,
33
-
34
- setSharedValue: (value, cleanupFunction = () => {}) => {
35
- sharedValue = value
36
- cleanup = cleanupFunction
37
- },
38
-
39
- useSharedValue: () => {
40
- useCount++
41
-
42
- let stopped = false
43
- let stopUsingReturnValue
44
- const stopUsing = () => {
45
- // ensure if stopUsing is called many times
46
- // it returns the same value and does not decrement useCount more than once
47
- if (stopped) {
48
- return stopUsingReturnValue
49
- }
50
-
51
- stopped = true
52
- useCount--
53
- if (useCount === 0) {
54
- unusedCallback()
55
- sharedValue = undefined
56
- stopUsingReturnValue = cleanup()
57
- } else {
58
- stopUsingReturnValue = undefined
59
- }
60
-
61
- return stopUsingReturnValue
62
- }
63
-
64
- return [sharedValue, stopUsing]
65
- },
66
- }
67
- return sharingToken
68
- }
69
-
70
- const argsToIdFallback = (args) => JSON.stringify(args)