1// Package js minifies ECMAScript 2021 following the language specification at https://tc39.es/ecma262/.
2package js
3
4import (
5 "bytes"
6 "io"
7
8 "github.com/tdewolff/minify/v2"
9 "github.com/tdewolff/parse/v2"
10 "github.com/tdewolff/parse/v2/js"
11)
12
13type blockType int
14
15const (
16 defaultBlock blockType = iota
17 functionBlock
18 iterationBlock
19)
20
21// Minifier is a JS minifier.
22type Minifier struct {
23 Precision int // number of significant digits
24 KeepVarNames bool
25 useAlphabetVarNames bool
26 Version int
27}
28
29func (o *Minifier) minVersion(version int) bool {
30 return o.Version == 0 || version <= o.Version
31}
32
33// Minify minifies JS data, it reads from r and writes to w.
34func Minify(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error {
35 return (&Minifier{}).Minify(m, w, r, params)
36}
37
38// Minify minifies JS data, it reads from r and writes to w.
39func (o *Minifier) Minify(_ *minify.M, w io.Writer, r io.Reader, _ map[string]string) error {
40 z := parse.NewInput(r)
41 ast, err := js.Parse(z, js.Options{WhileToFor: true})
42 if err != nil {
43 return err
44 }
45
46 // license comments
47 for _, comment := range ast.Comments {
48 if 3 < len(comment) && comment[2] == '!' {
49 w.Write(comment)
50 if comment[1] == '/' {
51 w.Write(newlineBytes)
52 }
53 } else if 2 < len(comment) && comment[0] == '#' && comment[1] == '!' {
54 w.Write(comment)
55 }
56 }
57
58 m := &jsMinifier{
59 o: o,
60 w: w,
61 renamer: newRenamer(!o.KeepVarNames, !o.useAlphabetVarNames),
62 }
63 m.hoistVars(&ast.BlockStmt)
64 ast.List = optimizeStmtList(ast.List, functionBlock)
65 for _, item := range ast.List {
66 m.writeSemicolon()
67 m.minifyStmt(item)
68 }
69
70 if _, err := w.Write(nil); err != nil {
71 return err
72 }
73 return nil
74}
75
76type expectExpr int
77
78const (
79 expectAny expectExpr = iota
80 expectExprStmt // in statement
81 expectExprBody // in arrow function body
82)
83
84type jsMinifier struct {
85 o *Minifier
86 w io.Writer
87
88 prev []byte
89 needsSemicolon bool // write a semicolon if required
90 needsSpace bool // write a space if next token is an identifier
91 expectExpr expectExpr // avoid ambiguous syntax such as an expression starting with function
92 groupedStmt bool // avoid ambiguous syntax by grouping the expression statement
93 inFor bool
94 spaceBefore byte
95
96 renamer *renamer
97}
98
99func (m *jsMinifier) write(b []byte) {
100 // 0 < len(b)
101 if m.needsSpace && js.IsIdentifierContinue(b) || m.spaceBefore == b[0] {
102 m.w.Write(spaceBytes)
103 }
104 m.w.Write(b)
105 m.prev = b
106 m.needsSpace = false
107 m.expectExpr = expectAny
108 m.spaceBefore = 0
109}
110
111func (m *jsMinifier) writeSpaceAfterIdent() {
112 // space after identifier and after regular expression (to prevent confusion with its tag)
113 if js.IsIdentifierEnd(m.prev) || 1 < len(m.prev) && m.prev[0] == '/' {
114 m.w.Write(spaceBytes)
115 }
116}
117
118func (m *jsMinifier) writeSpaceBeforeIdent() {
119 m.needsSpace = true
120}
121
122func (m *jsMinifier) writeSpaceBefore(c byte) {
123 m.spaceBefore = c
124}
125
126func (m *jsMinifier) requireSemicolon() {
127 m.needsSemicolon = true
128}
129
130func (m *jsMinifier) writeSemicolon() {
131 if m.needsSemicolon {
132 m.w.Write(semicolonBytes)
133 m.needsSemicolon = false
134 m.needsSpace = false
135 }
136}
137
138func (m *jsMinifier) minifyStmt(i js.IStmt) {
139 switch stmt := i.(type) {
140 case *js.ExprStmt:
141 m.expectExpr = expectExprStmt
142 m.minifyExpr(stmt.Value, js.OpExpr)
143 if m.groupedStmt {
144 m.write(closeParenBytes)
145 m.groupedStmt = false
146 }
147 m.requireSemicolon()
148 case *js.VarDecl:
149 m.minifyVarDecl(stmt, false)
150 m.requireSemicolon()
151 case *js.IfStmt:
152 hasIf := !isEmptyStmt(stmt.Body)
153 hasElse := !isEmptyStmt(stmt.Else)
154 if !hasIf && !hasElse {
155 break
156 }
157
158 m.write(ifOpenBytes)
159 m.minifyExpr(stmt.Cond, js.OpExpr)
160 m.write(closeParenBytes)
161
162 if !hasIf && hasElse {
163 m.requireSemicolon()
164 } else if hasIf {
165 if hasElse && endsInIf(stmt.Body) {
166 // prevent: if(a){if(b)c}else d; => if(a)if(b)c;else d;
167 m.write(openBraceBytes)
168 m.minifyStmt(stmt.Body)
169 m.write(closeBraceBytes)
170 m.needsSemicolon = false
171 } else {
172 m.minifyStmt(stmt.Body)
173 }
174 }
175 if hasElse {
176 m.writeSemicolon()
177 m.write(elseBytes)
178 m.writeSpaceBeforeIdent()
179 m.minifyStmt(stmt.Else)
180 }
181 case *js.BlockStmt:
182 m.renamer.renameScope(stmt.Scope)
183 m.minifyBlockStmt(stmt)
184 case *js.ReturnStmt:
185 m.write(returnBytes)
186 m.writeSpaceBeforeIdent()
187 m.minifyExpr(stmt.Value, js.OpExpr)
188 m.requireSemicolon()
189 case *js.LabelledStmt:
190 m.write(stmt.Label)
191 m.write(colonBytes)
192 m.minifyStmtOrBlock(stmt.Value, defaultBlock)
193 case *js.BranchStmt:
194 m.write(stmt.Type.Bytes())
195 if stmt.Label != nil {
196 m.write(spaceBytes)
197 m.write(stmt.Label)
198 }
199 m.requireSemicolon()
200 case *js.WithStmt:
201 m.write(withOpenBytes)
202 m.minifyExpr(stmt.Cond, js.OpExpr)
203 m.write(closeParenBytes)
204 m.minifyStmtOrBlock(stmt.Body, defaultBlock)
205 case *js.DoWhileStmt:
206 m.write(doBytes)
207 m.writeSpaceBeforeIdent()
208 m.minifyStmtOrBlock(stmt.Body, iterationBlock)
209 m.writeSemicolon()
210 m.write(whileOpenBytes)
211 m.minifyExpr(stmt.Cond, js.OpExpr)
212 m.write(closeParenBytes)
213 case *js.WhileStmt:
214 m.write(whileOpenBytes)
215 m.minifyExpr(stmt.Cond, js.OpExpr)
216 m.write(closeParenBytes)
217 m.minifyStmtOrBlock(stmt.Body, iterationBlock)
218 case *js.ForStmt:
219 stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock)
220 m.renamer.renameScope(stmt.Body.Scope)
221 m.write(forOpenBytes)
222 m.inFor = true
223 if decl, ok := stmt.Init.(*js.VarDecl); ok {
224 m.minifyVarDecl(decl, true)
225 } else {
226 m.minifyExpr(stmt.Init, js.OpLHS)
227 }
228 m.inFor = false
229 m.write(semicolonBytes)
230 m.minifyExpr(stmt.Cond, js.OpExpr)
231 m.write(semicolonBytes)
232 m.minifyExpr(stmt.Post, js.OpExpr)
233 m.write(closeParenBytes)
234 m.minifyBlockAsStmt(stmt.Body)
235 case *js.ForInStmt:
236 stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock)
237 m.renamer.renameScope(stmt.Body.Scope)
238 m.write(forOpenBytes)
239 m.inFor = true
240 if decl, ok := stmt.Init.(*js.VarDecl); ok {
241 m.minifyVarDecl(decl, false)
242 } else {
243 m.minifyExpr(stmt.Init, js.OpLHS)
244 }
245 m.inFor = false
246 m.writeSpaceAfterIdent()
247 m.write(inBytes)
248 m.writeSpaceBeforeIdent()
249 m.minifyExpr(stmt.Value, js.OpExpr)
250 m.write(closeParenBytes)
251 m.minifyBlockAsStmt(stmt.Body)
252 case *js.ForOfStmt:
253 stmt.Body.List = optimizeStmtList(stmt.Body.List, iterationBlock)
254 m.renamer.renameScope(stmt.Body.Scope)
255 if stmt.Await {
256 m.write(forAwaitOpenBytes)
257 } else {
258 m.write(forOpenBytes)
259 }
260 m.inFor = true
261 if decl, ok := stmt.Init.(*js.VarDecl); ok {
262 m.minifyVarDecl(decl, false)
263 } else {
264 m.minifyExpr(stmt.Init, js.OpLHS)
265 }
266 m.inFor = false
267 m.writeSpaceAfterIdent()
268 m.write(ofBytes)
269 m.writeSpaceBeforeIdent()
270 m.minifyExpr(stmt.Value, js.OpAssign)
271 m.write(closeParenBytes)
272 m.minifyBlockAsStmt(stmt.Body)
273 case *js.SwitchStmt:
274 m.write(switchOpenBytes)
275 m.minifyExpr(stmt.Init, js.OpExpr)
276 m.write(closeParenOpenBracketBytes)
277 m.needsSemicolon = false
278 for i, _ := range stmt.List {
279 stmt.List[i].List = optimizeStmtList(stmt.List[i].List, defaultBlock)
280 }
281 m.renamer.renameScope(stmt.Scope)
282 for _, clause := range stmt.List {
283 m.writeSemicolon()
284 m.write(clause.TokenType.Bytes())
285 if clause.Cond != nil {
286 m.writeSpaceBeforeIdent()
287 m.minifyExpr(clause.Cond, js.OpExpr)
288 }
289 m.write(colonBytes)
290 for _, item := range clause.List {
291 m.writeSemicolon()
292 m.minifyStmt(item)
293 }
294 }
295 m.write(closeBraceBytes)
296 m.needsSemicolon = false
297 case *js.ThrowStmt:
298 m.write(throwBytes)
299 m.writeSpaceBeforeIdent()
300 m.minifyExpr(stmt.Value, js.OpExpr)
301 m.requireSemicolon()
302 case *js.TryStmt:
303 m.write(tryBytes)
304 stmt.Body.List = optimizeStmtList(stmt.Body.List, defaultBlock)
305 m.renamer.renameScope(stmt.Body.Scope)
306 m.minifyBlockStmt(stmt.Body)
307 if stmt.Catch != nil {
308 m.write(catchBytes)
309 stmt.Catch.List = optimizeStmtList(stmt.Catch.List, defaultBlock)
310 if v, ok := stmt.Binding.(*js.Var); ok && v.Uses == 1 && m.o.minVersion(2019) {
311 stmt.Catch.Scope.Declared = stmt.Catch.Scope.Declared[1:]
312 stmt.Binding = nil
313 }
314 m.renamer.renameScope(stmt.Catch.Scope)
315 if stmt.Binding != nil {
316 m.write(openParenBytes)
317 m.minifyBinding(stmt.Binding)
318 m.write(closeParenBytes)
319 }
320 m.minifyBlockStmt(stmt.Catch)
321 }
322 if stmt.Finally != nil {
323 m.write(finallyBytes)
324 stmt.Finally.List = optimizeStmtList(stmt.Finally.List, defaultBlock)
325 m.renamer.renameScope(stmt.Finally.Scope)
326 m.minifyBlockStmt(stmt.Finally)
327 }
328 case *js.FuncDecl:
329 m.minifyFuncDecl(stmt, false)
330 case *js.ClassDecl:
331 m.minifyClassDecl(stmt)
332 case *js.DebuggerStmt:
333 m.write(debuggerBytes)
334 m.requireSemicolon()
335 case *js.EmptyStmt:
336 case *js.ImportStmt:
337 m.write(importBytes)
338 if stmt.Default != nil {
339 m.write(spaceBytes)
340 m.write(stmt.Default)
341 if len(stmt.List) != 0 {
342 m.write(commaBytes)
343 } else if stmt.Default != nil || len(stmt.List) != 0 {
344 m.write(spaceBytes)
345 }
346 }
347 if len(stmt.List) == 1 && len(stmt.List[0].Name) == 1 && stmt.List[0].Name[0] == '*' {
348 m.writeSpaceBeforeIdent()
349 m.minifyAlias(stmt.List[0])
350 if stmt.Default != nil || len(stmt.List) != 0 {
351 m.write(spaceBytes)
352 }
353 } else if 0 < len(stmt.List) {
354 m.write(openBraceBytes)
355 for i, item := range stmt.List {
356 if i != 0 {
357 m.write(commaBytes)
358 }
359 m.minifyAlias(item)
360 }
361 m.write(closeBraceBytes)
362 }
363 if stmt.Default != nil || len(stmt.List) != 0 {
364 m.write(fromBytes)
365 }
366 m.write(minifyString(stmt.Module, false))
367 m.requireSemicolon()
368 case *js.ExportStmt:
369 m.write(exportBytes)
370 if stmt.Decl != nil {
371 if stmt.Default {
372 m.write(spaceDefaultBytes)
373 m.writeSpaceBeforeIdent()
374 m.minifyExpr(stmt.Decl, js.OpAssign)
375 _, isHoistable := stmt.Decl.(*js.FuncDecl)
376 _, isClass := stmt.Decl.(*js.ClassDecl)
377 if !isHoistable && !isClass {
378 m.requireSemicolon()
379 }
380 } else {
381 m.writeSpaceBeforeIdent()
382 m.minifyStmt(stmt.Decl.(js.IStmt)) // can only be variable, function, or class decl
383 }
384 } else {
385 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] == '*') {
386 m.writeSpaceBeforeIdent()
387 m.minifyAlias(stmt.List[0])
388 if stmt.Module != nil && stmt.List[0].Name != nil {
389 m.write(spaceBytes)
390 }
391 } else if 0 < len(stmt.List) {
392 m.write(openBraceBytes)
393 for i, item := range stmt.List {
394 if i != 0 {
395 m.write(commaBytes)
396 }
397 m.minifyAlias(item)
398 }
399 m.write(closeBraceBytes)
400 }
401 if stmt.Module != nil {
402 m.write(fromBytes)
403 m.write(minifyString(stmt.Module, false))
404 }
405 m.requireSemicolon()
406 }
407 case *js.DirectivePrologueStmt:
408 stmt.Value[0] = '"'
409 stmt.Value[len(stmt.Value)-1] = '"'
410 m.write(stmt.Value)
411 m.requireSemicolon()
412 }
413}
414
415func (m *jsMinifier) minifyBlockStmt(stmt *js.BlockStmt) {
416 m.write(openBraceBytes)
417 m.needsSemicolon = false
418 for _, item := range stmt.List {
419 m.writeSemicolon()
420 m.minifyStmt(item)
421 }
422 m.write(closeBraceBytes)
423 m.needsSemicolon = false
424}
425
426func (m *jsMinifier) minifyBlockAsStmt(blockStmt *js.BlockStmt) {
427 // minify block when statement is expected, i.e. semicolon if empty or remove braces for single statement
428 // assume we already renamed the scope
429 hasLexicalVars := false
430 for _, v := range blockStmt.Scope.Declared[blockStmt.Scope.NumForDecls:] {
431 if v.Decl == js.LexicalDecl {
432 hasLexicalVars = true
433 break
434 }
435 }
436 if 1 < len(blockStmt.List) || hasLexicalVars {
437 m.minifyBlockStmt(blockStmt)
438 } else if len(blockStmt.List) == 1 {
439 m.minifyStmt(blockStmt.List[0])
440 } else {
441 m.write(semicolonBytes)
442 m.needsSemicolon = false
443 }
444}
445
446func (m *jsMinifier) minifyStmtOrBlock(i js.IStmt, blockType blockType) {
447 // minify stmt or a block
448 if blockStmt, ok := i.(*js.BlockStmt); ok {
449 blockStmt.List = optimizeStmtList(blockStmt.List, blockType)
450 m.renamer.renameScope(blockStmt.Scope)
451 m.minifyBlockAsStmt(blockStmt)
452 } else {
453 // optimizeStmtList can in some cases expand one stmt to two shorter stmts
454 list := optimizeStmtList([]js.IStmt{i}, blockType)
455 if len(list) == 1 {
456 m.minifyStmt(list[0])
457 } else if len(list) == 0 {
458 m.write(semicolonBytes)
459 m.needsSemicolon = false
460 } else {
461 m.minifyBlockStmt(&js.BlockStmt{List: list, Scope: js.Scope{}})
462 }
463 }
464}
465
466func (m *jsMinifier) minifyAlias(alias js.Alias) {
467 if alias.Name != nil {
468 if alias.Name[0] == '"' || alias.Name[0] == '\'' {
469 m.write(minifyString(alias.Name, false))
470 } else {
471 m.write(alias.Name)
472 }
473 if !bytes.Equal(alias.Name, starBytes) {
474 m.write(spaceBytes)
475 }
476 m.write(asSpaceBytes)
477 }
478 if alias.Binding != nil {
479 if alias.Binding[0] == '"' || alias.Binding[0] == '\'' {
480 m.write(minifyString(alias.Binding, false))
481 } else {
482 m.write(alias.Binding)
483 }
484 }
485}
486
487func (m *jsMinifier) minifyParams(params js.Params, removeUnused bool) {
488 // remove unused parameters from the end
489 j := len(params.List)
490 if removeUnused && params.Rest == nil {
491 for ; 0 < j; j-- {
492 if v, ok := params.List[j-1].Binding.(*js.Var); !ok || ok && 1 < v.Uses {
493 break
494 }
495 }
496 }
497
498 m.write(openParenBytes)
499 for i, item := range params.List[:j] {
500 if i != 0 {
501 m.write(commaBytes)
502 }
503 m.minifyBindingElement(item)
504 }
505 if params.Rest != nil {
506 if len(params.List) != 0 {
507 m.write(commaBytes)
508 }
509 m.write(ellipsisBytes)
510 m.minifyBinding(params.Rest)
511 }
512 m.write(closeParenBytes)
513}
514
515func (m *jsMinifier) minifyArguments(args js.Args) {
516 m.write(openParenBytes)
517 for i, item := range args.List {
518 if i != 0 {
519 m.write(commaBytes)
520 }
521 if item.Rest {
522 m.write(ellipsisBytes)
523 }
524 m.minifyExpr(item.Value, js.OpAssign)
525 }
526 m.write(closeParenBytes)
527}
528
529func (m *jsMinifier) minifyVarDecl(decl *js.VarDecl, onlyDefines bool) {
530 if len(decl.List) == 0 {
531 return
532 } else if decl.TokenType == js.ErrorToken {
533 // remove 'var' when hoisting variables
534 first := true
535 for _, item := range decl.List {
536 if item.Default != nil || !onlyDefines {
537 if !first {
538 m.write(commaBytes)
539 }
540 m.minifyBindingElement(item)
541 first = false
542 }
543 }
544 } else {
545 if decl.TokenType == js.VarToken && len(decl.List) <= 10000 {
546 // move single var decls forward and order for GZIP optimization
547 start := 0
548 if _, ok := decl.List[0].Binding.(*js.Var); !ok {
549 start++
550 }
551 for i := 0; i < len(decl.List); i++ {
552 item := decl.List[i]
553 if v, ok := item.Binding.(*js.Var); ok && item.Default == nil && len(v.Data) == 1 {
554 for j := start; j < len(decl.List); j++ {
555 if v2, ok := decl.List[j].Binding.(*js.Var); ok && decl.List[j].Default == nil && len(v2.Data) == 1 {
556 if m.renamer.identOrder[v2.Data[0]] < m.renamer.identOrder[v.Data[0]] {
557 continue
558 } else if m.renamer.identOrder[v2.Data[0]] == m.renamer.identOrder[v.Data[0]] {
559 break
560 }
561 }
562 decl.List = append(decl.List[:i], decl.List[i+1:]...)
563 decl.List = append(decl.List[:j], append([]js.BindingElement{item}, decl.List[j:]...)...)
564 break
565 }
566 }
567 }
568 }
569
570 m.write(decl.TokenType.Bytes())
571 m.writeSpaceBeforeIdent()
572 for i, item := range decl.List {
573 if i != 0 {
574 m.write(commaBytes)
575 }
576 m.minifyBindingElement(item)
577 }
578 }
579}
580
581func (m *jsMinifier) minifyFuncDecl(decl *js.FuncDecl, inExpr bool) {
582 parentRename := m.renamer.rename
583 m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames
584 m.hoistVars(&decl.Body)
585 decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock)
586
587 if decl.Async {
588 m.write(asyncSpaceBytes)
589 }
590 m.write(functionBytes)
591 if decl.Generator {
592 m.write(starBytes)
593 }
594
595 // TODO: remove function name, really necessary?
596 //if decl.Name != nil && decl.Name.Uses == 1 {
597 // scope := decl.Body.Scope
598 // for i, vorig := range scope.Declared {
599 // if decl.Name == vorig {
600 // scope.Declared = append(scope.Declared[:i], scope.Declared[i+1:]...)
601 // }
602 // }
603 //}
604
605 if inExpr {
606 m.renamer.renameScope(decl.Body.Scope)
607 }
608 if decl.Name != nil && (!inExpr || 1 < decl.Name.Uses) {
609 if !decl.Generator {
610 m.write(spaceBytes)
611 }
612 m.write(decl.Name.Data)
613 }
614 if !inExpr {
615 m.renamer.renameScope(decl.Body.Scope)
616 }
617
618 m.minifyParams(decl.Params, true)
619 m.minifyBlockStmt(&decl.Body)
620 m.renamer.rename = parentRename
621}
622
623func (m *jsMinifier) minifyMethodDecl(decl *js.MethodDecl) {
624 parentRename := m.renamer.rename
625 m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames
626 m.hoistVars(&decl.Body)
627 decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock)
628
629 if decl.Static {
630 m.write(staticBytes)
631 m.writeSpaceBeforeIdent()
632 }
633 if decl.Async {
634 m.write(asyncBytes)
635 if decl.Generator {
636 m.write(starBytes)
637 } else {
638 m.writeSpaceBeforeIdent()
639 }
640 } else if decl.Generator {
641 m.write(starBytes)
642 } else if decl.Get {
643 m.write(getBytes)
644 m.writeSpaceBeforeIdent()
645 } else if decl.Set {
646 m.write(setBytes)
647 m.writeSpaceBeforeIdent()
648 }
649 m.minifyPropertyName(decl.Name)
650 m.renamer.renameScope(decl.Body.Scope)
651 m.minifyParams(decl.Params, !decl.Set)
652 m.minifyBlockStmt(&decl.Body)
653 m.renamer.rename = parentRename
654}
655
656func (m *jsMinifier) minifyArrowFunc(decl *js.ArrowFunc) {
657 parentRename := m.renamer.rename
658 m.renamer.rename = !decl.Body.Scope.HasWith && !m.o.KeepVarNames
659 m.hoistVars(&decl.Body)
660 decl.Body.List = optimizeStmtList(decl.Body.List, functionBlock)
661
662 m.renamer.renameScope(decl.Body.Scope)
663 if decl.Async {
664 m.write(asyncBytes)
665 }
666 removeParens := false
667 if decl.Params.Rest == nil && len(decl.Params.List) == 1 && decl.Params.List[0].Default == nil {
668 if decl.Params.List[0].Binding == nil {
669 removeParens = true
670 } else if _, ok := decl.Params.List[0].Binding.(*js.Var); ok {
671 removeParens = true
672 }
673 }
674 if removeParens {
675 if decl.Async && decl.Params.List[0].Binding != nil {
676 // add space after async in: async a => ...
677 m.write(spaceBytes)
678 }
679 m.minifyBindingElement(decl.Params.List[0])
680 } else {
681 parentInFor := m.inFor
682 m.inFor = false
683 m.minifyParams(decl.Params, true)
684 m.inFor = parentInFor
685 }
686 m.write(arrowBytes)
687 removeBraces := false
688 if 0 < len(decl.Body.List) {
689 returnStmt, isReturn := decl.Body.List[len(decl.Body.List)-1].(*js.ReturnStmt)
690 if isReturn && returnStmt.Value != nil {
691 // merge expression statements to final return statement, remove function body braces
692 var list []js.IExpr
693 removeBraces = true
694 for _, item := range decl.Body.List[:len(decl.Body.List)-1] {
695 if expr, isExpr := item.(*js.ExprStmt); isExpr {
696 list = append(list, expr.Value)
697 } else {
698 removeBraces = false
699 break
700 }
701 }
702 if removeBraces {
703 list = append(list, returnStmt.Value)
704 expr := list[0]
705 if 0 < len(list) {
706 if 1 < len(list) {
707 expr = &js.CommaExpr{list}
708 }
709 expr = &js.GroupExpr{X: expr}
710 }
711 m.expectExpr = expectExprBody
712 m.minifyExpr(expr, js.OpAssign)
713 if m.groupedStmt {
714 m.write(closeParenBytes)
715 m.groupedStmt = false
716 }
717 }
718 } else if isReturn && returnStmt.Value == nil {
719 // remove empty return
720 decl.Body.List = decl.Body.List[:len(decl.Body.List)-1]
721 }
722 }
723 if !removeBraces {
724 m.minifyBlockStmt(&decl.Body)
725 }
726 m.renamer.rename = parentRename
727}
728
729func (m *jsMinifier) minifyClassDecl(decl *js.ClassDecl) {
730 m.write(classBytes)
731 if decl.Name != nil {
732 m.write(spaceBytes)
733 m.write(decl.Name.Data)
734 }
735 if decl.Extends != nil {
736 m.write(spaceExtendsBytes)
737 m.writeSpaceBeforeIdent()
738 m.minifyExpr(decl.Extends, js.OpLHS)
739 }
740 m.write(openBraceBytes)
741 m.needsSemicolon = false
742 for _, item := range decl.List {
743 m.writeSemicolon()
744 if item.StaticBlock != nil {
745 m.write(staticBytes)
746 m.minifyBlockStmt(item.StaticBlock)
747 } else if item.Method != nil {
748 m.minifyMethodDecl(item.Method)
749 } else {
750 if item.Static {
751 m.write(staticBytes)
752 if !item.Name.IsComputed() && item.Name.Literal.TokenType == js.IdentifierToken {
753 m.write(spaceBytes)
754 }
755 }
756 m.minifyPropertyName(item.Name)
757 if item.Init != nil {
758 m.write(equalBytes)
759 m.minifyExpr(item.Init, js.OpAssign)
760 }
761 m.requireSemicolon()
762 }
763 }
764 m.write(closeBraceBytes)
765 m.needsSemicolon = false
766}
767
768func (m *jsMinifier) minifyPropertyName(name js.PropertyName) {
769 if name.IsComputed() {
770 m.write(openBracketBytes)
771 m.minifyExpr(name.Computed, js.OpAssign)
772 m.write(closeBracketBytes)
773 } else if name.Literal.TokenType == js.StringToken {
774 m.write(minifyString(name.Literal.Data, false))
775 } else {
776 m.write(name.Literal.Data)
777 }
778}
779
780func (m *jsMinifier) minifyProperty(property js.Property) {
781 // property.Name is always set in ObjectLiteral
782 if property.Spread {
783 m.write(ellipsisBytes)
784 } else if v, ok := property.Value.(*js.Var); property.Name != nil && (!ok || !property.Name.IsIdent(v.Name())) {
785 // add 'old-name:' before BindingName as the latter will be renamed
786 m.minifyPropertyName(*property.Name)
787 m.write(colonBytes)
788 }
789 m.minifyExpr(property.Value, js.OpAssign)
790 if property.Init != nil {
791 m.write(equalBytes)
792 m.minifyExpr(property.Init, js.OpAssign)
793 }
794}
795
796func (m *jsMinifier) minifyBindingElement(element js.BindingElement) {
797 if element.Binding != nil {
798 parentInFor := m.inFor
799 m.inFor = false
800 m.minifyBinding(element.Binding)
801 m.inFor = parentInFor
802 if element.Default != nil {
803 m.write(equalBytes)
804 m.minifyExpr(element.Default, js.OpAssign)
805 }
806 }
807}
808
809func (m *jsMinifier) minifyBinding(ibinding js.IBinding) {
810 switch binding := ibinding.(type) {
811 case *js.Var:
812 m.write(binding.Data)
813 case *js.BindingArray:
814 m.write(openBracketBytes)
815 for i, item := range binding.List {
816 if i != 0 {
817 m.write(commaBytes)
818 }
819 m.minifyBindingElement(item)
820 }
821 if binding.Rest != nil {
822 if 0 < len(binding.List) {
823 m.write(commaBytes)
824 }
825 m.write(ellipsisBytes)
826 m.minifyBinding(binding.Rest)
827 }
828 m.write(closeBracketBytes)
829 case *js.BindingObject:
830 m.write(openBraceBytes)
831 for i, item := range binding.List {
832 if i != 0 {
833 m.write(commaBytes)
834 }
835 // item.Key is always set
836 if item.Key.IsComputed() {
837 m.minifyPropertyName(*item.Key)
838 m.write(colonBytes)
839 } else if v, ok := item.Value.Binding.(*js.Var); !ok || !item.Key.IsIdent(v.Data) {
840 // add 'old-name:' before BindingName as the latter will be renamed
841 m.minifyPropertyName(*item.Key)
842 m.write(colonBytes)
843 }
844 m.minifyBindingElement(item.Value)
845 }
846 if binding.Rest != nil {
847 if 0 < len(binding.List) {
848 m.write(commaBytes)
849 }
850 m.write(ellipsisBytes)
851 m.write(binding.Rest.Data)
852 }
853 m.write(closeBraceBytes)
854 }
855}
856
857func (m *jsMinifier) minifyExpr(i js.IExpr, prec js.OpPrec) {
858 if cond, ok := i.(*js.CondExpr); ok {
859 i = m.optimizeCondExpr(cond, prec)
860 } else if unary, ok := i.(*js.UnaryExpr); ok {
861 i = optimizeUnaryExpr(unary, prec)
862 }
863
864 switch expr := i.(type) {
865 case *js.Var:
866 for expr.Link != nil {
867 expr = expr.Link
868 }
869 data := expr.Data
870 if bytes.Equal(data, undefinedBytes) { // TODO: only if not defined
871 if js.OpUnary < prec {
872 m.write(groupedVoidZeroBytes)
873 } else {
874 m.write(voidZeroBytes)
875 }
876 } else if bytes.Equal(data, infinityBytes) { // TODO: only if not defined
877 if js.OpMul < prec {
878 m.write(groupedOneDivZeroBytes)
879 } else {
880 m.write(oneDivZeroBytes)
881 }
882 } else {
883 m.write(data)
884 }
885 case *js.LiteralExpr:
886 if expr.TokenType == js.DecimalToken {
887 m.write(decimalNumber(expr.Data, m.o.Precision))
888 } else if expr.TokenType == js.BinaryToken {
889 m.write(binaryNumber(expr.Data, m.o.Precision))
890 } else if expr.TokenType == js.OctalToken {
891 m.write(octalNumber(expr.Data, m.o.Precision))
892 } else if expr.TokenType == js.HexadecimalToken {
893 m.write(hexadecimalNumber(expr.Data, m.o.Precision))
894 } else if expr.TokenType == js.TrueToken {
895 if js.OpUnary < prec {
896 m.write(groupedNotZeroBytes)
897 } else {
898 m.write(notZeroBytes)
899 }
900 } else if expr.TokenType == js.FalseToken {
901 if js.OpUnary < prec {
902 m.write(groupedNotOneBytes)
903 } else {
904 m.write(notOneBytes)
905 }
906 } else if expr.TokenType == js.StringToken {
907 m.write(minifyString(expr.Data, true))
908 } else if expr.TokenType == js.RegExpToken {
909 // </script>/ => < /script>/
910 if 0 < len(m.prev) && m.prev[len(m.prev)-1] == '<' && bytes.HasPrefix(expr.Data, regExpScriptBytes) {
911 m.write(spaceBytes)
912 }
913 m.write(minifyRegExp(expr.Data))
914 } else {
915 m.write(expr.Data)
916 }
917 case *js.BinaryExpr:
918 mergeBinaryExpr(expr)
919 if expr.X == nil {
920 m.minifyExpr(expr.Y, prec)
921 break
922 }
923
924 precLeft := binaryLeftPrecMap[expr.Op]
925 // convert (a,b)&&c into a,b&&c but not a=(b,c)&&d into a=(b,c&&d)
926 if prec <= js.OpExpr {
927 if group, ok := expr.X.(*js.GroupExpr); ok {
928 if comma, ok := group.X.(*js.CommaExpr); ok && js.OpAnd <= exprPrec(comma.List[len(comma.List)-1]) {
929 expr.X = group.X
930 precLeft = js.OpExpr
931 }
932 }
933 }
934 if expr.Op == js.InstanceofToken || expr.Op == js.InToken {
935 group := expr.Op == js.InToken && m.inFor
936 if group {
937 m.write(openParenBytes)
938 }
939 m.minifyExpr(expr.X, precLeft)
940 m.writeSpaceAfterIdent()
941 m.write(expr.Op.Bytes())
942 m.writeSpaceBeforeIdent()
943 m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op])
944 if group {
945 m.write(closeParenBytes)
946 }
947 } else {
948 // TODO: has effect on GZIP?
949 //if expr.Op == js.EqEqToken || expr.Op == js.NotEqToken || expr.Op == js.EqEqEqToken || expr.Op == js.NotEqEqToken {
950 // // switch a==const for const==a, such as typeof a=="undefined" for "undefined"==typeof a (GZIP improvement)
951 // if _, ok := expr.Y.(*js.LiteralExpr); ok {
952 // expr.X, expr.Y = expr.Y, expr.X
953 // }
954 //}
955
956 if v, not, ok := isUndefinedOrNullVar(expr); ok {
957 // change a===null||a===undefined to a==null
958 op := js.EqEqToken
959 if not {
960 op = js.NotEqToken
961 }
962 expr = &js.BinaryExpr{op, v, &js.LiteralExpr{js.NullToken, nullBytes}}
963 }
964
965 m.minifyExpr(expr.X, precLeft)
966 if expr.Op == js.GtToken && m.prev[len(m.prev)-1] == '-' {
967 // 0 < len(m.prev) always
968 m.write(spaceBytes)
969 } else if expr.Op == js.EqEqEqToken || expr.Op == js.NotEqEqToken {
970 if left, ok := expr.X.(*js.UnaryExpr); ok && left.Op == js.TypeofToken {
971 if right, ok := expr.Y.(*js.LiteralExpr); ok && right.TokenType == js.StringToken {
972 if expr.Op == js.EqEqEqToken {
973 expr.Op = js.EqEqToken
974 } else {
975 expr.Op = js.NotEqToken
976 }
977 }
978 } else if right, ok := expr.Y.(*js.UnaryExpr); ok && right.Op == js.TypeofToken {
979 if left, ok := expr.X.(*js.LiteralExpr); ok && left.TokenType == js.StringToken {
980 if expr.Op == js.EqEqEqToken {
981 expr.Op = js.EqEqToken
982 } else {
983 expr.Op = js.NotEqToken
984 }
985 }
986 }
987 }
988 m.write(expr.Op.Bytes())
989 if expr.Op == js.AddToken {
990 // +++ => + ++
991 m.writeSpaceBefore('+')
992 } else if expr.Op == js.SubToken {
993 // --- => - --
994 m.writeSpaceBefore('-')
995 } else if expr.Op == js.DivToken {
996 // // => / /
997 m.writeSpaceBefore('/')
998 }
999 m.minifyExpr(expr.Y, binaryRightPrecMap[expr.Op])
1000 }
1001 case *js.UnaryExpr:
1002 if expr.Op == js.PostIncrToken || expr.Op == js.PostDecrToken {
1003 m.minifyExpr(expr.X, unaryPrecMap[expr.Op])
1004 m.write(expr.Op.Bytes())
1005 } else {
1006 isLtNot := expr.Op == js.NotToken && 0 < len(m.prev) && m.prev[len(m.prev)-1] == '<'
1007 m.write(expr.Op.Bytes())
1008 if expr.Op == js.DeleteToken || expr.Op == js.VoidToken || expr.Op == js.TypeofToken || expr.Op == js.AwaitToken {
1009 m.writeSpaceBeforeIdent()
1010 } else if expr.Op == js.PosToken {
1011 // +++ => + ++
1012 m.writeSpaceBefore('+')
1013 } else if expr.Op == js.NegToken || isLtNot {
1014 // --- => - --
1015 // <!-- => <! --
1016 m.writeSpaceBefore('-')
1017 } else if expr.Op == js.NotToken {
1018 if lit, ok := expr.X.(*js.LiteralExpr); ok && (lit.TokenType == js.StringToken || lit.TokenType == js.RegExpToken) {
1019 // !"string" => !1
1020 m.write(oneBytes)
1021 break
1022 } else if ok && lit.TokenType == js.DecimalToken {
1023 // !123 => !1 (except for !0)
1024 if num := minify.Number(lit.Data, m.o.Precision); len(num) == 1 && num[0] == '0' {
1025 m.write(zeroBytes)
1026 } else {
1027 m.write(oneBytes)
1028 }
1029 break
1030 }
1031 }
1032 m.minifyExpr(expr.X, unaryPrecMap[expr.Op])
1033 }
1034 case *js.DotExpr:
1035 if group, ok := expr.X.(*js.GroupExpr); ok {
1036 if lit, ok := group.X.(*js.LiteralExpr); ok && lit.TokenType == js.DecimalToken {
1037 num := minify.Number(lit.Data, m.o.Precision)
1038 isInt := true
1039 for _, c := range num {
1040 if c == '.' || c == 'e' || c == 'E' {
1041 isInt = false
1042 break
1043 }
1044 }
1045 if isInt {
1046 m.write(num)
1047 m.write(dotBytes)
1048 } else {
1049 m.write(num)
1050 }
1051 m.write(dotBytes)
1052 m.write(expr.Y.Data)
1053 break
1054 }
1055 }
1056 if prec < js.OpMember {
1057 m.minifyExpr(expr.X, js.OpCall)
1058 } else {
1059 m.minifyExpr(expr.X, js.OpMember)
1060 }
1061 if expr.Optional {
1062 m.write(questionBytes)
1063 } else if last := m.prev[len(m.prev)-1]; '0' <= last && last <= '9' {
1064 // 0 < len(m.prev) always
1065 isInteger := true
1066 for _, c := range m.prev[:len(m.prev)-1] {
1067 if c < '0' || '9' < c {
1068 isInteger = false
1069 break
1070 }
1071 }
1072 if isInteger {
1073 // prevent previous integer
1074 m.write(dotBytes)
1075 }
1076 }
1077 m.write(dotBytes)
1078 m.write(expr.Y.Data)
1079 case *js.GroupExpr:
1080 if cond, ok := expr.X.(*js.CondExpr); ok {
1081 expr.X = m.optimizeCondExpr(cond, js.OpExpr)
1082 }
1083 precInside := exprPrec(expr.X)
1084 if prec <= precInside || precInside == js.OpCoalesce && prec == js.OpBitOr {
1085 m.minifyExpr(expr.X, prec)
1086 } else {
1087 parentInFor := m.inFor
1088 m.inFor = false
1089 m.write(openParenBytes)
1090 m.minifyExpr(expr.X, js.OpExpr)
1091 m.write(closeParenBytes)
1092 m.inFor = parentInFor
1093 }
1094 case *js.ArrayExpr:
1095 parentInFor := m.inFor
1096 m.inFor = false
1097 m.write(openBracketBytes)
1098 for i, item := range expr.List {
1099 if i != 0 {
1100 m.write(commaBytes)
1101 }
1102 if item.Spread {
1103 m.write(ellipsisBytes)
1104 }
1105 m.minifyExpr(item.Value, js.OpAssign)
1106 }
1107 if 0 < len(expr.List) && expr.List[len(expr.List)-1].Value == nil {
1108 m.write(commaBytes)
1109 }
1110 m.write(closeBracketBytes)
1111 m.inFor = parentInFor
1112 case *js.ObjectExpr:
1113 parentInFor := m.inFor
1114 m.inFor = false
1115 groupedStmt := m.expectExpr != expectAny
1116 if groupedStmt {
1117 m.write(openParenBracketBytes)
1118 } else {
1119 m.write(openBraceBytes)
1120 }
1121 for i, item := range expr.List {
1122 if i != 0 {
1123 m.write(commaBytes)
1124 }
1125 m.minifyProperty(item)
1126 }
1127 m.write(closeBraceBytes)
1128 if groupedStmt {
1129 m.groupedStmt = true
1130 }
1131 m.inFor = parentInFor
1132 case *js.TemplateExpr:
1133 if expr.Tag != nil {
1134 if prec < js.OpMember {
1135 m.minifyExpr(expr.Tag, js.OpCall)
1136 } else {
1137 m.minifyExpr(expr.Tag, js.OpMember)
1138 }
1139 if expr.Optional {
1140 m.write(optChainBytes)
1141 }
1142 }
1143 parentInFor := m.inFor
1144 m.inFor = false
1145 for _, item := range expr.List {
1146 m.write(replaceEscapes(item.Value, '`', 1, 2))
1147 m.minifyExpr(item.Expr, js.OpExpr)
1148 }
1149 m.write(replaceEscapes(expr.Tail, '`', 1, 1))
1150 m.inFor = parentInFor
1151 case *js.NewExpr:
1152 if expr.Args == nil && js.OpLHS < prec && prec != js.OpNew {
1153 m.write(openNewBytes)
1154 m.writeSpaceBeforeIdent()
1155 m.minifyExpr(expr.X, js.OpNew)
1156 m.write(closeParenBytes)
1157 } else {
1158 m.write(newBytes)
1159 m.writeSpaceBeforeIdent()
1160 if expr.Args != nil {
1161 m.minifyExpr(expr.X, js.OpMember)
1162 m.minifyArguments(*expr.Args)
1163 } else {
1164 m.minifyExpr(expr.X, js.OpNew)
1165 }
1166 }
1167 case *js.NewTargetExpr:
1168 m.write(newTargetBytes)
1169 m.writeSpaceBeforeIdent()
1170 case *js.ImportMetaExpr:
1171 if m.expectExpr == expectExprStmt {
1172 m.write(openParenBytes)
1173 m.groupedStmt = true
1174 }
1175 m.write(importMetaBytes)
1176 m.writeSpaceBeforeIdent()
1177 case *js.YieldExpr:
1178 m.write(yieldBytes)
1179 m.writeSpaceBeforeIdent()
1180 if expr.X != nil {
1181 if expr.Generator {
1182 m.write(starBytes)
1183 m.minifyExpr(expr.X, js.OpAssign)
1184 } else if v, ok := expr.X.(*js.Var); !ok || !bytes.Equal(v.Name(), undefinedBytes) { // TODO: only if not defined
1185 m.minifyExpr(expr.X, js.OpAssign)
1186 }
1187 }
1188 case *js.CallExpr:
1189 m.minifyExpr(expr.X, js.OpCall)
1190 parentInFor := m.inFor
1191 m.inFor = false
1192 if expr.Optional {
1193 m.write(optChainBytes)
1194 }
1195 m.minifyArguments(expr.Args)
1196 m.inFor = parentInFor
1197 case *js.IndexExpr:
1198 if m.expectExpr == expectExprStmt {
1199 if v, ok := expr.X.(*js.Var); ok && bytes.Equal(v.Name(), letBytes) {
1200 m.write(notBytes)
1201 }
1202 }
1203 if prec < js.OpMember {
1204 m.minifyExpr(expr.X, js.OpCall)
1205 } else {
1206 m.minifyExpr(expr.X, js.OpMember)
1207 }
1208 if expr.Optional {
1209 m.write(optChainBytes)
1210 }
1211 if lit, ok := expr.Y.(*js.LiteralExpr); ok && lit.TokenType == js.StringToken && 2 < len(lit.Data) {
1212 if isIdent := js.AsIdentifierName(lit.Data[1 : len(lit.Data)-1]); isIdent {
1213 m.write(dotBytes)
1214 m.write(lit.Data[1 : len(lit.Data)-1])
1215 break
1216 } else if isNum := js.AsDecimalLiteral(lit.Data[1 : len(lit.Data)-1]); isNum {
1217 m.write(openBracketBytes)
1218 m.write(minify.Number(lit.Data[1:len(lit.Data)-1], 0))
1219 m.write(closeBracketBytes)
1220 break
1221 }
1222 }
1223 parentInFor := m.inFor
1224 m.inFor = false
1225 m.write(openBracketBytes)
1226 m.minifyExpr(expr.Y, js.OpExpr)
1227 m.write(closeBracketBytes)
1228 m.inFor = parentInFor
1229 case *js.CondExpr:
1230 m.minifyExpr(expr.Cond, js.OpCoalesce)
1231 m.write(questionBytes)
1232 m.minifyExpr(expr.X, js.OpAssign)
1233 m.write(colonBytes)
1234 m.minifyExpr(expr.Y, js.OpAssign)
1235 case *js.VarDecl:
1236 m.minifyVarDecl(expr, true) // happens in for statement or when vars were hoisted
1237 case *js.FuncDecl:
1238 grouped := m.expectExpr == expectExprStmt && prec != js.OpExpr
1239 if grouped {
1240 m.write(openParenBytes)
1241 } else if m.expectExpr == expectExprStmt {
1242 m.write(notBytes)
1243 }
1244 parentInFor, parentGroupedStmt := m.inFor, m.groupedStmt
1245 m.inFor, m.groupedStmt = false, false
1246 m.minifyFuncDecl(expr, true)
1247 m.inFor, m.groupedStmt = parentInFor, parentGroupedStmt
1248 if grouped {
1249 m.write(closeParenBytes)
1250 }
1251 case *js.ArrowFunc:
1252 parentGroupedStmt := m.groupedStmt
1253 m.groupedStmt = false
1254 m.minifyArrowFunc(expr)
1255 m.groupedStmt = parentGroupedStmt
1256 case *js.MethodDecl:
1257 parentGroupedStmt := m.groupedStmt
1258 m.groupedStmt = false
1259 m.minifyMethodDecl(expr) // only happens in object literal
1260 m.groupedStmt = parentGroupedStmt
1261 case *js.ClassDecl:
1262 if m.expectExpr == expectExprStmt {
1263 m.write(notBytes)
1264 }
1265 parentInFor, parentGroupedStmt := m.inFor, m.groupedStmt
1266 m.inFor, m.groupedStmt = false, false
1267 m.minifyClassDecl(expr)
1268 m.inFor, m.groupedStmt = parentInFor, parentGroupedStmt
1269 case *js.CommaExpr:
1270 for i, item := range expr.List {
1271 if i != 0 {
1272 m.write(commaBytes)
1273 }
1274 m.minifyExpr(item, js.OpAssign)
1275 }
1276 }
1277}