Adding upstream version 0.0~git20250409.f7acab6.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
b9b5d88025
commit
21b930d007
51 changed files with 11229 additions and 0 deletions
276
require/module.go
Normal file
276
require/module.go
Normal 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
587
require/module_test.go
Normal 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
275
require/resolve.go
Normal 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
7
require/testdata/m.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
function test() {
|
||||
return "passed";
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
test: test
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue