diff --git a/js/bundle.go b/js/bundle.go index 820893927ed..028e2d01076 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -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 { diff --git a/js/compiler/compiler.go b/js/compiler/compiler.go index 3e3286a4ef1..6673904fd29 100644 --- a/js/compiler/compiler.go +++ b/js/compiler/compiler.go @@ -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 { diff --git a/js/modules/require_impl.go b/js/modules/require_impl.go index 5cab7a4ba81..e3c9eb3f271 100644 --- a/js/modules/require_impl.go +++ b/js/modules/require_impl.go @@ -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) { diff --git a/js/path_resolution_test.go b/js/path_resolution_test.go index 6fa224a5149..919b59dd6cc 100644 --- a/js/path_resolution_test.go +++ b/js/path_resolution_test.go @@ -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) + } + }) + } +} diff --git a/js/tc39/breaking_test_errors-experimental_enhanced.json b/js/tc39/breaking_test_errors-experimental_enhanced.json index 80c0c043558..88528f6ad21 100644 --- a/js/tc39/breaking_test_errors-experimental_enhanced.json +++ b/js/tc39/breaking_test_errors-experimental_enhanced.json @@ -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)", diff --git a/js/tc39/breaking_test_errors-extended.json b/js/tc39/breaking_test_errors-extended.json index 80c0c043558..88528f6ad21 100644 --- a/js/tc39/breaking_test_errors-extended.json +++ b/js/tc39/breaking_test_errors-extended.json @@ -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)",