Skip to content

Commit

Permalink
js: refactor how modules are loaded
Browse files Browse the repository at this point in the history
This refactor tries to simplify the implementation of `require` and
connected code by splitting it heavily into different parts.

These changes are similar to what will be needed for native ESM support
as shown in #2563 , but without any of
the native parts and without doing anything that isn't currently needed.
This will hopefully make ESM PR much smaller and less intrusive.

This includes still keeping the wrong relativity of `require` as
explained in #2674.

It also tries to simplify connected code, but due to this being very
sensitive code and the changes already being quite big, this is done
only to an extent.

The lack of new tests is mostly due to there not being really any new
code and the tests that were created along these changes already being
merged months ago with #2782.

Future changes will try to address the above as well as potentially
moving the whole module types and logic in separate package to be reused
in tests.
  • Loading branch information
mstoykov committed Feb 8, 2023
1 parent 9c65657 commit de6e170
Show file tree
Hide file tree
Showing 8 changed files with 517 additions and 387 deletions.
293 changes: 154 additions & 139 deletions js/bundle.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TestNewBundle(t *testing.T) {
b, err := getSimpleBundle(t, "-", `export default function() {};`)
require.NoError(t, err)
assert.Equal(t, "file://-", b.Filename.String())
assert.Equal(t, "file:///", b.BaseInitContext.pwd.String())
assert.Equal(t, "file:///", b.pwd.String())
})
t.Run("CompatibilityMode", func(t *testing.T) {
t.Parallel()
Expand Down
70 changes: 70 additions & 0 deletions js/cjsmodule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package js

import (
"fmt"
"net/url"

"github.com/dop251/goja"
"go.k6.io/k6/js/compiler"
"go.k6.io/k6/js/modules"
)

// cjsModule represents a commonJS module
type cjsModule struct {
prg *goja.Program
url *url.URL
}

var _ module = &cjsModule{}

type cjsModuleInstance struct {
mod *cjsModule
moduleObj *goja.Object
vu modules.VU
}

func (c *cjsModule) Instantiate(vu modules.VU) moduleInstance {
return &cjsModuleInstance{vu: vu, mod: c}
}

func (c *cjsModuleInstance) execute() error {
rt := c.vu.Runtime()
exports := rt.NewObject()
c.moduleObj = rt.NewObject()
err := c.moduleObj.Set("exports", exports)
if err != nil {
return fmt.Errorf("error while getting ready to import commonJS, couldn't set exports property of module: %w",
err)
}

// Run the program.
f, err := rt.RunProgram(c.mod.prg)
if err != nil {
return err
}
if call, ok := goja.AssertFunction(f); ok {
if _, err = call(exports, c.moduleObj, exports); err != nil {
return err
}
}

return nil
}

func (c *cjsModuleInstance) exports() *goja.Object {
exportsV := c.moduleObj.Get("exports")
if goja.IsNull(exportsV) || goja.IsUndefined(exportsV) {
return nil
}
return exportsV.ToObject(c.vu.Runtime())
}

type cjsModuleLoader func(specifier *url.URL, name string) (*cjsModule, error)

func cjsmoduleFromString(fileURL *url.URL, data []byte, c *compiler.Compiler) (*cjsModule, error) {
pgm, _, err := c.Compile(string(data), fileURL.String(), false)
if err != nil {
return nil, err
}
return &cjsModule{prg: pgm, url: fileURL}, nil
}
93 changes: 93 additions & 0 deletions js/gomodule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package js

import (
"github.com/dop251/goja"
"go.k6.io/k6/js/modules"
)

// baseGoModule is a go module that does not implement modules.Module interface
// TODO maybe depracate those in the future
type baseGoModule struct {
mod interface{}
}

var _ module = &baseGoModule{}

func (b *baseGoModule) Instantiate(vu modules.VU) moduleInstance {
return &baseGoModuleInstance{mod: b.mod, vu: vu}
}

type baseGoModuleInstance struct {
mod interface{}
vu modules.VU
exportsO *goja.Object // this is so we only initialize the exports once per instance
}

func (b *baseGoModuleInstance) execute() error {
return nil
}

func (b *baseGoModuleInstance) exports() *goja.Object {
if b.exportsO == nil {
// TODO check this does not panic a lot
rt := b.vu.Runtime()
b.exportsO = rt.ToValue(b.mod).ToObject(rt)
}
return b.exportsO
}

// goModule is a go module which implements modules.Module
type goModule struct {
modules.Module
}

var _ module = &goModule{}

func (g *goModule) Instantiate(vu modules.VU) moduleInstance {
return &goModuleInstance{vu: vu, module: g}
}

type goModuleInstance struct {
modules.Instance
module *goModule
vu modules.VU
exportsO *goja.Object // this is so we only initialize the exports once per instance
}

var _ moduleInstance = &goModuleInstance{}

func (gi *goModuleInstance) execute() error {
gi.Instance = gi.module.NewModuleInstance(gi.vu)
return nil
}

func (gi *goModuleInstance) exports() *goja.Object {
if gi.exportsO == nil {
rt := gi.vu.Runtime()
gi.exportsO = rt.ToValue(toESModuleExports(gi.Instance.Exports())).ToObject(rt)
}
return gi.exportsO
}

func toESModuleExports(exp modules.Exports) interface{} {
if exp.Named == nil {
return exp.Default
}
if exp.Default == nil {
return exp.Named
}

result := make(map[string]interface{}, len(exp.Named)+2)

for k, v := range exp.Named {
result[k] = v
}
// Maybe check that those weren't set
result["default"] = exp.Default
// this so babel works with the `default` when it transpiles from ESM to commonjs.
// This should probably be removed once we have support for ESM directly. So that require doesn't get support for
// that while ESM has.
result["__esModule"] = true

return result
}
Loading

0 comments on commit de6e170

Please sign in to comment.