Skip to content

Commit

Permalink
fix #293: support js expressions in global names
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Oct 28, 2020
1 parent 4c00b53 commit 83b307f
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 53 deletions.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,42 @@ The breaking changes are as follows:
* `Loaders` → `Loader`
* `PureFunctions` → `Pure`
* The global name parameter now takes a JavaScript expression ([#293](https://github.com/evanw/esbuild/issues/293))
The global name parameter determines the name of the global variable created for exports with the IIFE output format. For example, a global name of `abc` would generate the following IIFE:
```js
var abc = (() => {
...
})();
```
Previously this name was injected into the source code verbatim without any validation. This meant a global name of `abc.def` would generate this code, which is a syntax error:
```js
var abc.def = (() => {
...
})();
```
With this release, a global name of `abc.def` will now generate the following code instead:
```js
var abc = abc || {};
abc.def = (() => {
...
})();
```
The full syntax is an identifier followed by one or more property accesses. If you need to include a `.` character in your property name, you can use an index expression instead. For example, the global name `versions['1.0']` will generate the following code:
```js
var versions = versions || {};
versions["1.0"] = (() => {
...
})();
```
* Removed the workaround for `document.all` with nullish coalescing and optional chaining
The `--strict:nullish-coalescing` and `--strict:optional-chaining` options have been removed. They only existed to address a theoretical problem where modern code that uses the new `??` and `?.` operators interacted with the legacy [`document.all` object](https://developer.mozilla.org/en-US/docs/Web/API/Document/all) that has been deprecated for a long time. Realistically this case is extremely unlikely to come up in practice, so these obscure options were removed to simplify the API and reduce code complexity. For what it's worth this behavior also matches [Terser](https://github.com/terser/terser), a commonly-used JavaScript minifier.
Expand Down
2 changes: 1 addition & 1 deletion internal/bundler/bundler_default_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func TestExportFormsIIFE(t *testing.T) {
options: config.Options{
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
ModuleName: "moduleName",
ModuleName: []string{"moduleName"},
AbsOutputFile: "/out.js",
},
})
Expand Down
8 changes: 4 additions & 4 deletions internal/bundler/bundler_importstar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ func TestReExportStarExternalIIFE(t *testing.T) {
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
ModuleName: "mod",
ModuleName: []string{"mod"},
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"foo": true,
Expand Down Expand Up @@ -1274,7 +1274,7 @@ func TestReExportStarIIFENoBundle(t *testing.T) {
Mode: config.ModeConvertFormat,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
ModuleName: "mod",
ModuleName: []string{"mod"},
},
})
}
Expand Down Expand Up @@ -1323,7 +1323,7 @@ func TestReExportStarAsExternalIIFE(t *testing.T) {
Mode: config.ModeBundle,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
ModuleName: "mod",
ModuleName: []string{"mod"},
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"foo": true,
Expand Down Expand Up @@ -1387,7 +1387,7 @@ func TestReExportStarAsIIFENoBundle(t *testing.T) {
Mode: config.ModeConvertFormat,
OutputFormat: config.FormatIIFE,
AbsOutputFile: "/out.js",
ModuleName: "mod",
ModuleName: []string{"mod"},
},
})
}
Expand Down
49 changes: 43 additions & 6 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1653,7 +1653,7 @@ func (c *linkerContext) createExportsForFile(sourceIndex uint32) {
}}}}

