github.com/dop251/goja_nodejs@v0.0.0-20240418154818-2aae10d4cbcf/require/module.go (about) 1 package require 2 3 import ( 4 "errors" 5 "io" 6 "io/fs" 7 "os" 8 "path" 9 "path/filepath" 10 "runtime" 11 "sync" 12 "syscall" 13 "text/template" 14 15 js "github.com/dop251/goja" 16 "github.com/dop251/goja/parser" 17 ) 18 19 type ModuleLoader func(*js.Runtime, *js.Object) 20 21 // SourceLoader represents a function that returns a file data at a given path. 22 // The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory. 23 // This error will be ignored by the resolver and the search will continue. Any other errors will be propagated. 24 type SourceLoader func(path string) ([]byte, error) 25 26 var ( 27 InvalidModuleError = errors.New("Invalid module") 28 IllegalModuleNameError = errors.New("Illegal module name") 29 NoSuchBuiltInModuleError = errors.New("No such built-in module") 30 ModuleFileDoesNotExistError = errors.New("module file does not exist") 31 ) 32 33 var native, builtin map[string]ModuleLoader 34 35 // Registry contains a cache of compiled modules which can be used by multiple Runtimes 36 type Registry struct { 37 sync.Mutex 38 native map[string]ModuleLoader 39 compiled map[string]*js.Program 40 41 srcLoader SourceLoader 42 globalFolders []string 43 } 44 45 type RequireModule struct { 46 r *Registry 47 runtime *js.Runtime 48 modules map[string]*js.Object 49 nodeModules map[string]*js.Object 50 } 51 52 func NewRegistry(opts ...Option) *Registry { 53 r := &Registry{} 54 55 for _, opt := range opts { 56 opt(r) 57 } 58 59 return r 60 } 61 62 func NewRegistryWithLoader(srcLoader SourceLoader) *Registry { 63 return NewRegistry(WithLoader(srcLoader)) 64 } 65 66 type Option func(*Registry) 67 68 // WithLoader sets a function which will be called by the require() function in order to get a source code for a 69 // module at the given path. The same function will be used to get external source maps. 70 // Note, this only affects the modules loaded by the require() function. If you need to use it as a source map 71 // loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions() 72 func WithLoader(srcLoader SourceLoader) Option { 73 return func(r *Registry) { 74 r.srcLoader = srcLoader 75 } 76 } 77 78 // WithGlobalFolders appends the given paths to the registry's list of 79 // global folders to search if the requested module is not found 80 // elsewhere. By default, a registry's global folders list is empty. 81 // In the reference Node.js implementation, the default global folders 82 // list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and 83 // $PREFIX/lib/node, see 84 // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders. 85 func WithGlobalFolders(globalFolders ...string) Option { 86 return func(r *Registry) { 87 r.globalFolders = globalFolders 88 } 89 } 90 91 // Enable adds the require() function to the specified runtime. 92 func (r *Registry) Enable(runtime *js.Runtime) *RequireModule { 93 rrt := &RequireModule{ 94 r: r, 95 runtime: runtime, 96 modules: make(map[string]*js.Object), 97 nodeModules: make(map[string]*js.Object), 98 } 99 100 runtime.Set("require", rrt.require) 101 return rrt 102 } 103 104 func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) { 105 r.Lock() 106 defer r.Unlock() 107 108 if r.native == nil { 109 r.native = make(map[string]ModuleLoader) 110 } 111 name = filepathClean(name) 112 r.native[name] = loader 113 } 114 115 // DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem. 116 func DefaultSourceLoader(filename string) ([]byte, error) { 117 fp := filepath.FromSlash(filename) 118 f, err := os.Open(fp) 119 if err != nil { 120 if errors.Is(err, fs.ErrNotExist) { 121 err = ModuleFileDoesNotExistError 122 } else if runtime.GOOS == "windows" { 123 if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect. 124 err = ModuleFileDoesNotExistError 125 } 126 } 127 return nil, err 128 } 129 130 defer f.Close() 131 // On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories 132 // which means we cannot rely on read() returning an error, we have to do stat() instead. 133 if fi, err := f.Stat(); err == nil { 134 if fi.IsDir() { 135 return nil, ModuleFileDoesNotExistError 136 } 137 } else { 138 return nil, err 139 } 140 return io.ReadAll(f) 141 } 142 143 func (r *Registry) getSource(p string) ([]byte, error) { 144 srcLoader := r.srcLoader 145 if srcLoader == nil { 146 srcLoader = DefaultSourceLoader 147 } 148 return srcLoader(p) 149 } 150 151 func (r *Registry) getCompiledSource(p string) (*js.Program, error) { 152 r.Lock() 153 defer r.Unlock() 154 155 prg := r.compiled[p] 156 if prg == nil { 157 buf, err := r.getSource(p) 158 if err != nil { 159 return nil, err 160 } 161 s := string(buf) 162 163 if path.Ext(p) == ".json" { 164 s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')" 165 } 166 167 source := "(function(exports, require, module) {" + s + "\n})" 168 parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader)) 169 if err != nil { 170 return nil, err 171 } 172 prg, err = js.CompileAST(parsed, false) 173 if err == nil { 174 if r.compiled == nil { 175 r.compiled = make(map[string]*js.Program) 176 } 177 r.compiled[p] = prg 178 } 179 return prg, err 180 } 181 return prg, nil 182 } 183 184 func (r *RequireModule) require(call js.FunctionCall) js.Value { 185 ret, err := r.Require(call.Argument(0).String()) 186 if err != nil { 187 if _, ok := err.(*js.Exception); !ok { 188 panic(r.runtime.NewGoError(err)) 189 } 190 panic(err) 191 } 192 return ret 193 } 194 195 func filepathClean(p string) string { 196 return path.Clean(p) 197 } 198 199 // Require can be used to import modules from Go source (similar to JS require() function). 200 func (r *RequireModule) Require(p string) (ret js.Value, err error) { 201 module, err := r.resolve(p) 202 if err != nil { 203 return 204 } 205 ret = module.Get("exports") 206 return 207 } 208 209 func Require(runtime *js.Runtime, name string) js.Value { 210 if r, ok := js.AssertFunction(runtime.Get("require")); ok { 211 mod, err := r(js.Undefined(), runtime.ToValue(name)) 212 if err != nil { 213 panic(err) 214 } 215 return mod 216 } 217 panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)")) 218 } 219 220 // RegisterNativeModule registers a module that isn't loaded through a SourceLoader, but rather through 221 // a provided ModuleLoader. Typically, this will be a module implemented in Go (although theoretically 222 // it can be anything, depending on the ModuleLoader implementation). 223 // Such modules take precedence over modules loaded through a SourceLoader, i.e. if a module name resolves as 224 // native, the native module is loaded, and the SourceLoader is not consulted. 225 // The binding is global and affects all instances of Registry. 226 // It should be called from a package init() function as it may not be used concurrently with require() calls. 227 // For registry-specific bindings see Registry.RegisterNativeModule. 228 func RegisterNativeModule(name string, loader ModuleLoader) { 229 if native == nil { 230 native = make(map[string]ModuleLoader) 231 } 232 name = filepathClean(name) 233 native[name] = loader 234 } 235 236 // RegisterCoreModule registers a nodejs core module. If the name does not start with "node:", the module 237 // will also be loadable as "node:<name>". Hence, for "builtin" modules (such as buffer, console, etc.) 238 // the name should not include the "node:" prefix, but for prefix-only core modules (such as "node:test") 239 // it should include the prefix. 240 func RegisterCoreModule(name string, loader ModuleLoader) { 241 if builtin == nil { 242 builtin = make(map[string]ModuleLoader) 243 } 244 name = filepathClean(name) 245 builtin[name] = loader 246 }