isolated-function 0.1.5 → 0.1.6
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 +49 -6
- package/package.json +1 -1
- package/src/compile/detect-dependencies.js +35 -0
- package/src/compile/index.js +59 -0
- package/src/compile/parse-dependency.js +31 -0
- package/src/compile/transform-dependencies.js +57 -0
- package/src/index.js +1 -1
- package/src/compile.js +0 -141
package/README.md
CHANGED
|
@@ -22,12 +22,14 @@
|
|
|
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
|
-
- [timeout](#timeout-1)
|
|
31
33
|
- [=\> (fn(\[...args\]), teardown())](#-fnargs-teardown)
|
|
32
34
|
- [fn](#fn)
|
|
33
35
|
- [teardown](#teardown)
|
|
@@ -164,6 +166,34 @@ await fn(100)
|
|
|
164
166
|
// => TimeoutError: Execution timed out
|
|
165
167
|
```
|
|
166
168
|
|
|
169
|
+
### Error handling
|
|
170
|
+
|
|
171
|
+
Any error during **isolated-function** execution will be propagated:
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
const [fn, cleanup] = isolatedFunction(() => {
|
|
175
|
+
throw new TypeError('oh no!')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const result = await fn()
|
|
179
|
+
// TypeError: oh no!
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
You can also return the error instead of throwing it with `{ throwError: false }`:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
const [fn, cleanup] = isolatedFunction(() => {
|
|
186
|
+
throw new TypeError('oh no!')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const { isFullfiled, value } = await fn()
|
|
190
|
+
|
|
191
|
+
if (!isFufilled) {
|
|
192
|
+
console.error(value)
|
|
193
|
+
// TypeError: oh no!
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
167
197
|
## API
|
|
168
198
|
|
|
169
199
|
### isolatedFunction(code, [options])
|
|
@@ -177,17 +207,30 @@ The hosted function to run.
|
|
|
177
207
|
|
|
178
208
|
#### options
|
|
179
209
|
|
|
180
|
-
#####
|
|
210
|
+
##### memory
|
|
181
211
|
|
|
182
|
-
Type: `number
|
|
212
|
+
Type: `number`<br>
|
|
213
|
+
Default: `Infinity`
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
Set the function memory limit, in megabytes.
|
|
216
|
+
|
|
217
|
+
##### throwError
|
|
218
|
+
|
|
219
|
+
Type: `boolean`<br>
|
|
220
|
+
Default: `false`
|
|
221
|
+
|
|
222
|
+
When is `true`, it returns the error rather than throw it.
|
|
223
|
+
|
|
224
|
+
The error will be accessible against `{ value: error, isFufilled: false }` object.
|
|
225
|
+
|
|
226
|
+
Set the function memory limit, in megabytes.
|
|
185
227
|
|
|
186
228
|
##### timeout
|
|
187
229
|
|
|
188
|
-
Type: `number
|
|
230
|
+
Type: `number`<br>
|
|
231
|
+
Default: `Infinity`
|
|
189
232
|
|
|
190
|
-
|
|
233
|
+
Timeout after a specified amount of time, in milliseconds.
|
|
191
234
|
|
|
192
235
|
### => (fn([...args]), teardown())
|
|
193
236
|
|
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.6",
|
|
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,59 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process')
|
|
4
|
+
const esbuild = require('esbuild')
|
|
5
|
+
const fs = require('fs/promises')
|
|
6
|
+
const { tmpdir } = require('os')
|
|
7
|
+
const $ = require('tinyspawn')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const transformDependencies = require('./transform-dependencies')
|
|
11
|
+
const detectDependencies = require('./detect-dependencies')
|
|
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
|
+
const getTmp = async content => {
|
|
26
|
+
const cwd = await fs.mkdtemp(path.join(tmpdir(), 'compile-'))
|
|
27
|
+
await fs.mkdir(cwd, { recursive: true })
|
|
28
|
+
|
|
29
|
+
const filepath = path.join(cwd, 'index.js')
|
|
30
|
+
await fs.writeFile(filepath, content)
|
|
31
|
+
|
|
32
|
+
const cleanup = () => fs.rm(cwd, { recursive: true, force: true })
|
|
33
|
+
return { filepath, cwd, content, cleanup }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = async snippet => {
|
|
37
|
+
const compiledTemplate = generateTemplate(snippet)
|
|
38
|
+
const dependencies = detectDependencies(compiledTemplate)
|
|
39
|
+
const tmp = await getTmp(transformDependencies(compiledTemplate))
|
|
40
|
+
|
|
41
|
+
await $(packageManager.init, { cwd: tmp.cwd })
|
|
42
|
+
await $(`${packageManager.install} ${dependencies.join(' ')}`, {
|
|
43
|
+
cwd: tmp.cwd
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const result = await esbuild.build({
|
|
47
|
+
entryPoints: [tmp.filepath],
|
|
48
|
+
bundle: true,
|
|
49
|
+
minify: MINIFY,
|
|
50
|
+
write: false,
|
|
51
|
+
platform: 'node'
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
await tmp.cleanup()
|
|
55
|
+
return getTmp(result.outputFiles[0].text)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports.detectDependencies = detectDependencies
|
|
59
|
+
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,7 +20,7 @@ const flags = ({ filename, memory }) => {
|
|
|
20
20
|
return flags.join(' ')
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
module.exports = (snippet, { timeout
|
|
23
|
+
module.exports = (snippet, { timeout, memory, throwError = true } = {}) => {
|
|
24
24
|
if (!['function', 'string'].includes(typeof snippet)) throw new TypeError('Expected a function')
|
|
25
25
|
const compilePromise = compile(snippet)
|
|
26
26
|
|
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
|