case config.FormatIIFE:
if c.options.ModuleName != "" {
if len(c.options.ModuleName) > 0 {
// "return require_foo();"
cjsWrapStmt = js_ast.Stmt{Data: &js_ast.SReturn{Value: &js_ast.Expr{Data: &js_ast.ECall{
Target: js_ast.Expr{Data: &js_ast.EIdentifier{Ref: repr.ast.WrapperRef}},
Expand Down Expand Up @@ -3396,13 +3396,13 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func([]ast
if c.options.OutputFormat == config.FormatIIFE {
var text string
indent = " "
if len(c.options.ModuleName) > 0 {
text = generateModuleNamePrefix(c.options)
}
if c.options.UnsupportedJSFeatures.Has(compat.Arrow) {
text = "(function()" + space + "{" + newline
text += "(function()" + space + "{" + newline
} else {
text = "(()" + space + "=>" + space + "{" + newline
}
if c.options.ModuleName != "" {
text = "var " + c.options.ModuleName + space + "=" + space + text
text += "(()" + space + "=>" + space + "{" + newline
}
prevOffset.advanceString(text)
j.AddString(text)
Expand Down Expand Up @@ -3653,6 +3653,43 @@ func (repr *chunkReprJS) generate(c *linkerContext, chunk *chunkInfo) func([]ast
}
}

func generateModuleNamePrefix(options *config.Options) string {
var text string
prefix := options.ModuleName[0]
space := " "
join := ";\n"

if options.RemoveWhitespace {
space = ""
join = ";"
}

if js_lexer.IsIdentifier(prefix) {
if options.ASCIIOnly {
prefix = string(js_printer.QuoteIdentifier(nil, prefix, options.UnsupportedJSFeatures))
}
text = fmt.Sprintf("var %s%s=%s", prefix, space, space)
} else {
prefix = fmt.Sprintf("this[%s]", js_printer.QuoteForJSON(prefix, options.ASCIIOnly))
text = fmt.Sprintf("%s%s=%s", prefix, space, space)
}

for _, name := range options.ModuleName[1:] {
oldPrefix := prefix
if js_lexer.IsIdentifier(name) {
if options.ASCIIOnly {
name = string(js_printer.QuoteIdentifier(nil, name, options.UnsupportedJSFeatures))
}
prefix = fmt.Sprintf("%s.%s", prefix, name)
} else {
prefix = fmt.Sprintf("%s[%s]", prefix, js_printer.QuoteForJSON(name, options.ASCIIOnly))
}
text += fmt.Sprintf("%s%s||%s{}%s%s%s=%s", oldPrefix, space, space, join, prefix, space, space)
}

return text
}

type compileResultCSS struct {
printedCSS string
sourceIndex uint32
Expand Down
2 changes: 1 addition & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ type Options struct {
AbsOutputFile string
AbsOutputDir string
OutputExtensions map[string]string
ModuleName string
ModuleName []string
TsConfigOverride string
ExtensionToLoader map[string]Loader
OutputFormat Format
Expand Down
19 changes: 18 additions & 1 deletion internal/js_lexer/js_lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ type Lexer struct {
SourceMappingURL js_ast.Span
Number float64
rescanCloseBraceAsTemplateToken bool
forGlobalName bool
json json

// The log is disabled during speculative scans that may backtrack
Expand All @@ -249,6 +250,17 @@ func NewLexer(log logger.Log, source logger.Source) Lexer {
return lexer
}

func NewLexerGlobalName(log logger.Log, source logger.Source) Lexer {
lexer := Lexer{
log: log,
source: source,
forGlobalName: true,
}
lexer.step()
lexer.Next()
return lexer
}

func NewLexerJSON(log logger.Log, source logger.Source, allowComments bool) Lexer {
lexer := Lexer{
log: log,
Expand Down Expand Up @@ -1173,6 +1185,10 @@ func (lexer *Lexer) Next() {
case '/':
// '/' or '/=' or '//' or '/* ... */'
lexer.step()
if lexer.forGlobalName {
lexer.Token = TSlash
break
}
switch lexer.codePoint {
case '=':
lexer.step()
Expand Down Expand Up @@ -1374,7 +1390,8 @@ func (lexer *Lexer) Next() {
}

case -1: // This indicates the end of the file
lexer.SyntaxError()
lexer.addError(logger.Loc{Start: int32(lexer.end)}, "Unterminated string literal")
panic(LexerPanic{})

case '\r':
if quote != '`' {
Expand Down
12 changes: 6 additions & 6 deletions internal/js_lexer/js_lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,18 +489,18 @@ func TestStringLiteral(t *testing.T) {
expectString(t, "'1\\\u20292'", "12")
expectLexerError(t, "'1\\\n\r2'", "<stdin>: error: Unterminated string literal\n")

expectLexerError(t, "\"'", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "'\"", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "'\\", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "'\\'", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "\"'", "<stdin>: error: Unterminated string literal\n")
expectLexerError(t, "'\"", "<stdin>: error: Unterminated string literal\n")
expectLexerError(t, "'\\", "<stdin>: error: Unterminated string literal\n")
expectLexerError(t, "'\\'", "<stdin>: error: Unterminated string literal\n")

expectLexerError(t, "'\\x", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "'\\x", "<stdin>: error: Unterminated string literal\n")
expectLexerError(t, "'\\x'", "<stdin>: error: Syntax error \"'\"\n")
expectLexerError(t, "'\\xG'", "<stdin>: error: Syntax error \"G\"\n")
expectLexerError(t, "'\\xF'", "<stdin>: error: Syntax error \"'\"\n")
expectLexerError(t, "'\\xFG'", "<stdin>: error: Syntax error \"G\"\n")

expectLexerError(t, "'\\u", "<stdin>: error: Unexpected end of file\n")
expectLexerError(t, "'\\u", "<stdin>: error: Unterminated string literal\n")
expectLexerError(t, "'\\u'", "<stdin>: error: Syntax error \"'\"\n")
expectLexerError(t, "'\\u0'", "<stdin>: error: Syntax error \"'\"\n")
expectLexerError(t, "'\\u00'", "<stdin>: error: Syntax error \"'\"\n")
Expand Down
48 changes: 48 additions & 0 deletions internal/js_parser/global_name_parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package js_parser

import (
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/logger"
)

func ParseGlobalName(log logger.Log, source logger.Source) (result []string, ok bool) {
ok = true
defer func() {
r := recover()
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
ok = false
} else if r != nil {
panic(r)
}
}()

lexer := js_lexer.NewLexerGlobalName(log, source)

// Start off with an identifier
result = append(result, lexer.Identifier)
lexer.Expect(js_lexer.TIdentifier)

// Follow with dot or index expressions
for lexer.Token != js_lexer.TEndOfFile {
switch lexer.Token {
case js_lexer.TDot:
lexer.Next()
if !lexer.IsIdentifierOrKeyword() {
lexer.Expect(js_lexer.TIdentifier)
}
result = append(result, lexer.Identifier)
lexer.Next()

case js_lexer.TOpenBracket:
lexer.Next()
result = append(result, js_lexer.UTF16ToString(lexer.StringLiteral))
lexer.Expect(js_lexer.TStringLiteral)
lexer.Expect(js_lexer.TCloseBracket)

default:
lexer.Expect(js_lexer.TDot)
}
}

return
}
2 changes: 1 addition & 1 deletion internal/js_parser/json_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func TestJSONString(t *testing.T) {
expectPrintedJSON(t, "\"\\uDC00\"", "\"\\uDC00\"")

// Invalid escapes
expectParseErrorJSON(t, "\"\\", "<stdin>: error: Unexpected end of file\n")
expectParseErrorJSON(t, "\"\\", "<stdin>: error: Unterminated string literal\n")
expectParseErrorJSON(t, "\"\\0\"", "<stdin>: error: Syntax error \"0\"\n")
expectParseErrorJSON(t, "\"\\1\"", "<stdin>: error: Syntax error \"1\"\n")
expectParseErrorJSON(t, "\"\\'\"", "<stdin>: error: Syntax error \"'\"\n")
Expand Down
66 changes: 35 additions & 31 deletions internal/js_printer/js_printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,38 @@ func QuoteForJSON(text string, asciiOnly bool) []byte {
return append(bytes, '"')
}

func QuoteIdentifier(js []byte, name string, unsupportedFeatures compat.JSFeature) []byte {
isASCII := false
asciiStart := 0
for i, c := range name {
if c >= firstASCII && c <= lastASCII {
// Fast path: a run of ASCII characters
if !isASCII {
isASCII = true
asciiStart = i
}
} else {
// Slow path: escape non-ACSII characters
if isASCII {
js = append(js, name[asciiStart:i]...)
isASCII = false
}
if c <= 0xFFFF {
js = append(js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15])
} else if !unsupportedFeatures.Has(compat.UnicodeEscapes) {
js = append(js, fmt.Sprintf("\\u{%X}", c)...)
} else {
panic("Internal error: Cannot encode identifier: Unicode escapes are unsupported")
}
}
}
if isASCII {
// Print one final run of ASCII characters
js = append(js, name[asciiStart:]...)
}
return js
}

func (p *printer) printQuotedUTF16(text []uint16, quote rune) {
temp := make([]byte, utf8.UTFMax)
js := p.js
Expand Down Expand Up @@ -792,38 +824,10 @@ func (p *printer) canPrintIdentifierUTF16(name []uint16) bool {

func (p *printer) printIdentifier(name string) {
if p.options.ASCIIOnly {
isASCII := false
asciiStart := 0
for i, c := range name {
if c >= firstASCII && c <= lastASCII {
// Fast path: a run of ASCII characters
if !isASCII {
isASCII = true
asciiStart = i
}
} else {
// Slow path: escape non-ACSII characters
if isASCII {
p.print(name[asciiStart:i])
isASCII = false
}
if c <= 0xFFFF {
p.js = append(p.js, '\\', 'u', hexChars[c>>12], hexChars[(c>>8)&15], hexChars[(c>>4)&15], hexChars[c&15])
} else if !p.options.UnsupportedFeatures.Has(compat.UnicodeEscapes) {
p.print(fmt.Sprintf("\\u{%X}", c))
} else {
panic("Internal error: Cannot encode identifier: Unicode escapes are unsupported")
}
}
}
if isASCII {
// Print one final run of ASCII characters
p.print(name[asciiStart:])
}
return
p.js = QuoteIdentifier(p.js, name, p.options.UnsupportedFeatures)
} else {
p.print(name)
}

p.print(name)
}

// This is the same as "printIdentifier(StringToUTF16(bytes))" without any
Expand Down
Loading

0 comments on commit 83b307f

Please sign in to comment.