forked from grafana/k6
-
Notifications
You must be signed in to change notification settings - Fork 0
/
resolution.go
166 lines (145 loc) · 4.74 KB
/
resolution.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
package modules
import (
"fmt"
"net/url"
"strings"
"github.com/dop251/goja"
"go.k6.io/k6/js/compiler"
"go.k6.io/k6/loader"
)
// FileLoader is a type alias for a function that returns the contents of the referenced file.
type FileLoader func(specifier *url.URL, name string) ([]byte, error)
type module interface {
instantiate(vu VU) moduleInstance
}
type moduleInstance interface {
execute() error
exports() *goja.Object
}
type moduleCacheElement struct {
mod module
err error
}
// ModuleResolver knows how to get base Module that can be initialized
type ModuleResolver struct {
cache map[string]moduleCacheElement
goModules map[string]interface{}
loadCJS FileLoader
compiler *compiler.Compiler
}
// NewModuleResolver returns a new module resolution instance that will resolve.
// goModules is map of import file to a go module
// loadCJS is used to load commonjs files
func NewModuleResolver(goModules map[string]interface{}, loadCJS FileLoader, c *compiler.Compiler) *ModuleResolver {
return &ModuleResolver{
goModules: goModules,
cache: make(map[string]moduleCacheElement),
loadCJS: loadCJS,
compiler: c,
}
}
func (mr *ModuleResolver) resolveSpecifier(basePWD *url.URL, arg string) (*url.URL, error) {
specifier, err := loader.Resolve(basePWD, arg)
if err != nil {
return nil, err
}
return specifier, nil
}
func (mr *ModuleResolver) requireModule(name string) (module, error) {
mod, ok := mr.goModules[name]
if !ok {
return nil, fmt.Errorf("unknown module: %s", name)
}
if m, ok := mod.(Module); ok {
return &goModule{Module: m}, nil
}
return &baseGoModule{mod: mod}, nil
}
func (mr *ModuleResolver) resolveLoaded(basePWD *url.URL, arg string, data []byte) (module, error) {
specifier, err := mr.resolveSpecifier(basePWD, arg)
if err != nil {
return nil, err
}
// try cache with the final specifier
if cached, ok := mr.cache[specifier.String()]; ok {
return cached.mod, cached.err
}
mod, err := cjsModuleFromString(specifier, data, mr.compiler)
mr.cache[specifier.String()] = moduleCacheElement{mod: mod, err: err}
return mod, err
}
func (mr *ModuleResolver) resolve(basePWD *url.URL, arg string) (module, error) {
if cached, ok := mr.cache[arg]; ok {
return cached.mod, cached.err
}
switch {
case arg == "k6", strings.HasPrefix(arg, "k6/"):
// Builtin or external modules ("k6", "k6/*", or "k6/x/*") are handled
// specially, as they don't exist on the filesystem.
mod, err := mr.requireModule(arg)
mr.cache[arg] = moduleCacheElement{mod: mod, err: err}
return mod, err
default:
specifier, err := mr.resolveSpecifier(basePWD, arg)
if err != nil {
return nil, err
}
// try cache with the final specifier
if cached, ok := mr.cache[specifier.String()]; ok {
return cached.mod, cached.err
}
// Fall back to loading
data, err := mr.loadCJS(specifier, arg)
if err != nil {
mr.cache[specifier.String()] = moduleCacheElement{err: err}
return nil, err
}
mod, err := cjsModuleFromString(specifier, data, mr.compiler)
mr.cache[specifier.String()] = moduleCacheElement{mod: mod, err: err}
return mod, err
}
}
// ModuleSystem is implementing an ESM like module system to resolve js modules for k6 usage
type ModuleSystem struct {
vu VU
instanceCache map[module]moduleInstance
resolver *ModuleResolver
}
// NewModuleSystem returns a new ModuleSystem for the provide VU using the provided resoluter
func NewModuleSystem(resolver *ModuleResolver, vu VU) *ModuleSystem {
return &ModuleSystem{
resolver: resolver,
instanceCache: make(map[module]moduleInstance),
vu: vu,
}
}
// Require is called when a module/file needs to be loaded by a script
func (ms *ModuleSystem) Require(pwd *url.URL, arg string) (*goja.Object, error) {
mod, err := ms.resolver.resolve(pwd, arg)
if err != nil {
return nil, err
}
if instance, ok := ms.instanceCache[mod]; ok {
return instance.exports(), nil
}
instance := mod.instantiate(ms.vu)
ms.instanceCache[mod] = instance
if err = instance.execute(); err != nil {
return nil, err
}
return instance.exports(), nil
}
// RunSourceData runs the provided sourceData and adds it to the cache.
// If a module with the same specifier as the source is already cached
// it will be used instead of reevaluating the source from the provided SourceData.
//
// TODO: this API will likely change as native ESM support will likely not let us have the exports
// as one big goja.Value that we can manipulate
func (ms *ModuleSystem) RunSourceData(source *loader.SourceData) (goja.Value, error) {
specifier := source.URL.String()
pwd := source.URL.JoinPath("../")
if _, err := ms.resolver.resolveLoaded(pwd, specifier, source.Data); err != nil {
return nil, err // TODO wrap as this should never happen
}
return ms.Require(pwd, specifier)
}