Skip to content

Commit

Permalink
fix #1084: lower "import()" for older targets
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Mar 29, 2021
1 parent 057daf9 commit 1aaaec2
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
}
Expand Down
10 changes: 10 additions & 0 deletions internal/compat/js_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const (
Const
DefaultArgument
Destructuring
DynamicImport
ExponentOperator
ExportStarAs
ForAwait
Expand Down Expand Up @@ -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},
Expand Down
52 changes: 52 additions & 0 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
24 changes: 21 additions & 3 deletions internal/js_printer/js_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++
Expand All @@ -1254,7 +1273,6 @@ func (p *printer) printRequireOrImportExpr(importRecordIndex uint32, leadingInte
p.options.Indent--
p.printIndent()
}
p.print(")")
return
}

Expand Down
11 changes: 11 additions & 0 deletions scripts/compat-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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) {
Expand Down
46 changes: 46 additions & 0 deletions scripts/js-api-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 1aaaec2

Please sign in to comment.