1
0
Fork 0

Adding upstream version 0.0~git20250409.f7acab6.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-22 11:36:18 +02:00
parent b9b5d88025
commit 21b930d007
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
51 changed files with 11229 additions and 0 deletions

276
require/module.go Normal file
View file

@ -0,0 +1,276 @@
package require
import (
"errors"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"runtime"
"sync"
"syscall"
"text/template"
js "github.com/dop251/goja"
"github.com/dop251/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)
// PathResolver is a function that should return a canonical path of the path parameter relative to the base. The base
// is expected to be already canonical as it would be a result of a previous call to the PathResolver for all cases
// except for the initial evaluation, but it's a responsibility of the caller to ensure that the name of the script
// is a canonical path. To match Node JS behaviour, it should resolve symlinks.
// The path parameter is the argument of the require() call. The returned value will be supplied to the SourceLoader.
type PathResolver func(base, path string) string
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
pathResolver PathResolver
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
}
}
// WithPathResolver sets a function which will be used to resolve paths (see PathResolver). If not specified, the
// DefaultPathResolver is used.
func WithPathResolver(pathResolver PathResolver) Option {
return func(r *Registry) {
r.pathResolver = pathResolver
}
}
// 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) {
f, err := os.Open(filename)
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)
}
// DefaultPathResolver is used if none was set (see WithPathResolver). It converts the path using filepath.FromSlash(),
// then joins it with base and resolves symlinks on the resulting path.
// Note, it does not make the path absolute, so to match nodejs behaviour, the initial script name should be set
// to an absolute path.
// The implementation is somewhat suboptimal because it runs filepath.EvalSymlinks() on the joint path, not using the
// fact that the base path is already resolved. This is because there is no way to resolve symlinks only in a portion
// of a path without re-implementing a significant part of filepath.FromSlash().
func DefaultPathResolver(base, path string) string {
p := filepath.Join(base, filepath.FromSlash(path))
if resolved, err := filepath.EvalSymlinks(p); err == nil {
p = resolved
}
return p
}
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 filepath.Ext(p) == ".json" {
s = "module.exports = JSON.parse('" + template.JSEscapeString(s) + "')"
}
source := "(function(exports,require,module,__filename,__dirname){" + 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:<name>". 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
}

587
require/module_test.go Normal file
View file

