@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 +0 -0
- package/README.md +45 -8
- package/package.json +9 -10
- package/src/function.js +23 -24
- package/src/index.js +68 -58
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
|
|
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
|
|
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": "
|
|
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": "^
|
|
34
|
+
"@browserless/errors": "^12.0.0",
|
|
35
35
|
"acorn": "~8.16.0",
|
|
36
36
|
"acorn-walk": "~8.3.5",
|
|
37
|
-
"isolated-function": "~0.1.
|
|
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
|
-
"
|
|
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 =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
return async (url, fnOpts = {}) => {
|
|
63
|
+
const browser = await getBrowser()
|
|
64
|
+
const browserless = await browser.createContext()
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
return browserless.withPage((page, goto) => async () => {
|
|
67
|
+
const { device, response } = await goto(page, { url, timeout, ...gotoOpts })
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
const targetId = await getTargetId(page)
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
const runFunctionOpts = {
|
|
72
|
+
url,
|
|
73
|
+
code,
|
|
74
|
+
device,
|
|
75
|
+
...opts,
|
|
76
|
+
...fnOpts,
|
|
77
|
+
...(response && { _response: serializeResponse(response) })
|
|
78
|
+
}
|
|
74
79
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
if (runFunctionOpts.code === code) {
|
|
81
|
+
runFunctionOpts.needsNetwork = needsNetwork
|
|
82
|
+
runFunctionOpts.source = source
|
|
83
|
+
}
|
|
79
84
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
if (!browserWSEndpoint) throw new Error('Browser WebSocket endpoint not found')
|
|
93
|
+
runFunctionOpts.browserWSEndpoint = browserWSEndpoint
|
|
94
|
+
runFunctionOpts.targetId = targetId
|
|
95
|
+
}
|
|
91
96
|
|
|
92
|
-
|
|
97
|
+
const result = await runFunction(runFunctionOpts)
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|