diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1ff2d5c46..790b9c2f693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ Top-level await (i.e. using the `await` keyword outside of an `async` function) is not yet part of the JavaScript language standard. The [feature proposal](https://github.com/tc39/proposal-top-level-await) is still at stage 3 and has not yet advanced to stage 4. However, V8 has already implemented it and it has shipped in Chrome 89 and node 14.8. This release allows top-level await to be used when the `--target=` flag is set to those compilation targets. +* Convert `import()` to `require()` if `import()` is not supported ([#1084](https://github.com/evanw/esbuild/issues/1084)) + + This release now converts dynamic `import()` expressions into `Promise.resolve().then(() => require())` expressions if the compilation target doesn't support them. This is the case for node before version 13.2, for example. + ## 0.11.0 **This release contains backwards-incompatible changes.** Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as [recommended by npm](https://docs.npmjs.com/cli/v6/using-npm/semver/)). You should either be pinning the exact version of `esbuild` in your `package.json` file or be using a version range syntax that only accepts patch upgrades such as `~0.10.0`. See the documentation about [semver](https://docs.npmjs.com/cli/v6/using-npm/semver/) for more information. diff --git a/internal/bundler/linker.go b/internal/bundler/linker.go index 726b1767add..1bfbd12964c 100644 --- a/internal/bundler/linker.go +++ b/internal/bundler/linker.go @@ -2670,7 +2670,8 @@ func (c *linkerContext) includePart(sourceIndex uint32, partIndex uint32, entryP if !record.SourceIndex.IsValid() || c.isExternalDynamicImport(record) { // This is an external import, so it needs the "__toModule" wrapper as // long as it's not a bare "require()" - if record.Kind != ast.ImportRequire && !c.options.OutputFormat.KeepES6ImportExportSyntax() { + if record.Kind != ast.ImportRequire && (!c.options.OutputFormat.KeepES6ImportExportSyntax() || + (record.Kind == ast.ImportDynamic && c.options.UnsupportedJSFeatures.Has(compat.DynamicImport))) { record.WrapWithToModule = true toModuleUses++ } diff --git a/internal/compat/js_table.go b/internal/compat/js_table.go index 846b9e17458..e226b50abc0 100644 --- a/internal/compat/js_table.go +++ b/internal/compat/js_table.go @@ -54,6 +54,7 @@ const ( Const DefaultArgument Destructuring + DynamicImport ExponentOperator ExportStarAs ForAwait @@ -210,6 +211,15 @@ var jsTable = map[JSFeature]map[Engine][]int{ Node: {6, 5}, Safari: {10}, }, + DynamicImport: { + Chrome: {63}, + Edge: {79}, + ES: {2015}, + Firefox: {67}, + IOS: {11}, + Node: {13, 2}, + Safari: {11, 1}, + }, ExponentOperator: { Chrome: {52}, Edge: {14}, diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index f08acd4ec8c..e3cd3833dff 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -57,6 +57,7 @@ type parser struct { requireRef js_ast.Ref moduleRef js_ast.Ref importMetaRef js_ast.Ref + promiseRef js_ast.Ref findSymbolHelper func(loc logger.Loc, name string) js_ast.Ref symbolForDefineHelper func(int) js_ast.Ref injectedDefineSymbols []js_ast.Ref @@ -1541,6 +1542,13 @@ func (p *parser) callRuntime(loc logger.Loc, name string, args []js_ast.Expr) js }} } +func (p *parser) makePromiseRef() js_ast.Ref { + if p.promiseRef == js_ast.InvalidRef { + p.promiseRef = p.newSymbol(js_ast.SymbolUnbound, "Promise") + } + return p.promiseRef +} + // The name is temporarily stored in the ref until the scope traversal pass // happens, at which point a symbol will be generated and the ref will point // to the symbol instead. @@ -11092,6 +11100,49 @@ func (p *parser) visitExprInOut(expr js_ast.Expr, in exprIn) (js_ast.Expr, exprO } } + // We need to convert this into a call to "require()" if ES6 syntax is + // not supported in the current output format. The full conversion: + // + // Before: + // import(foo) + // + // After: + // Promise.resolve().then(() => require(foo)) + // + // This is normally done by the printer since we don't know during the + // parsing stage whether this module is external or not. However, it's + // guaranteed to be external if the argument isn't a string. We handle + // this case here instead of in the printer because both the printer + // and the linker currently need an import record to handle this case + // correctly, and you need a string literal to get an import record. + if p.options.unsupportedJSFeatures.Has(compat.DynamicImport) { + var then js_ast.Expr + value := p.callRuntime(arg.Loc, "__toModule", []js_ast.Expr{{Loc: expr.Loc, Data: &js_ast.ECall{ + Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.requireRef}}, + Args: []js_ast.Expr{arg}, + }}}) + body := js_ast.FnBody{Loc: expr.Loc, Stmts: []js_ast.Stmt{{Loc: expr.Loc, Data: &js_ast.SReturn{Value: &value}}}} + if p.options.unsupportedJSFeatures.Has(compat.Arrow) { + then = js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EFunction{Fn: js_ast.Fn{Body: body}}} + } else { + then = js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EArrow{Body: body, PreferExpr: true}} + } + return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{ + Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EDot{ + Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.ECall{ + Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EDot{ + Target: js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EIdentifier{Ref: p.makePromiseRef()}}, + Name: "resolve", + NameLoc: expr.Loc, + }}, + }}, + Name: "then", + NameLoc: expr.Loc, + }}, + Args: []js_ast.Expr{then}, + }} + } + return js_ast.Expr{Loc: expr.Loc, Data: &js_ast.EImport{ Expr: arg, LeadingInteriorComments: e.LeadingInteriorComments, @@ -12581,6 +12632,7 @@ func newParser(log logger.Log, source logger.Source, lexer js_lexer.Lexer, optio allowIn: true, options: *options, runtimeImports: make(map[string]js_ast.Ref), + promiseRef: js_ast.InvalidRef, afterArrowBodyLoc: logger.Loc{Start: -1}, // For lowering private methods diff --git a/internal/js_printer/js_printer.go b/internal/js_printer/js_printer.go index e187bd46ede..6a454a57a0d 100644 --- a/internal/js_printer/js_printer.go +++ b/internal/js_printer/js_printer.go @@ -1237,8 +1237,27 @@ func (p *printer) printRequireOrImportExpr(importRecordIndex uint32, leadingInte } // External "import()" - p.printSpaceBeforeIdentifier() - p.print("import(") + if !p.options.UnsupportedFeatures.Has(compat.DynamicImport) { + p.printSpaceBeforeIdentifier() + p.print("import(") + defer p.print(")") + } else { + p.printSpaceBeforeIdentifier() + p.print("Promise.resolve()") + p.printDotThenPrefix() + defer p.printDotThenSuffix() + + // Wrap this with a call to "__toModule()" if this is a CommonJS file + if record.WrapWithToModule { + p.printSymbol(p.options.ToModuleRef) + p.print("(") + defer p.print(")") + } + + p.printSpaceBeforeIdentifier() + p.print("require(") + defer p.print(")") + } if len(leadingInteriorComments) > 0 { p.printNewline() p.options.Indent++ @@ -1254,7 +1273,6 @@ func (p *printer) printRequireOrImportExpr(importRecordIndex uint32, leadingInte p.options.Indent-- p.printIndent() } - p.print(")") return } diff --git a/scripts/compat-table.js b/scripts/compat-table.js index 5efeececb9d..34e9ea2f694 100644 --- a/scripts/compat-table.js +++ b/scripts/compat-table.js @@ -128,6 +128,7 @@ mergeVersions('Class', { es2015: true }) mergeVersions('Const', { es2015: true }) mergeVersions('DefaultArgument', { es2015: true }) mergeVersions('Destructuring', { es2015: true }) +mergeVersions('DynamicImport', { es2015: true }) mergeVersions('ForOf', { es2015: true }) mergeVersions('Generator', { es2015: true }) mergeVersions('Let', { es2015: true }) @@ -176,6 +177,16 @@ mergeVersions('TopLevelAwait', { node14_8: true, }) +// Manually copied from https://caniuse.com/es6-module-dynamic-import +mergeVersions('DynamicImport', { + chrome63: true, + edge79: true, + firefox67: true, + ios11: true, + node13_2: true, // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import + safari11_1: true, +}) + for (const test of [...es5.tests, ...es6.tests, ...stage4.tests, ...stage1to3.tests]) { const feature = features[test.name] if (feature) { diff --git a/scripts/js-api-tests.js b/scripts/js-api-tests.js index 1f60309b1b0..22dcd5cb4a5 100644 --- a/scripts/js-api-tests.js +++ b/scripts/js-api-tests.js @@ -3498,6 +3498,52 @@ let transformTests = { assert.strictEqual(code2, `foo;\n`) }, + async dynamicImportString({ esbuild }) { + const { code: code1 } = await esbuild.transform(`import('foo')`, { target: 'chrome63' }) + assert.strictEqual(code1, `import("foo");\n`) + }, + + async dynamicImportStringES6({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code2 } = await esbuild.transform(`import('foo')`, { target: 'chrome62' }) + assert.strictEqual(fromPromiseResolve(code2), `Promise.resolve().then(() => __toModule(require("foo")));\n`) + }, + + async dynamicImportStringES5({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code3 } = await esbuild.transform(`import('foo')`, { target: 'chrome48' }) + assert.strictEqual(fromPromiseResolve(code3), `Promise.resolve().then(function() {\n return __toModule(require("foo"));\n});\n`) + }, + + async dynamicImportStringES5Minify({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code4 } = await esbuild.transform(`import('foo')`, { target: 'chrome48', minifyWhitespace: true }) + assert.strictEqual(fromPromiseResolve(code4), `Promise.resolve().then(function(){return __toModule(require("foo"))});\n`) + }, + + async dynamicImportExpression({ esbuild }) { + const { code: code1 } = await esbuild.transform(`import(foo)`, { target: 'chrome63' }) + assert.strictEqual(code1, `import(foo);\n`) + }, + + async dynamicImportExpressionES6({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code2 } = await esbuild.transform(`import(foo)`, { target: 'chrome62' }) + assert.strictEqual(fromPromiseResolve(code2), `Promise.resolve().then(() => __toModule(require(foo)));\n`) + }, + + async dynamicImportExpressionES5({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code3 } = await esbuild.transform(`import(foo)`, { target: 'chrome48' }) + assert.strictEqual(fromPromiseResolve(code3), `Promise.resolve().then(function() {\n return __toModule(require(foo));\n});\n`) + }, + + async dynamicImportExpressionES5Minify({ esbuild }) { + const fromPromiseResolve = text => text.slice(text.indexOf('Promise.resolve')) + const { code: code4 } = await esbuild.transform(`import(foo)`, { target: 'chrome48', minifyWhitespace: true }) + assert.strictEqual(fromPromiseResolve(code4), `Promise.resolve().then(function(){return __toModule(require(foo))});\n`) + }, + async multipleEngineTargets({ esbuild }) { const check = async (target, expected) => assert.strictEqual((await esbuild.transform(`foo(a ?? b)`, { target })).code, expected)