isolated-function 0.1.5 → 0.1.7
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/README.md +56 -5
- package/package.json +1 -1
- package/src/compile/detect-dependencies.js +35 -0
- package/src/compile/index.js +60 -0
- package/src/compile/parse-dependency.js +31 -0
- package/src/compile/transform-dependencies.js +57 -0
- package/src/index.js +2 -2
- package/src/compile.js +0 -141
package/README.md
CHANGED
|
@@ -22,12 +22,15 @@
|
|
|
22
22
|
- [Auto install dependencies](#auto-install-dependencies)
|
|
23
23
|
- [Execution profiling](#execution-profiling)
|
|
24
24
|
- [Resource limits](#resource-limits)
|
|
25
|
+
- [Error handling](#error-handling)
|
|
25
26
|
- [API](#api)
|
|
26
27
|
- [isolatedFunction(code, \[options\])](#isolatedfunctioncode-options)
|
|
27
28
|
- [code](#code)
|
|
28
29
|
- [options](#options)
|
|
30
|
+
- [memory](#memory)
|
|
31
|
+
- [throwError](#throwerror)
|
|
29
32
|
- [timeout](#timeout)
|
|
30
|
-
- [
|
|
33
|
+
- [tmpdir](#tmpdir)
|
|
31
34
|
- [=\> (fn(\[...args\]), teardown())](#-fnargs-teardown)
|
|
32
35
|
- [fn](#fn)
|
|
33
36
|
- [teardown](#teardown)
|
|
@@ -164,6 +167,34 @@ await fn(100)
|
|
|
164
167
|
// => TimeoutError: Execution timed out
|
|
165
168
|
```
|
|
166
169
|
|
|
170
|
+
### Error handling
|
|
171
|
+
|
|
172
|
+
Any error during **isolated-function** execution will be propagated:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
const [fn, cleanup] = isolatedFunction(() => {
|
|
176
|
+
throw new TypeError('oh no!')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const result = await fn()
|
|
180
|
+
// TypeError: oh no!
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
You can also return the error instead of throwing it with `{ throwError: false }`:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
const [fn, cleanup] = isolatedFunction(() => {
|
|
187
|
+
throw new TypeError('oh no!')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
const { isFullfiled, value } = await fn()
|
|
191
|
+
|
|
192
|
+
if (!isFufilled) {
|
|
193
|
+
console.error(value)
|
|
194
|
+
// TypeError: oh no!
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
167
198
|
## API
|
|
168
199
|
|
|
169
200
|
### isolatedFunction(code, [options])
|
|
@@ -177,17 +208,37 @@ The hosted function to run.
|
|
|
177
208
|
|
|
178
209
|
#### options
|
|
179
210
|
|
|
211
|
+
##### memory
|
|
212
|
+
|
|
213
|
+
Type: `number`<br>
|
|
214
|
+
Default: `Infinity`
|
|
215
|
+
|
|
216
|
+
Set the function memory limit, in megabytes.
|
|
217
|
+
|
|
218
|
+
##### throwError
|
|
219
|
+
|
|
220
|
+
Type: `boolean`<br>
|
|
221
|
+
Default: `false`
|
|
222
|
+
|
|
223
|
+
When is `true`, it returns the error rather than throw it.
|
|
224
|
+
|
|
225
|
+
The error will be accessible against `{ value: error, isFufilled: false }` object.
|
|
226
|
+
|
|
227
|
+
Set the function memory limit, in megabytes.
|
|
228
|
+
|
|
180
229
|
##### timeout
|
|
181
230
|
|
|
182
|
-
Type: `number
|
|
231
|
+
Type: `number`<br>
|
|
232
|
+
Default: `Infinity`
|
|
183
233
|
|
|
184
234
|
Timeout after a specified amount of time, in milliseconds.
|
|
185
235
|
|
|
186
|
-
#####
|
|
236
|
+
##### tmpdir
|
|
187
237
|
|
|
188
|
-
Type: `
|
|
238
|
+
Type: `function`<br>
|
|
239
|
+
Default: `fs.mkdtemp(path.join(require('os').tmpdir(), 'compile-'))`
|
|
189
240
|
|
|
190
|
-
|
|
241
|
+
The temporal folder to use for installing code dependencies.
|
|
191
242
|
|
|
192
243
|
### => (fn([...args]), teardown())
|
|
193
244
|
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "isolated-function",
|
|
3
3
|
"description": "Runs untrusted code in a Node.js v8 sandbox.",
|
|
4
4
|
"homepage": "https://github.com/Kikobeats/isolated-function",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.7",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./src/index.js"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const walk = require('acorn-walk')
|
|
4
|
+
const acorn = require('acorn')
|
|
5
|
+
|
|
6
|
+
const parseDependency = require('./parse-dependency')
|
|
7
|
+
|
|
8
|
+
module.exports = code => {
|
|
9
|
+
const dependencies = new Set()
|
|
10
|
+
|
|
11
|
+
// Parse the code into an AST
|
|
12
|
+
const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
|
|
13
|
+
|
|
14
|
+
// Traverse the AST to find require and import statements
|
|
15
|
+
walk.simple(ast, {
|
|
16
|
+
CallExpression (node) {
|
|
17
|
+
if (
|
|
18
|
+
node.callee.name === 'require' &&
|
|
19
|
+
node.arguments.length === 1 &&
|
|
20
|
+
node.arguments[0].type === 'Literal'
|
|
21
|
+
) {
|
|
22
|
+
const dependency = node.arguments[0].value
|
|
23
|
+
dependencies.add(parseDependency(dependency))
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
ImportDeclaration (node) {
|
|
27
|
+
const source = node.source.value
|
|
28
|
+
dependencies.add(parseDependency(source))
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return Array.from(dependencies)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports.parseDependency = parseDependency
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const esbuild = require('esbuild')
|
|
5
|
+
const fs = require('fs/promises')
|
|
6
|
+
const $ = require('tinyspawn')
|
|
7
|
+
const path = require('path')
|
|
8
|
+
|
|
9
|
+
const transformDependencies = require('./transform-dependencies')
|
|
10
|
+
const detectDependencies = require('./detect-dependencies')
|
|
11
|
+
const generateTemplate = require('../template')
|
|
12
|
+
|
|
13
|
+
const MINIFY = process.env.ISOLATED_FUNCTIONS_MINIFY !== 'false'
|
|
14
|
+
|
|
15
|
+
const packageManager = (() => {
|
|
16
|
+
try {
|
|
17
|
+
execSync('which pnpm').toString().trim()
|
|
18
|
+
return { init: 'pnpm init', install: 'pnpm install' }
|
|
19
|
+
} catch {
|
|
20
|
+
return { init: 'npm init --yes', install: 'npm install' }
|
|
21
|
+
}
|
|
22
|
+
})()
|
|
23
|
+
|
|
24
|
+
const tmpdirDefault = () => fs.mkdtemp(path.join(require('os').tmpdir(), 'compile-'))
|
|
25
|
+
|
|
26
|
+
const getTmp = async (content, tmpdir) => {
|
|
27
|
+
const cwd = await tmpdir()
|
|
28
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
29
|
+
|
|
30
|
+
const filepath = path.join(cwd, 'index.js')
|
|
31
|
+
await fs.writeFile(filepath, content)
|
|
32
|
+
|
|
33
|
+
const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
|
|
34
|
+
return { filepath, cwd, content, cleanup }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = async (snippet, tmpdir = tmpdirDefault) => {
|
|
38
|
+
const compiledTemplate = generateTemplate(snippet)
|
|
39
|
+
const dependencies = detectDependencies(compiledTemplate)
|
|
40
|
+
const tmp = await getTmp(transformDependencies(compiledTemplate), tmpdir)
|
|
41
|
+
|
|
42
|
+
await $(packageManager.init, { cwd: tmp.cwd })
|
|
43
|
+
await $(`${packageManager.install} ${dependencies.join(' ')}`, {
|
|
44
|
+
cwd: tmp.cwd
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const result = await esbuild.build({
|
|
48
|
+
entryPoints: [tmp.filepath],
|
|
49
|
+
bundle: true,
|
|
50
|
+
minify: MINIFY,
|
|
51
|
+
write: false,
|
|
52
|
+
platform: 'node'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await tmp.cleanup()
|
|
56
|
+
return getTmp(result.outputFiles[0].text, tmpdir)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports.detectDependencies = detectDependencies
|
|
60
|
+
module.exports.transformDependencies = transformDependencies
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
module.exports = dependency => {
|
|
4
|
+
if (dependency.startsWith('@')) {
|
|
5
|
+
// Handle scoped packages
|
|
6
|
+
const slashIndex = dependency.indexOf('/')
|
|
7
|
+
if (slashIndex !== -1) {
|
|
8
|
+
const atVersionIndex = dependency.indexOf('@', slashIndex)
|
|
9
|
+
if (atVersionIndex !== -1) {
|
|
10
|
+
// Scoped package with version
|
|
11
|
+
const packageName = dependency.substring(0, atVersionIndex)
|
|
12
|
+
const version = dependency.substring(atVersionIndex + 1)
|
|
13
|
+
return `${packageName}@${version}`
|
|
14
|
+
} else {
|
|
15
|
+
// Scoped package without explicit version
|
|
16
|
+
return `${dependency}@latest`
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
// Non-scoped packages
|
|
21
|
+
const atVersionIndex = dependency.indexOf('@')
|
|
22
|
+
if (atVersionIndex !== -1) {
|
|
23
|
+
// Non-scoped package with version
|
|
24
|
+
const packageName = dependency.substring(0, atVersionIndex)
|
|
25
|
+
const version = dependency.substring(atVersionIndex + 1)
|
|
26
|
+
return `${packageName}@${version}`
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Non-scoped package without explicit version
|
|
30
|
+
return `${dependency}@latest`
|
|
31
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const acorn = require('acorn')
|
|
4
|
+
const walk = require('acorn-walk')
|
|
5
|
+
|
|
6
|
+
module.exports = code => {
|
|
7
|
+
const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
|
|
8
|
+
|
|
9
|
+
let newCode = ''
|
|
10
|
+
let lastIndex = 0
|
|
11
|
+
|
|
12
|
+
// Helper function to process and transform nodes
|
|
13
|
+
const processNode = node => {
|
|
14
|
+
if (node.type === 'Literal' && node.value.includes('@')) {
|
|
15
|
+
// Check if it's a scoped module
|
|
16
|
+
if (node.value.startsWith('@')) {
|
|
17
|
+
// Handle scoped packages
|
|
18
|
+
const slashIndex = node.value.indexOf('/')
|
|
19
|
+
if (slashIndex !== -1) {
|
|
20
|
+
const atVersionIndex = node.value.indexOf('@', slashIndex)
|
|
21
|
+
const moduleName =
|
|
22
|
+
atVersionIndex !== -1 ? node.value.substring(0, atVersionIndex) : node.value
|
|
23
|
+
// Append code before this node
|
|
24
|
+
newCode += code.substring(lastIndex, node.start)
|
|
25
|
+
// Append transformed dependency
|
|
26
|
+
newCode += `'${moduleName}'`
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// Handle non-scoped packages
|
|
30
|
+
const [moduleName] = node.value.split('@')
|
|
31
|
+
// Append code before this node
|
|
32
|
+
newCode += code.substring(lastIndex, node.start)
|
|
33
|
+
// Append transformed dependency
|
|
34
|
+
newCode += `'${moduleName}'`
|
|
35
|
+
}
|
|
36
|
+
// Update lastIndex to end of current node
|
|
37
|
+
lastIndex = node.end
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Traverse the AST to find require and import declarations
|
|
42
|
+
walk.simple(ast, {
|
|
43
|
+
CallExpression (node) {
|
|
44
|
+
if (node.callee.name === 'require' && node.arguments.length === 1) {
|
|
45
|
+
processNode(node.arguments[0])
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
ImportDeclaration (node) {
|
|
49
|
+
processNode(node.source)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Append remaining code after last modified dependency
|
|
54
|
+
newCode += code.substring(lastIndex)
|
|
55
|
+
|
|
56
|
+
return newCode
|
|
57
|
+
}
|
package/src/index.js
CHANGED
|
@@ -20,9 +20,9 @@ const flags = ({ filename, memory }) => {
|
|
|
20
20
|
return flags.join(' ')
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
module.exports = (snippet, { timeout
|
|
23
|
+
module.exports = (snippet, { tmpdir, timeout, memory, throwError = true } = {}) => {
|
|
24
24
|
if (!['function', 'string'].includes(typeof snippet)) throw new TypeError('Expected a function')
|
|
25
|
-
const compilePromise = compile(snippet)
|
|
25
|
+
const compilePromise = compile(snippet, tmpdir)
|
|
26
26
|
|
|
27
27
|
const fn = async (...args) => {
|
|
28
28
|
let duration
|
package/src/compile.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const { execSync } = require('child_process')
|
|
4
|
-
const walk = require('acorn-walk')
|
|
5
|
-
const esbuild = require('esbuild')
|
|
6
|
-
const fs = require('fs/promises')
|
|
7
|
-
const { tmpdir } = require('os')
|
|
8
|
-
const acorn = require('acorn')
|
|
9
|
-
const $ = require('tinyspawn')
|
|
10
|
-
const path = require('path')
|
|
11
|
-
|
|
12
|
-
const generateTemplate = require('./template')
|
|
13
|
-
|
|
14
|
-
const MINIFY = process.env.ISOLATED_FUNCTIONS_MINIFY !== 'false'
|
|
15
|
-
|
|
16
|
-
const packageManager = (() => {
|
|
17
|
-
try {
|
|
18
|
-
execSync('which pnpm').toString().trim()
|
|
19
|
-
return { init: 'pnpm init', install: 'pnpm install' }
|
|
20
|
-
} catch {
|
|
21
|
-
return { init: 'npm init --yes', install: 'npm install' }
|
|
22
|
-
}
|
|
23
|
-
})()
|
|
24
|
-
|
|
25
|
-
// Function to detect require and import statements using acorn
|
|
26
|
-
const detectDependencies = code => {
|
|
27
|
-
const dependencies = new Set()
|
|
28
|
-
|
|
29
|
-
// Parse the code into an AST
|
|
30
|
-
const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
|
|
31
|
-
|
|
32
|
-
// Traverse the AST to find require and import statements
|
|
33
|
-
walk.simple(ast, {
|
|
34
|
-
CallExpression (node) {
|
|
35
|
-
if (
|
|
36
|
-
node.callee.name === 'require' &&
|
|
37
|
-
node.arguments.length === 1 &&
|
|
38
|
-
node.arguments[0].type === 'Literal'
|
|
39
|
-
) {
|
|
40
|
-
const dependency = node.arguments[0].value
|
|
41
|
-
// Check if the dependency string contains '@' symbol for versioning
|
|
42
|
-
if (dependency.includes('@')) {
|
|
43
|
-
// Split by '@' to separate module name and version
|
|
44
|
-
const parts = dependency.split('@')
|
|
45
|
-
// Handle edge case where module name might also contain '@' (scoped packages)
|
|
46
|
-
const moduleName = parts.length > 2 ? `${parts[0]}@${parts[1]}` : parts[0]
|
|
47
|
-
const version = parts[parts.length - 1]
|
|
48
|
-
dependencies.add(`${moduleName}@${version}`)
|
|
49
|
-
} else {
|
|
50
|
-
dependencies.add(`${dependency}@latest`)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
ImportDeclaration (node) {
|
|
55
|
-
const source = node.source.value
|
|
56
|
-
if (source.includes('@')) {
|
|
57
|
-
const parts = source.split('@')
|
|
58
|
-
const moduleName = parts.length > 2 ? `${parts[0]}@${parts[1]}` : parts[0]
|
|
59
|
-
const version = parts[parts.length - 1]
|
|
60
|
-
dependencies.add(`${moduleName}@${version}`)
|
|
61
|
-
} else {
|
|
62
|
-
dependencies.add(`${source}@latest`)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
return Array.from(dependencies)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const transformDependencies = code => {
|
|
71
|
-
const ast = acorn.parse(code, { ecmaVersion: 2020, sourceType: 'module' })
|
|
72
|
-
|
|
73
|
-
let newCode = ''
|
|
74
|
-
let lastIndex = 0
|
|
75
|
-
|
|
76
|
-
// Helper function to process and transform nodes
|
|
77
|
-
const processNode = node => {
|
|
78
|
-
if (node.type === 'Literal' && node.value.includes('@')) {
|
|
79
|
-
// Extract module name without version
|
|
80
|
-
const [moduleName] = node.value.split('@')
|
|
81
|
-
// Append code before this node
|
|
82
|
-
newCode += code.substring(lastIndex, node.start)
|
|
83
|
-
// Append transformed dependency
|
|
84
|
-
newCode += `'${moduleName}'`
|
|
85
|
-
// Update lastIndex to end of current node
|
|
86
|
-
lastIndex = node.end
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Traverse the AST to find require and import declarations
|
|
91
|
-
walk.simple(ast, {
|
|
92
|
-
CallExpression (node) {
|
|
93
|
-
if (node.callee.name === 'require' && node.arguments.length === 1) {
|
|
94
|
-
processNode(node.arguments[0])
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
ImportDeclaration (node) {
|
|
98
|
-
processNode(node.source)
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Append remaining code after last modified dependency
|
|
103
|
-
newCode += code.substring(lastIndex)
|
|
104
|
-
|
|
105
|
-
return newCode
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const getTmp = async content => {
|
|
109
|
-
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
|
|
110
|
-
await fs.mkdir(cwd, { recursive: true })
|
|
111
|
-
|
|
112
|
-
const filepath = path.join(cwd, 'index.js')
|
|
113
|
-
await fs.writeFile(filepath, content)
|
|
114
|
-
|
|
115
|
-
const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
|
|
116
|
-
return { filepath, cwd, content, cleanup }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
module.exports = async snippet => {
|
|
120
|
-
const compiledTemplate = generateTemplate(snippet)
|
|
121
|
-
const dependencies = detectDependencies(compiledTemplate)
|
|
122
|
-
const tmp = await getTmp(transformDependencies(compiledTemplate))
|
|
123
|
-
|
|
124
|
-
await $(packageManager.init, { cwd: tmp.cwd })
|
|
125
|
-
await $(`${packageManager.install} ${dependencies.join(' ')}`, {
|
|
126
|
-
cwd: tmp.cwd
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const result = await esbuild.build({
|
|
130
|
-
entryPoints: [tmp.filepath],
|
|
131
|
-
bundle: true,
|
|
132
|
-
minify: MINIFY,
|
|
133
|
-
write: false,
|
|
134
|
-
platform: 'node'
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
await tmp.cleanup()
|
|
138
|
-
return getTmp(result.outputFiles[0].text)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
module.exports.detectDependencies = detectDependencies
|