Skip to content

Commit

Permalink
Implement import.meta.resolve() (#3873)
Browse files Browse the repository at this point in the history
* Implement import.meta.resolve()

This implements and tests `import.meta.resolve()` a way to get the path
to a module the same way ESM and/or `require` will do.

This doesn't read or load the file in question from the file.

This also showed that we currently always parse code as ECMAScript
modules. Even when the parsed/compiled file was to be wrapped as
CommonJS.

This likely had no other side effects as all other ESM specific syntax
is also either not implemented in k6 or forces CommonJS wrapping
disabled.

Closes #3856

* Update js/modules/require_impl.go

Co-authored-by: Joan López de la Franca Beltran <[email protected]>

* panic on error

---------

Co-authored-by: Joan López de la Franca Beltran <[email protected]>
  • Loading branch information
mstoykov and joanlopez authored Aug 1, 2024
1 parent dfd48f8 commit 913b280
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 8 deletions.
13 changes: 13 additions & 0 deletions js/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,19 @@ func (b *Bundle) setInitGlobals(rt *sobek.Runtime, vu *moduleVUImpl, modSys *mod
}
warnAboutModuleMixing("module")
warnAboutModuleMixing("exports")

rt.SetFinalImportMeta(func(o *sobek.Object, mr sobek.ModuleRecord) {
err := o.Set("resolve", func(specifier string) (string, error) {
u, err := modSys.Resolve(mr, specifier)
if err != nil {
return "", err
}
return u.String(), nil
})
if err != nil {
panic("error while creating `import.meta.resolve`: " + err.Error())
}
})
}