@ -0,0 +1,587 @@
package require
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"syscall"
"testing"
js "github.com/dop251/goja"
)
func mapFileSystemSourceLoader(files map[string]string) SourceLoader {
return func(p string) ([]byte, error) {
s, ok := files[filepath.ToSlash(p)]
if !ok {
return nil, ModuleFileDoesNotExistError
}
return []byte(s), nil
}
}
func TestRequireNativeModule(t *testing.T) {
const SCRIPT = `
var m = require("test/m");
m.test();
`
vm := js.New()
registry := new(Registry)
registry.Enable(vm)
RegisterNativeModule("test/m", func(runtime *js.Runtime, module *js.Object) {
o := module.Get("exports").(*js.Object)
o.Set("test", func(call js.FunctionCall) js.Value {
return runtime.ToValue("passed")
})
})
v, err := vm.RunString(SCRIPT)
if err != nil {
t.Fatal(err)
}
if !v.StrictEquals(vm.ToValue("passed")) {
t.Fatalf("Unexpected result: %v", v)
}
}
func TestRegisterCoreModule(t *testing.T) {
vm := js.New()
registry := new(Registry)
registry.Enable(vm)
RegisterCoreModule("coremod", func(runtime *js.Runtime, module *js.Object) {
o := module.Get("exports").(*js.Object)
o.Set("test", func(call js.FunctionCall) js.Value {
return runtime.ToValue("passed")
})
})
RegisterCoreModule("coremod1", func(runtime *js.Runtime, module *js.Object) {
o := module.Get("exports").(*js.Object)
o.Set("test", func(call js.FunctionCall) js.Value {
return runtime.ToValue("passed1")
})
})
RegisterCoreModule("node:test1", func(runtime *js.Runtime, module *js.Object) {
o := module.Get("exports").(*js.Object)
o.Set("test", func(call js.FunctionCall) js.Value {
return runtime.ToValue("test1 passed")
})
})
registry.RegisterNativeModule("bob", func(runtime *js.Runtime, module *js.Object) {
})
_, err := vm.RunString(`
const m1 = require("coremod");
const m2 = require("node:coremod");
if (m1 !== m2) {
throw new Error("Modules are not equal");
}
if (m1.test() !== "passed") {
throw new Error("m1.test() has failed");
}
const m3 = require("node:coremod1");
const m4 = require("coremod1");
if (m3 !== m4) {
throw new Error("Modules are not equal (1)");
}
if (m3.test() !== "passed1") {
throw new Error("m3.test() has failed");
}
try {
require("node:bob");
} catch (e) {
if (!e.message.includes("No such built-in module")) {
throw e;
}
}
require("bob");
try {
require("test1");
throw new Error("Expected exception");
} catch (e) {
if (!e.message.includes("Invalid module")) {
throw e;
}
}
if (require("node:test1").test() !== "test1 passed") {
throw new Error("test1.test() has failed");
}
`)
if err != nil {
t.Fatal(err)
}
}
func TestRequireRegistryNativeModule(t *testing.T) {
const SCRIPT = `
var log = require("test/log");
log.print('passed');
`
logWithOutput := func(w io.Writer, prefix string) ModuleLoader {
return func(vm *js.Runtime, module *js.Object) {
o := module.Get("exports").(*js.Object)
o.Set("print", func(call js.FunctionCall) js.Value {
fmt.Fprint(w, prefix, call.Argument(0).String())
return js.Undefined()
})
}
}
vm1 := js.New()
buf1 := &bytes.Buffer{}
registry1 := new(Registry)
registry1.Enable(vm1)
registry1.RegisterNativeModule("test/log", logWithOutput(buf1, "vm1 "))
vm2 := js.New()
buf2 := &bytes.Buffer{}
registry2 := new(Registry)
registry2.Enable(vm2)
registry2.RegisterNativeModule("test/log", logWithOutput(buf2, "vm2 "))
_, err := vm1.RunString(SCRIPT)
if err != nil {
t.Fatal(err)
}
s := buf1.String()
if s != "vm1 passed" {
t.Fatalf("vm1: Unexpected result: %q", s)
}
_, err = vm2.RunString(SCRIPT)
if err != nil {
t.Fatal(err)
}
s = buf2.String()
if s != "vm2 passed" {
t.Fatalf("vm2: Unexpected result: %q", s)
}
}
func TestRequire(t *testing.T) {
absPath, err := filepath.Abs("./testdata/m.js")
if err != nil {
t.Fatal(err)
}
isWindows := runtime.GOOS == "windows"
tests := []struct {
path string
ok bool
}{
{
"./testdata/m.js",
true,
},
{
"../require/testdata/m.js",
true,
},
{
absPath,
true,
},
{
`.\testdata\m.js`,
isWindows,
},
{
`..\require\testdata\m.js`,
isWindows,
},
}
const SCRIPT = `
var m = require(testPath);
m.test();
`
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
vm := js.New()
vm.Set("testPath", test.path)
registry := new(Registry)
registry.Enable(vm)
v, err := vm.RunString(SCRIPT)
ok := err == nil
if ok != test.ok {
t.Fatalf("Expected ok to be %v, got %v (%v)", test.ok, ok, err)
}
if !ok {
return
}
if !v.StrictEquals(vm.ToValue("passed")) {
t.Fatalf("Unexpected result: %v", v)
}
})
}
}
func TestSourceLoader(t *testing.T) {
const SCRIPT = `
var m = require("m.js");
m.test();
`
const MODULE = `
function test() {
return "passed1";
}
exports.test = test;
`
vm := js.New()
registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) {
if name == "m.js" {
return []byte(MODULE), nil
}
return nil, errors.New("Module does not exist")
}))
registry.Enable(vm)
v, err := vm.RunString(SCRIPT)
if err != nil {
t.Fatal(err)
}
if !v.StrictEquals(vm.ToValue("passed1")) {
t.Fatalf("Unexpected result: %v", v)
}
}
func TestStrictModule(t *testing.T) {
const SCRIPT = `
var m = require("m.js");
m.test();
`
const MODULE = `
"use strict";
function test() {
var a = "passed1";
eval("var a = 'not passed'");
return a;
}
exports.test = test;
`
vm := js.New()
registry := NewRegistry(WithGlobalFolders("."), WithLoader(func(name string) ([]byte, error) {
if name == "m.js" {
return []byte(MODULE), nil
}
return nil, errors.New("Module does not exist")
}))
registry.Enable(vm)
v, err := vm.RunString(SCRIPT)
if err != nil {
t.Fatal(err)
}
if !v.StrictEquals(vm.ToValue("passed1")) {
t.Fatalf("Unexpected result: %v", v)
}
}
func TestResolve(t *testing.T) {
testRequire := func(src, fpath string, globalFolders []string, fs map[string]string) (*js.Runtime, js.Value, error) {
vm := js.New()
r := NewRegistry(WithGlobalFolders(globalFolders...), WithLoader(mapFileSystemSourceLoader(fs)))
r.Enable(vm)
t.Logf("Require(%s)", fpath)
ret, err := vm.RunScript(path.Join(src, "test.js"), fmt.Sprintf("require('%s')", fpath))
if err != nil {
return nil, nil, err
}
return vm, ret, nil
}
globalFolders := []string{
"/usr/lib/node_modules",
"/home/src/.node_modules",
}
fs := map[string]string{
"/home/src/app/app.js": `exports.name = "app"`,
"/home/src/app2/app2.json": `{"name": "app2"}`,
"/home/src/app3/index.js": `exports.name = "app3"`,
"/home/src/app4/index.json": `{"name": "app4"}`,
"/home/src/app5/package.json": `{"main": "app5.js"}`,
"/home/src/app5/app5.js": `exports.name = "app5"`,
"/home/src/app6/package.json": `{"main": "."}`,
"/home/src/app6/index.js": `exports.name = "app6"`,
"/home/src/app7/package.json": `{"main": "./a/b/c/file.js"}`,
"/home/src/app7/a/b/c/file.js": `exports.name = "app7"`,
"/usr/lib/node_modules/app8": `exports.name = "app8"`,
"/home/src/app9/app9.js": `exports.name = require('./a/file.js').name`,
"/home/src/app9/a/file.js": `exports.name = require('./b/file.js').name`,
"/home/src/app9/a/b/file.js": `exports.name = require('./c/file.js').name`,
"/home/src/app9/a/b/c/file.js": `exports.name = "app9"`,
"/home/src/.node_modules/app10": `exports.name = "app10"`,
"/home/src/app11/app11.js": `exports.name = require('d/file.js').name`,
"/home/src/app11/a/b/c/app11.js": `exports.name = require('d/file.js').name`,
"/home/src/app11/node_modules/d/file.js": `exports.name = "app11"`,
"/app12.js": `exports.name = require('a/file.js').name`,
"/node_modules/a/file.js": `exports.name = "app12"`,
"/app13/app13.js": `exports.name = require('b/file.js').name`,
"/node_modules/b/file.js": `exports.name = "app13"`,
"node_modules/app14/index.js": `exports.name = "app14"`,
"../node_modules/app15/index.js": `exports.name = "app15"`,
}
for i, tc := range []struct {
src string
path string
ok bool
field string
value string
}{
{"/home/src", "./app/app", true, "name", "app"},
{"/home/src", "./app/app.js", true, "name", "app"},
{"/home/src", "./app/bad.js", false, "", ""},
{"/home/src", "./app2/app2", true, "name", "app2"},
{"/home/src", "./app2/app2.json", true, "name", "app2"},
{"/home/src", "./app/bad.json", false, "", ""},
{"/home/src", "./app3", true, "name", "app3"},
{"/home/src", "./appbad", false, "", ""},
{"/home/src", "./app4", true, "name", "app4"},
{"/home/src", "./appbad", false, "", ""},
{"/home/src", "./app5", true, "name", "app5"},
{"/home/src", "./app6", true, "name", "app6"},
{"/home/src", "./app7", true, "name", "app7"},
{"/home/src", "app8", true, "name", "app8"},
{"/home/src", "./app9/app9", true, "name", "app9"},
{"/home/src", "app10", true, "name", "app10"},
{"/home/src", "./app11/app11.js", true, "name", "app11"},
{"/home/src", "./app11/a/b/c/app11.js", true, "name", "app11"},
{"/", "./app12", true, "name", "app12"},
{"/", "./app13/app13", true, "name", "app13"},
{".", "app14", true, "name", "app14"},
{"..", "nonexistent", false, "", ""},
} {
vm, mod, err := testRequire(tc.src, tc.path, globalFolders, fs)
if err != nil {
if tc.ok {
t.Errorf("%d: require() failed: %v", i, err)
}
continue
} else {
if !tc.ok {
t.Errorf("%d: expected to fail, but did not", i)
continue
}
}
f := mod.ToObject(vm).Get(tc.field)
if f == nil {
t.Errorf("%v: field %q not found", i, tc.field)
continue
}
value := f.String()
if value != tc.value {
t.Errorf("%v: got %q expected %q", i, value, tc.value)
}
}
}
func TestRequireCycle(t *testing.T) {
vm := js.New()
r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{
"a.js": `var b = require('./b.js'); exports.done = true;`,
"b.js": `var a = require('./a.js'); exports.done = true;`,
})))
r.Enable(vm)
res, err := vm.RunString(`
var a = require('./a.js');
var b = require('./b.js');
a.done && b.done;
`)
if err != nil {
t.Fatal(err)
}
if v := res.Export(); v != true {
t.Fatalf("Unexpected result: %v", v)
}
}
func TestErrorPropagation(t *testing.T) {
vm := js.New()
r := NewRegistry(WithLoader(mapFileSystemSourceLoader(map[string]string{
"m.js": `throw 'test passed';`,
})))
rr := r.Enable(vm)
_, err := rr.Require("./m")
if err == nil {
t.Fatal("Expected an error")
}
if ex, ok := err.(*js.Exception); ok {
if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
t.Fatalf("Unexpected Exception: %v", ex)
}
} else {
t.Fatal(err)
}
}
func TestSourceMapLoader(t *testing.T) {
vm := js.New()
r := NewRegistry(WithLoader(func(p string) ([]byte, error) {
switch filepath.ToSlash(p) {
case "dir/m.js":
return []byte(`throw 'test passed';
//# sourceMappingURL=m.js.map`), nil
case "dir/m.js.map":
return []byte(`{"version":3,"file":"m.js","sourceRoot":"","sources":["m.ts"],"names":[],"mappings":";AAAA"}
`), nil
}
return nil, ModuleFileDoesNotExistError
}))
rr := r.Enable(vm)
_, err := rr.Require("./dir/m")
if err == nil {
t.Fatal("Expected an error")
}
if ex, ok := err.(*js.Exception); ok {
if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
t.Fatalf("Unexpected Exception: %v", ex)
}
} else {
t.Fatal(err)
}
}
func testsetup() (string, func(), error) {
name, err := os.MkdirTemp("", "goja-nodejs-require-test")
if err != nil {
return "", nil, err
}
return name, func() {
os.RemoveAll(name)
}, nil
}
func TestDefaultModuleLoader(t *testing.T) {
workdir, teardown, err := testsetup()
if err != nil {
t.Fatal(err)
}
defer teardown()
err = os.Chdir(workdir)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("module", 0755)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile("module/index.js", []byte(`throw 'test passed';`), 0644)
if err != nil {
t.Fatal(err)
}
vm := js.New()
r := NewRegistry()
rr := r.Enable(vm)
_, err = rr.Require("./module")
if err == nil {
t.Fatal("Expected an error")
}
if ex, ok := err.(*js.Exception); ok {
if !ex.Value().StrictEquals(vm.ToValue("test passed")) {
t.Fatalf("Unexpected Exception: %v", ex)
}
} else {
t.Fatal(err)
}
}
func TestDefaultPathResolver(t *testing.T) {
workdir, teardown, err := testsetup()
if err != nil {
t.Fatal(err)
}
defer teardown()
err = os.Chdir(workdir)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/a", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/a/node_modules", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("node_modules/b", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Symlink("../../b", "node_modules/a/node_modules/b")
if err != nil {
if runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(1314)) { // ERROR_PRIVILEGE_NOT_HELD
t.Skip("Creating symlinks on Windows requires admin privileges")
}
t.Fatal(err)
}
err = os.WriteFile("node_modules/b/index.js", []byte(``), 0644)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile("node_modules/a/index.js", []byte(`require('b')`), 0644)
if err != nil {
t.Fatal(err)
}
vm := js.New()
r := NewRegistry()
rr := r.Enable(vm)
_, err = rr.Require("a")
if err != nil {
t.Fatal(err)
}
}

