// Package js minifies ECMAScript 2021 following the language specification at https://tc39.es/ecma262/. package js import ( "bytes" "io" "github.com/tdewolff/minify/v2" "github.com/tdewolff/parse/v2" "github.com/tdewolff/parse/v2/js" ) type blockType int const ( defaultBlock blockType = iota functionBlock iterationBlock ) // DefaultMinifier is the default minifier. var DefaultMinifier = &Minifier{} // Minifier is a JS minifier. type Minifier struct { Precision int // number of significant digits KeepVarNames bool } // Minify minifies JS data, it reads from r and writes to w. func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error { return DefaultMinifier.Minify(m, w, r, params) } // Minify minifies JS data, it reads from r and writes to w. func (o *Minifier) Minify(_ *minify.M, w io.Writer, r io.Reader, _ map[string]string) error { z := parse.NewInput(r) ast, err := js.Parse(z) if err != nil { return err } // license comments for _, comment := range ast.Comments { if 3 < len(comment) && comment[2] == '!' { w.Write(comment) if comment[1] == '/' { w.Write(newlineBytes) } } else if 2 < len(comment) && comment[0] == '#' && comment[1] == '!' { w.Write(comment) } } m := &jsMinifier{ o: o, w: w, renamer: newRenamer(ast, ast.Undeclared, !o.KeepVarNames), } m.hoistVars(&ast.BlockStmt) ast.List = m.optimizeStmtList(ast.List, functionBlock) for _, item := range ast.List { m.writeSemicolon() m.minifyStmt(item) } if _, err := w.Write(nil); err != nil { return err } return nil } type expectExpr int const ( expectAny expectExpr = iota expectExprStmt // in statement expectExprBody // in arrow function body ) type jsMinifier struct { o *Minifier w io.Writer prev []byte needsSemicolon bool // write a semicolon if required needsSpace bool // write a space if next token is an identifier expectExpr expectExpr // avoid ambiguous syntax such as an expression starting with function groupedStmt bool // avoid ambiguous syntax by grouping the expression statement inFor bool spaceBefore byte varsHoisted *js.VarDecl // set when variables are hoisted to this declaration renamer *renamer } func (m *jsMinifier) write(b []byte) { // 0 < len(b) if m.needsSpace && js.IsIdentifierContinue(b) || m.spaceBefore == b[0] { m.w.Write(spaceBytes) } m.w.Write(b) m.prev = b m.needsSpace = false m.expectExpr = expectAny m.spaceBefore = 0 } func (m *jsMinifier) writeSpaceAfterIdent() { if js.IsIdentifierEnd(m.prev) || 1 < len(m.prev) && m.prev[0] == '/' { m.w.Write(spaceBytes) } } func (m *jsMinifier) writeSpaceBeforeIdent() { m.needsSpace = true } func (m *jsMinifier) writeSpaceBefore(c byte) { m.spaceBefore = c } func (m *jsMinifier) requireSemicolon() { m.needsSemicolon = true } func (m *jsMinifier) writeSemicolon() { if m.needsSemicolon { m.w.Write(semicolonBytes) m.needsSemicolon = false m.needsSpace = false } } func (m *jsMinifier) minifyStmt(i js.IStmt) { switch stmt := i.(type) { case *js.ExprStmt: m.expectExpr = expectExprStmt m.minifyExpr(stmt.Value, js.OpExpr) if m.groupedStmt { m.write(closeParenBytes) m.groupedStmt = false } m.requireSemicolon() case *js.VarDecl: m.minifyVarDecl(stmt, false) m.requireSemicolon() case *js.IfStmt: hasIf := !m.isEmptyStmt(stmt.Body) hasElse := !m.isEmptyStmt(stmt.Else) m.write(ifOpenBytes) m.minifyExpr(stmt.Cond, js.OpExpr) m.write(closeParenBytes) if !hasIf && hasElse { m.requireSemicolon() } else if hasIf { if hasElse && endsInIf(stmt.Body) { // prevent: if(a){if(b)c}else d; => if(a)if(b)c;else d; m.write(openBraceBytes) m.minifyStmt(stmt.Body) m.write(closeBraceBytes) m.needsSemicolon = false } else { m.minifyStmt(stmt.Body) } } if hasElse { m.writeSemicolon() m.write(elseBytes) m.writeSpaceBeforeIdent() m.minifyStmt(stmt.Else) } case *js.BlockStmt: m.renamer.renameScope(stmt.Scope) m.minifyBlockStmt(*stmt) case *js.ReturnStmt: m.write(returnBytes) m.writeSpaceBeforeIdent() m.minifyExpr(stmt.Value, js.OpExpr) m.requireSemicolon() case *js.LabelledStmt: m.write(stmt.Label) m.write(colonBytes) m.minifyStmt(stmt.Value) case *js.BranchStmt: m.write(stmt.Type.Bytes()) if stmt.Label != nil { m.write(spaceBytes) m.write(stmt.Label) } m.requireSemicolon() case *js.WithStmt: m.write(withOpenBytes) m.minifyExpr(stmt.Cond, js.OpExpr) m.write(closeParenBytes) m.minifyStmtOrBlock(stmt.Body, defaultBlock) case *js.DoWhileStmt: m.write(doBytes) m.writeSpaceBeforeIdent() m.minifyStmtOrBlock(stmt.Body, iterationBlock) m.writeSemicolon() m.write(whileOpenBytes) m.minifyExpr(stmt.Cond, js.OpExpr) m.write(closeParenBytes) case *js.WhileStmt: m.write(whileOpenBytes) m.minifyExpr(stmt.Cond, js.OpExpr) m.write(closeParenBytes) m.minifyStmtOrBlock(stmt.Body, iterationBlock) case *js.ForStmt: m.renamer.renameScope(stmt.Body.Scope) m.write(forOpenBytes) m.inFor = true if decl, ok := stmt.Init.(*js.VarDecl); ok { m.minifyVarDecl(decl, false) } else { m.minifyExpr(stmt.Init, js.OpLHS) } m.inFor = false m.write(semicolonBytes) m.minifyExpr(stmt.Cond, js.OpExpr) m.write(semicolonBytes) m.minifyExpr(stmt.Post, js.OpExpr) m.write(closeParenBytes) m.minifyBlockAsStmt(&stmt.Body, iterationBlock) case *js.ForInStmt: m.renamer.renameScope(stmt.Body.Scope) m.write(forOpenBytes) m.inFor = true if decl, ok := stmt.Init.(*js.VarDecl); ok { m.minifyVarDecl(decl, false) } else { m.minifyExpr(stmt.Init, js.OpLHS) } m.inFor = false m.writeSpaceAfterIdent() m.write(inBytes) m.writeSpaceBeforeIdent() m.minifyExpr(stmt.Value, js.OpExpr) m.write(closeParenBytes) m.minifyBlockAsStmt(&stmt.Body, iterationBlock) case *js.ForOfStmt: m.renamer.renameScope(stmt.Body.Scope) if stmt.Await { m.write(forAwaitOpenBytes) } else { m.write(forOpenBytes) } m.inFor = true if decl, ok := stmt.Init.(*js.VarDecl); ok { m.minifyVarDecl(decl, false) } else { m.minifyExpr(stmt.Init, js.OpLHS) } m.inFor = false m.writeSpaceAfterIdent() m.write(ofBytes) m.writeSpaceBeforeIdent() m.minifyExpr(stmt.Value, js.OpAssign) m.write(closeParenBytes) m.minifyBlockAsStmt(&stmt.Body, iterationBlock) case *js.SwitchStmt: m.write(switchOpenBytes) m.minifyExpr(stmt.Init, js.OpExpr) m.write(closeParenOpenBracketBytes) m.needsSemicolon = false for _, clause := range stmt.List { m.writeSemicolon() m.write(clause.TokenType.Bytes()) if clause.Cond != nil { m.writeSpaceBeforeIdent() m.minifyExpr(clause.Cond, js.OpExpr) } m.write(colonBytes) clause.List = m.optimizeStmtList(clause.List, defaultBlock) for _, item := range clause.List { m.writeSemicolon() m.minifyStmt(item) } } m.write(closeBraceBytes) m.needsSemicolon = false case *js.ThrowStmt: m.write(throwBytes) m.writeSpaceBeforeIdent() m.minifyExpr(stmt.Value, js.OpExpr) m.requireSemicolon() case *js.TryStmt: m.write(tryBytes) m.renamer.renameScope(stmt.Body.Scope) stmt.Body.List = m.optimizeStmtList(stmt.Body.List, defaultBlock) m.minifyBlockStmt(stmt.Body) if stmt.Catch != nil { m.write(catchBytes) m.renamer.renameScope(stmt.Catch.Scope) if stmt.Binding != nil { m.write(openParenBytes) m.minifyBinding(stmt.Binding) m.write(closeParenBytes) } stmt.Catch.List = m.optimizeStmtList(stmt.Catch.List, defaultBlock) m.minifyBlockStmt(*stmt.Catch) } if stmt.Finally != nil { m.write(finallyBytes) m.renamer.renameScope(stmt.Finally.Scope) stmt.Finally.List = m.optimizeStmtList(stmt.Finally.List, defaultBlock) m.minifyBlockStmt(*stmt.Finally) } case *js.FuncDecl: m.minifyFuncDecl(*stmt, false) case *js.ClassDecl: m.minifyClassDecl(*stmt) case *js.DebuggerStmt: m.write(debuggerBytes) m.requireSemicolon() case *js.EmptyStmt: case *js.ImportStmt: m.write(importBytes) if stmt.Default != nil { m.write(spaceBytes) m.write(stmt.Default) if len(stmt.List) != 0 { m.write(commaBytes) } else if stmt.Default != nil || len(stmt.List) != 0 { m.write(spaceBytes) } } if len(stmt.List) == 1 && len(stmt.List[0].Name) == 1 && stmt.List[0].Name[0] == '*' { m.writeSpaceBeforeIdent() m.minifyAlias(stmt.List[0]) if stmt.Default != nil || len(stmt.List) != 0 { m.write(spaceBytes) } } else if 0 < len(stmt.List) { m.write(openBraceBytes) for i, item := range stmt.List { if i != 0 { m.write(commaBytes) } m.minifyAlias(item) } m.write(closeBraceBytes) } if stmt.Default != nil || len(stmt.List) != 0 { m.write(fromBytes) } m.write(stmt.Module) m.requireSemicolon() case *js.ExportStmt: m.write(exportBytes) if stmt.Decl != nil { if stmt.Default { m.write(spaceDefaultBytes) m.writeSpaceBeforeIdent() m.minifyExpr(stmt.Decl, js.OpAssign) _, isHoistable := stmt.Decl.(*js.FuncDecl) _, isClass := stmt.Decl.(*js.ClassDecl) if !isHoistable && !isClass { m.requireSemicolon() } } else { m.writeSpaceBeforeIdent() m.minifyStmt(stmt.Decl.(js.IStmt)) // can only be variable, function, or class decl } } else { if len(stmt.List) == 1 && (len(stmt.List[0].Name) == 1 && stmt.List[0].Name[0] == '*' || stmt.List[0].Name == nil && len(stmt.List[0].Binding) == 1 && stmt.List[0].Binding[0] == '*') { m.writeSpaceBeforeIdent() m.minifyAlias(stmt.List[0]) if stmt.Module != nil && stmt.List[0].Name != nil { m.write(spaceBytes) } } else if 0 < len(stmt.List) { m.write(openBraceBytes) for i, item := range stmt.List { if i != 0 { m.write(commaBytes) } m.minifyAlias(item) } m.write(closeBraceBytes) } if stmt.Module != nil { m.write(fromBytes) m.write(stmt.Module) } m.requireSemicolon() } case *js.DirectivePrologueStmt: m.write(stmt.Value) m.requireSemicolon() } } func (m *jsMinifier) minifyBlockStmt(stmt js.BlockStmt) { m.write(openBraceBytes) m.needsSemicolon = false for _, item := range stmt.List { m.writeSemicolon() m.minifyStmt(item) } m.write(closeBraceBytes) m.needsSemicolon = false } func (m *jsMinifier) minifyBlockAsStmt(blockStmt *js.BlockStmt, blockType blockType) { // minify block when statement is expected, i.e. semicolon if empty or remove braces for single statement // assume we already renamed the scope blockStmt.List = m.optimizeStmtList(blockStmt.List, blockType) hasLexicalVars := false for _, v := range blockStmt.Scope.Declared[blockStmt.Scope.NumForInit:] { if v.Decl == js.LexicalDecl { hasLexicalVars = true break } } if 1 < len(blockStmt.List) || hasLexicalVars { m.minifyBlockStmt(*blockStmt) } else if len(blockStmt.List) == 1 { m.minifyStmt(blockStmt.List[0]) } else { m.write(semicolonBytes) m.needsSemicolon = false } } func (m *jsMinifier) minifyStmtOrBlock(i js.IStmt, blockType blockType) { // minify stmt or a block if blockStmt, ok := i.(*js.BlockStmt); ok { m.renamer.renameScope(blockStmt.Scope) m.minifyBlockAsStmt(blockStmt, blockType) } else { // optimizeStmtList can in some cases expand one stmt to two shorter stmts list := m.optimizeStmtList([]js.IStmt{i}, blockType) if len(list) == 1 { m.minifyStmt(list[0]) } else if len(list) == 0 { m.write(semicolonBytes) m.needsSemicolon = false } else { m.minifyBlockStmt(js.BlockStmt{List: list, Scope: js.Scope{}}) } } } func (m *jsMinifier) minifyAlias(alias js.Alias) { if alias.Name != nil { m.write(alias.Name) if !bytes.Equal(alias.Name, starBytes) { m.write(spaceBytes) } m.write(asSpaceBytes) } if alias.Binding != nil { m.write(alias.Binding) } } func (m *jsMinifier) minifyParams(params js.Params) { m.write(openParenBytes) for i, item := range params.List { if i != 0 { m.write(commaBytes) } m.minifyBindingElement(item) } if params.Rest != nil { if len(params.List) != 0 { m.write(commaBytes) } m.write(ellipsisBytes) m.minifyBinding(params.Rest) } m.write(closeParenBytes) } func (m *jsMinifier) minifyArguments(args js.Args) { m.write(openParenBytes) for i, item := range args.List { if i != 0 { m.write(commaBytes) } if item.Rest { m.write(ellipsisBytes) } m.minifyExpr(item.Value, js.OpAssign) } m.write(closeParenBytes) } func (m *jsMinifier) minifyVarDecl(decl *js.VarDecl, onlyDefines bool) { if decl.TokenType == js.VarToken && m.varsHoisted != nil && decl != m.varsHoisted { // remove 'var' when hoisting variables first := true for _, item := range decl.List { if item.Default != nil || !onlyDefines { if !first { m.write(commaBytes) } m.minifyBindingElement(item) first = false } } } else { m.write(decl.TokenType.Bytes()) m.writeSpaceBeforeIdent() for i, item := range decl.List { if i != 0 { m.write(commaBytes) } m.minifyBindingElement(item) } } } func (m *jsMinifier) minifyFuncDecl(decl js.FuncDecl, inExpr bool) { parentRename := m.renamer.rename m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames parentVarsHoisted := m.hoistVars(&decl.Body) if decl.Async { m.write(asyncSpaceBytes) } m.write(functionBytes) if decl.Generator { m.write(starBytes) } if inExpr { m.renamer.renameScope(decl.Body.Scope) } if decl.Name != nil && (!inExpr || 1 < decl.Name.Uses) { if !decl.Generator { m.write(spaceBytes) } m.write(decl.Name.Data) } if !inExpr { m.renamer.renameScope(decl.Body.Scope) } m.minifyParams(decl.Params) decl.Body.List = m.optimizeStmtList(decl.Body.List, functionBlock) m.minifyBlockStmt(decl.Body) m.varsHoisted = parentVarsHoisted m.renamer.rename = parentRename } func (m *jsMinifier) minifyMethodDecl(decl js.MethodDecl) { parentRename := m.renamer.rename m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames parentVarsHoisted := m.hoistVars(&decl.Body) if decl.Static { m.write(staticBytes) m.writeSpaceBeforeIdent() } if decl.Async { m.write(asyncBytes) if decl.Generator { m.write(starBytes) } else { m.writeSpaceBeforeIdent() } } else if decl.Generator { m.write(starBytes) } else if decl.Get { m.write(getBytes) m.writeSpaceBeforeIdent() } else if decl.Set { m.write(setBytes) m.writeSpaceBeforeIdent() } m.minifyPropertyName(decl.Name) m.renamer.renameScope(decl.Body.Scope) m.minifyParams(decl.Params) decl.Body.List = m.optimizeStmtList(decl.Body.List, functionBlock) m.minifyBlockStmt(decl.Body) m.varsHoisted = parentVarsHoisted m.renamer.rename = parentRename } func (m *jsMinifier) minifyArrowFunc(decl js.ArrowFunc) { parentRename := m.renamer.rename m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames parentVarsHoisted := m.hoistVars(&decl.Body) m.renamer.renameScope(decl.Body.Scope) if decl.Async { m.write(asyncBytes) } removeParens := false if decl.Params.Rest == nil && len(decl.Params.List) == 1 && decl.Params.List[0].Default == nil { if decl.Params.List[0].Binding == nil { removeParens = true } else if _, ok := decl.Params.List[0].Binding.(*js.Var); ok { removeParens = true } } if removeParens { if decl.Async && decl.Params.List[0].Binding != nil { // add space after async in: async a => ... m.write(spaceBytes) } m.minifyBindingElement(decl.Params.List[0]) } else { parentInFor := m.inFor m.inFor = false m.minifyParams(decl.Params) m.inFor = parentInFor } m.write(arrowBytes) removeBraces := false if 0 < len(decl.Body.List) { returnStmt, isReturn := decl.Body.List[len(decl.Body.List)-1].(*js.ReturnStmt) if isReturn && returnStmt.Value != nil { // merge expression statements to final return statement, remove function body braces var list []js.IExpr removeBraces = true for _, item := range decl.Body.List[:len(decl.Body.List)-1] { if expr, isExpr := item.(*js.ExprStmt); isExpr { list = append(list, expr.Value) } else { removeBraces = false break } } if removeBraces { list = append(list, returnStmt.Value) expr := list[0] for _, right := range list[1:] { expr = &js.BinaryExpr{Op: js.CommaToken, X: expr, Y: right} } if 0 < len(list) { expr = &js.GroupExpr{X: expr} } m.expectExpr = expectExprBody m.minifyExpr(expr, js.OpAssign) if m.groupedStmt { m.write(closeParenBytes) m.groupedStmt = false } } } else if isReturn && returnStmt.Value == nil { // remove empty return decl.Body.List = decl.Body.List[:len(decl.Body.List)-1] } } if !removeBraces { decl.Body.List = m.optimizeStmtList(decl.Body.List, functionBlock) m.minifyBlockStmt(decl.Body) } m.varsHoisted = parentVarsHoisted m.renamer.rename = parentRename } func (m *jsMinifier) minifyClassDecl(decl js.ClassDecl) { m.write(classBytes) if decl.Name != nil { m.write(spaceBytes) m.write(decl.Name.Data) } if decl.Extends != nil { m.write(spaceExtendsBytes) m.writeSpaceBeforeIdent() m.minifyExpr(decl.Extends, js.OpLHS) } m.write(openBraceBytes) m.needsSemicolon = false for _, item := range decl.Definitions { m.writeSemicolon() m.minifyPropertyName(item.Name) if item.Init != nil { m.write(equalBytes) m.minifyExpr(item.Init, js.OpAssign) } m.requireSemicolon() } if 0 < len(decl.Methods) { m.writeSemicolon() for _, item := range decl.Methods { m.minifyMethodDecl(item) } } m.write(closeBraceBytes) m.needsSemicolon = false } func (m *jsMinifier) minifyPropertyName(name js.PropertyName) { if name.IsComputed() { m.write(openBracketBytes) m.minifyExpr(name.Computed, js.OpAssign) m.write(closeBracketBytes) } else { m.write(name.Literal.Data) } } func (m *jsMinifier) minifyProperty(property js.Property) { // property.Name is always set in ObjectLiteral if property.Spread { m.write(ellipsisBytes) } else if v, ok := property.Value.(*js.Var); property.Name != nil && (!ok || !property.Name.IsIdent(v.Name())) { // add 'old-name:' before BindingName as the latter will be renamed m.minifyPropertyName(*property.Name) m.write(colonBytes) } m.minifyExpr(property.Value, js.OpAssign) if property.Init != nil { m.write(equalBytes) m.minifyExpr(property.Init, js.OpAssign) } } func (m *jsMinifier) minifyBindingElement(element js.BindingElement) { if element.Binding != nil { parentInFor := m.inFor m.inFor = false m.minifyBinding(element.Binding) m.inFor = parentInFor if element.Default != nil { m.write(equalBytes) m.minifyExpr(element.Default, js.OpAssign) } } } func (m *jsMinifier) minifyBinding(ibinding js.IBinding) { switch binding := ibinding.(type) { case *js.Var: m.write(binding.Data) case *js.BindingArray: m.write(openBracketBytes) for i, item := range binding.List { if i != 0 { m.write(commaBytes) } m.minifyBindingElement(item) } if binding.Rest != nil { if 0 < len(binding.List) { m.write(commaBytes) } m.write(ellipsisBytes) m.minifyBinding(binding.Rest) } m.write(closeBracketBytes) case *js.BindingObject: m.write(openBraceBytes) for i, item := range binding.List { if i != 0 { m.write(commaBytes) } // item.Key is always set if item.Key.IsComputed() { m.minifyPropertyName(*item.Key) m.write(colonBytes) } else if v, ok := item.Value.Binding.(*js.Var); !ok || !item.Key.IsIdent(v.Data) { // add 'old-name:' before BindingName as the latter will be renamed m.minifyPropertyName(*item.Key) m.write(colonBytes) } m.minifyBindingElement(item.Value) } if binding.Rest != nil { if 0 < len(binding.List) { m.write(commaBytes) } m.write(ellipsisBytes) m.write(binding.Rest.Data) } m.write(closeBraceBytes) } } func (m *jsMinifier) minifyBinaryExpr(expr *js.BinaryExpr) bool { if lit, ok := expr.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken && expr.Op == js.AddToken { // merge strings that are added together n := len(lit.Data) - 2 strings := []*js.LiteralExpr{lit} left := expr for { if 50 < len(strings) { return false // limit recursion } else if lit, ok := left.X.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken { n += len(lit.Data) - 2 strings = append(strings, lit) break } else if left, ok = left.X.(*js.BinaryExpr); ok && left.Op == js.AddToken { if lit, ok := left.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken { n += len(lit.Data) - 2 strings = append(strings, lit) continue } } return false } b := make([]byte, 0, n+2) b = append(b, strings[len(strings)-1].Data[:len(strings[len(strings)-1].Data)-1]...) for i := len(strings) - 2; 0 < i; i-- { b = append(b, strings[i].Data[1:len(strings[i].Data)-1]...) } b = append(b, strings[0].Data[1:]...) b[len(b)-1] = b[0] // unescaped quotes will be repaired in minifyString m.write(minifyString(b)) return true } return false } func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) { switch expr := i.(type) { case *js.Var: for expr.Link != nil { expr = expr.Link } data := expr.Data if bytes.Equal(data, undefinedBytes) { // TODO: only if not defined if js.OpUnary < prec { m.write(groupedVoidZeroBytes) } else { m.write(voidZeroBytes) } } else if bytes.Equal(data, infinityBytes) { // TODO: only if not defined if js.OpMul < prec { m.write(groupedOneDivZeroBytes) } else { m.write(oneDivZeroBytes) } } else { m.write(data) } case *js.LiteralExpr: if expr.TokenType == js.DecimalToken { m.write(minify.Number(expr.Data, m.o.Precision)) } else if expr.TokenType == js.BinaryToken { m.write(binaryNumber(expr.Data, m.o.Precision)) } else if expr.TokenType == js.OctalToken { m.write(octalNumber(expr.Data, m.o.Precision)) } else if expr.TokenType == js.HexadecimalToken { m.write(hexadecimalNumber(expr.Data, m.o.Precision)) } else if expr.TokenType == js.TrueToken { if js.OpUnary < prec { m.write(groupedNotZeroBytes) } else { m.write(notZeroBytes) } } else if expr.TokenType == js.FalseToken { if js.OpUnary < prec { m.write(groupedNotOneBytes) } else { m.write(notOneBytes) } } else if expr.TokenType == js.StringToken { m.write(minifyString(expr.Data)) } else { m.write(expr.Data) } case *js.BinaryExpr: if m.minifyBinaryExpr(expr) { break } precLeft := binaryLeftPrecMap[expr.Op] // convert (a,b)&&c into a,b&&c but not a=(b,c)&&d into a=(b,c&&d) if prec <= js.OpExpr { if group, ok := expr.X.(*js.GroupExpr); ok { if binary, ok := group.X.(*js.BinaryExpr); ok && binary.Op == js.CommaToken && js.OpAnd <= exprPrec(binary.Y) { expr.X = group.X precLeft = js.OpExpr } } } if expr.Op == js.InstanceofToken || expr.Op == js.InToken { group := expr.Op == js.InToken && m.inFor if group { m.write(openParenBytes) } m.minifyExpr(expr.X, precLeft) m.writeSpaceAfterIdent() m.write(expr.Op.Bytes()) m.writeSpaceBeforeIdent() m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op]) if group { m.write(closeParenBytes) } } else { m.minifyExpr(expr.X, precLeft) if expr.Op == js.GtToken && m.prev[len(m.prev)-1] == '-' { m.write(spaceBytes) } else if expr.Op == js.EqEqEqToken || expr.Op == js.NotEqEqToken { if left, ok := expr.X.(*js.UnaryExpr); ok && left.Op == js.TypeofToken { if right, ok := expr.Y.(*js.LiteralExpr); ok && right.TokenType == js.StringToken { if expr.Op == js.EqEqEqToken { expr.Op = js.EqEqToken } else { expr.Op = js.NotEqToken } } } else if right, ok := expr.Y.(*js.UnaryExpr); ok && right.Op == js.TypeofToken { if left, ok := expr.X.(*js.LiteralExpr); ok && left.TokenType == js.StringToken { if expr.Op == js.EqEqEqToken { expr.Op = js.EqEqToken } else { expr.Op = js.NotEqToken } } } } m.write(expr.Op.Bytes()) if expr.Op == js.AddToken { // +++ => + ++ m.writeSpaceBefore('+') } else if expr.Op == js.SubToken { // --- => - -- m.writeSpaceBefore('-') } else if expr.Op == js.DivToken { // // => / / m.writeSpaceBefore('/') } m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op]) } case *js.UnaryExpr: if expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken { m.minifyExpr(expr.X, unaryPrecMap[expr.Op]) m.write(expr.Op.Bytes()) } else { isLtNot := expr.Op == js.NotToken && len(m.prev) == 1 && m.prev[0] == '<' m.write(expr.Op.Bytes()) if expr.Op == js.DeleteToken || expr.Op == js.VoidToken || expr.Op == js.TypeofToken || expr.Op == js.AwaitToken { m.writeSpaceBeforeIdent() } else if expr.Op == js.PosToken { // +++ => + ++ m.writeSpaceBefore('+') } else if expr.Op == js.NegToken || isLtNot { // --- => - -- //