Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix namespaced object export when default is the only one available #4058

Merged
merged 11 commits into from
Nov 15, 2024
79 changes: 59 additions & 20 deletions js/module_loading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,30 +617,69 @@ func TestDefaultNamedExports(t *testing.T) {

func TestStarImport(t *testing.T) {
t.Parallel()
fs := fsext.NewMemMapFs()
err := writeToFs(fs, map[string]any{
"/commonjs_file.js": `exports.something = 5;`,

t.Run("esm_spec", func(t *testing.T) {
t.Parallel()
fs := fsext.NewMemMapFs()
err := writeToFs(fs, map[string]any{
"/commonjs_file.js": `exports.something = 5;`,
})
require.NoError(t, err)

r1, err := getSimpleRunner(t, "/script.js", `
import * as cjs from "./commonjs_file.js"; // commonjs
import * as k6 from "k6"; // "new" go module
// TODO: test with basic go module maybe

if (cjs.something != 5) {
throw "cjs.something has wrong value" + cjs.something;
}
if (typeof k6.sleep != "function") {
throw "k6.sleep has wrong type" + typeof k6.sleep;
}
export default () => {}
`, fs, lib.RuntimeOptions{CompatibilityMode: null.StringFrom("extended")})
require.NoError(t, err)

arc := r1.MakeArchive()
_, err = getSimpleArchiveRunner(t, arc)
require.NoError(t, err)
})
require.NoError(t, err)

r1, err := getSimpleRunner(t, "/script.js", `
import * as cjs from "./commonjs_file.js"; // commonjs
import * as k6 from "k6"; // "new" go module
// TODO: test with basic go module maybe
t.Run("default_to_namespaced_object", func(t *testing.T) {
t.Parallel()

if (cjs.something != 5) {
throw "cjs.something has wrong value" + cjs.something;
}
if (typeof k6.sleep != "function") {
throw "k6.sleep has wrong type" + typeof k6.sleep;
}
export default () => {}
`, fs, lib.RuntimeOptions{CompatibilityMode: null.StringFrom("extended")})
require.NoError(t, err)
r1, err := getSimpleRunner(t, "/script.js", `
import http from "k6/http";
import * as httpNO from "k6/http";

arc := r1.MakeArchive()
_, err = getSimpleArchiveRunner(t, arc)
require.NoError(t, err)
const httpKeys = Object.keys(http);
const httpNOKeys = Object.keys(httpNO);

// 1. Check if both have the same number of properties
if (httpKeys.length !== httpNOKeys.length) {
throw "Objects have a different number of properties.";
}

// 2. Check if all properties match
for (const key of httpKeys) {
if (!Object.prototype.hasOwnProperty.call(httpNO, key)) {
throw `+"`Property ${key} is missing in the second object.`"+`;
}

if (http[key] !== httpNO[key]) {
throw `+"`Property ${key} does not match between the objects.`"+`;
}
}

export default () => {}
`, lib.RuntimeOptions{CompatibilityMode: null.StringFrom("extended")})
require.NoError(t, err)

arc := r1.MakeArchive()
_, err = getSimpleArchiveRunner(t, arc)
require.NoError(t, err)
})
}

func TestIndirectExportDefault(t *testing.T) {
Expand Down
17 changes: 14 additions & 3 deletions js/modules/gomodule.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,21 @@ func (gm *goModule) Instantiate(rt *sobek.Runtime) (sobek.CyclicModuleInstance,
if gm.exportedNames == nil {
named := mi.Exports().Named

gm.exportedNames = make([]string, len(named))
for name := range named {
gm.exportedNames = append(gm.exportedNames, name)
switch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect you used switch as there were more than one cases earlier?

Copy link
Contributor Author

@ankur22 ankur22 Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored it since i think it's easier to follow when there are more than one conditions in the same function, instead of a if/else/else if etc. I also believe it's more go like to work with switch instead of if/else/else if.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do consider it go like in the cases where:

  1. it can't be rewritten as a signel if/else pair - this can
  2. or when using a case can be used to check a single variable against multiple values and that is the only thing happening:
s := "apple";
switch s{
case "cucumber", "pepper", "leek":
  return "vegetable";
default:
  return "not vegetable"
}

As in both of this case it makes it easier to read. I would argue that using a switch in other cases makes me think there is something strange going on.

I will let other people also look at this and see what they think on it - so this is not a request for a change at this point, but a nit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, happy to hear more feedback, before merging/reverting the switch change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reverted it back to if/else in 37a78c6

case named == nil && mi.Exports().Default != nil:
// If named length is 0 but default is defined, then try to work with
// default and extract the names of the object's properties. This
// behavior isn't ESM compatible, but we do want to allow defaults to
// be imported as namespaced object, which is also how node works.
obj := rt.ToValue(mi.Exports().Default).ToObject(rt)
gm.exportedNames = obj.GetOwnPropertyNames()
default:
gm.exportedNames = make([]string, 0, len(named))
for name := range named {
gm.exportedNames = append(gm.exportedNames, name)
}
}

for _, callback := range gm.exportedNamesCallbacks {
callback(gm.exportedNames)
}
Expand Down