275
require/resolve.go Normal file
View file

@ -0,0 +1,275 @@
package require
import (
"encoding/json"
"errors"
"path/filepath"
"runtime"
"strings"
js "github.com/dop251/goja"
)
const NodePrefix = "node:"
func (r *RequireModule) resolvePath(base, name string) string {
if r.r.pathResolver != nil {
return r.r.pathResolver(base, name)
}
return DefaultPathResolver(base, name)
}
// NodeJS module search algorithm described by
// https://nodejs.org/api/modules.html#modules_all_together
func (r *RequireModule) resolve(modpath string) (module *js.Object, err error) {
var start string
err = nil
if !filepath.IsAbs(modpath) {
start = r.getCurrentModulePath()
}
p := r.resolvePath(start, modpath)
if isFileOrDirectoryPath(modpath) {
if module = r.modules[p]; module != nil {
return
}
module, err = r.loadAsFileOrDirectory(p)
if err == nil && module != nil {
r.modules[p] = module
}
} else {
module, err = r.loadNative(modpath)
if err == nil {
return
} else {
if err == InvalidModuleError {
err = nil
} else {
return
}
}
if module = r.nodeModules[p]; module != nil {
return
}
module, err = r.loadNodeModules(modpath, start)
if err == nil && module != nil {
r.nodeModules[p] = module
}
}
if module == nil && err == nil {
err = InvalidModuleError
}
return
}
func (r *RequireModule) loadNative(path string) (*js.Object, error) {
module := r.modules[path]
if module != nil {
return module, nil
}
var ldr ModuleLoader
if ldr = r.r.native[path]; ldr == nil {
ldr = native[path]
}
var isBuiltIn, withPrefix bool
if ldr == nil {
ldr = builtin[path]
if ldr == nil && strings.HasPrefix(path, NodePrefix) {
ldr = builtin[path[len(NodePrefix):]]
if ldr == nil {
return nil, NoSuchBuiltInModuleError
}
withPrefix = true
}
isBuiltIn = true
}
if ldr != nil {
module = r.createModuleObject()
r.modules[path] = module
if isBuiltIn {
if withPrefix {
r.modules[path[len(NodePrefix):]] = module
} else {
if !strings.HasPrefix(path, NodePrefix) {
r.modules[NodePrefix+path] = module
}
}
}
ldr(r.runtime, module)
return module, nil
}
return nil, InvalidModuleError
}
func (r *RequireModule) loadAsFileOrDirectory(path string) (module *js.Object, err error) {
if module, err = r.loadAsFile(path); module != nil || err != nil {
return
}
return r.loadAsDirectory(path)
}
func (r *RequireModule) loadAsFile(path string) (module *js.Object, err error) {
if module, err = r.loadModule(path); module != nil || err != nil {
return
}
p := path + ".js"
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = path + ".json"
return r.loadModule(p)
}
func (r *RequireModule) loadIndex(modpath string) (module *js.Object, err error) {
p := r.resolvePath(modpath, "index.js")
if module, err = r.loadModule(p); module != nil || err != nil {
return
}
p = r.resolvePath(modpath, "index.json")
return r.loadModule(p)
}
func (r *RequireModule) loadAsDirectory(modpath string) (module *js.Object, err error) {
p := r.resolvePath(modpath, "package.json")
buf, err := r.r.getSource(p)
if err != nil {
return r.loadIndex(modpath)
}
var pkg struct {
Main string
}
err = json.Unmarshal(buf, &pkg)
if err != nil || len(pkg.Main) == 0 {
return r.loadIndex(modpath)
}
m := r.resolvePath(modpath, pkg.Main)
if module, err = r.loadAsFile(m); module != nil || err != nil {
return
}
return r.loadIndex(m)
}
func (r *RequireModule) loadNodeModule(modpath, start string) (*js.Object, error) {
return r.loadAsFileOrDirectory(r.resolvePath(start, modpath))
}
func (r *RequireModule) loadNodeModules(modpath, start string) (module *js.Object, err error) {
for _, dir := range r.r.globalFolders {
if module, err = r.loadNodeModule(modpath, dir); module != nil || err != nil {
return
}
}
for {
var p string
if filepath.Base(start) != "node_modules" {
p = filepath.Join(start, "node_modules")
} else {
p = start
}
if module, err = r.loadNodeModule(modpath, p); module != nil || err != nil {
return
}
if start == ".." { // Dir('..') is '.'
break
}
parent := filepath.Dir(start)
if parent == start {
break
}
start = parent
}
return
}
func (r *RequireModule) getCurrentModulePath() string {
var buf [2]js.StackFrame
frames := r.runtime.CaptureCallStack(2, buf[:0])
if len(frames) < 2 {
return "."
}
return filepath.Dir(frames[1].SrcName())
}
func (r *RequireModule) createModuleObject() *js.Object {
module := r.runtime.NewObject()
module.Set("exports", r.runtime.NewObject())
return module
}
func (r *RequireModule) loadModule(path string) (*js.Object, error) {
module := r.modules[path]
if module == nil {
module = r.createModuleObject()
r.modules[path] = module
err := r.loadModuleFile(path, module)
if err != nil {
module = nil
delete(r.modules, path)
if errors.Is(err, ModuleFileDoesNotExistError) {
err = nil
}
}
return module, err
}
return module, nil
}
func (r *RequireModule) loadModuleFile(path string, jsModule *js.Object) error {
prg, err := r.r.getCompiledSource(path)
if err != nil {
return err
}
f, err := r.runtime.RunProgram(prg)
if err != nil {
return err
}
if call, ok := js.AssertFunction(f); ok {
jsExports := jsModule.Get("exports")
jsRequire := r.runtime.Get("require")
// Run the module source, with "jsExports" as "this",
// "jsExports" as the "exports" variable, "jsRequire"
// as the "require" variable and "jsModule" as the
// "module" variable (Nodejs capable).
_, err = call(jsExports, jsExports, jsRequire, jsModule, r.runtime.ToValue(path), r.runtime.ToValue(filepath.Dir(path)))
if err != nil {
return err
}
} else {
return InvalidModuleError
}
return nil
}
func isFileOrDirectoryPath(path string) bool {
result := path == "." || path == ".." ||
strings.HasPrefix(path, "/") ||
strings.HasPrefix(path, "./") ||
strings.HasPrefix(path, "../")
if runtime.GOOS == "windows" {
result = result ||
strings.HasPrefix(path, `.\`) ||
strings.HasPrefix(path, `..\`) ||
filepath.IsAbs(path)
}
return result
}

7
require/testdata/m.js vendored Normal file
View file

@ -0,0 +1,7 @@
function test() {
return "passed";
}
module.exports = {
test: test
}