go.ketch.com/lib/goja_nodejs@v0.0.1/require/module.go (about)

     1  package require
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"path"
     7  	"path/filepath"
     8  	"runtime"
     9  	"sync"
    10  	"syscall"
    11  	"text/template"
    12  
    13  	js "go.ketch.com/lib/goja"
    14  	"go.ketch.com/lib/goja/parser"
    15  )
    16  
    17  type ModuleLoader func(*js.Runtime, *js.Object)
    18  
    19  // SourceLoader represents a function that returns a file data at a given path.
    20  // The function should return ModuleFileDoesNotExistError if the file either doesn't exist or is a directory.
    21  // This error will be ignored by the resolver and the search will continue. Any other errors will be propagated.
    22  type SourceLoader func(path string) ([]byte, error)
    23  
    24  var (
    25  	InvalidModuleError     = errors.New("Invalid module")
    26  	IllegalModuleNameError = errors.New("Illegal module name")
    27  
    28  	ModuleFileDoesNotExistError = errors.New("module file does not exist")
    29  )
    30  
    31  var native map[string]ModuleLoader
    32  
    33  // Registry contains a cache of compiled modules which can be used by multiple Runtimes
    34  type Registry struct {
    35  	sync.Mutex
    36  	native   map[string]ModuleLoader
    37  	compiled map[string]*js.Program
    38  
    39  	srcLoader     SourceLoader
    40  	globalFolders []string
    41  }
    42  
    43  type RequireModule struct {
    44  	r           *Registry
    45  	runtime     *js.Runtime
    46  	modules     map[string]*js.Object
    47  	nodeModules map[string]*js.Object
    48  }
    49  
    50  func NewRegistry(opts ...Option) *Registry {
    51  	r := &Registry{}
    52  
    53  	for _, opt := range opts {
    54  		opt(r)
    55  	}
    56  
    57  	return r
    58  }
    59  
    60  func NewRegistryWithLoader(srcLoader SourceLoader) *Registry {
    61  	return NewRegistry(WithLoader(srcLoader))
    62  }
    63  
    64  type Option func(*Registry)
    65  
    66  // WithLoader sets a function which will be called by the require() function in order to get a source code for a
    67  // module at the given path. The same function will be used to get external source maps.
    68  // Note, this only affects the modules loaded by the require() function. If you need to use it as a source map
    69  // loader for code parsed in a different way (such as runtime.RunString() or eval()), use (*Runtime).SetParserOptions()
    70  func WithLoader(srcLoader SourceLoader) Option {
    71  	return func(r *Registry) {
    72  		r.srcLoader = srcLoader
    73  	}
    74  }
    75  
    76  // WithGlobalFolders appends the given paths to the registry's list of
    77  // global folders to search if the requested module is not found
    78  // elsewhere.  By default, a registry's global folders list is empty.
    79  // In the reference Node.js implementation, the default global folders
    80  // list is $NODE_PATH, $HOME/.node_modules, $HOME/.node_libraries and
    81  // $PREFIX/lib/node, see
    82  // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders.
    83  func WithGlobalFolders(globalFolders ...string) Option {
    84  	return func(r *Registry) {
    85  		r.globalFolders = globalFolders
    86  	}
    87  }
    88  
    89  // Enable adds the require() function to the specified runtime.
    90  func (r *Registry) Enable(runtime *js.Runtime) *RequireModule {
    91  	rrt := &RequireModule{
    92  		r:           r,
    93  		runtime:     runtime,
    94  		modules:     make(map[string]*js.Object),
    95  		nodeModules: make(map[string]*js.Object),
    96  	}
    97  
    98  	runtime.Set("require", rrt.require)
    99  	return rrt
   100  }
   101  
   102  func (r *Registry) RegisterNativeModule(name string, loader ModuleLoader) {
   103  	r.Lock()
   104  	defer r.Unlock()
   105  
   106  	if r.native == nil {
   107  		r.native = make(map[string]ModuleLoader)
   108  	}
   109  	name = filepathClean(name)
   110  	r.native[name] = loader
   111  }
   112  
   113  // DefaultSourceLoader is used if none was set (see WithLoader()). It simply loads files from the host's filesystem.
   114  func DefaultSourceLoader(filename string) ([]byte, error) {
   115  	fp := filepath.FromSlash(filename)
   116  	data, err := os.ReadFile(fp)
   117  	if err != nil {
   118  		if os.IsNotExist(err) || errors.Is(err, syscall.EISDIR) {
   119  			err = ModuleFileDoesNotExistError
   120  		} else if runtime.GOOS == "windows" {
   121  			if errors.Is(err, syscall.Errno(0x7b)) { // ERROR_INVALID_NAME, The filename, directory name, or volume label syntax is incorrect.
   122  				err = ModuleFileDoesNotExistError
   123  			} else {
   124  				// temporary workaround for https://go.ketch.com/lib/goja_nodejs/issues/21
   125  				fi, err1 := os.Stat(fp)
   126  				if err1 == nil && fi.IsDir() {
   127  					err = ModuleFileDoesNotExistError
   128  				}
   129  			}
   130  		}
   131  	}
   132  	return data, err
   133  }
   134  
   135  func (r *Registry) getSource(p string) ([]byte, error) {
   136  	srcLoader := r.srcLoader
   137  	if srcLoader == nil {
   138  		srcLoader = DefaultSourceLoader
   139  	}
   140  	return srcLoader(p)
   141  }
   142  
   143  func (r *Registry) getCompiledSource(p string) (*js.Program, error) {
   144  	r.Lock()
   145  	defer r.Unlock()
   146  
   147  	prg := r.compiled[p]
   148  	if prg == nil {
   149  		buf, err := r.getSource(p)
   150  		if err != nil {
   151  			return nil, err
   152  		}
   153  		s := string(buf)
   154  
   155  		if path.Ext(p) == ".json" {
   156  			s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
   157  		}
   158  
   159  		source := "(function(exports, require, module) {" + s + "\n})"
   160  		parsed, err := js.Parse(p, source, parser.WithSourceMapLoader(r.srcLoader))
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		prg, err = js.CompileAST(parsed, false)
   165  		if err == nil {
   166  			if r.compiled == nil {
   167  				r.compiled = make(map[string]*js.Program)
   168  			}
   169  			r.compiled[p] = prg
   170  		}
   171  		return prg, err
   172  	}
   173  	return prg, nil
   174  }
   175  
   176  func (r *RequireModule) require(call js.FunctionCall) js.Value {
   177  	ret, err := r.Require(call.Argument(0).String())
   178  	if err != nil {
   179  		if _, ok := err.(*js.Exception); !ok {
   180  			panic(r.runtime.NewGoError(err))
   181  		}
   182  		panic(err)
   183  	}
   184  	return ret
   185  }
   186  
   187  func filepathClean(p string) string {
   188  	return path.Clean(p)
   189  }
   190  
   191  // Require can be used to import modules from Go source (similar to JS require() function).
   192  func (r *RequireModule) Require(p string) (ret js.Value, err error) {
   193  	module, err := r.resolve(p)
   194  	if err != nil {
   195  		return
   196  	}
   197  	ret = module.Get("exports")
   198  	return
   199  }
   200  
   201  func Require(runtime *js.Runtime, name string) js.Value {
   202  	if r, ok := js.AssertFunction(runtime.Get("require")); ok {
   203  		mod, err := r(js.Undefined(), runtime.ToValue(name))
   204  		if err != nil {
   205  			panic(err)
   206  		}
   207  		return mod
   208  	}
   209  	panic(runtime.NewTypeError("Please enable require for this runtime using new(require.Registry).Enable(runtime)"))
   210  }
   211  
   212  func RegisterNativeModule(name string, loader ModuleLoader) {
   213  	if native == nil {
   214  		native = make(map[string]ModuleLoader)
   215  	}
   216  	name = filepathClean(name)
   217  	native[name] = loader
   218  }