@browserless/function 10.12.15 → 12.0.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/LICENSE.md CHANGED
File without changes
package/README.md CHANGED
@@ -37,9 +37,21 @@ The `@browserless/function` package allows you to:
37
37
 
38
38
  ### Usage
39
39
 
40
+ First, create a function instance by calling the factory:
41
+
42
+ ```js
43
+ const createFunction = require('@browserless/function')()
44
+ ```
45
+
46
+ You can optionally pass a `tmpdir` for the sandbox working directory:
47
+
40
48
  ```js
41
- const createFunction = require('@browserless/function')
49
+ const createFunction = require('@browserless/function')({ tmpdir: '/tmp/functions' })
50
+ ```
51
+
52
+ Then use it to run arbitrary JavaScript:
42
53
 
54
+ ```js
43
55
  // Simple function without page access
44
56
  const code = ({ query }) => query.value * 2
45
57
 
@@ -55,7 +67,7 @@ console.log(result)
55
67
  When your code references `page`, browserless automatically provides access to the Puppeteer page:
56
68
 
57
69
  ```js
58
- const createFunction = require('@browserless/function')
70
+ const createFunction = require('@browserless/function')()
59
71
 
60
72
  // Function with page access
61
73
  const code = async ({ page }) => {
@@ -89,9 +101,34 @@ The function returns a structured result:
89
101
  |----------|-------------|
90
102
  | `isFulfilled` | `true` if execution succeeded, `false` if error |
91
103
  | `value` | Return value (success) or error object (failure) |
92
- | `profiling` | Execution timing and performance data |
104
+ | `profiling` | Execution timing and resource usage (see below) |
93
105
  | `logging` | Captured console output (`log`, `warn`, `error`, etc.) |
94
106
 
107
+ #### Profiling
108
+
109
+ The `profiling` object contains phased timing and resource data:
110
+
111
+ | Property | Description |
112
+ |----------|-------------|
113
+ | `phases.compile` | Time to compile the sandbox script (ms) |
114
+ | `phases.spawn` | Time to spawn the child process (ms) |
115
+ | `phases.run` | Time executing the user function (ms) |
116
+ | `phases.total` | Total wall-clock time (ms) |
117
+ | `cpu` | CPU time consumed (ms) |
118
+ | `memory` | Peak memory usage (bytes) |
119
+
120
+ ### Teardown
121
+
122
+ Call `.teardown()` on the factory instance to clean up sandbox resources:
123
+
124
+ ```js
125
+ const createFunction = require('@browserless/function')({ tmpdir: '/tmp/functions' })
126
+
127
+ // ... use createFunction ...
128
+
129
+ await createFunction.teardown()
130
+ ```
131
+
95
132
  ### Options
96
133
 
97
134
  ```js
@@ -121,7 +158,7 @@ const myFn = createFunction(code, {
121
158
  #### Interact with page elements
122
159
 
123
160
  ```js
124
- const createFunction = require('@browserless/function')
161
+ const createFunction = require('@browserless/function')()
125
162
 
126
163
  const code = async ({ page }) => {
127
164
  await page.waitForSelector('button.submit')
@@ -138,7 +175,7 @@ const result = await clickAndGetTitle('https://example.com')
138
175
  #### Inject external scripts
139
176
 
140
177
  ```js
141
- const createFunction = require('@browserless/function')
178
+ const createFunction = require('@browserless/function')()
142
179
 
143
180
  const code = ({ page }) => page.evaluate('jQuery.fn.jquery')
144
181
 
@@ -155,7 +192,7 @@ const result = await getjQueryVersion('https://example.com')
155
192
  #### Use npm modules in sandbox
156
193
 
157
194
  ```js
158
- const createFunction = require('@browserless/function')
195
+ const createFunction = require('@browserless/function')()
159
196
 
160
197
  const code = async ({ page }) => {
161
198
  const _ = require('lodash')
@@ -170,7 +207,7 @@ const result = await countWords('https://example.com')
170
207
  #### Handle errors
171
208
 
172
209
  ```js
173
- const createFunction = require('@browserless/function')
210
+ const createFunction = require('@browserless/function')()
174
211
 
175
212
  const code = () => {
176
213
  throw new Error('Something went wrong')
@@ -192,7 +229,7 @@ This is an **extended functionality package** for advanced use cases. It makes t
192
229
  | Package | Purpose |
193
230
  |---------|---------|
194
231
  | `@browserless/errors` | Error normalization and typed errors |
195
- | `isolated-function` | Secure VM sandbox execution |
232
+ | `isolated-function` | Secure sandboxed execution via child processes |
196
233
  | `require-one-of` | Auto-detects browserless installation |
197
234
  | `acorn` / `acorn-walk` | AST parsing to detect page usage in code |
198
235
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@browserless/function",
3
3
  "description": "Execute arbitrary JavaScript code in a secure browser sandbox with access to page DOM and network.",
4
4
  "homepage": "https://browserless.js.org",
5
- "version": "10.12.15",
5
+ "version": "12.0.0",
6
6
  "main": "src/index.js",
7
7
  "author": {
8
8
  "email": "hello@microlink.io",
@@ -31,16 +31,16 @@
31
31
  "vm"
32
32
  ],
33
33
  "dependencies": {
34
- "@browserless/errors": "^10.12.14",
34
+ "@browserless/errors": "^12.0.0",
35
35
  "acorn": "~8.16.0",
36
36
  "acorn-walk": "~8.3.5",
37
- "isolated-function": "~0.1.51",
37
+ "isolated-function": "~0.1.55",
38
38
  "require-one-of": "~1.0.24"
39
39
  },
40
40
  "devDependencies": {
41
- "@browserless/test": "^10.12.6",
42
41
  "ava": "5",
43
- "lodash": "latest"
42
+ "lodash": "latest",
43
+ "@browserless/test": "12.0.0"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">= 20"
@@ -48,14 +48,13 @@
48
48
  "files": [
49
49
  "src"
50
50
  ],
51
- "scripts": {
52
- "test": "ava"
53
- },
54
51
  "license": "MIT",
55
52
  "ava": {
56
53
  "serial": true,
57
54
  "timeout": "2m",
58
55
  "workerThreads": false
59
56
  },
60
- "gitHead": "f6663d9a8b3ac2fdb28d524d7a4e3365e47dc972"
61
- }
57
+ "scripts": {
58
+ "test": "ava"
59
+ }
60
+ }
package/src/function.js CHANGED
@@ -1,33 +1,32 @@
1
1
  'use strict'
2
2
 
3
- const isolatedFunction = require('isolated-function')
4
3
  const template = require('./template')
5
4
 
6
5
  const [nodeMajor] = process.version.slice(1).split('.').map(Number)
7
6
 
8
- module.exports = async ({
9
- url,
10
- code,
11
- vmOpts,
12
- browserWSEndpoint,
13
- needsNetwork = template.isUsingPage(code),
14
- source = template(code, needsNetwork),
15
- ...opts
16
- }) => {
17
- const permissions = needsNetwork && nodeMajor >= 25 ? ['net'] : []
18
- const vmOptsAllow = vmOpts?.allow || {}
19
- const [fn, teardown] = isolatedFunction(source, {
20
- ...vmOpts,
21
- allow: {
22
- ...vmOptsAllow,
23
- permissions: [...(vmOptsAllow.permissions || []), ...permissions]
24
- },
25
- throwError: false
26
- })
27
- const result = await fn(url, browserWSEndpoint, opts)
28
- await teardown()
29
- return result
30
- }
7
+ module.exports =
8
+ isolatedFunction =>
9
+ async ({
10
+ url,
11
+ code,
12
+ vmOpts,
13
+ browserWSEndpoint,
14
+ needsNetwork = template.isUsingPage(code),
15
+ source = template(code, needsNetwork),
16
+ ...opts
17
+ }) => {
18
+ const permissions = needsNetwork && nodeMajor >= 25 ? ['net'] : []
19
+ const vmOptsAllow = vmOpts?.allow || {}
20
+ const fn = isolatedFunction(source, {
21
+ ...vmOpts,
22
+ allow: {
23
+ ...vmOptsAllow,
24
+ permissions: [...(vmOptsAllow.permissions || []), ...permissions]
25
+ },
26
+ throwError: false
27
+ })
28
+ return fn(url, browserWSEndpoint, opts)
29
+ }
31
30
 
32
31
  module.exports.isUsingPage = template.isUsingPage
33
32
  module.exports.buildTemplate = template
package/src/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const { isBrowserlessError, ensureError } = require('@browserless/errors')
4
+ const createIsolatedFunction = require('isolated-function')
4
5
  const requireOneOf = require('require-one-of')
5
- const runFunction = require('./function')
6
+ const createRunFunction = require('./function')
6
7
 
7
8
  const stringify = fn => fn.toString().trim().replace(/;$/, '')
8
9
 
@@ -29,72 +30,81 @@ const serializeResponse = response => ({
29
30
  fromServiceWorker: response.fromServiceWorker()
30
31
  })
31
32
 
32
- module.exports = (
33
- fn,
34
- {
35
- getBrowserless = requireOneOf(['browserless']),
36
- retry = 2,
37
- timeout = 30000,
38
- gotoOpts,
39
- ...opts
40
- } = {}
41
- ) => {
42
- const code = stringify(fn)
43
- const needsNetwork = runFunction.isUsingPage(code)
44
- const source = runFunction.buildTemplate(code, needsNetwork)
45
- let browserPromise
46
-
47
- const getBrowser = async () => {
48
- if (!browserPromise) {
49
- browserPromise = Promise.resolve(getBrowserless()).catch(error => {
50
- browserPromise = undefined
51
- throw error
52
- })
33
+ module.exports = ({ tmpdir } = {}) => {
34
+ const isolatedFunction = createIsolatedFunction({ tmpdir })
35
+ const runFunction = createRunFunction(isolatedFunction)
36
+
37
+ const createFunction = (
38
+ fn,
39
+ {
40
+ getBrowserless = requireOneOf(['browserless']),
41
+ retry = 2,
42
+ timeout = 30000,
43
+ gotoOpts,
44
+ ...opts
45
+ } = {}
46
+ ) => {
47
+ const code = stringify(fn)
48
+ const needsNetwork = createRunFunction.isUsingPage(code)
49
+ const source = createRunFunction.buildTemplate(code, needsNetwork)
50
+ let browserPromise
51
+
52
+ const getBrowser = async () => {
53
+ if (!browserPromise) {
54
+ browserPromise = Promise.resolve(getBrowserless()).catch(error => {
55
+ browserPromise = undefined
56
+ throw error
57
+ })
58
+ }
59
+ return browserPromise
53
60
  }
54
- return browserPromise
55
- }
56
61
 
57
- return async (url, fnOpts = {}) => {
58
- const browser = await getBrowser()
59
- const browserless = await browser.createContext()
62
+ return async (url, fnOpts = {}) => {
63
+ const browser = await getBrowser()
64
+ const browserless = await browser.createContext()
60
65
 
61
- return browserless.withPage((page, goto) => async () => {
62
- const { device, response } = await goto(page, { url, timeout, ...gotoOpts })
66
+ return browserless.withPage((page, goto) => async () => {
67
+ const { device, response } = await goto(page, { url, timeout, ...gotoOpts })
63
68
 
64
- const targetId = await getTargetId(page)
69
+ const targetId = await getTargetId(page)
65
70
 
66
- const runFunctionOpts = {
67
- url,
68
- code,
69
- device,
70
- ...opts,
71
- ...fnOpts,
72
- ...(response && { _response: serializeResponse(response) })
73
- }
71
+ const runFunctionOpts = {
72
+ url,
73
+ code,
74
+ device,
75
+ ...opts,
76
+ ...fnOpts,
77
+ ...(response && { _response: serializeResponse(response) })
78
+ }
74
79
 
75
- if (runFunctionOpts.code === code) {
76
- runFunctionOpts.needsNetwork = needsNetwork
77
- runFunctionOpts.source = source
78
- }
80
+ if (runFunctionOpts.code === code) {
81
+ runFunctionOpts.needsNetwork = needsNetwork
82
+ runFunctionOpts.source = source
83
+ }
79
84
 
80
- if (runFunctionOpts.needsNetwork !== false) {
81
- const browserFromPage = typeof page.browser === 'function' ? page.browser() : undefined
82
- const browserWSEndpoint =
83
- browserFromPage && typeof browserFromPage.wsEndpoint === 'function'
84
- ? browserFromPage.wsEndpoint()
85
- : undefined
85
+ if (runFunctionOpts.needsNetwork !== false) {
86
+ const browserFromPage = typeof page.browser === 'function' ? page.browser() : undefined
87
+ const browserWSEndpoint =
88
+ browserFromPage && typeof browserFromPage.wsEndpoint === 'function'
89
+ ? browserFromPage.wsEndpoint()
90
+ : undefined
86
91
 
87
- if (!browserWSEndpoint) throw new Error('Browser WebSocket endpoint not found')
88
- runFunctionOpts.browserWSEndpoint = browserWSEndpoint
89
- runFunctionOpts.targetId = targetId
90
- }
92
+ if (!browserWSEndpoint) throw new Error('Browser WebSocket endpoint not found')
93
+ runFunctionOpts.browserWSEndpoint = browserWSEndpoint
94
+ runFunctionOpts.targetId = targetId
95
+ }
91
96
 
92
- const result = await runFunction(runFunctionOpts)
97
+ const result = await runFunction(runFunctionOpts)
93
98
 
94
- if (result.isFulfilled) return result
95
- const error = ensureError(result.value)
96
- if (isBrowserlessError(error)) throw error
97
- return result
98
- })()
99
+ if (result.isFulfilled) return result
100
+ const error = ensureError(result.value)
101
+ if (isBrowserlessError(error)) throw error
102
+ return result
103
+ })()
104
+ }
99
105
  }
106
+
107
+ createFunction.teardown = () => isolatedFunction.teardown()
108
+
109
+ return createFunction
100
110
  }