func generateFileLoad(b *Bundle) modules.FileLoader {
Expand Down
16 changes: 12 additions & 4 deletions js/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,26 @@ func (ps *parsingState) parseImpl(src, filename string, commonJSWrap bool) (*ast
code = ps.wrap(code, filename)
ps.commonJSWrapped = true
}
opts := parser.WithDisableSourceMaps
var opts []parser.Option
if ps.loader != nil {
opts = parser.WithSourceMapLoader(ps.sourceMapLoader)
opts = append(opts, parser.WithSourceMapLoader(ps.sourceMapLoader))
} else {
opts = append(opts, parser.WithDisableSourceMaps)
}

if !commonJSWrap {
opts = append(opts, parser.IsModule)
}
prg, err := parser.ParseFile(nil, filename, code, 0, opts, parser.IsModule)

prg, err := parser.ParseFile(nil, filename, code, 0, opts...)

if ps.couldntLoadSourceMap {
ps.couldntLoadSourceMap = false // reset
// we probably don't want to abort scripts which have source maps but they can't be found,
// this also will be a breaking change, so if we couldn't we retry with it disabled
ps.compiler.logger.WithError(ps.srcMapError).Warnf("Couldn't load source map for %s", filename)
prg, err = parser.ParseFile(nil, filename, code, 0, parser.WithDisableSourceMaps, parser.IsModule)
ps.loader = nil
return ps.parseImpl(src, filename, commonJSWrap)
}

if err == nil {
Expand Down
11 changes: 11 additions & 0 deletions js/modules/require_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ func (ms *ModuleSystem) getModuleInstanceFromGoModule(wm *goModule) (wmi *goModu
return gmi, nil
}

// Resolve returns what the provided specifier will get resolved to if it was to be imported
// To be used by other parts to get the path
func (ms *ModuleSystem) Resolve(mr sobek.ModuleRecord, specifier string) (*url.URL, error) {
if specifier == "" {
return nil, errors.New("require() can't be used with an empty specifier")
}

baseModuleURL := ms.resolver.reversePath(mr)
return ms.resolver.resolveSpecifier(baseModuleURL, specifier)
}

// CurrentlyRequiredModule returns the module that is currently being required.
// It is mostly used for old and somewhat buggy behaviour of the `open` call
func (ms *ModuleSystem) CurrentlyRequiredModule() (*url.URL, error) {
Expand Down
209 changes: 209 additions & 0 deletions js/path_resolution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,212 @@ func writeToFs(fs fsext.Fs, in map[string]any) error {
}
return nil
}

func TestImportMetaResolve(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
fsMap map[string]any
expectedLogs []string
expectedError string
}{
"simple": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/A/A/A/script.js": `
let data = require(import.meta.resolve("../../../B/data.js"));
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
"intermediate": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
module.exports = require(import.meta.resolve("../../B/data.js"));
`,
"/A/A/A/A/script.js": `
let data = require(import.meta.resolve("./../../../C/B/script.js"))
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
expectedError: `import not supported in script`,
},
"intermediate fixed": {
fsMap: map[string]any{
"/A/B/data.js": "export default 'export content'",
"/A/C/B/script.js": `
export default require("../../B/data.js").default;
`,
"/A/A/A/A/script.js": `
let data = require("./../../../C/B/script.js").default;
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
"complex": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
export default () => require(import.meta.resolve("./../../B/data.js"));
`,
"/A/B/B/script.js": `
export default require(import.meta.resolve("./../../C/B/script.js")).default();
`,
"/A/A/A/A/script.js": `
let data = require(import.meta.resolve("./../../../B/B/script.js")).default;
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
"complex wrong": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
export default () => require(import.meta.resolve("./../data.js"));
`,
"/A/B/B/script.js": `
export default require(import.meta.resolve("./../../C/B/script.js")).default();
`,
"/A/A/A/A/script.js": `
let data = require(import.meta.resolve("./../../../B/B/script.js")).default;
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
expectedError: `A/C/data.js" couldn't be found`,
},
"complex wrong fixed": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
export default (specifier) => require(specifier);
`,
"/A/B/B/script.js": `
export default require(import.meta.resolve("./../../C/B/script.js")).default(
import.meta.resolve("./../data.js")
);
`,
"/A/A/A/A/script.js": `
let data = require(import.meta.resolve("./../../../B/B/script.js")).default;
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
"ESM and require": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
export default function () {
// Here the path is relative to this module not the calling one
return require(import.meta.resolve("./../../B/data.js"));
}
`,
"/A/B/B/script.js": `
import s from "./../../C/B/script.js"
export default require(import.meta.resolve("./../../C/B/script.js")).default();
`,
"/A/A/A/A/script.js": `
import data from "./../../../B/B/script.js"
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
"ESM and require wrong": {
fsMap: map[string]any{
"/A/B/data.js": "module.exports='export content'",
"/A/C/B/script.js": `
export default function () {
return require(import.meta.resolve("./../data.js"));
}
`,
"/A/B/B/script.js": `
import s from "./../../C/B/script.js"
export default require(import.meta.resolve("./../../C/B/script.js")).default();
`,
"/A/A/A/A/script.js": `
import data from "./../../../B/B/script.js"
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
expectedError: `The moduleSpecifier "file:///A/C/data.js" couldn't be found on local disk.`,
},
"full ESM": {
fsMap: map[string]any{
"/A/B/data.js": "export default 'export content'",
"/A/C/B/script.js": `
export default function (specifier) {
return require(specifier).default;
}
`,
"/A/B/B/script.js": `
import s from "./../../C/B/script.js"
let l = s(import.meta.resolve("../data.js")); // this will resolve with this module as root
export default l;
`,
"/A/A/A/A/script.js": `
import data from "./../../../B/B/script.js"
if (data != "export content") {
throw new Error("wrong content " + data);
}
export default function() {}
`,
},
},
}
for name, testCase := range testCases {
name, testCase := name, testCase

t.Run(name, func(t *testing.T) {
t.Parallel()
fs := fsext.NewMemMapFs()
err := writeToFs(fs, testCase.fsMap)
fs = fsext.NewCacheOnReadFs(fs, fsext.NewMemMapFs(), 0)
require.NoError(t, err)
logger, hook := testutils.NewLoggerWithHook(t, logrus.WarnLevel)
b, err := getSimpleBundle(t, "/main.js", `export { default } from "/A/A/A/A/script.js"`, fs, logger)

if testCase.expectedError != "" {
require.ErrorContains(t, err, testCase.expectedError)
return
}
require.NoError(t, err)

_, err = b.Instantiate(context.Background(), 0)
require.NoError(t, err)
logs := hook.Drain()

if len(testCase.expectedLogs) == 0 {
require.Empty(t, logs)
return
}
require.Equal(t, len(logs), len(testCase.expectedLogs))

for i, log := range logs {
require.Contains(t, log.Message, testCase.expectedLogs[i], "log line %d", i)
}
})
}
}
6 changes: 4 additions & 2 deletions js/tc39/breaking_test_errors-experimental_enhanced.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
"test/language/expressions/class/static-init-await-reference.js-strict:true": "test/language/expressions/class/static-init-await-reference.js: test/language/expressions/class/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/function/static-init-await-reference.js-strict:true": "test/language/expressions/function/static-init-await-reference.js: test/language/expressions/function/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/generators/static-init-await-reference.js-strict:true": "test/language/expressions/generators/static-init-await-reference.js: test/language/expressions/generators/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js-strict:true": "panic while running test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: interface conversion: *sobek.Program is not sobek.ModuleRecord: missing method Evaluate",
"test/language/expressions/import.meta/same-object-returned.js-strict:true": "panic while running test/language/expressions/import.meta/same-object-returned.js: interface conversion: *sobek.Program is not sobek.ModuleRecord: missing method Evaluate",
"test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js-strict:true": "test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: file:///TestTC39/test262/test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: Line 28:25 import not supported in script (and 19 more errors)",
"test/language/expressions/import.meta/same-object-returned.js-strict:true": "test/language/expressions/import.meta/same-object-returned.js: file:///TestTC39/test262/test/language/expressions/import.meta/same-object-returned.js: Line 28:9 import not supported in script (and 3 more errors)",
"test/language/expressions/import.meta/syntax/goal-module-nested-function.js-strict:true": "test/language/expressions/import.meta/syntax/goal-module-nested-function.js: file:///TestTC39/test262/test/language/expressions/import.meta/syntax/goal-module-nested-function.js: Line 16:3 import not supported in script (and 4 more errors)",
"test/language/expressions/import.meta/syntax/goal-module.js-strict:true": "test/language/expressions/import.meta/syntax/goal-module.js: file:///TestTC39/test262/test/language/expressions/import.meta/syntax/goal-module.js: Line 15:1 import not supported in script (and 3 more errors)",
"test/language/expressions/import.meta/syntax/goal-script.js-strict:true": "test/language/expressions/import.meta/syntax/goal-script.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/expressions/in/private-field-rhs-await-absent.js-strict:true": "test/language/expressions/in/private-field-rhs-await-absent.js: test/language/expressions/in/private-field-rhs-await-absent.js: Line 24:10 Unexpected token await",
"test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js-strict:true": "test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js: test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js: Line 29:4 Unexpected token ILLEGAL (and 6 more errors)",
Expand Down
6 changes: 4 additions & 2 deletions js/tc39/breaking_test_errors-extended.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@
"test/language/expressions/class/static-init-await-reference.js-strict:true": "test/language/expressions/class/static-init-await-reference.js: test/language/expressions/class/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/function/static-init-await-reference.js-strict:true": "test/language/expressions/function/static-init-await-reference.js: test/language/expressions/function/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/generators/static-init-await-reference.js-strict:true": "test/language/expressions/generators/static-init-await-reference.js: test/language/expressions/generators/static-init-await-reference.js: Line 15:5 Unexpected token await (and 1 more errors)",
"test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js-strict:true": "panic while running test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: interface conversion: *sobek.Program is not sobek.ModuleRecord: missing method Evaluate",
"test/language/expressions/import.meta/same-object-returned.js-strict:true": "panic while running test/language/expressions/import.meta/same-object-returned.js: interface conversion: *sobek.Program is not sobek.ModuleRecord: missing method Evaluate",
"test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js-strict:true": "test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: file:///TestTC39/test262/test/language/expressions/import.meta/import-meta-is-an-ordinary-object.js: Line 28:25 import not supported in script (and 19 more errors)",
"test/language/expressions/import.meta/same-object-returned.js-strict:true": "test/language/expressions/import.meta/same-object-returned.js: file:///TestTC39/test262/test/language/expressions/import.meta/same-object-returned.js: Line 28:9 import not supported in script (and 3 more errors)",
"test/language/expressions/import.meta/syntax/goal-module-nested-function.js-strict:true": "test/language/expressions/import.meta/syntax/goal-module-nested-function.js: file:///TestTC39/test262/test/language/expressions/import.meta/syntax/goal-module-nested-function.js: Line 16:3 import not supported in script (and 4 more errors)",
"test/language/expressions/import.meta/syntax/goal-module.js-strict:true": "test/language/expressions/import.meta/syntax/goal-module.js: file:///TestTC39/test262/test/language/expressions/import.meta/syntax/goal-module.js: Line 15:1 import not supported in script (and 3 more errors)",
"test/language/expressions/import.meta/syntax/goal-script.js-strict:true": "test/language/expressions/import.meta/syntax/goal-script.js: error is not an object (Test262: This statement should not be evaluated.)",
"test/language/expressions/in/private-field-rhs-await-absent.js-strict:true": "test/language/expressions/in/private-field-rhs-await-absent.js: test/language/expressions/in/private-field-rhs-await-absent.js: Line 24:10 Unexpected token await",
"test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js-strict:true": "test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js: test/language/expressions/object/cpn-obj-lit-computed-property-name-from-integer-separators.js: Line 29:4 Unexpected token ILLEGAL (and 6 more errors)",
Expand Down

0 comments on commit 913b280

Please sign in to comment.