-
Notifications
You must be signed in to change notification settings - Fork 1
/
compiler.go
193 lines (162 loc) · 4.65 KB
/
compiler.go
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
// This compiler was inspired from grafana k6 project
// A big kudos goes to goja library
// Do check it out: https://github.com/dop251/goja
package compiler
import (
_ "embed"
"errors"
"fmt"
"strings"
"sync"
"github.com/1-platform/api-catalog/internal/cli/compiler/modules"
"github.com/1-platform/api-catalog/internal/cli/reportmanager"
"github.com/dop251/goja"
)
//go:embed babel.min.js
var babelBundle string
var (
compileBabelOnce sync.Once
globalBabelCode *goja.Program
errGlobalBabelCode error
)
var ErrExceptionInPluginCode = errors.New("plugin code error")
type Logger interface {
Info(str string)
Warn(str string)
Error(str string)
Log(str string)
}
type Compiler struct {
babel *Babel
ModuleLoader *modules.ModuleLoader
logger Logger
}
func New(logger Logger) (*Compiler, error) {
runtime := NewRuntime()
setConsole(runtime, logger)
moduleLoader := modules.New(runtime)
b, err := newBabel(runtime)
if err != nil {
return nil, err
}
cmp := &Compiler{babel: b, ModuleLoader: moduleLoader, logger: logger}
return cmp, nil
}
type Babel struct {
runtime *goja.Runtime
this goja.Value
transformer goja.Callable
}
func NewRuntime() *goja.Runtime {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
return runtime
}
// to support console logging
func setConsole(runtime *goja.Runtime, logger Logger) {
runtime.Set("console", map[string]func(goja.FunctionCall) goja.Value{
"log": func(fc goja.FunctionCall) goja.Value {
var sb strings.Builder
for _, r := range fc.Arguments {
sb.WriteString(r.String())
}
logger.Log(sb.String())
return nil
},
"error": func(fc goja.FunctionCall) goja.Value {
var sb strings.Builder
for _, r := range fc.Arguments {
sb.WriteString(r.String())
}
logger.Error(sb.String())
return nil
},
"warn": func(fc goja.FunctionCall) goja.Value {
var sb strings.Builder
for _, r := range fc.Arguments {
sb.WriteString(r.String())
}
logger.Warn(sb.String())
return nil
},
})
}
// console logger
func newBabel(runtime *goja.Runtime) (*Babel, error) {
// load up and compile babel file only once in a run
// for concurrency
compileBabelOnce.Do(func() {
globalBabelCode, errGlobalBabelCode = goja.Compile("babel.js", babelBundle, false)
})
if errGlobalBabelCode != nil {
return nil, errGlobalBabelCode
}
_, err := runtime.RunProgram(globalBabelCode)
if err != nil {
return nil, err
}
// REF: https://babeljs.io/docs/en/babel-standalone
babel := runtime.Get("Babel")
b := &Babel{runtime: runtime, this: babel}
if err := runtime.ExportTo(babel.ToObject(runtime).Get("transform"), &b.transformer); err != nil {
return nil, err
}
return b, nil
}
func (c *Compiler) Transform(rawCode string) (*goja.Program, error) {
// change the code to commonjs using babel
v, err := c.babel.transformer(c.babel.this, c.babel.runtime.ToValue(rawCode), c.babel.runtime.ToValue(map[string]interface{}{
"presets": []string{"env"},
}))
if err != nil {
return nil, err
}
code := v.ToObject(c.babel.runtime).Get("code").String()
// wrap the commonjs module inside a function
// This will private scope each functions we execute
// Compile to a goja program thus can be executed anytime with goja
pgm, err := goja.Compile("test", fmt.Sprintf(`(function(exports){
%s
})`, code), true)
if err != nil {
return nil, err
}
return pgm, nil
}
type KeyValuePairs struct {
Key string `json:"key"`
Value string `json:"value"`
}
// these are the data that will be given to js code execution env
type RunConfig struct {
ApiSchema map[string]interface{} `json:"schema"`
Type string `json:"type"`
SetScore func(category string, score float32) `json:"setScore"`
Report func(body *reportmanager.ReportDef) `json:"report"`
}
func (c *Compiler) Run(pgm *goja.Program, cfg *RunConfig, ruleOpt map[string]any) error {
v, err := c.babel.runtime.RunProgram(pgm)
if err != nil {
return err
}
// the wrapped function is preparing to execute
call, ok := goja.AssertFunction(v)
if !ok {
return fmt.Errorf("failed to get exports")
}
// the argument export
export := c.babel.runtime.NewObject()
// execute the wrapper function now export contains default function
call(goja.Undefined(), export)
// execute the default function with configuration passed
fn := export.Get("default")
call, ok = goja.AssertFunction(fn)
if !ok {
return fmt.Errorf("failed to get exports")
}
_, err = call(goja.Undefined(), c.babel.runtime.ToValue(cfg), c.babel.runtime.ToValue(ruleOpt))
if err != nil {
return fmt.Errorf("%s%w", err.Error(), ErrExceptionInPluginCode)
}
return nil
}