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 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
- ##### timeout
210
+ ##### memory
181
211
 
182
- Type: `number`
212
+ Type: `number`<br>
213
+ Default: `Infinity`
183
214
 
184
- Timeout after a specified amount of time, in milliseconds.
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
- Set the functino memory limit, in megabytes.
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",
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 = 0, memory, throwError = true } = {}) => {
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