package require import ( "errors" "io" "io/fs" "os" "path" "path/filepath" "runtime" "sync" "syscall" "text/template" js "apigo.cc/ai/ai/goja" "apigo.cc/ai/ai/goja/parser" ) type ModuleLoader func(*js.Runtime, *js.Object) // SourceLoader represents a function that returns a file data at a given path. // The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory. // This error will be ignored by the resolver and the search will continue. Any other errors will be propagated. type SourceLoader func(path string) ([]byte, error) var ( InvalidModuleError = errors.New("Invalid module") IllegalModuleNameError = errors.New("Illegal module name") NoSuchBuiltInModuleError = errors.New("No such built-in module") ModuleFileDoesNotExistError = errors.New("module file does not exist") ) var native, builtin map[string]ModuleLoader // Registry contains a cache of compiled modules which can be used by multiple Runtimes type Registry struct { sync.Mutex native map[string]ModuleLoader compiled map[string]*js.Program srcLoader SourceLoader globalFolders []string } type RequireModule struct { r *Registry runtime *js.Runtime modules map[string]*js.Object nodeModules map[string]*js.Object } func NewRegistry(opts ...Option) *Registry { r := &Registry{} for _, opt := range opts { opt(r) } return r } func NewRegistryWithLoader(srcLoader SourceLoader) *Registry { return NewRegistry(WithLoader(srcLoader)) } type Option func(*Registry) // WithLoader sets a function which will be called by the require() function in order to get a source code for a // module at the given path. The same function will be used to get external source maps. // Note, this only affects the modules loaded by the require() function. If you need to use it as a source map // loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions() func WithLoader(srcLoader SourceLoader) Option { return func(r *Registry) { r.srcLoader = srcLoader } } // WithGlobalFolders appends the given paths to the registry's list of // global folders to search if the requested module is not found // elsewhere. By default, a registry's global folders list is empty. // In the reference Node.js implementation, the default global folders // list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and // $PREFIX/lib/node, see // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders. func WithGlobalFolders(globalFolders ...string) Option { return func(r *Registry) { r.globalFolders = globalFolders } } // Enable adds the require() function to the specified runtime. func (r *Registry) Enable(runtime *js.Runtime) *RequireModule { rrt := &RequireModule{ r: r, runtime: runtime, modules: make(map[string]*js.Object), nodeModules: make(map[string]*js.Object), } runtime.Set("require", rrt.require) return rrt } func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) { r.Lock() defer r.Unlock() if r.native == nil { r.native = make(map[string]ModuleLoader) } name = filepathClean(name) r.native[name] = loader } // DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem. func DefaultSourceLoader(filename string) ([]byte, error) { fp := filepath.FromSlash(filename) f, err := os.Open(fp) if err != nil { if errors.Is(err, fs.ErrNotExist) { err = ModuleFileDoesNotExistError } else if runtime.GOOS == "windows" { if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect. err = ModuleFileDoesNotExistError } } return nil, err } defer f.Close() // On some systems (e.g. plan9 and FreeBSD) it is possible to use the standard read() call on directories // which means we cannot rely on read() returning an error, we have to do stat() instead. if fi, err := f.Stat(); err == nil { if fi.IsDir() { return nil, ModuleFileDoesNotExistError } } else { return nil, err } return io.ReadAll(f) } func (r *Registry) getSource(p string) ([]byte, error) { srcLoader := r.srcLoader if srcLoader == nil { srcLoader = DefaultSourceLoader } return srcLoader(p) } func (r *Registry) getCompiledSource(p string) (*js.Program, error) { r.Lock() defer r.Unlock() prg := r.compiled[p] if prg == nil { buf, err := r.getSource(p) if err != nil { return nil, err } s := string(buf) if path.Ext(p) == ".json" { s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')" } source := "(function(exports, require, module) {" + s + "\n})" parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader)) if err != nil { return nil, err } prg, err = js.CompileAST(parsed, false) if err == nil { if r.compiled == nil { r.compiled = make(map[string]*js.Program) } r.compiled[p] = prg } return prg, err } return prg, nil } func (r *RequireModule) require(call js.FunctionCall) js.Value { ret, err := r.Require(call.Argument(0).String()) if err != nil { if _, ok := err.(*js.Exception); !ok { panic(r.runtime.NewGoError(err)) } panic(err) } return ret } func filepathClean(p string) string { return path.Clean(p) } // Require can be used to import modules from Go source (similar to JS require() function). func (r *RequireModule) Require(p string) (ret js.Value, err error) { module, err := r.resolve(p) if err != nil { return } ret = module.Get("exports") return } func Require(runtime *js.Runtime, name string) js.Value { if r, ok := js.AssertFunction(runtime.Get("require")); ok { mod, err := r(js.Undefined(), runtime.ToValue(name)) if err != nil { panic(err) } return mod } panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)")) } // RegisterNativeModule registers a module that isn't loaded through a SourceLoader, but rather through // a provided ModuleLoader. Typically, this will be a module implemented in Go (although theoretically // it can be anything, depending on the ModuleLoader implementation). // Such modules take precedence over modules loaded through a SourceLoader, i.e. if a module name resolves as // native, the native module is loaded, and the SourceLoader is not consulted. // The binding is global and affects all instances of Registry. // It should be called from a package init() function as it may not be used concurrently with require() calls. // For registry-specific bindings see Registry.RegisterNativeModule. func RegisterNativeModule(name string, loader ModuleLoader) { if native == nil { native = make(map[string]ModuleLoader) } name = filepathClean(name) native[name] = loader } // RegisterCoreModule registers a nodejs core module. If the name does not start with "node:", the module // will also be loadable as "node:". Hence, for "builtin" modules (such as buffer, console, etc.) // the name should not include the "node:" prefix, but for prefix-only core modules (such as "node:test") // it should include the prefix. func RegisterCoreModule(name string, loader ModuleLoader) { if builtin == nil { builtin = make(map[string]ModuleLoader) } name = filepathClean(name) builtin[name] = loader }