-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
executable file
·141 lines (115 loc) · 3.54 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const realFs = require('fs')
const { resolve } = require('path')
const vm = require('vm')
const recast = require('recast')
const buildNode = recast.types.builders
const realRequire = require
function tryFunction (fn, args) {
if (typeof fn === 'function') {
if (fn.length >= args.length) {
return fn
} else {
throw new Error('Not enough arguments')
}
}
}
// Check if a path is a direct child of the given parent
// This is useful for detecting top-level as child of ast.program
function childOf (path, parent) {
return path.parentPath.parentPath.value === parent
}
function lastChild (path, parent) {
if (!childOf(path, parent)) return false
return parent.body[parent.body.length - 1] === path.node
}
function compileFunction (code, args, isExpression) {
if (!isExpression) {
return vm.runInThisContext(code)
}
// Only exists in Node.js 10+
if (typeof vm.compileFunction === 'function') {
return vm.compileFunction(code, args)
}
return vm.runInThisContext(
`(function (${args.join(', ')}) {${code}})`
)
}
function buildFunction (contents, args, options) {
let compiled
try {
const ast = recast.parse(contents, options)
let isExpression = false
recast.visit(ast, {
visitExpressionStatement (path) {
if (lastChild(path, ast.program)) {
const { expression } = path.node
if (expression.type !== 'ArrowFunctionExpression') {
path.replace(buildNode.returnStatement(path.node.expression))
isExpression = true
return false
}
}
this.traverse(path)
},
// TODO: Only do this if the function declaration is the last node in the program body?
visitFunctionDeclaration (path) {
if (lastChild(path, ast.program)) {
const { id, params, body } = path.node
// Expression-ify the function declaration
const expression = buildNode.functionExpression(id, params, body)
path.replace(buildNode.expressionStatement(expression))
return false
}
this.traverse(path)
},
visitReturnStatement (path) {
if (lastChild(path, ast.program)) {
const { argument } = path.node
if (argument.type === 'FunctionExpression' || argument.type === 'ArrowFunctionExpression') {
path.replace(buildNode.expressionStatement(argument))
return false
}
isExpression = true
}
this.traverse(path)
}
})
const { code } = recast.print(ast)
compiled = compileFunction(code, args, isExpression)
} catch (err) {
const error = new Error('Failed to parse code')
error.originalError = err
throw error
}
return tryFunction(compiled, args)
}
function ensureFunction (maybeFunction, args = [], options = {}) {
const {
cwd = process.cwd(),
fs = realFs,
require = realRequire,
parserOptions = {}
} = options
// Is it already a function?
{
const fn = tryFunction(maybeFunction, args)
if (fn) return fn
}
const path = resolve(cwd, maybeFunction)
// Is it a module?
let mod
try {
mod = require(path)
} catch (err) { }
{
const fn = tryFunction(mod, args)
if (fn) return fn
}
// Is it a path to a file containing the function?
const fileExists = fs.existsSync(path)
if (!fileExists) return buildFunction(maybeFunction, args, parserOptions)
// Is it just a raw js file?
const contents = fs.readFileSync(path).toString()
return buildFunction(contents, args, parserOptions)
}
module.exports = ensureFunction