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 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
- - [timeout](#timeout-1)
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
- ##### timeout
236
+ ##### tmpdir
187
237
 
188
- Type: `number`
238
+ Type: `function`<br>
239
+ Default: `fs.mkdtemp(path.join(require('os').tmpdir(), 'compile-'))`
189
240
 
190
- Set the functino memory limit, in megabytes.
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",
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 = 0, memory, throwError = true } = {}) => {
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