Support watching for modified npm packages

Allow array of build configs.
This commit is contained in:
trading_peter 2022-09-01 22:44:20 +02:00
parent a0d81ca2be
commit bea89e2a80
451 changed files with 259 additions and 232340 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
vendor/

13
go.mod
View File

@ -1,18 +1,21 @@
module github.com/trading-peter/gowebbuild module github.com/trading-peter/gowebbuild
go 1.17 go 1.18
require ( require (
github.com/evanw/esbuild v0.14.5 github.com/evanw/esbuild v0.14.50
github.com/goyek/goyek v0.6.0 github.com/goyek/goyek v0.6.3
github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71 github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71
github.com/otiai10/copy v1.7.0 github.com/otiai10/copy v1.7.0
github.com/radovskyb/watcher v1.0.7 github.com/radovskyb/watcher v1.0.7
github.com/tidwall/gjson v1.14.1
) )
require ( require (
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/fsnotify.v1 v1.4.7 // indirect
) )

23
go.sum
View File

@ -1,11 +1,11 @@
github.com/evanw/esbuild v0.14.5 h1:Gh/vGvDL/g++7erzQZofohZqFBzQblWfLdtYCf15zcQ= github.com/evanw/esbuild v0.14.50 h1:h7sijkRPGB9ckpIOc6FMZ81/NMy/4g40LhsBAtPa3/I=
github.com/evanw/esbuild v0.14.5/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY= github.com/evanw/esbuild v0.14.50/go.mod h1:dkwI35DCMf0iR+tJDiCEiPKZ4A+AotmmeLpPEv3dl9k=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/goyek/goyek v0.6.0 h1:2YQ4V3X7q+zFF98IBWMc1WRwfzs0TQ8jrwOKY3XRQRk= github.com/goyek/goyek v0.6.3 h1:t0h3gWdlvGeSChltiyAyka9Mlcp3CEPDRssRf0XHDTM=
github.com/goyek/goyek v0.6.0/go.mod h1:UGjZz3juJL2l2eMqRbxQYjG8ieyKb7WMYPv0KB0KVxA= github.com/goyek/goyek v0.6.3/go.mod h1:UGjZz3juJL2l2eMqRbxQYjG8ieyKb7WMYPv0KB0KVxA=
github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71 h1:24NdJ5N6gtrcoeS4JwLMeruKFmg20QdF/5UnX5S/j18= github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71 h1:24NdJ5N6gtrcoeS4JwLMeruKFmg20QdF/5UnX5S/j18=
github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71/go.mod h1:ozZLfjiLmXytkIUh200wMeuoQJ4ww06wN+KZtFP6j3g= github.com/jaschaephraim/lrserver v0.0.0-20171129202958-50d19f603f71/go.mod h1:ozZLfjiLmXytkIUh200wMeuoQJ4ww06wN+KZtFP6j3g=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -23,11 +23,18 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 h1:6wSTsvPddg9gc/mVEEyk9oOAoxn+bT4Z9q1zx+4RwA4= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=

123
linker.go Normal file
View File

@ -0,0 +1,123 @@
package main
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"github.com/otiai10/copy"
"github.com/radovskyb/watcher"
"github.com/tidwall/gjson"
)
func link(from, to string) chan struct{} {
requestBuildCh := make(chan struct{})
// Load package.json in destination.
destPkg := readFileContent(filepath.Join(to, "package.json"))
depsRaw := gjson.Get(destPkg, "dependencies").Map()
deps := map[string]bool{}
for k := range depsRaw {
deps[k] = true
}
packages := map[string]string{}
packageFiles := findFiles(from, "package.json")
for i := range packageFiles {
content := readFileContent(packageFiles[i])
name := gjson.Get(content, "name").String()
if deps[name] {
packages[name] = filepath.Dir(packageFiles[i])
}
}
go func() {
w := watcher.New()
w.SetMaxEvents(1)
w.FilterOps(watcher.Write, watcher.Rename, watcher.Move, watcher.Create, watcher.Remove)
if err := w.AddRecursive(from); err != nil {
fmt.Println(err.Error())
os.Exit(1)
}
go func() {
for {
select {
case event := <-w.Event:
fmt.Printf("File %s changed\n", event.Path)
for k, v := range packages {
if strings.HasPrefix(event.Path, v) {
src := filepath.Dir(event.Path)
dest := filepath.Join(to, "node_modules", k)
fmt.Printf("Copying %s to %s\n", src, dest)
err := copy.Copy(src, dest, copy.Options{
Skip: func(src string) (bool, error) {
ok, _ := filepath.Match("*.js", filepath.Base(src))
if ok && !strings.Contains(src, "node_modules") {
return false, nil
}
return true, nil
},
Sync: true,
})
if err != nil {
fmt.Printf("Failed to copy %s: %v\n", k, err)
}
requestBuildCh <- struct{}{}
}
}
case err := <-w.Error:
fmt.Println(err.Error())
case <-w.Closed:
return
}
}
}()
fmt.Printf("Watching packages in %s\n", from)
if err := w.Start(time.Millisecond * 100); err != nil {
fmt.Println(err.Error())
}
}()
return requestBuildCh
}
func findFiles(root, name string) []string {
paths := []string{}
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() && filepath.Base(path) == name && !strings.Contains(path, "node_modules") {
paths = append(paths, path)
}
return nil
})
return paths
}
func readFileContent(path string) string {
pkgData, err := os.ReadFile(path)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
return string(pkgData)
}

129
main.go
View File

@ -41,11 +41,39 @@ type options struct {
Search string Search string
Replace string Replace string
} }
Link struct {
From string
To string
}
}
func readCfg(cfgPath string) []options {
cfgContent, err := os.ReadFile(cfgPath)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
optsSetups := []options{}
err = json.Unmarshal(cfgContent, &optsSetups)
if err != nil {
opt := options{}
err = json.Unmarshal(cfgContent, &opt)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
optsSetups = append(optsSetups, opt)
}
return optsSetups
} }
func main() { func main() {
flow := &goyek.Flow{} flow := &goyek.Flow{}
opts := options{}
cfgPathParam := flow.RegisterStringParam(goyek.StringParam{ cfgPathParam := flow.RegisterStringParam(goyek.StringParam{
Name: "c", Name: "c",
@ -65,30 +93,22 @@ func main() {
Params: goyek.Params{cfgPathParam, prodParam}, Params: goyek.Params{cfgPathParam, prodParam},
Action: func(tf *goyek.TF) { Action: func(tf *goyek.TF) {
cfgPath := cfgPathParam.Get(tf) cfgPath := cfgPathParam.Get(tf)
cfgContent, err := os.ReadFile(cfgPath) os.Chdir(filepath.Dir(cfgPath))
opts := readCfg(cfgPath)
if err != nil { for _, o := range opts {
fmt.Printf("%+v\n", err) cp(o)
os.Exit(1)
}
err = json.Unmarshal(cfgContent, &opts)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
cp(opts)
if prodParam.Get(tf) { if prodParam.Get(tf) {
opts.ESBuild.MinifyIdentifiers = true o.ESBuild.MinifyIdentifiers = true
opts.ESBuild.MinifySyntax = true o.ESBuild.MinifySyntax = true
opts.ESBuild.MinifyWhitespace = true o.ESBuild.MinifyWhitespace = true
opts.ESBuild.Sourcemap = api.SourceMapNone o.ESBuild.Sourcemap = api.SourceMapNone
} }
api.Build(opts.ESBuild) api.Build(o.ESBuild)
replace(opts) replace(o)
}
}, },
} }
@ -98,25 +118,16 @@ func main() {
Params: goyek.Params{cfgPathParam}, Params: goyek.Params{cfgPathParam},
Action: func(tf *goyek.TF) { Action: func(tf *goyek.TF) {
cfgPath := cfgPathParam.Get(tf) cfgPath := cfgPathParam.Get(tf)
cfgContent, err := os.ReadFile(cfgPath) os.Chdir(filepath.Dir(cfgPath))
optsSetups := readCfg(cfgPath)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
err = json.Unmarshal(cfgContent, &opts)
if err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
}
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
fmt.Println("Starting live reload server") for i := range optsSetups {
opts := optsSetups[i]
go func() { go func(opts options) {
w := watcher.New() w := watcher.New()
w.SetMaxEvents(1) w.SetMaxEvents(1)
w.FilterOps(watcher.Write, watcher.Rename, watcher.Move, watcher.Create, watcher.Remove) w.FilterOps(watcher.Write, watcher.Rename, watcher.Move, watcher.Create, watcher.Remove)
@ -155,24 +166,7 @@ func main() {
if err := w.Start(time.Millisecond * 100); err != nil { if err := w.Start(time.Millisecond * 100); err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
} }
}() }(opts)
go func() {
lr := lrserver.New(lrserver.DefaultName, lrserver.DefaultPort)
go func() {
for {
<-triggerReload
lr.Reload("")
}
}()
lr.SetStatusLog(nil)
err := lr.ListenAndServe()
if err != nil {
panic(err)
}
}()
if opts.Serve.Path != "" { if opts.Serve.Path != "" {
go func() { go func() {
@ -193,6 +187,37 @@ func main() {
}() }()
} }
if opts.Link.From != "" {
reqBuildCh := link(opts.Link.From, opts.Link.To)
go func() {
for range reqBuildCh {
cp(opts)
build(opts)
replace(opts)
}
}()
}
}
go func() {
fmt.Println("Starting live reload server")
lr := lrserver.New(lrserver.DefaultName, lrserver.DefaultPort)
go func() {
for {
<-triggerReload
lr.Reload("")
}
}()
lr.SetStatusLog(nil)
err := lr.ListenAndServe()
if err != nil {
panic(err)
}
}()
<-c <-c
fmt.Println("\nExit") fmt.Println("\nExit")
os.Exit(0) os.Exit(0)

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Evan Wallace
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,7 +0,0 @@
package api_helpers
// This flag is set by the CLI to activate the timer. It's put here instead of
// by the timer to discourage code from checking this flag. Only the code that
// creates the root timer should check this flag. Other code should check that
// the timer is not null to detect if the timer is being used or not.
var UseTimer bool

View File

@ -1,141 +0,0 @@
package ast
import "github.com/evanw/esbuild/internal/logger"
// This file contains data structures that are used with the AST packages for
// both JavaScript and CSS. This helps the bundler treat both AST formats in
// a somewhat format-agnostic manner.
type ImportKind uint8
const (
// An entry point provided by the user
ImportEntryPoint ImportKind = iota
// An ES6 import or re-export statement
ImportStmt
// A call to "require()"
ImportRequire
// An "import()" expression with a string argument
ImportDynamic
// A call to "require.resolve()"
ImportRequireResolve
// A CSS "@import" rule
ImportAt
// A CSS "@import" rule with import conditions
ImportAtConditional
// A CSS "url(...)" token
ImportURL
)
func (kind ImportKind) StringForMetafile() string {
switch kind {
case ImportStmt:
return "import-statement"
case ImportRequire:
return "require-call"
case ImportDynamic:
return "dynamic-import"
case ImportRequireResolve:
return "require-resolve"
case ImportAt, ImportAtConditional:
return "import-rule"
case ImportURL:
return "url-token"
case ImportEntryPoint:
return "entry-point"
default:
panic("Internal error")
}
}
func (kind ImportKind) IsFromCSS() bool {
return kind == ImportAt || kind == ImportURL
}
type ImportRecord struct {
Range logger.Range
Path logger.Path
Assertions *[]AssertEntry
// The resolved source index for an internal import (within the bundle) or
// nil for an external import (not included in the bundle)
SourceIndex Index32
// Sometimes the parser creates an import record and decides it isn't needed.
// For example, TypeScript code may have import statements that later turn
// out to be type-only imports after analyzing the whole file.
IsUnused bool
// If this is true, the import contains syntax like "* as ns". This is used
// to determine whether modules that have no exports need to be wrapped in a
// CommonJS wrapper or not.
ContainsImportStar bool
// If this is true, the import contains an import for the alias "default",
// either via the "import x from" or "import {default as x} from" syntax.
ContainsDefaultAlias bool
// If true, this "export * from 'path'" statement is evaluated at run-time by
// calling the "__reExport()" helper function
CallsRunTimeReExportFn bool
// Tell the printer to wrap this call to "require()" in "__toESM(...)"
WrapWithToESM bool
// Tell the printer to wrap this ESM exports object in "__toCJS(...)"
WrapWithToCJS bool
// Tell the printer to use the runtime "__require()" instead of "require()"
CallRuntimeRequire bool
// True for the following cases:
//
// try { require('x') } catch { handle }
// try { await import('x') } catch { handle }
// try { require.resolve('x') } catch { handle }
// import('x').catch(handle)
// import('x').then(_, handle)
//
// In these cases we shouldn't generate an error if the path could not be
// resolved.
HandlesImportErrors bool
// If true, this was originally written as a bare "import 'file'" statement
WasOriginallyBareImport bool
Kind ImportKind
}
type AssertEntry struct {
Key []uint16 // An identifier or a string
Value []uint16 // Always a string
KeyLoc logger.Loc
ValueLoc logger.Loc
PreferQuotedKey bool
}
// This stores a 32-bit index where the zero value is an invalid index. This is
// a better alternative to storing the index as a pointer since that has the
// same properties but takes up more space and costs an extra pointer traversal.
type Index32 struct {
flippedBits uint32
}
func MakeIndex32(index uint32) Index32 {
return Index32{flippedBits: ^index}
}
func (i Index32) IsValid() bool {
return i.flippedBits != 0
}
func (i Index32) GetIndex() uint32 {
return ^i.flippedBits
}

File diff suppressed because it is too large Load Diff

View File

@ -1,132 +0,0 @@
package bundler
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/graph"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_printer"
)
// Set this to true and then load the resulting metafile in "graph-debugger.html"
// to debug graph information.
//
// This is deliberately not exposed in the final binary. It is *very* internal
// and only exists to help debug esbuild itself. Make sure this is always set
// back to false before committing.
const debugVerboseMetafile = false
func (c *linkerContext) generateExtraDataForFileJS(sourceIndex uint32) string {
if !debugVerboseMetafile {
return ""
}
file := &c.graph.Files[sourceIndex]
repr := file.InputFile.Repr.(*graph.JSRepr)
sb := strings.Builder{}
quoteSym := func(ref js_ast.Ref) string {
name := fmt.Sprintf("%d:%d [%s]", ref.SourceIndex, ref.InnerIndex, c.graph.Symbols.Get(ref).OriginalName)
return string(js_printer.QuoteForJSON(name, c.options.ASCIIOnly))
}
sb.WriteString(`,"parts":[`)
for partIndex, part := range repr.AST.Parts {
if partIndex > 0 {
sb.WriteByte(',')
}
var isFirst bool
code := ""
sb.WriteString(fmt.Sprintf(`{"isLive":%v`, part.IsLive))
sb.WriteString(fmt.Sprintf(`,"canBeRemovedIfUnused":%v`, part.CanBeRemovedIfUnused))
if partIndex == int(js_ast.NSExportPartIndex) {
sb.WriteString(`,"nsExportPartIndex":true`)
} else if ast.MakeIndex32(uint32(partIndex)) == repr.Meta.WrapperPartIndex {
sb.WriteString(`,"wrapperPartIndex":true`)
} else if len(part.Stmts) > 0 {
start := part.Stmts[0].Loc.Start
end := len(file.InputFile.Source.Contents)
if partIndex+1 < len(repr.AST.Parts) {
if nextStmts := repr.AST.Parts[partIndex+1].Stmts; len(nextStmts) > 0 {
if nextStart := nextStmts[0].Loc.Start; nextStart >= start {
end = int(nextStart)
}
}
}
code = file.InputFile.Source.Contents[start:end]
}
// importRecords
sb.WriteString(`,"importRecords":[`)
isFirst = true
for _, importRecordIndex := range part.ImportRecordIndices {
record := repr.AST.ImportRecords[importRecordIndex]
if !record.SourceIndex.IsValid() {
continue
}
if isFirst {
isFirst = false
} else {
sb.WriteByte(',')
}
path := c.graph.Files[record.SourceIndex.GetIndex()].InputFile.Source.PrettyPath
sb.WriteString(fmt.Sprintf(`{"source":%s}`, js_printer.QuoteForJSON(path, c.options.ASCIIOnly)))
}
sb.WriteByte(']')
// declaredSymbols
sb.WriteString(`,"declaredSymbols":[`)
isFirst = true
for _, declSym := range part.DeclaredSymbols {
if !declSym.IsTopLevel {
continue
}
if isFirst {
isFirst = false
} else {
sb.WriteByte(',')
}
sb.WriteString(fmt.Sprintf(`{"name":%s}`, quoteSym(declSym.Ref)))
}
sb.WriteByte(']')
// symbolUses
sb.WriteString(`,"symbolUses":[`)
isFirst = true
for ref, uses := range part.SymbolUses {
if isFirst {
isFirst = false
} else {
sb.WriteByte(',')
}
sb.WriteString(fmt.Sprintf(`{"name":%s,"countEstimate":%d}`, quoteSym(ref), uses.CountEstimate))
}
sb.WriteByte(']')
// dependencies
sb.WriteString(`,"dependencies":[`)
for i, dep := range part.Dependencies {
if i > 0 {
sb.WriteByte(',')
}
sb.WriteString(fmt.Sprintf(`{"source":%s,"partIndex":%d}`,
js_printer.QuoteForJSON(c.graph.Files[dep.SourceIndex].InputFile.Source.PrettyPath, c.options.ASCIIOnly),
dep.PartIndex,
))
}
sb.WriteByte(']')
// code
sb.WriteString(`,"code":`)
sb.Write(js_printer.QuoteForJSON(strings.TrimRight(code, "\n"), c.options.ASCIIOnly))
sb.WriteByte('}')
}
sb.WriteString(`]`)
return sb.String()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
package cache
import (
"sync"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/runtime"
)
// This is a cache of the parsed contents of a set of files. The idea is to be
// able to reuse the results of parsing between builds and make subsequent
// builds faster by avoiding redundant parsing work. This only works if:
//
// * The AST information in the cache must be considered immutable. There is
// no way to enforce this in Go, but please be disciplined about this. The
// ASTs are shared in between builds. Any information that must be mutated
// in the AST during a build must be done on a shallow clone of the data if
// the mutation happens after parsing (i.e. a clone that clones everything
// that will be mutated and shares only the parts that won't be mutated).
//
// * The information in the cache must not depend at all on the contents of
// any file other than the file being cached. Invalidating an entry in the
// cache does not also invalidate any entries that depend on that file, so
// caching information that depends on other files can result in incorrect
// results due to reusing stale data. For example, do not "bake in" some
// value imported from another file.
//
// * Cached ASTs must only be reused if the parsing options are identical
// between builds. For example, it would be bad if the AST parser depended
// on options inherited from a nearby "package.json" file but those options
// were not part of the cache key. Then the cached AST could incorrectly be
// reused even if the contents of that "package.json" file have changed.
//
type CacheSet struct {
SourceIndexCache SourceIndexCache
FSCache FSCache
CSSCache CSSCache
JSONCache JSONCache
JSCache JSCache
}
func MakeCacheSet() *CacheSet {
return &CacheSet{
SourceIndexCache: SourceIndexCache{
entries: make(map[sourceIndexKey]uint32),
nextSourceIndex: runtime.SourceIndex + 1,
},
FSCache: FSCache{
entries: make(map[string]*fsEntry),
},
CSSCache: CSSCache{
entries: make(map[logger.Path]*cssCacheEntry),
},
JSONCache: JSONCache{
entries: make(map[logger.Path]*jsonCacheEntry),
},
JSCache: JSCache{
entries: make(map[logger.Path]*jsCacheEntry),
},
}
}
type SourceIndexCache struct {
mutex sync.Mutex
entries map[sourceIndexKey]uint32
nextSourceIndex uint32
}
type SourceIndexKind uint8
const (
SourceIndexNormal SourceIndexKind = iota
SourceIndexJSStubForCSS
)
type sourceIndexKey struct {
path logger.Path
kind SourceIndexKind
}
func (c *SourceIndexCache) LenHint() uint32 {
c.mutex.Lock()
defer c.mutex.Unlock()
// Add some extra room at the end for a new file or two without reallocating
const someExtraRoom = 16
return c.nextSourceIndex + someExtraRoom
}
func (c *SourceIndexCache) Get(path logger.Path, kind SourceIndexKind) uint32 {
key := sourceIndexKey{path: path, kind: kind}
c.mutex.Lock()
defer c.mutex.Unlock()
if sourceIndex, ok := c.entries[key]; ok {
return sourceIndex
}
sourceIndex := c.nextSourceIndex
c.nextSourceIndex++
c.entries[key] = sourceIndex
return sourceIndex
}

View File

@ -1,190 +0,0 @@
package cache
import (
"sync"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_parser"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_parser"
"github.com/evanw/esbuild/internal/logger"
)
// This cache intends to avoid unnecessarily re-parsing files in subsequent
// builds. For a given path, parsing can be avoided if the contents of the file
// and the options for the parser are the same as last time. Even if the
// contents of the file are the same, the options for the parser may have
// changed if they depend on some other file ("package.json" for example).
//
// This cache checks if the file contents have changed even though we have
// the ability to detect if a file has changed on the file system by reading
// its metadata. First of all, if the file contents are cached then they should
// be the same pointer, which makes the comparison trivial. Also we want to
// cache the AST for plugins in the common case that the plugin output stays
// the same.
////////////////////////////////////////////////////////////////////////////////
// CSS
type CSSCache struct {
mutex sync.Mutex
entries map[logger.Path]*cssCacheEntry
}
type cssCacheEntry struct {
source logger.Source
options css_parser.Options
ast css_ast.AST
msgs []logger.Msg
}
func (c *CSSCache) Parse(log logger.Log, source logger.Source, options css_parser.Options) css_ast.AST {
// Check the cache
entry := func() *cssCacheEntry {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.entries[source.KeyPath]
}()
// Cache hit
if entry != nil && entry.source == source && entry.options == options {
for _, msg := range entry.msgs {
log.AddMsg(msg)
}
return entry.ast
}
// Cache miss
tempLog := logger.NewDeferLog(logger.DeferLogAll)
ast := css_parser.Parse(tempLog, source, options)
msgs := tempLog.Done()
for _, msg := range msgs {
log.AddMsg(msg)
}
// Create the cache entry
entry = &cssCacheEntry{
source: source,
options: options,
ast: ast,
msgs: msgs,
}
// Save for next time
c.mutex.Lock()
defer c.mutex.Unlock()
c.entries[source.KeyPath] = entry
return ast
}
////////////////////////////////////////////////////////////////////////////////
// JSON
type JSONCache struct {
mutex sync.Mutex
entries map[logger.Path]*jsonCacheEntry
}
type jsonCacheEntry struct {
source logger.Source
options js_parser.JSONOptions
expr js_ast.Expr
ok bool
msgs []logger.Msg
}
func (c *JSONCache) Parse(log logger.Log, source logger.Source, options js_parser.JSONOptions) (js_ast.Expr, bool) {
// Check the cache
entry := func() *jsonCacheEntry {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.entries[source.KeyPath]
}()
// Cache hit
if entry != nil && entry.source == source && entry.options == options {
for _, msg := range entry.msgs {
log.AddMsg(msg)
}
return entry.expr, entry.ok
}
// Cache miss
tempLog := logger.NewDeferLog(logger.DeferLogAll)
expr, ok := js_parser.ParseJSON(tempLog, source, options)
msgs := tempLog.Done()
for _, msg := range msgs {
log.AddMsg(msg)
}
// Create the cache entry
entry = &jsonCacheEntry{
source: source,
options: options,
expr: expr,
ok: ok,
msgs: msgs,
}
// Save for next time
c.mutex.Lock()
defer c.mutex.Unlock()
c.entries[source.KeyPath] = entry
return expr, ok
}
////////////////////////////////////////////////////////////////////////////////
// JS
type JSCache struct {
mutex sync.Mutex
entries map[logger.Path]*jsCacheEntry
}
type jsCacheEntry struct {
source logger.Source
options js_parser.Options
ast js_ast.AST
ok bool
msgs []logger.Msg
}
func (c *JSCache) Parse(log logger.Log, source logger.Source, options js_parser.Options) (js_ast.AST, bool) {
// Check the cache
entry := func() *jsCacheEntry {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.entries[source.KeyPath]
}()
// Cache hit
if entry != nil && entry.source == source && entry.options.Equal(&options) {
for _, msg := range entry.msgs {
log.AddMsg(msg)
}
return entry.ast, entry.ok
}
// Cache miss
tempLog := logger.NewDeferLog(logger.DeferLogAll)
ast, ok := js_parser.Parse(tempLog, source, options)
msgs := tempLog.Done()
for _, msg := range msgs {
log.AddMsg(msg)
}
// Create the cache entry
entry = &jsCacheEntry{
source: source,
options: options,
ast: ast,
ok: ok,
msgs: msgs,
}
// Save for next time
c.mutex.Lock()
defer c.mutex.Unlock()
c.entries[source.KeyPath] = entry
return ast, ok
}

View File

@ -1,52 +0,0 @@
package cache
import (
"sync"
"github.com/evanw/esbuild/internal/fs"
)
// This cache uses information from the "stat" syscall to try to avoid re-
// reading files from the file system during subsequent builds if the file
// hasn't changed. The assumption is reading the file metadata is faster than
// reading the file contents.
type FSCache struct {
mutex sync.Mutex
entries map[string]*fsEntry
}
type fsEntry struct {
contents string
modKey fs.ModKey
isModKeyUsable bool
}
func (c *FSCache) ReadFile(fs fs.FS, path string) (contents string, canonicalError error, originalError error) {
entry := func() *fsEntry {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.entries[path]
}()
// If the file's modification key hasn't changed since it was cached, assume
// the contents of the file are also the same and skip reading the file.
modKey, modKeyErr := fs.ModKey(path)
if entry != nil && entry.isModKeyUsable && modKeyErr == nil && entry.modKey == modKey {
return entry.contents, nil, nil
}
contents, err, originalError := fs.ReadFile(path)
if err != nil {
return "", err, originalError
}
c.mutex.Lock()
defer c.mutex.Unlock()
c.entries[path] = &fsEntry{
contents: contents,
modKey: modKey,
isModKeyUsable: modKeyErr == nil,
}
return contents, nil, nil
}

View File

@ -1,45 +0,0 @@
package compat
type v struct {
major uint16
minor uint8
patch uint8
}
// Returns <0 if "a < b"
// Returns 0 if "a == b"
// Returns >0 if "a > b"
func compareVersions(a v, b []int) int {
diff := int(a.major)
if len(b) > 0 {
diff -= b[0]
}
if diff == 0 {
diff = int(a.minor)
if len(b) > 1 {
diff -= b[1]
}
}
if diff == 0 {
diff = int(a.patch)
if len(b) > 2 {
diff -= b[2]
}
}
return diff
}
// The start is inclusive and the end is exclusive
type versionRange struct {
start v
end v // Use 0.0.0 for "no end"
}
func isVersionSupported(ranges []versionRange, version []int) bool {
for _, r := range ranges {
if compareVersions(r.start, version) <= 0 && (r.end == (v{}) || compareVersions(r.end, version) > 0) {
return true
}
}
return false
}

View File

@ -1,72 +0,0 @@
package compat
type CSSFeature uint32
const (
HexRGBA CSSFeature = 1 << iota
RebeccaPurple
// This feature includes all of the following:
// - Allow floats in rgb() and rgba()
// - hsl() can accept alpha values
// - rgb() can accept alpha values
// - Space-separated functional color notations
Modern_RGB_HSL
InsetProperty
)
func (features CSSFeature) Has(feature CSSFeature) bool {
return (features & feature) != 0
}
var cssTable = map[CSSFeature]map[Engine][]versionRange{
// Data from: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
HexRGBA: {
Chrome: {{start: v{62, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{49, 0, 0}}},
IOS: {{start: v{9, 3, 0}}},
Safari: {{start: v{9, 1, 0}}},
},
RebeccaPurple: {
Chrome: {{start: v{38, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
Firefox: {{start: v{33, 0, 0}}},
IOS: {{start: v{8, 0, 0}}},
Safari: {{start: v{9, 0, 0}}},
},
Modern_RGB_HSL: {
Chrome: {{start: v{66, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{52, 0, 0}}},
IOS: {{start: v{12, 2, 0}}},
Safari: {{start: v{12, 1, 0}}},
},
// Data from: https://developer.mozilla.org/en-US/docs/Web/CSS/inset
InsetProperty: {
Chrome: {{start: v{87, 0, 0}}},
Edge: {{start: v{87, 0, 0}}},
Firefox: {{start: v{66, 0, 0}}},
IOS: {{start: v{14, 5, 0}}},
Safari: {{start: v{14, 1, 0}}},
},
}
// Return all features that are not available in at least one environment
func UnsupportedCSSFeatures(constraints map[Engine][]int) (unsupported CSSFeature) {
for feature, engines := range cssTable {
for engine, version := range constraints {
if engine == ES || engine == Node {
// Specifying "--target=es2020" shouldn't affect CSS
continue
}
if versionRanges, ok := engines[engine]; !ok || !isVersionSupported(versionRanges, version) {
unsupported |= feature
}
}
}
return
}

View File

@ -1,465 +0,0 @@
// This file was automatically generated by "compat-table.js"
package compat
type Engine uint8
const (
Chrome Engine = iota
Edge
ES
Firefox
IOS
Node
Safari
)
func (e Engine) String() string {
switch e {
case Chrome:
return "chrome"
case Edge:
return "edge"
case ES:
return "es"
case Firefox:
return "firefox"
case IOS:
return "ios"
case Node:
return "node"
case Safari:
return "safari"
}
return ""
}
type JSFeature uint64
const (
ArbitraryModuleNamespaceNames JSFeature = 1 << iota
ArraySpread
Arrow
AsyncAwait
AsyncGenerator
BigInt
Class
ClassField
ClassPrivateAccessor
ClassPrivateBrandCheck
ClassPrivateField
ClassPrivateMethod
ClassPrivateStaticAccessor
ClassPrivateStaticField
ClassPrivateStaticMethod
ClassStaticBlocks
ClassStaticField
Const
DefaultArgument
Destructuring
DynamicImport
ExponentOperator
ExportStarAs
ForAwait
ForOf
Generator
Hashbang
ImportAssertions
ImportMeta
Let
LogicalAssignment
NestedRestBinding
NewTarget
NodeColonPrefixImport
NodeColonPrefixRequire
NullishCoalescing
ObjectAccessors
ObjectExtensions
ObjectRestSpread
OptionalCatchBinding
OptionalChain
RestArgument
TemplateLiteral
TopLevelAwait
UnicodeEscapes
)
func (features JSFeature) Has(feature JSFeature) bool {
return (features & feature) != 0
}
var jsTable = map[JSFeature]map[Engine][]versionRange{
ArbitraryModuleNamespaceNames: {
Chrome: {{start: v{90, 0, 0}}},
Firefox: {{start: v{87, 0, 0}}},
Node: {{start: v{16, 0, 0}}},
},
ArraySpread: {
Chrome: {{start: v{46, 0, 0}}},
Edge: {{start: v{13, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{36, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{5, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
Arrow: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{13, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{45, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
AsyncAwait: {
Chrome: {{start: v{55, 0, 0}}},
Edge: {{start: v{15, 0, 0}}},
ES: {{start: v{2017, 0, 0}}},
Firefox: {{start: v{52, 0, 0}}},
IOS: {{start: v{11, 0, 0}}},
Node: {{start: v{7, 6, 0}}},
Safari: {{start: v{11, 0, 0}}},
},
AsyncGenerator: {
Chrome: {{start: v{63, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2018, 0, 0}}},
Firefox: {{start: v{57, 0, 0}}},
IOS: {{start: v{12, 0, 0}}},
Node: {{start: v{10, 0, 0}}},
Safari: {{start: v{12, 0, 0}}},
},
BigInt: {
Chrome: {{start: v{67, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{68, 0, 0}}},
IOS: {{start: v{14, 0, 0}}},
Node: {{start: v{10, 4, 0}}},
Safari: {{start: v{14, 0, 0}}},
},
Class: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{13, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{45, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
ClassField: {
Chrome: {{start: v{73, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{69, 0, 0}}},
IOS: {{start: v{14, 0, 0}}},
Node: {{start: v{12, 0, 0}}},
Safari: {{start: v{14, 0, 0}}},
},
ClassPrivateAccessor: {
Chrome: {{start: v{84, 0, 0}}},
Edge: {{start: v{84, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{14, 6, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
ClassPrivateBrandCheck: {
Chrome: {{start: v{91, 0, 0}}},
Edge: {{start: v{91, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{16, 9, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
ClassPrivateField: {
Chrome: {{start: v{84, 0, 0}}},
Edge: {{start: v{84, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{14, 6, 0}}},
Safari: {{start: v{14, 1, 0}}},
},
ClassPrivateMethod: {
Chrome: {{start: v{84, 0, 0}}},
Edge: {{start: v{84, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{14, 6, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
ClassPrivateStaticAccessor: {
Chrome: {{start: v{84, 0, 0}}},
Edge: {{start: v{84, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{14, 6, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
ClassPrivateStaticField: {
Chrome: {{start: v{74, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{12, 0, 0}}},
Safari: {{start: v{14, 1, 0}}},
},
ClassPrivateStaticMethod: {
Chrome: {{start: v{84, 0, 0}}},
Edge: {{start: v{84, 0, 0}}},
Firefox: {{start: v{90, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{14, 6, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
ClassStaticBlocks: {
Chrome: {{start: v{91, 0, 0}}},
Node: {{start: v{16, 11, 0}}},
},
ClassStaticField: {
Chrome: {{start: v{73, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{75, 0, 0}}},
IOS: {{start: v{15, 0, 0}}},
Node: {{start: v{12, 0, 0}}},
Safari: {{start: v{14, 1, 0}}},
},
Const: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{51, 0, 0}}},
IOS: {{start: v{11, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{11, 0, 0}}},
},
DefaultArgument: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{53, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
Destructuring: {
Chrome: {{start: v{51, 0, 0}}},
Edge: {{start: v{18, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{53, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 5, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
DynamicImport: {
Chrome: {{start: v{63, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{67, 0, 0}}},
IOS: {{start: v{11, 0, 0}}},
Node: {{start: v{12, 20, 0}, end: v{13, 0, 0}}, {start: v{13, 2, 0}}},
Safari: {{start: v{11, 1, 0}}},
},
ExponentOperator: {
Chrome: {{start: v{52, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2016, 0, 0}}},
Firefox: {{start: v{52, 0, 0}}},
IOS: {{start: v{10, 3, 0}}},
Node: {{start: v{7, 0, 0}}},
Safari: {{start: v{10, 1, 0}}},
},
ExportStarAs: {
Chrome: {{start: v{72, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{80, 0, 0}}},
Node: {{start: v{12, 0, 0}}},
},
ForAwait: {
Chrome: {{start: v{63, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2018, 0, 0}}},
Firefox: {{start: v{57, 0, 0}}},
IOS: {{start: v{12, 0, 0}}},
Node: {{start: v{10, 0, 0}}},
Safari: {{start: v{12, 0, 0}}},
},
ForOf: {
Chrome: {{start: v{51, 0, 0}}},
Edge: {{start: v{15, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{53, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 5, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
Generator: {
Chrome: {{start: v{50, 0, 0}}},
Edge: {{start: v{13, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{53, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
Hashbang: {
Chrome: {{start: v{74, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
Firefox: {{start: v{67, 0, 0}}},
IOS: {{start: v{13, 4, 0}}},
Node: {{start: v{12, 0, 0}}},
Safari: {{start: v{13, 1, 0}}},
},
ImportAssertions: {
Chrome: {{start: v{91, 0, 0}}},
},
ImportMeta: {
Chrome: {{start: v{64, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{62, 0, 0}}},
IOS: {{start: v{12, 0, 0}}},
Node: {{start: v{10, 4, 0}}},
Safari: {{start: v{11, 1, 0}}},
},
Let: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{51, 0, 0}}},
IOS: {{start: v{11, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{11, 0, 0}}},
},
LogicalAssignment: {
Chrome: {{start: v{85, 0, 0}}},
Edge: {{start: v{85, 0, 0}}},
ES: {{start: v{2021, 0, 0}}},
Firefox: {{start: v{79, 0, 0}}},
IOS: {{start: v{14, 0, 0}}},
Node: {{start: v{15, 0, 0}}},
Safari: {{start: v{14, 0, 0}}},
},
NestedRestBinding: {
Chrome: {{start: v{49, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2016, 0, 0}}},
Firefox: {{start: v{47, 0, 0}}},
IOS: {{start: v{10, 3, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 1, 0}}},
},
NewTarget: {
Chrome: {{start: v{46, 0, 0}}},
Edge: {{start: v{14, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{41, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{5, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
NodeColonPrefixImport: {
Node: {{start: v{12, 20, 0}, end: v{13, 0, 0}}, {start: v{14, 13, 1}}},
},
NodeColonPrefixRequire: {
Node: {{start: v{14, 18, 0}, end: v{15, 0, 0}}, {start: v{16, 0, 0}}},
},
NullishCoalescing: {
Chrome: {{start: v{80, 0, 0}}},
Edge: {{start: v{80, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{72, 0, 0}}},
IOS: {{start: v{13, 4, 0}}},
Node: {{start: v{14, 0, 0}}},
Safari: {{start: v{13, 1, 0}}},
},
ObjectAccessors: {
Chrome: {{start: v{5, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
ES: {{start: v{5, 0, 0}}},
Firefox: {{start: v{2, 0, 0}}},
IOS: {{start: v{6, 0, 0}}},
Node: {{start: v{0, 10, 0}}},
Safari: {{start: v{3, 1, 0}}},
},
ObjectExtensions: {
Chrome: {{start: v{44, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{34, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{4, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
ObjectRestSpread: {
ES: {{start: v{2018, 0, 0}}},
Firefox: {{start: v{55, 0, 0}}},
IOS: {{start: v{11, 3, 0}}},
Safari: {{start: v{11, 1, 0}}},
},
OptionalCatchBinding: {
Chrome: {{start: v{66, 0, 0}}},
Edge: {{start: v{79, 0, 0}}},
ES: {{start: v{2019, 0, 0}}},
Firefox: {{start: v{58, 0, 0}}},
IOS: {{start: v{11, 3, 0}}},
Node: {{start: v{10, 0, 0}}},
Safari: {{start: v{11, 1, 0}}},
},
OptionalChain: {
Chrome: {{start: v{91, 0, 0}}},
Edge: {{start: v{91, 0, 0}}},
ES: {{start: v{2020, 0, 0}}},
Firefox: {{start: v{74, 0, 0}}},
IOS: {{start: v{13, 4, 0}}},
Node: {{start: v{16, 9, 0}}},
Safari: {{start: v{13, 1, 0}}},
},
RestArgument: {
Chrome: {{start: v{47, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{43, 0, 0}}},
IOS: {{start: v{10, 0, 0}}},
Node: {{start: v{6, 0, 0}}},
Safari: {{start: v{10, 0, 0}}},
},
TemplateLiteral: {
Chrome: {{start: v{41, 0, 0}}},
Edge: {{start: v{13, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{34, 0, 0}}},
IOS: {{start: v{9, 0, 0}}},
Node: {{start: v{4, 0, 0}}},
Safari: {{start: v{9, 0, 0}}},
},
TopLevelAwait: {
Chrome: {{start: v{89, 0, 0}}},
Edge: {{start: v{89, 0, 0}}},
Firefox: {{start: v{89, 0, 0}}},
Node: {{start: v{14, 8, 0}}},
Safari: {{start: v{15, 0, 0}}},
},
UnicodeEscapes: {
Chrome: {{start: v{44, 0, 0}}},
Edge: {{start: v{12, 0, 0}}},
ES: {{start: v{2015, 0, 0}}},
Firefox: {{start: v{53, 0, 0}}},
IOS: {{start: v{9, 0, 0}}},
Node: {{start: v{4, 0, 0}}},
Safari: {{start: v{9, 0, 0}}},
},
}
// Return all features that are not available in at least one environment
func UnsupportedJSFeatures(constraints map[Engine][]int) (unsupported JSFeature) {
for feature, engines := range jsTable {
for engine, version := range constraints {
if versionRanges, ok := engines[engine]; !ok || !isVersionSupported(versionRanges, version) {
unsupported |= feature
}
}
}
return
}

View File

@ -1,583 +0,0 @@
package config
import (
"fmt"
"regexp"
"strings"
"sync"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
)
type JSXOptions struct {
Factory JSXExpr
Fragment JSXExpr
Parse bool
Preserve bool
}
type JSXExpr struct {
Parts []string
Constant js_ast.E
}
type TSOptions struct {
Parse bool
NoAmbiguousLessThan bool
}
type Platform uint8
const (
PlatformBrowser Platform = iota
PlatformNode
PlatformNeutral
)
type StrictOptions struct {
// Loose: "class Foo { foo = 1 }" => "class Foo { constructor() { this.foo = 1; } }"
// Strict: "class Foo { foo = 1 }" => "class Foo { constructor() { __publicField(this, 'foo', 1); } }"
//
// The disadvantage of strictness here is code bloat and performance. The
// advantage is following the class field specification accurately. For
// example, loose mode will incorrectly trigger setter methods while strict
// mode won't.
ClassFields bool
}
type SourceMap uint8
const (
SourceMapNone SourceMap = iota
SourceMapInline
SourceMapLinkedWithComment
SourceMapExternalWithoutComment
SourceMapInlineAndExternal
)
type LegalComments uint8
const (
LegalCommentsInline LegalComments = iota
LegalCommentsNone
LegalCommentsEndOfFile
LegalCommentsLinkedWithComment
LegalCommentsExternalWithoutComment
)
func (lc LegalComments) HasExternalFile() bool {
return lc == LegalCommentsLinkedWithComment || lc == LegalCommentsExternalWithoutComment
}
type Loader int
const (
LoaderNone Loader = iota
LoaderJS
LoaderJSX
LoaderTS
LoaderTSNoAmbiguousLessThan // Used with ".mts" and ".cts"
LoaderTSX
LoaderJSON
LoaderText
LoaderBase64
LoaderDataURL
LoaderFile
LoaderBinary
LoaderCSS
LoaderDefault
)
func (loader Loader) IsTypeScript() bool {
switch loader {
case LoaderTS, LoaderTSNoAmbiguousLessThan, LoaderTSX:
return true
default:
return false
}
}
func (loader Loader) CanHaveSourceMap() bool {
switch loader {
case LoaderJS, LoaderJSX, LoaderTS, LoaderTSNoAmbiguousLessThan, LoaderTSX, LoaderCSS:
return true
default:
return false
}
}
type Format uint8
const (
// This is used when not bundling. It means to preserve whatever form the
// import or export was originally in. ES6 syntax stays ES6 syntax and
// CommonJS syntax stays CommonJS syntax.
FormatPreserve Format = iota
// IIFE stands for immediately-invoked function expression. That looks like
// this:
//
// (() => {
// ... bundled code ...
// })();
//
// If the optional GlobalName is configured, then we'll write out this:
//
// let globalName = (() => {
// ... bundled code ...
// return exports;
// })();
//
FormatIIFE
// The CommonJS format looks like this:
//
// ... bundled code ...
// module.exports = exports;
//
FormatCommonJS
// The ES module format looks like this:
//
// ... bundled code ...
// export {...};
//
FormatESModule
)
func (f Format) KeepES6ImportExportSyntax() bool {
return f == FormatPreserve || f == FormatESModule
}
func (f Format) String() string {
switch f {
case FormatIIFE:
return "iife"
case FormatCommonJS:
return "cjs"
case FormatESModule:
return "esm"
}
return ""
}
type StdinInfo struct {
Loader Loader
Contents string
SourceFile string
AbsResolveDir string
}
type WildcardPattern struct {
Prefix string
Suffix string
}
type ExternalModules struct {
NodeModules map[string]bool
AbsPaths map[string]bool
Patterns []WildcardPattern
}
type Mode uint8
const (
ModePassThrough Mode = iota
ModeConvertFormat
ModeBundle
)
type MaybeBool uint8
const (
Unspecified MaybeBool = iota
True
False
)
type Options struct {
Mode Mode
ModuleType js_ast.ModuleType
PreserveSymlinks bool
RemoveWhitespace bool
MinifyIdentifiers bool
MangleSyntax bool
ProfilerNames bool
CodeSplitting bool
WatchMode bool
AllowOverwrite bool
LegalComments LegalComments
// If true, make sure to generate a single file that can be written to stdout
WriteToStdout bool
OmitRuntimeForTests bool
UnusedImportsTS UnusedImportsTS
UseDefineForClassFields MaybeBool
ASCIIOnly bool
KeepNames bool
IgnoreDCEAnnotations bool
TreeShaking bool
Defines *ProcessedDefines
TS TSOptions
JSX JSXOptions
Platform Platform
TargetFromAPI TargetFromAPI
UnsupportedJSFeatures compat.JSFeature
UnsupportedCSSFeatures compat.CSSFeature
TSTarget *TSTarget
// This is the original information that was used to generate the
// unsupported feature sets above. It's used for error messages.
OriginalTargetEnv string
ExtensionOrder []string
MainFields []string
Conditions []string
AbsNodePaths []string // The "NODE_PATH" variable from Node.js
ExternalModules ExternalModules
AbsOutputFile string
AbsOutputDir string
AbsOutputBase string
OutputExtensionJS string
OutputExtensionCSS string
GlobalName []string
TsConfigOverride string
ExtensionToLoader map[string]Loader
OutputFormat Format
PublicPath string
InjectAbsPaths []string
InjectedDefines []InjectedDefine
InjectedFiles []InjectedFile
JSBanner string
JSFooter string
CSSBanner string
CSSFooter string
EntryPathTemplate []PathTemplate
ChunkPathTemplate []PathTemplate
AssetPathTemplate []PathTemplate
Plugins []Plugin
NeedsMetafile bool
SourceMap SourceMap
SourceRoot string
ExcludeSourcesContent bool
Stdin *StdinInfo
}
type TargetFromAPI uint8
const (
// In this state, the "target" field in "tsconfig.json" is respected
TargetWasUnconfigured TargetFromAPI = iota
// In this state, the "target" field in "tsconfig.json" is overridden
TargetWasConfigured
// In this state, "useDefineForClassFields" is true unless overridden
TargetWasConfiguredIncludingESNext
)
type UnusedImportsTS uint8
const (
// "import { unused } from 'foo'" => "" (TypeScript's default behavior)
UnusedImportsRemoveStmt UnusedImportsTS = iota
// "import { unused } from 'foo'" => "import 'foo'" ("importsNotUsedAsValues" != "remove")
UnusedImportsKeepStmtRemoveValues
// "import { unused } from 'foo'" => "import { unused } from 'foo'" ("preserveValueImports" == true)
UnusedImportsKeepValues
)
func UnusedImportsFromTsconfigValues(preserveImportsNotUsedAsValues bool, preserveValueImports bool) UnusedImportsTS {
if preserveValueImports {
return UnusedImportsKeepValues
}
if preserveImportsNotUsedAsValues {
return UnusedImportsKeepStmtRemoveValues
}
return UnusedImportsRemoveStmt
}
type TSTarget struct {
Source logger.Source
Range logger.Range
Target string
UnsupportedJSFeatures compat.JSFeature
}
type PathPlaceholder uint8
const (
NoPlaceholder PathPlaceholder = iota
// The relative path from the original parent directory to the configured
// "outbase" directory, or to the lowest common ancestor directory
DirPlaceholder
// The original name of the file, or the manual chunk name, or the name of
// the type of output file ("entry" or "chunk" or "asset")
NamePlaceholder
// A hash of the contents of this file, and the contents and output paths of
// all dependencies (except for their hash placeholders)
HashPlaceholder
// The original extension of the file, or the name of the output file
// (e.g. "css", "svg", "png")
ExtPlaceholder
)
type PathTemplate struct {
Data string
Placeholder PathPlaceholder
}
type PathPlaceholders struct {
Dir *string
Name *string
Hash *string
Ext *string
}
func (placeholders PathPlaceholders) Get(placeholder PathPlaceholder) *string {
switch placeholder {
case DirPlaceholder:
return placeholders.Dir
case NamePlaceholder:
return placeholders.Name
case HashPlaceholder:
return placeholders.Hash
case ExtPlaceholder:
return placeholders.Ext
}
return nil
}
func TemplateToString(template []PathTemplate) string {
if len(template) == 1 && template[0].Placeholder == NoPlaceholder {
// Avoid allocations in this case
return template[0].Data
}
sb := strings.Builder{}
for _, part := range template {
sb.WriteString(part.Data)
switch part.Placeholder {
case DirPlaceholder:
sb.WriteString("[dir]")
case NamePlaceholder:
sb.WriteString("[name]")
case HashPlaceholder:
sb.WriteString("[hash]")
case ExtPlaceholder:
sb.WriteString("[ext]")
}
}
return sb.String()
}
func HasPlaceholder(template []PathTemplate, placeholder PathPlaceholder) bool {
for _, part := range template {
if part.Placeholder == placeholder {
return true
}
}
return false
}
func SubstituteTemplate(template []PathTemplate, placeholders PathPlaceholders) []PathTemplate {
// Don't allocate if no substitution is possible and the template is already minimal
shouldSubstitute := false
for i, part := range template {
if placeholders.Get(part.Placeholder) != nil || (part.Placeholder == NoPlaceholder && i+1 < len(template)) {
shouldSubstitute = true
break
}
}
if !shouldSubstitute {
return template
}
// Otherwise, substitute and merge as appropriate
result := make([]PathTemplate, 0, len(template))
for _, part := range template {
if sub := placeholders.Get(part.Placeholder); sub != nil {
part.Data += *sub
part.Placeholder = NoPlaceholder
}
if last := len(result) - 1; last >= 0 && result[last].Placeholder == NoPlaceholder {
last := &result[last]
last.Data += part.Data
last.Placeholder = part.Placeholder
} else {
result = append(result, part)
}
}
return result
}
func ShouldCallRuntimeRequire(mode Mode, outputFormat Format) bool {
return mode == ModeBundle && outputFormat != FormatCommonJS
}
type InjectedDefine struct {
Source logger.Source
Data js_ast.E
Name string
}
type InjectedFile struct {
Source logger.Source
Exports []InjectableExport
DefineName string
}
type InjectableExport struct {
Alias string
Loc logger.Loc
}
var filterMutex sync.Mutex
var filterCache map[string]*regexp.Regexp
func compileFilter(filter string) (result *regexp.Regexp) {
if filter == "" {
// Must provide a filter
return nil
}
ok := false
// Cache hit?
(func() {
filterMutex.Lock()
defer filterMutex.Unlock()
if filterCache != nil {
result, ok = filterCache[filter]
}
})()
if ok {
return
}
// Cache miss
result, err := regexp.Compile(filter)
if err != nil {
return nil
}
// Cache for next time
filterMutex.Lock()
defer filterMutex.Unlock()
if filterCache == nil {
filterCache = make(map[string]*regexp.Regexp)
}
filterCache[filter] = result
return
}
func CompileFilterForPlugin(pluginName string, kind string, filter string) (*regexp.Regexp, error) {
if filter == "" {
return nil, fmt.Errorf("[%s] %q is missing a filter", pluginName, kind)
}
result := compileFilter(filter)
if result == nil {
return nil, fmt.Errorf("[%s] %q filter is not a valid Go regular expression: %q", pluginName, kind, filter)
}
return result, nil
}
func PluginAppliesToPath(path logger.Path, filter *regexp.Regexp, namespace string) bool {
return (namespace == "" || path.Namespace == namespace) && filter.MatchString(path.Text)
}
////////////////////////////////////////////////////////////////////////////////
// Plugin API
type Plugin struct {
Name string
OnStart []OnStart
OnResolve []OnResolve
OnLoad []OnLoad
}
type OnStart struct {
Name string
Callback func() OnStartResult
}
type OnStartResult struct {
Msgs []logger.Msg
ThrownError error
}
type OnResolve struct {
Name string
Filter *regexp.Regexp
Namespace string
Callback func(OnResolveArgs) OnResolveResult
}
type OnResolveArgs struct {
Path string
Importer logger.Path
ResolveDir string
Kind ast.ImportKind
PluginData interface{}
}
type OnResolveResult struct {
PluginName string
Path logger.Path
External bool
IsSideEffectFree bool
PluginData interface{}
Msgs []logger.Msg
ThrownError error
AbsWatchFiles []string
AbsWatchDirs []string
}
type OnLoad struct {
Name string
Filter *regexp.Regexp
Namespace string
Callback func(OnLoadArgs) OnLoadResult
}
type OnLoadArgs struct {
Path logger.Path
PluginData interface{}
}
type OnLoadResult struct {
PluginName string
Contents *string
AbsResolveDir string
Loader Loader
PluginData interface{}
Msgs []logger.Msg
ThrownError error
AbsWatchFiles []string
AbsWatchDirs []string
}

View File

@ -1,969 +0,0 @@
package config
import (
"math"
"strings"
"sync"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
)
var processedGlobalsMutex sync.Mutex
var processedGlobals *ProcessedDefines
// If something is in this list, then a direct identifier expression or property
// access chain matching this will be assumed to have no side effects and will
// be removed.
//
// This also means code is allowed to be reordered past things in this list. For
// example, if "console.log" is in this list, permitting reordering allows for
// "if (a) console.log(b); else console.log(c)" to be reordered and transformed
// into "console.log(a ? b : c)". Notice that "a" and "console.log" are in a
// different order, which can only happen if evaluating the "console.log"
// property access can be assumed to not change the value of "a".
//
// Note that membership in this list says nothing about whether calling any of
// these functions has any side effects. It only says something about
// referencing these function without calling them.
var knownGlobals = [][]string{
// These global identifiers should exist in all JavaScript environments. This
// deliberately omits "NaN", "Infinity", and "undefined" because these are
// treated as automatically-inlined constants instead of identifiers.
{"Array"},
{"Boolean"},
{"Function"},
{"Math"},
{"Number"},
{"Object"},
{"RegExp"},
{"String"},
// Object: Static methods
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Static_methods
{"Object", "assign"},
{"Object", "create"},
{"Object", "defineProperties"},
{"Object", "defineProperty"},
{"Object", "entries"},
{"Object", "freeze"},
{"Object", "fromEntries"},
{"Object", "getOwnPropertyDescriptor"},
{"Object", "getOwnPropertyDescriptors"},
{"Object", "getOwnPropertyNames"},
{"Object", "getOwnPropertySymbols"},
{"Object", "getPrototypeOf"},
{"Object", "is"},
{"Object", "isExtensible"},
{"Object", "isFrozen"},
{"Object", "isSealed"},
{"Object", "keys"},
{"Object", "preventExtensions"},
{"Object", "seal"},
{"Object", "setPrototypeOf"},
{"Object", "values"},
// Object: Instance methods
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object#Instance_methods
{"Object", "prototype", "__defineGetter__"},
{"Object", "prototype", "__defineSetter__"},
{"Object", "prototype", "__lookupGetter__"},
{"Object", "prototype", "__lookupSetter__"},
{"Object", "prototype", "hasOwnProperty"},
{"Object", "prototype", "isPrototypeOf"},
{"Object", "prototype", "propertyIsEnumerable"},
{"Object", "prototype", "toLocaleString"},
{"Object", "prototype", "toString"},
{"Object", "prototype", "unwatch"},
{"Object", "prototype", "valueOf"},
{"Object", "prototype", "watch"},
// Math: Static properties
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_properties
{"Math", "E"},
{"Math", "LN10"},
{"Math", "LN2"},
{"Math", "LOG10E"},
{"Math", "LOG2E"},
{"Math", "PI"},
{"Math", "SQRT1_2"},
{"Math", "SQRT2"},
// Math: Static methods
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math#Static_methods
{"Math", "abs"},
{"Math", "acos"},
{"Math", "acosh"},
{"Math", "asin"},
{"Math", "asinh"},
{"Math", "atan"},
{"Math", "atan2"},
{"Math", "atanh"},
{"Math", "cbrt"},
{"Math", "ceil"},
{"Math", "clz32"},
{"Math", "cos"},
{"Math", "cosh"},
{"Math", "exp"},
{"Math", "expm1"},
{"Math", "floor"},
{"Math", "fround"},
{"Math", "hypot"},
{"Math", "imul"},
{"Math", "log"},
{"Math", "log10"},
{"Math", "log1p"},
{"Math", "log2"},
{"Math", "max"},
{"Math", "min"},
{"Math", "pow"},
{"Math", "random"},
{"Math", "round"},
{"Math", "sign"},
{"Math", "sin"},
{"Math", "sinh"},
{"Math", "sqrt"},
{"Math", "tan"},
{"Math", "tanh"},
{"Math", "trunc"},
// Reflect: Static methods
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect#static_methods
{"Reflect", "apply"},
{"Reflect", "construct"},
{"Reflect", "defineProperty"},
{"Reflect", "deleteProperty"},
{"Reflect", "get"},
{"Reflect", "getOwnPropertyDescriptor"},
{"Reflect", "getPrototypeOf"},
{"Reflect", "has"},
{"Reflect", "isExtensible"},
{"Reflect", "ownKeys"},
{"Reflect", "preventExtensions"},
{"Reflect", "set"},
{"Reflect", "setPrototypeOf"},
// Other globals present in both the browser and node (except "eval" because
// it has special behavior)
{"AbortController"},
{"AbortSignal"},
{"AggregateError"},
{"ArrayBuffer"},
{"BigInt"},
{"DataView"},
{"Date"},
{"Error"},
{"EvalError"},
{"Event"},
{"EventTarget"},
{"Float32Array"},
{"Float64Array"},
{"Int16Array"},
{"Int32Array"},
{"Int8Array"},
{"Intl"},
{"JSON"},
{"Map"},
{"MessageChannel"},
{"MessageEvent"},
{"MessagePort"},
{"Promise"},
{"Proxy"},
{"RangeError"},
{"ReferenceError"},
{"Reflect"},
{"Set"},
{"Symbol"},
{"SyntaxError"},
{"TextDecoder"},
{"TextEncoder"},
{"TypeError"},
{"URIError"},
{"URL"},
{"URLSearchParams"},
{"Uint16Array"},
{"Uint32Array"},
{"Uint8Array"},
{"Uint8ClampedArray"},
{"WeakMap"},
{"WeakSet"},
{"WebAssembly"},
{"clearInterval"},
{"clearTimeout"},
{"console"},
{"decodeURI"},
{"decodeURIComponent"},
{"encodeURI"},
{"encodeURIComponent"},
{"escape"},
{"globalThis"},
{"isFinite"},
{"isNaN"},
{"parseFloat"},
{"parseInt"},
{"queueMicrotask"},
{"setInterval"},
{"setTimeout"},
{"unescape"},
// Console method references are assumed to have no side effects
// https://developer.mozilla.org/en-US/docs/Web/API/console
{"console", "assert"},
{"console", "clear"},
{"console", "count"},
{"console", "countReset"},
{"console", "debug"},
{"console", "dir"},
{"console", "dirxml"},
{"console", "error"},
{"console", "group"},
{"console", "groupCollapsed"},
{"console", "groupEnd"},
{"console", "info"},
{"console", "log"},
{"console", "table"},
{"console", "time"},
{"console", "timeEnd"},
{"console", "timeLog"},
{"console", "trace"},
{"console", "warn"},
// CSSOM APIs
{"CSSAnimation"},
{"CSSFontFaceRule"},
{"CSSImportRule"},
{"CSSKeyframeRule"},
{"CSSKeyframesRule"},
{"CSSMediaRule"},
{"CSSNamespaceRule"},
{"CSSPageRule"},
{"CSSRule"},
{"CSSRuleList"},
{"CSSStyleDeclaration"},
{"CSSStyleRule"},
{"CSSStyleSheet"},
{"CSSSupportsRule"},
{"CSSTransition"},
// SVG DOM
{"SVGAElement"},
{"SVGAngle"},
{"SVGAnimateElement"},
{"SVGAnimateMotionElement"},
{"SVGAnimateTransformElement"},
{"SVGAnimatedAngle"},
{"SVGAnimatedBoolean"},
{"SVGAnimatedEnumeration"},
{"SVGAnimatedInteger"},
{"SVGAnimatedLength"},
{"SVGAnimatedLengthList"},
{"SVGAnimatedNumber"},
{"SVGAnimatedNumberList"},
{"SVGAnimatedPreserveAspectRatio"},
{"SVGAnimatedRect"},
{"SVGAnimatedString"},
{"SVGAnimatedTransformList"},
{"SVGAnimationElement"},
{"SVGCircleElement"},
{"SVGClipPathElement"},
{"SVGComponentTransferFunctionElement"},
{"SVGDefsElement"},
{"SVGDescElement"},
{"SVGElement"},
{"SVGEllipseElement"},
{"SVGFEBlendElement"},
{"SVGFEColorMatrixElement"},
{"SVGFEComponentTransferElement"},
{"SVGFECompositeElement"},
{"SVGFEConvolveMatrixElement"},
{"SVGFEDiffuseLightingElement"},
{"SVGFEDisplacementMapElement"},
{"SVGFEDistantLightElement"},
{"SVGFEDropShadowElement"},
{"SVGFEFloodElement"},
{"SVGFEFuncAElement"},
{"SVGFEFuncBElement"},
{"SVGFEFuncGElement"},
{"SVGFEFuncRElement"},
{"SVGFEGaussianBlurElement"},
{"SVGFEImageElement"},
{"SVGFEMergeElement"},
{"SVGFEMergeNodeElement"},
{"SVGFEMorphologyElement"},
{"SVGFEOffsetElement"},
{"SVGFEPointLightElement"},
{"SVGFESpecularLightingElement"},
{"SVGFESpotLightElement"},
{"SVGFETileElement"},
{"SVGFETurbulenceElement"},
{"SVGFilterElement"},
{"SVGForeignObjectElement"},
{"SVGGElement"},
{"SVGGeometryElement"},
{"SVGGradientElement"},
{"SVGGraphicsElement"},
{"SVGImageElement"},
{"SVGLength"},
{"SVGLengthList"},
{"SVGLineElement"},
{"SVGLinearGradientElement"},
{"SVGMPathElement"},
{"SVGMarkerElement"},
{"SVGMaskElement"},
{"SVGMatrix"},
{"SVGMetadataElement"},
{"SVGNumber"},
{"SVGNumberList"},
{"SVGPathElement"},
{"SVGPatternElement"},
{"SVGPoint"},
{"SVGPointList"},
{"SVGPolygonElement"},
{"SVGPolylineElement"},
{"SVGPreserveAspectRatio"},
{"SVGRadialGradientElement"},
{"SVGRect"},
{"SVGRectElement"},
{"SVGSVGElement"},
{"SVGScriptElement"},
{"SVGSetElement"},
{"SVGStopElement"},
{"SVGStringList"},
{"SVGStyleElement"},
{"SVGSwitchElement"},
{"SVGSymbolElement"},
{"SVGTSpanElement"},
{"SVGTextContentElement"},
{"SVGTextElement"},
{"SVGTextPathElement"},
{"SVGTextPositioningElement"},
{"SVGTitleElement"},
{"SVGTransform"},
{"SVGTransformList"},
{"SVGUnitTypes"},
{"SVGUseElement"},
{"SVGViewElement"},
// Other browser APIs
//
// This list contains all globals present in modern versions of Chrome, Safari,
// and Firefox except for the following properties, since they have a side effect
// of triggering layout (https://gist.github.com/paulirish/5d52fb081b3570c81e3a):
//
// - scrollX
// - scrollY
// - innerWidth
// - innerHeight
// - pageXOffset
// - pageYOffset
//
// The following globals have also been removed since they sometimes throw an
// exception when accessed, which is a side effect (for more information see
// https://stackoverflow.com/a/33047477):
//
// - localStorage
// - sessionStorage
//
{"AnalyserNode"},
{"Animation"},
{"AnimationEffect"},
{"AnimationEvent"},
{"AnimationPlaybackEvent"},
{"AnimationTimeline"},
{"Attr"},
{"Audio"},
{"AudioBuffer"},
{"AudioBufferSourceNode"},
{"AudioDestinationNode"},
{"AudioListener"},
{"AudioNode"},
{"AudioParam"},
{"AudioProcessingEvent"},
{"AudioScheduledSourceNode"},
{"BarProp"},
{"BeforeUnloadEvent"},
{"BiquadFilterNode"},
{"Blob"},
{"BlobEvent"},
{"ByteLengthQueuingStrategy"},
{"CDATASection"},
{"CSS"},
{"CanvasGradient"},
{"CanvasPattern"},
{"CanvasRenderingContext2D"},
{"ChannelMergerNode"},
{"ChannelSplitterNode"},
{"CharacterData"},
{"ClipboardEvent"},
{"CloseEvent"},
{"Comment"},
{"CompositionEvent"},
{"ConvolverNode"},
{"CountQueuingStrategy"},
{"Crypto"},
{"CustomElementRegistry"},
{"CustomEvent"},
{"DOMException"},
{"DOMImplementation"},
{"DOMMatrix"},
{"DOMMatrixReadOnly"},
{"DOMParser"},
{"DOMPoint"},
{"DOMPointReadOnly"},
{"DOMQuad"},
{"DOMRect"},
{"DOMRectList"},
{"DOMRectReadOnly"},
{"DOMStringList"},
{"DOMStringMap"},
{"DOMTokenList"},
{"DataTransfer"},
{"DataTransferItem"},
{"DataTransferItemList"},
{"DelayNode"},
{"Document"},
{"DocumentFragment"},
{"DocumentTimeline"},
{"DocumentType"},
{"DragEvent"},
{"DynamicsCompressorNode"},
{"Element"},
{"ErrorEvent"},
{"EventSource"},
{"File"},
{"FileList"},
{"FileReader"},
{"FocusEvent"},
{"FontFace"},
{"FormData"},
{"GainNode"},
{"Gamepad"},
{"GamepadButton"},
{"GamepadEvent"},
{"Geolocation"},
{"GeolocationPositionError"},
{"HTMLAllCollection"},
{"HTMLAnchorElement"},
{"HTMLAreaElement"},
{"HTMLAudioElement"},
{"HTMLBRElement"},
{"HTMLBaseElement"},
{"HTMLBodyElement"},
{"HTMLButtonElement"},
{"HTMLCanvasElement"},
{"HTMLCollection"},
{"HTMLDListElement"},
{"HTMLDataElement"},
{"HTMLDataListElement"},
{"HTMLDetailsElement"},
{"HTMLDirectoryElement"},
{"HTMLDivElement"},
{"HTMLDocument"},
{"HTMLElement"},
{"HTMLEmbedElement"},
{"HTMLFieldSetElement"},
{"HTMLFontElement"},
{"HTMLFormControlsCollection"},
{"HTMLFormElement"},
{"HTMLFrameElement"},
{"HTMLFrameSetElement"},
{"HTMLHRElement"},
{"HTMLHeadElement"},
{"HTMLHeadingElement"},
{"HTMLHtmlElement"},
{"HTMLIFrameElement"},
{"HTMLImageElement"},
{"HTMLInputElement"},
{"HTMLLIElement"},
{"HTMLLabelElement"},
{"HTMLLegendElement"},
{"HTMLLinkElement"},
{"HTMLMapElement"},
{"HTMLMarqueeElement"},
{"HTMLMediaElement"},
{"HTMLMenuElement"},
{"HTMLMetaElement"},
{"HTMLMeterElement"},
{"HTMLModElement"},
{"HTMLOListElement"},
{"HTMLObjectElement"},
{"HTMLOptGroupElement"},
{"HTMLOptionElement"},
{"HTMLOptionsCollection"},
{"HTMLOutputElement"},
{"HTMLParagraphElement"},
{"HTMLParamElement"},
{"HTMLPictureElement"},
{"HTMLPreElement"},
{"HTMLProgressElement"},
{"HTMLQuoteElement"},
{"HTMLScriptElement"},
{"HTMLSelectElement"},
{"HTMLSlotElement"},
{"HTMLSourceElement"},
{"HTMLSpanElement"},
{"HTMLStyleElement"},
{"HTMLTableCaptionElement"},
{"HTMLTableCellElement"},
{"HTMLTableColElement"},
{"HTMLTableElement"},
{"HTMLTableRowElement"},
{"HTMLTableSectionElement"},
{"HTMLTemplateElement"},
{"HTMLTextAreaElement"},
{"HTMLTimeElement"},
{"HTMLTitleElement"},
{"HTMLTrackElement"},
{"HTMLUListElement"},
{"HTMLUnknownElement"},
{"HTMLVideoElement"},
{"HashChangeEvent"},
{"Headers"},
{"History"},
{"IDBCursor"},
{"IDBCursorWithValue"},
{"IDBDatabase"},
{"IDBFactory"},
{"IDBIndex"},
{"IDBKeyRange"},
{"IDBObjectStore"},
{"IDBOpenDBRequest"},
{"IDBRequest"},
{"IDBTransaction"},
{"IDBVersionChangeEvent"},
{"Image"},
{"ImageData"},
{"InputEvent"},
{"IntersectionObserver"},
{"IntersectionObserverEntry"},
{"KeyboardEvent"},
{"KeyframeEffect"},
{"Location"},
{"MediaCapabilities"},
{"MediaElementAudioSourceNode"},
{"MediaEncryptedEvent"},
{"MediaError"},
{"MediaList"},
{"MediaQueryList"},
{"MediaQueryListEvent"},
{"MediaRecorder"},
{"MediaSource"},
{"MediaStream"},
{"MediaStreamAudioDestinationNode"},
{"MediaStreamAudioSourceNode"},
{"MediaStreamTrack"},
{"MediaStreamTrackEvent"},
{"MimeType"},
{"MimeTypeArray"},
{"MouseEvent"},
{"MutationEvent"},
{"MutationObserver"},
{"MutationRecord"},
{"NamedNodeMap"},
{"Navigator"},
{"Node"},
{"NodeFilter"},
{"NodeIterator"},
{"NodeList"},
{"Notification"},
{"OfflineAudioCompletionEvent"},
{"Option"},
{"OscillatorNode"},
{"PageTransitionEvent"},
{"Path2D"},
{"Performance"},
{"PerformanceEntry"},
{"PerformanceMark"},
{"PerformanceMeasure"},
{"PerformanceNavigation"},
{"PerformanceObserver"},
{"PerformanceObserverEntryList"},
{"PerformanceResourceTiming"},
{"PerformanceTiming"},
{"PeriodicWave"},
{"Plugin"},
{"PluginArray"},
{"PointerEvent"},
{"PopStateEvent"},
{"ProcessingInstruction"},
{"ProgressEvent"},
{"PromiseRejectionEvent"},
{"RTCCertificate"},
{"RTCDTMFSender"},
{"RTCDTMFToneChangeEvent"},
{"RTCDataChannel"},
{"RTCDataChannelEvent"},
{"RTCIceCandidate"},
{"RTCPeerConnection"},
{"RTCPeerConnectionIceEvent"},
{"RTCRtpReceiver"},
{"RTCRtpSender"},
{"RTCRtpTransceiver"},
{"RTCSessionDescription"},
{"RTCStatsReport"},
{"RTCTrackEvent"},
{"RadioNodeList"},
{"Range"},
{"ReadableStream"},
{"Request"},
{"ResizeObserver"},
{"ResizeObserverEntry"},
{"Response"},
{"Screen"},
{"ScriptProcessorNode"},
{"SecurityPolicyViolationEvent"},
{"Selection"},
{"ShadowRoot"},
{"SourceBuffer"},
{"SourceBufferList"},
{"SpeechSynthesisEvent"},
{"SpeechSynthesisUtterance"},
{"StaticRange"},
{"Storage"},
{"StorageEvent"},
{"StyleSheet"},
{"StyleSheetList"},
{"Text"},
{"TextMetrics"},
{"TextTrack"},
{"TextTrackCue"},
{"TextTrackCueList"},
{"TextTrackList"},
{"TimeRanges"},
{"TrackEvent"},
{"TransitionEvent"},
{"TreeWalker"},
{"UIEvent"},
{"VTTCue"},
{"ValidityState"},
{"VisualViewport"},
{"WaveShaperNode"},
{"WebGLActiveInfo"},
{"WebGLBuffer"},
{"WebGLContextEvent"},
{"WebGLFramebuffer"},
{"WebGLProgram"},
{"WebGLQuery"},
{"WebGLRenderbuffer"},
{"WebGLRenderingContext"},
{"WebGLSampler"},
{"WebGLShader"},
{"WebGLShaderPrecisionFormat"},
{"WebGLSync"},
{"WebGLTexture"},
{"WebGLUniformLocation"},
{"WebKitCSSMatrix"},
{"WebSocket"},
{"WheelEvent"},
{"Window"},
{"Worker"},
{"XMLDocument"},
{"XMLHttpRequest"},
{"XMLHttpRequestEventTarget"},
{"XMLHttpRequestUpload"},
{"XMLSerializer"},
{"XPathEvaluator"},
{"XPathExpression"},
{"XPathResult"},
{"XSLTProcessor"},
{"alert"},
{"atob"},
{"blur"},
{"btoa"},
{"cancelAnimationFrame"},
{"captureEvents"},
{"close"},
{"closed"},
{"confirm"},
{"customElements"},
{"devicePixelRatio"},
{"document"},
{"event"},
{"fetch"},
{"find"},
{"focus"},
{"frameElement"},
{"frames"},
{"getComputedStyle"},
{"getSelection"},
{"history"},
{"indexedDB"},
{"isSecureContext"},
{"length"},
{"location"},
{"locationbar"},
{"matchMedia"},
{"menubar"},
{"moveBy"},
{"moveTo"},
{"name"},
{"navigator"},
{"onabort"},
{"onafterprint"},
{"onanimationend"},
{"onanimationiteration"},
{"onanimationstart"},
{"onbeforeprint"},
{"onbeforeunload"},
{"onblur"},
{"oncanplay"},
{"oncanplaythrough"},
{"onchange"},
{"onclick"},
{"oncontextmenu"},
{"oncuechange"},
{"ondblclick"},
{"ondrag"},
{"ondragend"},
{"ondragenter"},
{"ondragleave"},
{"ondragover"},
{"ondragstart"},
{"ondrop"},
{"ondurationchange"},
{"onemptied"},
{"onended"},
{"onerror"},
{"onfocus"},
{"ongotpointercapture"},
{"onhashchange"},
{"oninput"},
{"oninvalid"},
{"onkeydown"},
{"onkeypress"},
{"onkeyup"},
{"onlanguagechange"},
{"onload"},
{"onloadeddata"},
{"onloadedmetadata"},
{"onloadstart"},
{"onlostpointercapture"},
{"onmessage"},
{"onmousedown"},
{"onmouseenter"},
{"onmouseleave"},
{"onmousemove"},
{"onmouseout"},
{"onmouseover"},
{"onmouseup"},
{"onoffline"},
{"ononline"},
{"onpagehide"},
{"onpageshow"},
{"onpause"},
{"onplay"},
{"onplaying"},
{"onpointercancel"},
{"onpointerdown"},
{"onpointerenter"},
{"onpointerleave"},
{"onpointermove"},
{"onpointerout"},
{"onpointerover"},
{"onpointerup"},
{"onpopstate"},
{"onprogress"},
{"onratechange"},
{"onrejectionhandled"},
{"onreset"},
{"onresize"},
{"onscroll"},
{"onseeked"},
{"onseeking"},
{"onselect"},
{"onstalled"},
{"onstorage"},
{"onsubmit"},
{"onsuspend"},
{"ontimeupdate"},
{"ontoggle"},
{"ontransitioncancel"},
{"ontransitionend"},
{"ontransitionrun"},
{"ontransitionstart"},
{"onunhandledrejection"},
{"onunload"},
{"onvolumechange"},
{"onwaiting"},
{"onwebkitanimationend"},
{"onwebkitanimationiteration"},
{"onwebkitanimationstart"},
{"onwebkittransitionend"},
{"onwheel"},
{"open"},
{"opener"},
{"origin"},
{"outerHeight"},
{"outerWidth"},
{"parent"},
{"performance"},
{"personalbar"},
{"postMessage"},
{"print"},
{"prompt"},
{"releaseEvents"},
{"requestAnimationFrame"},
{"resizeBy"},
{"resizeTo"},
{"screen"},
{"screenLeft"},
{"screenTop"},
{"screenX"},
{"screenY"},
{"scroll"},
{"scrollBy"},
{"scrollTo"},
{"scrollbars"},
{"self"},
{"speechSynthesis"},
{"status"},
{"statusbar"},
{"stop"},
{"toolbar"},
{"top"},
{"webkitURL"},
{"window"},
}
type DefineArgs struct {
Loc logger.Loc
FindSymbol func(logger.Loc, string) js_ast.Ref
SymbolForDefine func(int) js_ast.Ref
}
type DefineFunc func(DefineArgs) js_ast.E
type DefineData struct {
DefineFunc DefineFunc
// True if accessing this value is known to not have any side effects. For
// example, a bare reference to "Object.create" can be removed because it
// does not have any observable side effects.
CanBeRemovedIfUnused bool
// True if a call to this value is known to not have any side effects. For
// example, a bare call to "Object()" can be removed because it does not
// have any observable side effects.
CallCanBeUnwrappedIfUnused bool
}
func mergeDefineData(old DefineData, new DefineData) DefineData {
if old.CanBeRemovedIfUnused {
new.CanBeRemovedIfUnused = true
}
if old.CallCanBeUnwrappedIfUnused {
new.CallCanBeUnwrappedIfUnused = true
}
return new
}
type DotDefine struct {
Parts []string
Data DefineData
}
type ProcessedDefines struct {
IdentifierDefines map[string]DefineData
DotDefines map[string][]DotDefine
}
// This transformation is expensive, so we only want to do it once. Make sure
// to only call processDefines() once per compilation. Unfortunately Golang
// doesn't have an efficient way to copy a map and the overhead of copying
// all of the properties into a new map once for every new parser noticeably
// slows down our benchmarks.
func ProcessDefines(userDefines map[string]DefineData) ProcessedDefines {
// Optimization: reuse known globals if there are no user-specified defines
hasUserDefines := len(userDefines) != 0
if !hasUserDefines {
processedGlobalsMutex.Lock()
if processedGlobals != nil {
defer processedGlobalsMutex.Unlock()
return *processedGlobals
}
processedGlobalsMutex.Unlock()
}
result := ProcessedDefines{
IdentifierDefines: make(map[string]DefineData),
DotDefines: make(map[string][]DotDefine),
}
// Mark these property accesses as free of side effects. That means they can
// be removed if their result is unused. We can't just remove all unused
// property accesses since property accesses can have side effects. For
// example, the property access "a.b.c" has the side effect of throwing an
// exception if "a.b" is undefined.
for _, parts := range knownGlobals {
tail := parts[len(parts)-1]
if len(parts) == 1 {
result.IdentifierDefines[tail] = DefineData{CanBeRemovedIfUnused: true}
} else {
result.DotDefines[tail] = append(result.DotDefines[tail], DotDefine{Parts: parts, Data: DefineData{CanBeRemovedIfUnused: true}})
}
}
// Swap in certain literal values because those can be constant folded
result.IdentifierDefines["undefined"] = DefineData{
DefineFunc: func(DefineArgs) js_ast.E { return js_ast.EUndefinedShared },
}
result.IdentifierDefines["NaN"] = DefineData{
DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.NaN()} },
}
result.IdentifierDefines["Infinity"] = DefineData{
DefineFunc: func(DefineArgs) js_ast.E { return &js_ast.ENumber{Value: math.Inf(1)} },
}
// Then copy the user-specified defines in afterwards, which will overwrite
// any known globals above.
for key, data := range userDefines {
parts := strings.Split(key, ".")
// Identifier defines are special-cased
if len(parts) == 1 {
result.IdentifierDefines[key] = mergeDefineData(result.IdentifierDefines[key], data)
continue
}
tail := parts[len(parts)-1]
dotDefines := result.DotDefines[tail]
found := false
// Try to merge with existing dot defines first
for i, define := range dotDefines {
if arePartsEqual(parts, define.Parts) {
define := &dotDefines[i]
define.Data = mergeDefineData(define.Data, data)
found = true
break
}
}
if !found {
dotDefines = append(dotDefines, DotDefine{Parts: parts, Data: data})
}
result.DotDefines[tail] = dotDefines
}
// Potentially cache the result for next time
if !hasUserDefines {
processedGlobalsMutex.Lock()
defer processedGlobalsMutex.Unlock()
if processedGlobals == nil {
processedGlobals = &result
}
}
return result
}
func arePartsEqual(a []string, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@ -1,646 +0,0 @@
package css_ast
import (
"strconv"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
)
// CSS syntax comes in two layers: a minimal syntax that generally accepts
// anything that looks vaguely like CSS, and a large set of built-in rules
// (the things browsers actually interpret). That way CSS parsers can read
// unknown rules and skip over them without having to stop due to errors.
//
// This AST format is mostly just the minimal syntax. It parses unknown rules
// into a tree with enough information that it can write them back out again.
// There are some additional layers of syntax including selectors and @-rules
// which allow for better pretty-printing and minification.
//
// Most of the AST just references ranges of the original file by keeping the
// original "Token" values around from the lexer. This is a memory-efficient
// representation that helps provide good parsing and printing performance.
type AST struct {
ImportRecords []ast.ImportRecord
Rules []Rule
SourceMapComment logger.Span
ApproximateLineCount int32
}
// We create a lot of tokens, so make sure this layout is memory-efficient.
// The layout here isn't optimal because it biases for convenience (e.g.
// "string" could be shorter) but at least the ordering of fields was
// deliberately chosen to minimize size.
type Token struct {
// This is the raw contents of the token most of the time. However, it
// contains the decoded string contents for "TString" tokens.
Text string // 16 bytes
// Contains the child tokens for component values that are simple blocks.
// These are either "(", "{", "[", or function tokens. The closing token is
// implicit and is not stored.
Children *[]Token // 8 bytes
// URL tokens have an associated import record at the top-level of the AST.
// This index points to that import record.
ImportRecordIndex uint32 // 4 bytes
// The division between the number and the unit for "TDimension" tokens.
UnitOffset uint16 // 2 bytes
// This will never be "TWhitespace" because whitespace isn't stored as a
// token directly. Instead it is stored in "HasWhitespaceAfter" on the
// previous token. This is to make it easier to pattern-match against
// tokens when handling CSS rules, since whitespace almost always doesn't
// matter. That way you can pattern match against e.g. "rgb(r, g, b)" and
// not have to handle all possible combinations of embedded whitespace
// tokens.
//
// There is one exception to this: when in verbatim whitespace mode and
// the token list is non-empty and is only whitespace tokens. In that case
// a single whitespace token is emitted. This is because otherwise there
// would be no tokens to attach the whitespace before/after flags to.
Kind css_lexer.T // 1 byte
// These flags indicate the presence of a "TWhitespace" token before or after
// this token. There should be whitespace printed between two tokens if either
// token indicates that there should be whitespace. Note that whitespace may
// be altered by processing in certain situations (e.g. minification).
Whitespace WhitespaceFlags // 1 byte
}
type WhitespaceFlags uint8
const (
WhitespaceBefore WhitespaceFlags = 1 << iota
WhitespaceAfter
)
func (a Token) Equal(b Token) bool {
if a.Kind == b.Kind && a.Text == b.Text && a.ImportRecordIndex == b.ImportRecordIndex && a.Whitespace == b.Whitespace {
if a.Children == nil && b.Children == nil {
return true
}
if a.Children != nil && b.Children != nil && TokensEqual(*a.Children, *b.Children) {
return true
}
}
return false
}
func TokensEqual(a []Token, b []Token) bool {
if len(a) != len(b) {
return false
}
for i, c := range a {
if !c.Equal(b[i]) {
return false
}
}
return true
}
func HashTokens(hash uint32, tokens []Token) uint32 {
hash = helpers.HashCombine(hash, uint32(len(tokens)))
for _, t := range tokens {
hash = helpers.HashCombine(hash, uint32(t.Kind))
hash = helpers.HashCombineString(hash, t.Text)
if t.Children != nil {
hash = HashTokens(hash, *t.Children)
}
}
return hash
}
func (a Token) EqualIgnoringWhitespace(b Token) bool {
if a.Kind == b.Kind && a.Text == b.Text && a.ImportRecordIndex == b.ImportRecordIndex {
if a.Children == nil && b.Children == nil {
return true
}
if a.Children != nil && b.Children != nil && TokensEqualIgnoringWhitespace(*a.Children, *b.Children) {
return true
}
}
return false
}
func TokensEqualIgnoringWhitespace(a []Token, b []Token) bool {
if len(a) != len(b) {
return false
}
for i, c := range a {
if !c.EqualIgnoringWhitespace(b[i]) {
return false
}
}
return true
}
func TokensAreCommaSeparated(tokens []Token) bool {
if n := len(tokens); (n & 1) != 0 {
for i := 1; i < n; i += 2 {
if tokens[i].Kind != css_lexer.TComma {
return false
}
}
return true
}
return false
}
func (t Token) FractionForPercentage() (float64, bool) {
if t.Kind == css_lexer.TPercentage {
if f, err := strconv.ParseFloat(t.PercentageValue(), 64); err == nil {
if f < 0 {
return 0, true
}
if f > 100 {
return 1, true
}
return f / 100.0, true
}
}
return 0, false
}
// https://drafts.csswg.org/css-values-3/#lengths
// For zero lengths the unit identifier is optional
// (i.e. can be syntactically represented as the <number> 0).
func (t *Token) TurnLengthIntoNumberIfZero() bool {
if t.Kind == css_lexer.TDimension && t.DimensionValue() == "0" {
t.Kind = css_lexer.TNumber
t.Text = "0"
return true
}
return false
}
func (t *Token) TurnLengthOrPercentageIntoNumberIfZero() bool {
if t.Kind == css_lexer.TPercentage && t.PercentageValue() == "0" {
t.Kind = css_lexer.TNumber
t.Text = "0"
return true
}
return t.TurnLengthIntoNumberIfZero()
}
func (t Token) PercentageValue() string {
return t.Text[:len(t.Text)-1]
}
func (t Token) DimensionValue() string {
return t.Text[:t.UnitOffset]
}
func (t Token) DimensionUnit() string {
return t.Text[t.UnitOffset:]
}
func (t Token) DimensionUnitIsSafeLength() bool {
switch t.DimensionUnit() {
// These units can be reasonably expected to be supported everywhere.
// Information used: https://developer.mozilla.org/en-US/docs/Web/CSS/length
case "cm", "em", "in", "mm", "pc", "pt", "px":
return true
}
return false
}
func (t Token) IsZero() bool {
return t.Kind == css_lexer.TNumber && t.Text == "0"
}
func (t Token) IsOne() bool {
return t.Kind == css_lexer.TNumber && t.Text == "1"
}
func (t Token) IsAngle() bool {
if t.Kind == css_lexer.TDimension {
unit := t.DimensionUnit()
return unit == "deg" || unit == "grad" || unit == "rad" || unit == "turn"
}
return false
}
func CloneTokensWithImportRecords(
tokensIn []Token, importRecordsIn []ast.ImportRecord,
tokensOut []Token, importRecordsOut []ast.ImportRecord,
) ([]Token, []ast.ImportRecord) {
for _, t := range tokensIn {
// If this is a URL token, also clone the import record
if t.Kind == css_lexer.TURL {
importRecordIndex := uint32(len(importRecordsOut))
importRecordsOut = append(importRecordsOut, importRecordsIn[t.ImportRecordIndex])
t.ImportRecordIndex = importRecordIndex
}
// Also search for URL tokens in this token's children
if t.Children != nil {
var children []Token
children, importRecordsOut = CloneTokensWithImportRecords(*t.Children, importRecordsIn, children, importRecordsOut)
t.Children = &children
}
tokensOut = append(tokensOut, t)
}
return tokensOut, importRecordsOut
}
type Rule struct {
Loc logger.Loc
Data R
}
type R interface {
Equal(rule R) bool
Hash() (uint32, bool)
}
func RulesEqual(a []Rule, b []Rule) bool {
if len(a) != len(b) {
return false
}
for i, c := range a {
if !c.Data.Equal(b[i].Data) {
return false
}
}
return true
}
func HashRules(hash uint32, rules []Rule) uint32 {
hash = helpers.HashCombine(hash, uint32(len(rules)))
for _, child := range rules {
if childHash, ok := child.Data.Hash(); ok {
hash = helpers.HashCombine(hash, childHash)
} else {
hash = helpers.HashCombine(hash, 0)
}
}
return hash
}
type RAtCharset struct {
Encoding string
}
func (a *RAtCharset) Equal(rule R) bool {
b, ok := rule.(*RAtCharset)
return ok && a.Encoding == b.Encoding
}
func (r *RAtCharset) Hash() (uint32, bool) {
hash := uint32(1)
hash = helpers.HashCombineString(hash, r.Encoding)
return hash, true
}
type RAtImport struct {
ImportRecordIndex uint32
ImportConditions []Token
}
func (*RAtImport) Equal(rule R) bool {
return false
}
func (r *RAtImport) Hash() (uint32, bool) {
return 0, false
}
type RAtKeyframes struct {
AtToken string
Name string
Blocks []KeyframeBlock
}
type KeyframeBlock struct {
Selectors []string
Rules []Rule
}
func (a *RAtKeyframes) Equal(rule R) bool {
b, ok := rule.(*RAtKeyframes)
if ok && a.AtToken == b.AtToken && a.Name == b.Name && len(a.Blocks) == len(b.Blocks) {
for i, ai := range a.Blocks {
bi := b.Blocks[i]
if len(ai.Selectors) != len(bi.Selectors) {
return false
}
for j, aj := range ai.Selectors {
if aj != bi.Selectors[j] {
return false
}
}
if !RulesEqual(ai.Rules, bi.Rules) {
return false
}
}
return true
}
return false
}
func (r *RAtKeyframes) Hash() (uint32, bool) {
hash := uint32(2)
hash = helpers.HashCombineString(hash, r.AtToken)
hash = helpers.HashCombineString(hash, r.Name)
hash = helpers.HashCombine(hash, uint32(len(r.Blocks)))
for _, block := range r.Blocks {
hash = helpers.HashCombine(hash, uint32(len(block.Selectors)))
for _, sel := range block.Selectors {
hash = helpers.HashCombineString(hash, sel)
}
hash = HashRules(hash, block.Rules)
}
return hash, true
}
type RKnownAt struct {
AtToken string
Prelude []Token
Rules []Rule
}
func (a *RKnownAt) Equal(rule R) bool {
b, ok := rule.(*RKnownAt)
return ok && a.AtToken == b.AtToken && TokensEqual(a.Prelude, b.Prelude) && RulesEqual(a.Rules, a.Rules)
}
func (r *RKnownAt) Hash() (uint32, bool) {
hash := uint32(3)
hash = helpers.HashCombineString(hash, r.AtToken)
hash = HashTokens(hash, r.Prelude)
hash = HashRules(hash, r.Rules)
return hash, true
}
type RUnknownAt struct {
AtToken string
Prelude []Token
Block []Token
}
func (a *RUnknownAt) Equal(rule R) bool {
b, ok := rule.(*RUnknownAt)
return ok && a.AtToken == b.AtToken && TokensEqual(a.Prelude, b.Prelude) && TokensEqual(a.Block, a.Block)
}
func (r *RUnknownAt) Hash() (uint32, bool) {
hash := uint32(4)
hash = helpers.HashCombineString(hash, r.AtToken)
hash = HashTokens(hash, r.Prelude)
hash = HashTokens(hash, r.Block)
return hash, true
}
type RSelector struct {
Selectors []ComplexSelector
Rules []Rule
}
func (a *RSelector) Equal(rule R) bool {
b, ok := rule.(*RSelector)
if ok && len(a.Selectors) == len(b.Selectors) {
for i, sel := range a.Selectors {
if !sel.Equal(b.Selectors[i]) {
return false
}
}
return RulesEqual(a.Rules, b.Rules)
}
return false
}
func (r *RSelector) Hash() (uint32, bool) {
hash := uint32(5)
hash = helpers.HashCombine(hash, uint32(len(r.Selectors)))
for _, complex := range r.Selectors {
hash = helpers.HashCombine(hash, uint32(len(complex.Selectors)))
for _, sel := range complex.Selectors {
if sel.TypeSelector != nil {
hash = helpers.HashCombineString(hash, sel.TypeSelector.Name.Text)
} else {
hash = helpers.HashCombine(hash, 0)
}
hash = helpers.HashCombine(hash, uint32(len(sel.SubclassSelectors)))
for _, sub := range sel.SubclassSelectors {
hash = helpers.HashCombine(hash, sub.Hash())
}
hash = helpers.HashCombineString(hash, sel.Combinator)
}
}
hash = HashRules(hash, r.Rules)
return hash, true
}
type RQualified struct {
Prelude []Token
Rules []Rule
}
func (a *RQualified) Equal(rule R) bool {
b, ok := rule.(*RQualified)
return ok && TokensEqual(a.Prelude, b.Prelude) && RulesEqual(a.Rules, b.Rules)
}
func (r *RQualified) Hash() (uint32, bool) {
hash := uint32(6)
hash = HashTokens(hash, r.Prelude)
hash = HashRules(hash, r.Rules)
return hash, true
}
type RDeclaration struct {
KeyText string
Value []Token
KeyRange logger.Range
Key D // Compare using this instead of "Key" for speed
Important bool
}
func (a *RDeclaration) Equal(rule R) bool {
b, ok := rule.(*RDeclaration)
return ok && a.KeyText == b.KeyText && TokensEqual(a.Value, b.Value) && a.Important == b.Important
}
func (r *RDeclaration) Hash() (uint32, bool) {
hash := uint32(7)
hash = helpers.HashCombine(hash, uint32(r.Key))
hash = HashTokens(hash, r.Value)
return hash, true
}
type RBadDeclaration struct {
Tokens []Token
}
func (a *RBadDeclaration) Equal(rule R) bool {
b, ok := rule.(*RBadDeclaration)
return ok && TokensEqual(a.Tokens, b.Tokens)
}
func (r *RBadDeclaration) Hash() (uint32, bool) {
hash := uint32(8)
hash = HashTokens(hash, r.Tokens)
return hash, true
}
type RComment struct {
Text string
}
func (a *RComment) Equal(rule R) bool {
b, ok := rule.(*RComment)
return ok && a.Text == b.Text
}
func (r *RComment) Hash() (uint32, bool) {
hash := uint32(9)
hash = helpers.HashCombineString(hash, r.Text)
return hash, true
}
type ComplexSelector struct {
Selectors []CompoundSelector
}
func (a ComplexSelector) Equal(b ComplexSelector) bool {
if len(a.Selectors) != len(b.Selectors) {
return false
}
for i, ai := range a.Selectors {
bi := b.Selectors[i]
if ai.HasNestPrefix != bi.HasNestPrefix || ai.Combinator != bi.Combinator {
return false
}
if ats, bts := ai.TypeSelector, bi.TypeSelector; (ats == nil) != (bts == nil) {
return false
} else if ats != nil && bts != nil && !ats.Equal(*bts) {
return false
}
if len(ai.SubclassSelectors) != len(bi.SubclassSelectors) {
return false
}
for j, aj := range ai.SubclassSelectors {
if !aj.Equal(bi.SubclassSelectors[j]) {
return false
}
}
}
return true
}
type CompoundSelector struct {
HasNestPrefix bool // "&"
Combinator string // Optional, may be ""
TypeSelector *NamespacedName
SubclassSelectors []SS
}
type NameToken struct {
Kind css_lexer.T
Text string
}
type NamespacedName struct {
// If present, this is an identifier or "*" and is followed by a "|" character
NamespacePrefix *NameToken
// This is an identifier or "*"
Name NameToken
}
func (a NamespacedName) Equal(b NamespacedName) bool {
return a.Name == b.Name && (a.NamespacePrefix == nil) == (b.NamespacePrefix == nil) &&
(a.NamespacePrefix == nil || b.NamespacePrefix == nil || *a.NamespacePrefix == *b.NamespacePrefix)
}
type SS interface {
Equal(ss SS) bool
Hash() uint32
}
type SSHash struct {
Name string
}
func (a *SSHash) Equal(ss SS) bool {
b, ok := ss.(*SSHash)
return ok && a.Name == b.Name
}
func (ss *SSHash) Hash() uint32 {
hash := uint32(1)
hash = helpers.HashCombineString(hash, ss.Name)
return hash
}
type SSClass struct {
Name string
}
func (a *SSClass) Equal(ss SS) bool {
b, ok := ss.(*SSClass)
return ok && a.Name == b.Name
}
func (ss *SSClass) Hash() uint32 {
hash := uint32(2)
hash = helpers.HashCombineString(hash, ss.Name)
return hash
}
type SSAttribute struct {
NamespacedName NamespacedName
MatcherOp string // Either "" or one of: "=" "~=" "|=" "^=" "$=" "*="
MatcherValue string
MatcherModifier byte // Either 0 or one of: 'i' 'I' 's' 'S'
}
func (a *SSAttribute) Equal(ss SS) bool {
b, ok := ss.(*SSAttribute)
return ok && a.NamespacedName.Equal(b.NamespacedName) && a.MatcherOp == b.MatcherOp &&
a.MatcherValue == b.MatcherValue && a.MatcherModifier == b.MatcherModifier
}
func (ss *SSAttribute) Hash() uint32 {
hash := uint32(3)
hash = helpers.HashCombineString(hash, ss.NamespacedName.Name.Text)
hash = helpers.HashCombineString(hash, ss.MatcherOp)
hash = helpers.HashCombineString(hash, ss.MatcherValue)
return hash
}
type SSPseudoClass struct {
Name string
Args []Token
IsElement bool // If true, this is prefixed by "::" instead of ":"
}
func (a *SSPseudoClass) Equal(ss SS) bool {
b, ok := ss.(*SSPseudoClass)
return ok && a.Name == b.Name && TokensEqual(a.Args, b.Args) && a.IsElement == b.IsElement
}
func (ss *SSPseudoClass) Hash() uint32 {
hash := uint32(4)
hash = helpers.HashCombineString(hash, ss.Name)
hash = HashTokens(hash, ss.Args)
return hash
}

View File

@ -1,642 +0,0 @@
package css_ast
type D uint16
const (
DUnknown D = iota
DAlignContent
DAlignItems
DAlignSelf
DAlignmentBaseline
DAll
DAnimation
DAnimationDelay
DAnimationDirection
DAnimationDuration
DAnimationFillMode
DAnimationIterationCount
DAnimationName
DAnimationPlayState
DAnimationTimingFunction
DBackfaceVisibility
DBackground
DBackgroundAttachment
DBackgroundClip
DBackgroundColor
DBackgroundImage
DBackgroundOrigin
DBackgroundPosition
DBackgroundPositionX
DBackgroundPositionY
DBackgroundRepeat
DBackgroundSize
DBaselineShift
DBlockSize
DBorder
DBorderBlockEnd
DBorderBlockEndColor
DBorderBlockEndStyle
DBorderBlockEndWidth
DBorderBlockStart
DBorderBlockStartColor
DBorderBlockStartStyle
DBorderBlockStartWidth
DBorderBottom
DBorderBottomColor
DBorderBottomLeftRadius
DBorderBottomRightRadius
DBorderBottomStyle
DBorderBottomWidth
DBorderCollapse
DBorderColor
DBorderImage
DBorderImageOutset
DBorderImageRepeat
DBorderImageSlice
DBorderImageSource
DBorderImageWidth
DBorderInlineEnd
DBorderInlineEndColor
DBorderInlineEndStyle
DBorderInlineEndWidth
DBorderInlineStart
DBorderInlineStartColor
DBorderInlineStartStyle
DBorderInlineStartWidth
DBorderLeft
DBorderLeftColor
DBorderLeftStyle
DBorderLeftWidth
DBorderRadius
DBorderRight
DBorderRightColor
DBorderRightStyle
DBorderRightWidth
DBorderSpacing
DBorderStyle
DBorderTop
DBorderTopColor
DBorderTopLeftRadius
DBorderTopRightRadius
DBorderTopStyle
DBorderTopWidth
DBorderWidth
DBottom
DBoxShadow
DBoxSizing
DBreakAfter
DBreakBefore
DBreakInside
DCaptionSide
DCaretColor
DClear
DClip
DClipPath
DClipRule
DColor
DColorInterpolation
DColorInterpolationFilters
DColumnCount
DColumnFill
DColumnGap
DColumnRule
DColumnRuleColor
DColumnRuleStyle
DColumnRuleWidth
DColumnSpan
DColumnWidth
DColumns
DContent
DCounterIncrement
DCounterReset
DCssFloat
DCssText
DCursor
DDirection
DDisplay
DDominantBaseline
DEmptyCells
DFill
DFillOpacity
DFillRule
DFilter
DFlex
DFlexBasis
DFlexDirection
DFlexFlow
DFlexGrow
DFlexShrink
DFlexWrap
DFloat
DFloodColor
DFloodOpacity
DFont
DFontFamily
DFontFeatureSettings
DFontKerning
DFontSize
DFontSizeAdjust
DFontStretch
DFontStyle
DFontSynthesis
DFontVariant
DFontVariantCaps
DFontVariantEastAsian
DFontVariantLigatures
DFontVariantNumeric
DFontVariantPosition
DFontWeight
DGap
DGlyphOrientationVertical
DGrid
DGridArea
DGridAutoColumns
DGridAutoFlow
DGridAutoRows
DGridColumn
DGridColumnEnd
DGridColumnGap
DGridColumnStart
DGridGap
DGridRow
DGridRowEnd
DGridRowGap
DGridRowStart
DGridTemplate
DGridTemplateAreas
DGridTemplateColumns
DGridTemplateRows
DHeight
DHyphens
DImageOrientation
DImageRendering
DInlineSize
DInset
DJustifyContent
DJustifyItems
DJustifySelf
DLeft
DLetterSpacing
DLightingColor
DLineBreak
DLineHeight
DListStyle
DListStyleImage
DListStylePosition
DListStyleType
DMargin
DMarginBlockEnd
DMarginBlockStart
DMarginBottom
DMarginInlineEnd
DMarginInlineStart
DMarginLeft
DMarginRight
DMarginTop
DMarker
DMarkerEnd
DMarkerMid
DMarkerStart
DMask
DMaskComposite
DMaskImage
DMaskPosition
DMaskRepeat
DMaskSize
DMaskType
DMaxBlockSize
DMaxHeight
DMaxInlineSize
DMaxWidth
DMinBlockSize
DMinHeight
DMinInlineSize
DMinWidth
DObjectFit
DObjectPosition
DOpacity
DOrder
DOrphans
DOutline
DOutlineColor
DOutlineOffset
DOutlineStyle
DOutlineWidth
DOverflow
DOverflowAnchor
DOverflowWrap
DOverflowX
DOverflowY
DOverscrollBehavior
DOverscrollBehaviorBlock
DOverscrollBehaviorInline
DOverscrollBehaviorX
DOverscrollBehaviorY
DPadding
DPaddingBlockEnd
DPaddingBlockStart
DPaddingBottom
DPaddingInlineEnd
DPaddingInlineStart
DPaddingLeft
DPaddingRight
DPaddingTop
DPageBreakAfter
DPageBreakBefore
DPageBreakInside
DPaintOrder
DPerspective
DPerspectiveOrigin
DPlaceContent
DPlaceItems
DPlaceSelf
DPointerEvents
DPosition
DQuotes
DResize
DRight
DRotate
DRowGap
DRubyAlign
DRubyPosition
DScale
DScrollBehavior
DShapeRendering
DStopColor
DStopOpacity
DStroke
DStrokeDasharray
DStrokeDashoffset
DStrokeLinecap
DStrokeLinejoin
DStrokeMiterlimit
DStrokeOpacity
DStrokeWidth
DTabSize
DTableLayout
DTextAlign
DTextAlignLast
DTextAnchor
DTextCombineUpright
DTextDecoration
DTextDecorationColor
DTextDecorationLine
DTextDecorationStyle
DTextEmphasis
DTextEmphasisColor
DTextEmphasisPosition
DTextEmphasisStyle
DTextIndent
DTextJustify
DTextOrientation
DTextOverflow
DTextRendering
DTextShadow
DTextTransform
DTextUnderlinePosition
DTop
DTouchAction
DTransform
DTransformBox
DTransformOrigin
DTransformStyle
DTransition
DTransitionDelay
DTransitionDuration
DTransitionProperty
DTransitionTimingFunction
DTranslate
DUnicodeBidi
DUserSelect
DVerticalAlign
DVisibility
DWhiteSpace
DWidows
DWidth
DWillChange
DWordBreak
DWordSpacing
DWordWrap
DWritingMode
DZIndex
DZoom
)
var KnownDeclarations = map[string]D{
"align-content": DAlignContent,
"align-items": DAlignItems,
"align-self": DAlignSelf,
"alignment-baseline": DAlignmentBaseline,
"all": DAll,
"animation": DAnimation,
"animation-delay": DAnimationDelay,
"animation-direction": DAnimationDirection,
"animation-duration": DAnimationDuration,
"animation-fill-mode": DAnimationFillMode,
"animation-iteration-count": DAnimationIterationCount,
"animation-name": DAnimationName,
"animation-play-state": DAnimationPlayState,
"animation-timing-function": DAnimationTimingFunction,
"backface-visibility": DBackfaceVisibility,
"background": DBackground,
"background-attachment": DBackgroundAttachment,
"background-clip": DBackgroundClip,
"background-color": DBackgroundColor,
"background-image": DBackgroundImage,
"background-origin": DBackgroundOrigin,
"background-position": DBackgroundPosition,
"background-position-x": DBackgroundPositionX,
"background-position-y": DBackgroundPositionY,
"background-repeat": DBackgroundRepeat,
"background-size": DBackgroundSize,
"baseline-shift": DBaselineShift,
"block-size": DBlockSize,
"border": DBorder,
"border-block-end": DBorderBlockEnd,
"border-block-end-color": DBorderBlockEndColor,
"border-block-end-style": DBorderBlockEndStyle,
"border-block-end-width": DBorderBlockEndWidth,
"border-block-start": DBorderBlockStart,
"border-block-start-color": DBorderBlockStartColor,
"border-block-start-style": DBorderBlockStartStyle,
"border-block-start-width": DBorderBlockStartWidth,
"border-bottom": DBorderBottom,
"border-bottom-color": DBorderBottomColor,
"border-bottom-left-radius": DBorderBottomLeftRadius,
"border-bottom-right-radius": DBorderBottomRightRadius,
"border-bottom-style": DBorderBottomStyle,
"border-bottom-width": DBorderBottomWidth,
"border-collapse": DBorderCollapse,
"border-color": DBorderColor,
"border-image": DBorderImage,
"border-image-outset": DBorderImageOutset,
"border-image-repeat": DBorderImageRepeat,
"border-image-slice": DBorderImageSlice,
"border-image-source": DBorderImageSource,
"border-image-width": DBorderImageWidth,
"border-inline-end": DBorderInlineEnd,
"border-inline-end-color": DBorderInlineEndColor,
"border-inline-end-style": DBorderInlineEndStyle,
"border-inline-end-width": DBorderInlineEndWidth,
"border-inline-start": DBorderInlineStart,
"border-inline-start-color": DBorderInlineStartColor,
"border-inline-start-style": DBorderInlineStartStyle,
"border-inline-start-width": DBorderInlineStartWidth,
"border-left": DBorderLeft,
"border-left-color": DBorderLeftColor,
"border-left-style": DBorderLeftStyle,
"border-left-width": DBorderLeftWidth,
"border-radius": DBorderRadius,
"border-right": DBorderRight,
"border-right-color": DBorderRightColor,
"border-right-style": DBorderRightStyle,
"border-right-width": DBorderRightWidth,
"border-spacing": DBorderSpacing,
"border-style": DBorderStyle,
"border-top": DBorderTop,
"border-top-color": DBorderTopColor,
"border-top-left-radius": DBorderTopLeftRadius,
"border-top-right-radius": DBorderTopRightRadius,
"border-top-style": DBorderTopStyle,
"border-top-width": DBorderTopWidth,
"border-width": DBorderWidth,
"bottom": DBottom,
"box-shadow": DBoxShadow,
"box-sizing": DBoxSizing,
"break-after": DBreakAfter,
"break-before": DBreakBefore,
"break-inside": DBreakInside,
"caption-side": DCaptionSide,
"caret-color": DCaretColor,
"clear": DClear,
"clip": DClip,
"clip-path": DClipPath,
"clip-rule": DClipRule,
"color": DColor,
"color-interpolation": DColorInterpolation,
"color-interpolation-filters": DColorInterpolationFilters,
"column-count": DColumnCount,
"column-fill": DColumnFill,
"column-gap": DColumnGap,
"column-rule": DColumnRule,
"column-rule-color": DColumnRuleColor,
"column-rule-style": DColumnRuleStyle,
"column-rule-width": DColumnRuleWidth,
"column-span": DColumnSpan,
"column-width": DColumnWidth,
"columns": DColumns,
"content": DContent,
"counter-increment": DCounterIncrement,
"counter-reset": DCounterReset,
"css-float": DCssFloat,
"css-text": DCssText,
"cursor": DCursor,
"direction": DDirection,
"display": DDisplay,
"dominant-baseline": DDominantBaseline,
"empty-cells": DEmptyCells,
"fill": DFill,
"fill-opacity": DFillOpacity,
"fill-rule": DFillRule,
"filter": DFilter,
"flex": DFlex,
"flex-basis": DFlexBasis,
"flex-direction": DFlexDirection,
"flex-flow": DFlexFlow,
"flex-grow": DFlexGrow,
"flex-shrink": DFlexShrink,
"flex-wrap": DFlexWrap,
"float": DFloat,
"flood-color": DFloodColor,
"flood-opacity": DFloodOpacity,
"font": DFont,
"font-family": DFontFamily,
"font-feature-settings": DFontFeatureSettings,
"font-kerning": DFontKerning,
"font-size": DFontSize,
"font-size-adjust": DFontSizeAdjust,
"font-stretch": DFontStretch,
"font-style": DFontStyle,
"font-synthesis": DFontSynthesis,
"font-variant": DFontVariant,
"font-variant-caps": DFontVariantCaps,
"font-variant-east-asian": DFontVariantEastAsian,
"font-variant-ligatures": DFontVariantLigatures,
"font-variant-numeric": DFontVariantNumeric,
"font-variant-position": DFontVariantPosition,
"font-weight": DFontWeight,
"gap": DGap,
"glyph-orientation-vertical": DGlyphOrientationVertical,
"grid": DGrid,
"grid-area": DGridArea,
"grid-auto-columns": DGridAutoColumns,
"grid-auto-flow": DGridAutoFlow,
"grid-auto-rows": DGridAutoRows,
"grid-column": DGridColumn,
"grid-column-end": DGridColumnEnd,
"grid-column-gap": DGridColumnGap,
"grid-column-start": DGridColumnStart,
"grid-gap": DGridGap,
"grid-row": DGridRow,
"grid-row-end": DGridRowEnd,
"grid-row-gap": DGridRowGap,
"grid-row-start": DGridRowStart,
"grid-template": DGridTemplate,
"grid-template-areas": DGridTemplateAreas,
"grid-template-columns": DGridTemplateColumns,
"grid-template-rows": DGridTemplateRows,
"height": DHeight,
"hyphens": DHyphens,
"image-orientation": DImageOrientation,
"image-rendering": DImageRendering,
"inline-size": DInlineSize,
"inset": DInset,
"justify-content": DJustifyContent,
"justify-items": DJustifyItems,
"justify-self": DJustifySelf,
"left": DLeft,
"letter-spacing": DLetterSpacing,
"lighting-color": DLightingColor,
"line-break": DLineBreak,
"line-height": DLineHeight,
"list-style": DListStyle,
"list-style-image": DListStyleImage,
"list-style-position": DListStylePosition,
"list-style-type": DListStyleType,
"margin": DMargin,
"margin-block-end": DMarginBlockEnd,
"margin-block-start": DMarginBlockStart,
"margin-bottom": DMarginBottom,
"margin-inline-end": DMarginInlineEnd,
"margin-inline-start": DMarginInlineStart,
"margin-left": DMarginLeft,
"margin-right": DMarginRight,
"margin-top": DMarginTop,
"marker": DMarker,
"marker-end": DMarkerEnd,
"marker-mid": DMarkerMid,
"marker-start": DMarkerStart,
"mask": DMask,
"mask-composite": DMaskComposite,
"mask-image": DMaskImage,
"mask-position": DMaskPosition,
"mask-repeat": DMaskRepeat,
"mask-size": DMaskSize,
"mask-type": DMaskType,
"max-block-size": DMaxBlockSize,
"max-height": DMaxHeight,
"max-inline-size": DMaxInlineSize,
"max-width": DMaxWidth,
"min-block-size": DMinBlockSize,
"min-height": DMinHeight,
"min-inline-size": DMinInlineSize,
"min-width": DMinWidth,
"object-fit": DObjectFit,
"object-position": DObjectPosition,
"opacity": DOpacity,
"order": DOrder,
"orphans": DOrphans,
"outline": DOutline,
"outline-color": DOutlineColor,
"outline-offset": DOutlineOffset,
"outline-style": DOutlineStyle,
"outline-width": DOutlineWidth,
"overflow": DOverflow,
"overflow-anchor": DOverflowAnchor,
"overflow-wrap": DOverflowWrap,
"overflow-x": DOverflowX,
"overflow-y": DOverflowY,
"overscroll-behavior": DOverscrollBehavior,
"overscroll-behavior-block": DOverscrollBehaviorBlock,
"overscroll-behavior-inline": DOverscrollBehaviorInline,
"overscroll-behavior-x": DOverscrollBehaviorX,
"overscroll-behavior-y": DOverscrollBehaviorY,
"padding": DPadding,
"padding-block-end": DPaddingBlockEnd,
"padding-block-start": DPaddingBlockStart,
"padding-bottom": DPaddingBottom,
"padding-inline-end": DPaddingInlineEnd,
"padding-inline-start": DPaddingInlineStart,
"padding-left": DPaddingLeft,
"padding-right": DPaddingRight,
"padding-top": DPaddingTop,
"page-break-after": DPageBreakAfter,
"page-break-before": DPageBreakBefore,
"page-break-inside": DPageBreakInside,
"paint-order": DPaintOrder,
"perspective": DPerspective,
"perspective-origin": DPerspectiveOrigin,
"place-content": DPlaceContent,
"place-items": DPlaceItems,
"place-self": DPlaceSelf,
"pointer-events": DPointerEvents,
"position": DPosition,
"quotes": DQuotes,
"resize": DResize,
"right": DRight,
"rotate": DRotate,
"row-gap": DRowGap,
"ruby-align": DRubyAlign,
"ruby-position": DRubyPosition,
"scale": DScale,
"scroll-behavior": DScrollBehavior,
"shape-rendering": DShapeRendering,
"stop-color": DStopColor,
"stop-opacity": DStopOpacity,
"stroke": DStroke,
"stroke-dasharray": DStrokeDasharray,
"stroke-dashoffset": DStrokeDashoffset,
"stroke-linecap": DStrokeLinecap,
"stroke-linejoin": DStrokeLinejoin,
"stroke-miterlimit": DStrokeMiterlimit,
"stroke-opacity": DStrokeOpacity,
"stroke-width": DStrokeWidth,
"tab-size": DTabSize,
"table-layout": DTableLayout,
"text-align": DTextAlign,
"text-align-last": DTextAlignLast,
"text-anchor": DTextAnchor,
"text-combine-upright": DTextCombineUpright,
"text-decoration": DTextDecoration,
"text-decoration-color": DTextDecorationColor,
"text-decoration-line": DTextDecorationLine,
"text-decoration-style": DTextDecorationStyle,
"text-emphasis": DTextEmphasis,
"text-emphasis-color": DTextEmphasisColor,
"text-emphasis-position": DTextEmphasisPosition,
"text-emphasis-style": DTextEmphasisStyle,
"text-indent": DTextIndent,
"text-justify": DTextJustify,
"text-orientation": DTextOrientation,
"text-overflow": DTextOverflow,
"text-rendering": DTextRendering,
"text-shadow": DTextShadow,
"text-transform": DTextTransform,
"text-underline-position": DTextUnderlinePosition,
"top": DTop,
"touch-action": DTouchAction,
"transform": DTransform,
"transform-box": DTransformBox,
"transform-origin": DTransformOrigin,
"transform-style": DTransformStyle,
"transition": DTransition,
"transition-delay": DTransitionDelay,
"transition-duration": DTransitionDuration,
"transition-property": DTransitionProperty,
"transition-timing-function": DTransitionTimingFunction,
"translate": DTranslate,
"unicode-bidi": DUnicodeBidi,
"user-select": DUserSelect,
"vertical-align": DVerticalAlign,
"visibility": DVisibility,
"white-space": DWhiteSpace,
"widows": DWidows,
"width": DWidth,
"will-change": DWillChange,
"word-break": DWordBreak,
"word-spacing": DWordSpacing,
"word-wrap": DWordWrap,
"writing-mode": DWritingMode,
"z-index": DZIndex,
"zoom": DZoom,
}

View File

@ -1,969 +0,0 @@
package css_lexer
import (
"strings"
"unicode/utf8"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
)
// The lexer converts a source file to a stream of tokens. Unlike esbuild's
// JavaScript lexer, this CSS lexer runs to completion before the CSS parser
// begins, resulting in a single array of all tokens in the file.
type T uint8
const eof = -1
const (
TEndOfFile T = iota
TAtKeyword
TBadString
TBadURL
TCDC // "-->"
TCDO // "<!--"
TCloseBrace
TCloseBracket
TCloseParen
TColon
TComma
TDelim
TDelimAmpersand
TDelimAsterisk
TDelimBar
TDelimCaret
TDelimDollar
TDelimDot
TDelimEquals
TDelimExclamation
TDelimGreaterThan
TDelimMinus
TDelimPlus
TDelimSlash
TDelimTilde
TDimension
TFunction
THash
TIdent
TNumber
TOpenBrace
TOpenBracket
TOpenParen
TPercentage
TSemicolon
TString
TURL
TWhitespace
)
var tokenToString = []string{
"end of file",
"@-keyword",
"bad string token",
"bad URL token",
"\"-->\"",
"\"<!--\"",
"\"}\"",
"\"]\"",
"\")\"",
"\":\"",
"\",\"",
"delimiter",
"\"&\"",
"\"*\"",
"\"|\"",
"\"^\"",
"\"$\"",
"\".\"",
"\"=\"",
"\"!\"",
"\">\"",
"\"-\"",
"\"+\"",
"\"/\"",
"\"~\"",
"dimension",
"function token",
"hash token",
"identifier",
"number",
"\"{\"",
"\"[\"",
"\"(\"",
"percentage",
"\";\"",
"string token",
"URL token",
"whitespace",
}
func (t T) String() string {
return tokenToString[t]
}
func (t T) IsNumeric() bool {
return t == TNumber || t == TPercentage || t == TDimension
}
// This token struct is designed to be memory-efficient. It just references a
// range in the input file instead of directly containing the substring of text
// since a range takes up less memory than a string.
type Token struct {
Range logger.Range // 8 bytes
UnitOffset uint16 // 2 bytes
Kind T // 1 byte
IsID bool // 1 byte
}
func (token Token) DecodedText(contents string) string {
raw := contents[token.Range.Loc.Start:token.Range.End()]
switch token.Kind {
case TIdent, TDimension:
return decodeEscapesInToken(raw)
case TAtKeyword, THash:
return decodeEscapesInToken(raw[1:])
case TFunction:
return decodeEscapesInToken(raw[:len(raw)-1])
case TString:
return decodeEscapesInToken(raw[1 : len(raw)-1])
case TURL:
start := 4
end := len(raw) - 1
// Trim leading and trailing whitespace
for start < end && isWhitespace(rune(raw[start])) {
start++
}
for start < end && isWhitespace(rune(raw[end-1])) {
end--
}
return decodeEscapesInToken(raw[start:end])
}
return raw
}
type lexer struct {
log logger.Log
source logger.Source
tracker logger.LineColumnTracker
current int
codePoint rune
Token Token
legalCommentsBefore []Comment
approximateNewlineCount int
sourceMappingURL logger.Span
}
type Comment struct {
Text string
Loc logger.Loc
TokenIndexAfter uint32
}
type TokenizeResult struct {
Tokens []Token
LegalComments []Comment
ApproximateLineCount int32
SourceMapComment logger.Span
}
func Tokenize(log logger.Log, source logger.Source) TokenizeResult {
lexer := lexer{
log: log,
source: source,
tracker: logger.MakeLineColumnTracker(&source),
}
lexer.step()
// The U+FEFF character is usually a zero-width non-breaking space. However,
// when it's used at the start of a text stream it is called a BOM (byte order
// mark) instead and indicates that the text stream is UTF-8 encoded. This is
// problematic for us because CSS does not treat U+FEFF as whitespace. Only
// " \t\r\n\f" characters are treated as whitespace. Skip over the BOM if it
// is present so it doesn't cause us trouble when we try to parse it.
if lexer.codePoint == '\uFEFF' {
lexer.step()
}
lexer.next()
var tokens []Token
var comments []Comment
for lexer.Token.Kind != TEndOfFile {
if lexer.legalCommentsBefore != nil {
for _, comment := range lexer.legalCommentsBefore {
comment.TokenIndexAfter = uint32(len(tokens))
comments = append(comments, comment)
}
lexer.legalCommentsBefore = nil
}
tokens = append(tokens, lexer.Token)
lexer.next()
}
if lexer.legalCommentsBefore != nil {
for _, comment := range lexer.legalCommentsBefore {
comment.TokenIndexAfter = uint32(len(tokens))
comments = append(comments, comment)
}
lexer.legalCommentsBefore = nil
}
return TokenizeResult{
Tokens: tokens,
LegalComments: comments,
ApproximateLineCount: int32(lexer.approximateNewlineCount) + 1,
SourceMapComment: lexer.sourceMappingURL,
}
}
func (lexer *lexer) step() {
codePoint, width := utf8.DecodeRuneInString(lexer.source.Contents[lexer.current:])
// Use -1 to indicate the end of the file
if width == 0 {
codePoint = eof
}
// Track the approximate number of newlines in the file so we can preallocate
// the line offset table in the printer for source maps. The line offset table
// is the #1 highest allocation in the heap profile, so this is worth doing.
// This count is approximate because it handles "\n" and "\r\n" (the common
// cases) but not "\r" or "\u2028" or "\u2029". Getting this wrong is harmless
// because it's only a preallocation. The array will just grow if it's too small.
if codePoint == '\n' {
lexer.approximateNewlineCount++
}
lexer.codePoint = codePoint
lexer.Token.Range.Len = int32(lexer.current) - lexer.Token.Range.Loc.Start
lexer.current += width
}
func (lexer *lexer) next() {
// Reference: https://www.w3.org/TR/css-syntax-3/
for {
lexer.Token = Token{Range: logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}}}
switch lexer.codePoint {
case eof:
lexer.Token.Kind = TEndOfFile
case '/':
lexer.step()
switch lexer.codePoint {
case '*':
lexer.step()
lexer.consumeToEndOfMultiLineComment(lexer.Token.Range)
continue
case '/':
lexer.step()
lexer.consumeToEndOfSingleLineComment()
continue
}
lexer.Token.Kind = TDelimSlash
case ' ', '\t', '\n', '\r', '\f':
lexer.step()
for {
if isWhitespace(lexer.codePoint) {
lexer.step()
} else if lexer.codePoint == '/' && lexer.current < len(lexer.source.Contents) && lexer.source.Contents[lexer.current] == '*' {
startRange := logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}, Len: 2}
lexer.step()
lexer.step()
lexer.consumeToEndOfMultiLineComment(startRange)
} else {
break
}
}
lexer.Token.Kind = TWhitespace
case '"', '\'':
lexer.Token.Kind = lexer.consumeString()
case '#':
lexer.step()
if IsNameContinue(lexer.codePoint) || lexer.isValidEscape() {
lexer.Token.Kind = THash
if lexer.wouldStartIdentifier() {
lexer.Token.IsID = true
}
lexer.consumeName()
} else {
lexer.Token.Kind = TDelim
}
case '(':
lexer.step()
lexer.Token.Kind = TOpenParen
case ')':
lexer.step()
lexer.Token.Kind = TCloseParen
case '[':
lexer.step()
lexer.Token.Kind = TOpenBracket
case ']':
lexer.step()
lexer.Token.Kind = TCloseBracket
case '{':
lexer.step()
lexer.Token.Kind = TOpenBrace
case '}':
lexer.step()
lexer.Token.Kind = TCloseBrace
case ',':
lexer.step()
lexer.Token.Kind = TComma
case ':':
lexer.step()
lexer.Token.Kind = TColon
case ';':
lexer.step()
lexer.Token.Kind = TSemicolon
case '+':
if lexer.wouldStartNumber() {
lexer.Token.Kind = lexer.consumeNumeric()
} else {
lexer.step()
lexer.Token.Kind = TDelimPlus
}
case '.':
if lexer.wouldStartNumber() {
lexer.Token.Kind = lexer.consumeNumeric()
} else {
lexer.step()
lexer.Token.Kind = TDelimDot
}
case '-':
if lexer.wouldStartNumber() {
lexer.Token.Kind = lexer.consumeNumeric()
} else if lexer.current+2 <= len(lexer.source.Contents) && lexer.source.Contents[lexer.current:lexer.current+2] == "->" {
lexer.step()
lexer.step()
lexer.step()
lexer.Token.Kind = TCDC
} else if lexer.wouldStartIdentifier() {
lexer.Token.Kind = lexer.consumeIdentLike()
} else {
lexer.step()
lexer.Token.Kind = TDelimMinus
}
case '<':
if lexer.current+3 <= len(lexer.source.Contents) && lexer.source.Contents[lexer.current:lexer.current+3] == "!--" {
lexer.step()
lexer.step()
lexer.step()
lexer.step()
lexer.Token.Kind = TCDO
} else {
lexer.step()
lexer.Token.Kind = TDelim
}
case '@':
lexer.step()
if lexer.wouldStartIdentifier() {
lexer.consumeName()
lexer.Token.Kind = TAtKeyword
} else {
lexer.Token.Kind = TDelim
}
case '\\':
if lexer.isValidEscape() {
lexer.Token.Kind = lexer.consumeIdentLike()
} else {
lexer.step()
lexer.log.Add(logger.Error, &lexer.tracker, lexer.Token.Range, "Invalid escape")
lexer.Token.Kind = TDelim
}
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
lexer.Token.Kind = lexer.consumeNumeric()
case '>':
lexer.step()
lexer.Token.Kind = TDelimGreaterThan
case '~':
lexer.step()
lexer.Token.Kind = TDelimTilde
case '&':
lexer.step()
lexer.Token.Kind = TDelimAmpersand
case '*':
lexer.step()
lexer.Token.Kind = TDelimAsterisk
case '|':
lexer.step()
lexer.Token.Kind = TDelimBar
case '!':
lexer.step()
lexer.Token.Kind = TDelimExclamation
case '=':
lexer.step()
lexer.Token.Kind = TDelimEquals
case '^':
lexer.step()
lexer.Token.Kind = TDelimCaret
case '$':
lexer.step()
lexer.Token.Kind = TDelimDollar
default:
if IsNameStart(lexer.codePoint) {
lexer.Token.Kind = lexer.consumeIdentLike()
} else {
lexer.step()
lexer.Token.Kind = TDelim
}
}
return
}
}
func (lexer *lexer) consumeToEndOfMultiLineComment(startRange logger.Range) {
startOfSourceMappingURL := 0
isLegalComment := false
switch lexer.codePoint {
case '#', '@':
// Keep track of the contents of the "sourceMappingURL=" comment
if strings.HasPrefix(lexer.source.Contents[lexer.current:], " sourceMappingURL=") {
startOfSourceMappingURL = lexer.current + len(" sourceMappingURL=")
}
case '!':
// Remember if this is a legal comment
isLegalComment = true
}
for {
switch lexer.codePoint {
case '*':
endOfSourceMappingURL := lexer.current - 1
lexer.step()
if lexer.codePoint == '/' {
commentEnd := lexer.current
lexer.step()
// Record the source mapping URL
if startOfSourceMappingURL != 0 {
r := logger.Range{Loc: logger.Loc{Start: int32(startOfSourceMappingURL)}}
text := lexer.source.Contents[startOfSourceMappingURL:endOfSourceMappingURL]
for int(r.Len) < len(text) && !isWhitespace(rune(text[r.Len])) {
r.Len++
}
lexer.sourceMappingURL = logger.Span{Text: text[:r.Len], Range: r}
}
// Record legal comments
if text := lexer.source.Contents[startRange.Loc.Start:commentEnd]; isLegalComment || containsAtPreserveOrAtLicense(text) {
text = helpers.RemoveMultiLineCommentIndent(lexer.source.Contents[:startRange.Loc.Start], text)
lexer.legalCommentsBefore = append(lexer.legalCommentsBefore, Comment{Loc: startRange.Loc, Text: text})
}
return
}
case eof: // This indicates the end of the file
lexer.log.AddWithNotes(logger.Error, &lexer.tracker, logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}},
"Expected \"*/\" to terminate multi-line comment",
[]logger.MsgData{lexer.tracker.MsgData(startRange, "The multi-line comment starts here:")})
return
default:
lexer.step()
}
}
}
func containsAtPreserveOrAtLicense(text string) bool {
for i, c := range text {
if c == '@' && (strings.HasPrefix(text[i+1:], "preserve") || strings.HasPrefix(text[i+1:], "license")) {
return true
}
}
return false
}
func (lexer *lexer) consumeToEndOfSingleLineComment() {
for !isNewline(lexer.codePoint) && lexer.codePoint != eof {
lexer.step()
}
lexer.log.Add(logger.Warning, &lexer.tracker, lexer.Token.Range, "Comments in CSS use \"/* ... */\" instead of \"//\"")
}
func (lexer *lexer) isValidEscape() bool {
if lexer.codePoint != '\\' {
return false
}
c, _ := utf8.DecodeRuneInString(lexer.source.Contents[lexer.current:])
return !isNewline(c)
}
func (lexer *lexer) wouldStartIdentifier() bool {
if IsNameStart(lexer.codePoint) {
return true
}
if lexer.codePoint == '-' {
c, width := utf8.DecodeRuneInString(lexer.source.Contents[lexer.current:])
if c == utf8.RuneError && width <= 1 {
return false // Decoding error
}
if IsNameStart(c) || c == '-' {
return true
}
if c == '\\' {
c2, _ := utf8.DecodeRuneInString(lexer.source.Contents[lexer.current+width:])
return !isNewline(c2)
}
return false
}
return lexer.isValidEscape()
}
func WouldStartIdentifierWithoutEscapes(text string) bool {
c, width := utf8.DecodeRuneInString(text)
if c == utf8.RuneError && width <= 1 {
return false // Decoding error
}
if IsNameStart(c) {
return true
}
if c == '-' {
c2, width2 := utf8.DecodeRuneInString(text[width:])
if c2 == utf8.RuneError && width2 <= 1 {
return false // Decoding error
}
if IsNameStart(c2) || c2 == '-' {
return true
}
}
return false
}
func (lexer *lexer) wouldStartNumber() bool {
if lexer.codePoint >= '0' && lexer.codePoint <= '9' {
return true
} else if lexer.codePoint == '.' {
contents := lexer.source.Contents
if lexer.current < len(contents) {
c := contents[lexer.current]
return c >= '0' && c <= '9'
}
} else if lexer.codePoint == '+' || lexer.codePoint == '-' {
contents := lexer.source.Contents
n := len(contents)
if lexer.current < n {
c := contents[lexer.current]
if c >= '0' && c <= '9' {
return true
}
if c == '.' && lexer.current+1 < n {
c = contents[lexer.current+1]
return c >= '0' && c <= '9'
}
}
}
return false
}
func (lexer *lexer) consumeName() string {
// Common case: no escapes, identifier is a substring of the input
for IsNameContinue(lexer.codePoint) {
lexer.step()
}
raw := lexer.source.Contents[lexer.Token.Range.Loc.Start:lexer.Token.Range.End()]
if !lexer.isValidEscape() {
return raw
}
// Uncommon case: escapes, identifier is allocated
sb := strings.Builder{}
sb.WriteString(raw)
sb.WriteRune(lexer.consumeEscape())
for {
if IsNameContinue(lexer.codePoint) {
sb.WriteRune(lexer.codePoint)
lexer.step()
} else if lexer.isValidEscape() {
sb.WriteRune(lexer.consumeEscape())
} else {
break
}
}
return sb.String()
}
func (lexer *lexer) consumeEscape() rune {
lexer.step() // Skip the backslash
c := lexer.codePoint
if hex, ok := isHex(c); ok {
lexer.step()
for i := 0; i < 5; i++ {
if next, ok := isHex(lexer.codePoint); ok {
lexer.step()
hex = hex*16 + next
} else {
break
}
}
if isWhitespace(lexer.codePoint) {
lexer.step()
}
if hex == 0 || (hex >= 0xD800 && hex <= 0xDFFF) || hex > 0x10FFFF {
return utf8.RuneError
}
return rune(hex)
}
if c == eof {
return utf8.RuneError
}
lexer.step()
return c
}
func (lexer *lexer) consumeIdentLike() T {
name := lexer.consumeName()
if lexer.codePoint == '(' {
lexer.step()
if len(name) == 3 {
u, r, l := name[0], name[1], name[2]
if (u == 'u' || u == 'U') && (r == 'r' || r == 'R') && (l == 'l' || l == 'L') {
for isWhitespace(lexer.codePoint) {
lexer.step()
}
if lexer.codePoint != '"' && lexer.codePoint != '\'' {
return lexer.consumeURL()
}
}
}
return TFunction
}
return TIdent
}
func (lexer *lexer) consumeURL() T {
validURL:
for {
switch lexer.codePoint {
case ')':
lexer.step()
return TURL
case eof:
loc := logger.Loc{Start: lexer.Token.Range.End()}
lexer.log.Add(logger.Error, &lexer.tracker, logger.Range{Loc: loc}, "Expected \")\" to end URL token")
return TBadURL
case ' ', '\t', '\n', '\r', '\f':
lexer.step()
for isWhitespace(lexer.codePoint) {
lexer.step()
}
if lexer.codePoint != ')' {
loc := logger.Loc{Start: lexer.Token.Range.End()}
lexer.log.Add(logger.Error, &lexer.tracker, logger.Range{Loc: loc}, "Expected \")\" to end URL token")
break validURL
}
lexer.step()
return TURL
case '"', '\'', '(':
r := logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}, Len: 1}
lexer.log.Add(logger.Error, &lexer.tracker, r, "Expected \")\" to end URL token")
break validURL
case '\\':
if !lexer.isValidEscape() {
r := logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}, Len: 1}
lexer.log.Add(logger.Error, &lexer.tracker, r, "Invalid escape")
break validURL
}
lexer.consumeEscape()
default:
if isNonPrintable(lexer.codePoint) {
r := logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}, Len: 1}
lexer.log.Add(logger.Error, &lexer.tracker, r, "Unexpected non-printable character in URL token")
}
lexer.step()
}
}
// Consume the remnants of a bad url
for {
switch lexer.codePoint {
case ')', eof:
lexer.step()
return TBadURL
case '\\':
if lexer.isValidEscape() {
lexer.consumeEscape()
}
}
lexer.step()
}
}
func (lexer *lexer) consumeString() T {
quote := lexer.codePoint
lexer.step()
for {
switch lexer.codePoint {
case '\\':
lexer.step()
// Handle Windows CRLF
if lexer.codePoint == '\r' {
lexer.step()
if lexer.codePoint == '\n' {
lexer.step()
}
continue
}
// Otherwise, fall through to ignore the character after the backslash
case eof:
lexer.log.Add(logger.Error, &lexer.tracker,
logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}},
"Unterminated string token")
return TBadString
case '\n', '\r', '\f':
lexer.log.Add(logger.Error, &lexer.tracker,
logger.Range{Loc: logger.Loc{Start: lexer.Token.Range.End()}},
"Unterminated string token")
return TBadString
case quote:
lexer.step()
return TString
}
lexer.step()
}
}
func (lexer *lexer) consumeNumeric() T {
// Skip over leading sign
if lexer.codePoint == '+' || lexer.codePoint == '-' {
lexer.step()
}
// Skip over leading digits
for lexer.codePoint >= '0' && lexer.codePoint <= '9' {
lexer.step()
}
// Skip over digits after dot
if lexer.codePoint == '.' {
lexer.step()
for lexer.codePoint >= '0' && lexer.codePoint <= '9' {
lexer.step()
}
}
// Skip over exponent
if lexer.codePoint == 'e' || lexer.codePoint == 'E' {
contents := lexer.source.Contents
// Look ahead before advancing to make sure this is an exponent, not a unit
if lexer.current < len(contents) {
c := contents[lexer.current]
if (c == '+' || c == '-') && lexer.current+1 < len(contents) {
c = contents[lexer.current+1]
}
// Only consume this if it's an exponent
if c >= '0' && c <= '9' {
lexer.step()
if lexer.codePoint == '+' || lexer.codePoint == '-' {
lexer.step()
}
for lexer.codePoint >= '0' && lexer.codePoint <= '9' {
lexer.step()
}
}
}
}
// Determine the numeric type
if lexer.wouldStartIdentifier() {
lexer.Token.UnitOffset = uint16(lexer.Token.Range.Len)
lexer.consumeName()
return TDimension
}
if lexer.codePoint == '%' {
lexer.step()
return TPercentage
}
return TNumber
}
func IsNameStart(c rune) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c >= 0x80 || c == '\x00'
}
func IsNameContinue(c rune) bool {
return IsNameStart(c) || (c >= '0' && c <= '9') || c == '-'
}
func isNewline(c rune) bool {
switch c {
case '\n', '\r', '\f':
return true
}
return false
}
func isWhitespace(c rune) bool {
switch c {
case ' ', '\t', '\n', '\r', '\f':
return true
}
return false
}
func isHex(c rune) (int, bool) {
if c >= '0' && c <= '9' {
return int(c - '0'), true
}
if c >= 'a' && c <= 'f' {
return int(c + (10 - 'a')), true
}
if c >= 'A' && c <= 'F' {
return int(c + (10 - 'A')), true
}
return 0, false
}
func isNonPrintable(c rune) bool {
return c <= 0x08 || c == 0x0B || (c >= 0x0E && c <= 0x1F) || c == 0x7F
}
func decodeEscapesInToken(inner string) string {
i := 0
for i < len(inner) {
if c := inner[i]; c == '\\' || c == '\x00' {
break
}
i++
}
if i == len(inner) {
return inner
}
sb := strings.Builder{}
sb.WriteString(inner[:i])
inner = inner[i:]
for len(inner) > 0 {
c, width := utf8.DecodeRuneInString(inner)
inner = inner[width:]
if c != '\\' {
if c == '\x00' {
c = utf8.RuneError
}
sb.WriteRune(c)
continue
}
if len(inner) == 0 {
sb.WriteRune(utf8.RuneError)
continue
}
c, width = utf8.DecodeRuneInString(inner)
inner = inner[width:]
hex, ok := isHex(c)
if !ok {
if c == '\n' || c == '\f' {
continue
}
// Handle Windows CRLF
if c == '\r' {
c, width = utf8.DecodeRuneInString(inner)
if c == '\n' {
inner = inner[width:]
}
continue
}
// If we get here, this is not a valid escape. However, this is still
// allowed. In this case the backslash is just ignored.
sb.WriteRune(c)
continue
}
// Parse up to five additional hex characters (so six in total)
for i := 0; i < 5 && len(inner) > 0; i++ {
c, width = utf8.DecodeRuneInString(inner)
if next, ok := isHex(c); ok {
inner = inner[width:]
hex = hex*16 + next
} else {
break
}
}
if len(inner) > 0 {
c, width = utf8.DecodeRuneInString(inner)
if isWhitespace(c) {
inner = inner[width:]
}
}
if hex == 0 || (hex >= 0xD800 && hex <= 0xDFFF) || hex > 0x10FFFF {
sb.WriteRune(utf8.RuneError)
continue
}
sb.WriteRune(rune(hex))
}
return sb.String()
}

View File

@ -1,256 +0,0 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) commaToken() css_ast.Token {
t := css_ast.Token{
Kind: css_lexer.TComma,
Text: ",",
}
if !p.options.RemoveWhitespace {
t.Whitespace = css_ast.WhitespaceAfter
}
return t
}
func expandTokenQuad(tokens []css_ast.Token, allowedIdent string) (result [4]css_ast.Token, ok bool) {
n := len(tokens)
if n < 1 || n > 4 {
return
}
// Don't do this if we encounter any unexpected tokens such as "var()"
for i := 0; i < n; i++ {
if t := tokens[i]; !t.Kind.IsNumeric() && (t.Kind != css_lexer.TIdent || allowedIdent == "" || t.Text != allowedIdent) {
return
}
}
result[0] = tokens[0]
if n > 1 {
result[1] = tokens[1]
} else {
result[1] = result[0]
}
if n > 2 {
result[2] = tokens[2]
} else {
result[2] = result[0]
}
if n > 3 {
result[3] = tokens[3]
} else {
result[3] = result[1]
}
ok = true
return
}
func compactTokenQuad(a css_ast.Token, b css_ast.Token, c css_ast.Token, d css_ast.Token, removeWhitespace bool) []css_ast.Token {
tokens := []css_ast.Token{a, b, c, d}
if tokens[3].EqualIgnoringWhitespace(tokens[1]) {
if tokens[2].EqualIgnoringWhitespace(tokens[0]) {
if tokens[1].EqualIgnoringWhitespace(tokens[0]) {
tokens = tokens[:1]
} else {
tokens = tokens[:2]
}
} else {
tokens = tokens[:3]
}
}
for i := range tokens {
var whitespace css_ast.WhitespaceFlags
if !removeWhitespace || i > 0 {
whitespace |= css_ast.WhitespaceBefore
}
if i+1 < len(tokens) {
whitespace |= css_ast.WhitespaceAfter
}
tokens[i].Whitespace = whitespace
}
return tokens
}
func (p *parser) processDeclarations(rules []css_ast.Rule) []css_ast.Rule {
margin := boxTracker{key: css_ast.DMargin, keyText: "margin", allowAuto: true}
padding := boxTracker{key: css_ast.DPadding, keyText: "padding", allowAuto: false}
inset := boxTracker{key: css_ast.DInset, keyText: "inset", allowAuto: true}
borderRadius := borderRadiusTracker{}
for i, rule := range rules {
decl, ok := rule.Data.(*css_ast.RDeclaration)
if !ok {
continue
}
switch decl.Key {
case css_ast.DBackgroundColor,
css_ast.DBorderBlockEndColor,
css_ast.DBorderBlockStartColor,
css_ast.DBorderBottomColor,
css_ast.DBorderColor,
css_ast.DBorderInlineEndColor,
css_ast.DBorderInlineStartColor,
css_ast.DBorderLeftColor,
css_ast.DBorderRightColor,
css_ast.DBorderTopColor,
css_ast.DCaretColor,
css_ast.DColor,
css_ast.DColumnRuleColor,
css_ast.DFill,
css_ast.DFloodColor,
css_ast.DLightingColor,
css_ast.DOutlineColor,
css_ast.DStopColor,
css_ast.DStroke,
css_ast.DTextDecorationColor,
css_ast.DTextEmphasisColor:
if len(decl.Value) == 1 {
decl.Value[0] = p.lowerColor(decl.Value[0])
if p.options.MangleSyntax {
t := decl.Value[0]
if hex, ok := parseColor(t); ok {
decl.Value[0] = p.mangleColor(t, hex)
}
}
}
case css_ast.DFont:
if p.options.MangleSyntax {
decl.Value = p.mangleFont(decl.Value)
}
case css_ast.DFontFamily:
if p.options.MangleSyntax {
if value, ok := p.mangleFontFamily(decl.Value); ok {
decl.Value = value
}
}
case css_ast.DFontWeight:
if len(decl.Value) == 1 && p.options.MangleSyntax {
decl.Value[0] = p.mangleFontWeight(decl.Value[0])
}
case css_ast.DTransform:
if p.options.MangleSyntax {
decl.Value = p.mangleTransforms(decl.Value)
}
case css_ast.DBoxShadow:
if p.options.MangleSyntax {
decl.Value = p.mangleBoxShadows(decl.Value)
}
// Margin
case css_ast.DMargin:
if p.options.MangleSyntax {
margin.mangleSides(rules, decl, i, p.options.RemoveWhitespace)
}
case css_ast.DMarginTop:
if p.options.MangleSyntax {
margin.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxTop)
}
case css_ast.DMarginRight:
if p.options.MangleSyntax {
margin.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxRight)
}
case css_ast.DMarginBottom:
if p.options.MangleSyntax {
margin.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxBottom)
}
case css_ast.DMarginLeft:
if p.options.MangleSyntax {
margin.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxLeft)
}
// Padding
case css_ast.DPadding:
if p.options.MangleSyntax {
padding.mangleSides(rules, decl, i, p.options.RemoveWhitespace)
}
case css_ast.DPaddingTop:
if p.options.MangleSyntax {
padding.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxTop)
}
case css_ast.DPaddingRight:
if p.options.MangleSyntax {
padding.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxRight)
}
case css_ast.DPaddingBottom:
if p.options.MangleSyntax {
padding.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxBottom)
}
case css_ast.DPaddingLeft:
if p.options.MangleSyntax {
padding.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxLeft)
}
// Inset
case css_ast.DInset:
if !p.options.UnsupportedCSSFeatures.Has(compat.InsetProperty) && p.options.MangleSyntax {
inset.mangleSides(rules, decl, i, p.options.RemoveWhitespace)
}
case css_ast.DTop:
if !p.options.UnsupportedCSSFeatures.Has(compat.InsetProperty) && p.options.MangleSyntax {
inset.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxTop)
}
case css_ast.DRight:
if !p.options.UnsupportedCSSFeatures.Has(compat.InsetProperty) && p.options.MangleSyntax {
inset.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxRight)
}
case css_ast.DBottom:
if !p.options.UnsupportedCSSFeatures.Has(compat.InsetProperty) && p.options.MangleSyntax {
inset.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxBottom)
}
case css_ast.DLeft:
if !p.options.UnsupportedCSSFeatures.Has(compat.InsetProperty) && p.options.MangleSyntax {
inset.mangleSide(rules, decl, i, p.options.RemoveWhitespace, boxLeft)
}
// Border radius
case css_ast.DBorderRadius:
if p.options.MangleSyntax {
borderRadius.mangleCorners(rules, decl, i, p.options.RemoveWhitespace)
}
case css_ast.DBorderTopLeftRadius:
if p.options.MangleSyntax {
borderRadius.mangleCorner(rules, decl, i, p.options.RemoveWhitespace, borderRadiusTopLeft)
}
case css_ast.DBorderTopRightRadius:
if p.options.MangleSyntax {
borderRadius.mangleCorner(rules, decl, i, p.options.RemoveWhitespace, borderRadiusTopRight)
}
case css_ast.DBorderBottomRightRadius:
if p.options.MangleSyntax {
borderRadius.mangleCorner(rules, decl, i, p.options.RemoveWhitespace, borderRadiusBottomRight)
}
case css_ast.DBorderBottomLeftRadius:
if p.options.MangleSyntax {
borderRadius.mangleCorner(rules, decl, i, p.options.RemoveWhitespace, borderRadiusBottomLeft)
}
}
}
// Compact removed rules
if p.options.MangleSyntax {
end := 0
for _, rule := range rules {
if rule.Data != nil {
rules[end] = rule
end++
}
}
rules = rules[:end]
}
return rules
}

View File

@ -1,213 +0,0 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
const (
borderRadiusTopLeft = iota
borderRadiusTopRight
borderRadiusBottomRight
borderRadiusBottomLeft
)
type borderRadiusCorner struct {
firstToken css_ast.Token
secondToken css_ast.Token
unitSafety unitSafetyTracker
ruleIndex uint32 // The index of the originating rule in the rules array
wasSingleRule bool // True if the originating rule was just for this side
}
type borderRadiusTracker struct {
corners [4]borderRadiusCorner
important bool // True if all active rules were flagged as "!important"
}
func (borderRadius *borderRadiusTracker) updateCorner(rules []css_ast.Rule, corner int, new borderRadiusCorner) {
if old := borderRadius.corners[corner]; old.firstToken.Kind != css_lexer.TEndOfFile &&
(!new.wasSingleRule || old.wasSingleRule) &&
old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
rules[old.ruleIndex] = css_ast.Rule{}
}
borderRadius.corners[corner] = new
}
func (borderRadius *borderRadiusTracker) mangleCorners(rules []css_ast.Rule, decl *css_ast.RDeclaration, index int, removeWhitespace bool) {
// Reset if we see a change in the "!important" flag
if borderRadius.important != decl.Important {
borderRadius.corners = [4]borderRadiusCorner{}
borderRadius.important = decl.Important
}
tokens := decl.Value
beforeSplit := len(tokens)
afterSplit := len(tokens)
// Search for the single slash if present
for i, t := range tokens {
if t.Kind == css_lexer.TDelimSlash {
if beforeSplit == len(tokens) {
beforeSplit = i
afterSplit = i + 1
} else {
// Multiple slashes are an error
borderRadius.corners = [4]borderRadiusCorner{}
return
}
}
}
// Use a single tracker for the whole rule
unitSafety := unitSafetyTracker{}
for _, t := range tokens[:beforeSplit] {
unitSafety.includeUnitOf(t)
}
for _, t := range tokens[afterSplit:] {
unitSafety.includeUnitOf(t)
}
firstRadii, firstRadiiOk := expandTokenQuad(tokens[:beforeSplit], "")
lastRadii, lastRadiiOk := expandTokenQuad(tokens[afterSplit:], "")
// Stop now if the pattern wasn't matched
if !firstRadiiOk || (beforeSplit < afterSplit && !lastRadiiOk) {
borderRadius.corners = [4]borderRadiusCorner{}
return
}
// Handle the first radii
for corner, t := range firstRadii {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
borderRadius.updateCorner(rules, corner, borderRadiusCorner{
firstToken: t,
secondToken: t,
unitSafety: unitSafety,
ruleIndex: uint32(index),
})
}
// Handle the last radii
if lastRadiiOk {
for corner, t := range lastRadii {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
borderRadius.corners[corner].secondToken = t
}
}
// Success
borderRadius.compactRules(rules, decl.KeyRange, removeWhitespace)
}
func (borderRadius *borderRadiusTracker) mangleCorner(rules []css_ast.Rule, decl *css_ast.RDeclaration, index int, removeWhitespace bool, corner int) {
// Reset if we see a change in the "!important" flag
if borderRadius.important != decl.Important {
borderRadius.corners = [4]borderRadiusCorner{}
borderRadius.important = decl.Important
}
if tokens := decl.Value; (len(tokens) == 1 && tokens[0].Kind.IsNumeric()) ||
(len(tokens) == 2 && tokens[0].Kind.IsNumeric() && tokens[1].Kind.IsNumeric()) {
firstToken := tokens[0]
secondToken := firstToken
if len(tokens) == 2 {
secondToken = tokens[1]
}
// Check to see if these units are safe to use in every browser
unitSafety := unitSafetyTracker{}
unitSafety.includeUnitOf(firstToken)
unitSafety.includeUnitOf(secondToken)
// Only collapse "0unit" into "0" if the unit is safe
if unitSafety.status == unitSafe && firstToken.TurnLengthIntoNumberIfZero() {
tokens[0] = firstToken
}
if len(tokens) == 2 {
if unitSafety.status == unitSafe && secondToken.TurnLengthIntoNumberIfZero() {
tokens[1] = secondToken
}
// If both tokens are equal, merge them into one
if firstToken.EqualIgnoringWhitespace(secondToken) {
tokens[0].Whitespace &= ^css_ast.WhitespaceAfter
decl.Value = tokens[:1]
}
}
borderRadius.updateCorner(rules, corner, borderRadiusCorner{
firstToken: firstToken,
secondToken: secondToken,
unitSafety: unitSafety,
ruleIndex: uint32(index),
wasSingleRule: true,
})
borderRadius.compactRules(rules, decl.KeyRange, removeWhitespace)
} else {
borderRadius.corners = [4]borderRadiusCorner{}
}
}
func (borderRadius *borderRadiusTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, removeWhitespace bool) {
// All tokens must be present
if eof := css_lexer.TEndOfFile; borderRadius.corners[0].firstToken.Kind == eof || borderRadius.corners[1].firstToken.Kind == eof ||
borderRadius.corners[2].firstToken.Kind == eof || borderRadius.corners[3].firstToken.Kind == eof {
return
}
// All tokens must have the same unit
for _, side := range borderRadius.corners[1:] {
if !side.unitSafety.isSafeWith(borderRadius.corners[0].unitSafety) {
return
}
}
// Generate the most minimal representation
tokens := compactTokenQuad(
borderRadius.corners[0].firstToken,
borderRadius.corners[1].firstToken,
borderRadius.corners[2].firstToken,
borderRadius.corners[3].firstToken,
removeWhitespace,
)
secondTokens := compactTokenQuad(
borderRadius.corners[0].secondToken,
borderRadius.corners[1].secondToken,
borderRadius.corners[2].secondToken,
borderRadius.corners[3].secondToken,
removeWhitespace,
)
if !css_ast.TokensEqualIgnoringWhitespace(tokens, secondTokens) {
var whitespace css_ast.WhitespaceFlags
if !removeWhitespace {
whitespace = css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
}
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimSlash,
Text: "/",
Whitespace: whitespace,
})
tokens = append(tokens, secondTokens...)
}
// Remove all of the existing declarations
rules[borderRadius.corners[0].ruleIndex] = css_ast.Rule{}
rules[borderRadius.corners[1].ruleIndex] = css_ast.Rule{}
rules[borderRadius.corners[2].ruleIndex] = css_ast.Rule{}
rules[borderRadius.corners[3].ruleIndex] = css_ast.Rule{}
// Insert the combined declaration where the last rule was
rules[borderRadius.corners[3].ruleIndex].Data = &css_ast.RDeclaration{
Key: css_ast.DBorderRadius,
KeyText: "border-radius",
Value: tokens,
KeyRange: keyRange,
Important: borderRadius.important,
}
}

View File

@ -1,198 +0,0 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/logger"
)
const (
boxTop = iota
boxRight
boxBottom
boxLeft
)
type boxSide struct {
token css_ast.Token
unitSafety unitSafetyTracker
ruleIndex uint32 // The index of the originating rule in the rules array
wasSingleRule bool // True if the originating rule was just for this side
}
type boxTracker struct {
key css_ast.D
keyText string
allowAuto bool // If true, allow the "auto" keyword
sides [4]boxSide
important bool // True if all active rules were flagged as "!important"
}
type unitSafetyStatus uint8
const (
unitSafe unitSafetyStatus = iota // "margin: 0 1px 2cm 3%;"
unitUnsafeSingle // "margin: 0 1vw 2vw 3vw;"
unitUnsafeMixed // "margin: 0 1vw 2vh 3ch;"
)
// We can only compact rules together if they have the same unit safety level.
// We want to avoid a situation where the browser treats some of the original
// rules as valid and others as invalid.
//
// Safe:
// top: 1px; left: 0; bottom: 1px; right: 0;
// top: 1Q; left: 2Q; bottom: 3Q; right: 4Q;
//
// Unsafe:
// top: 1vh; left: 2vw; bottom: 3vh; right: 4vw;
// top: 1Q; left: 2Q; bottom: 3Q; right: 0;
// inset: 1Q 0 0 0; top: 0;
//
type unitSafetyTracker struct {
status unitSafetyStatus
unit string
}
func (a unitSafetyTracker) isSafeWith(b unitSafetyTracker) bool {
return a.status == b.status && a.status != unitUnsafeMixed && (a.status != unitUnsafeSingle || a.unit == b.unit)
}
func (t *unitSafetyTracker) includeUnitOf(token css_ast.Token) {
switch token.Kind {
case css_lexer.TNumber:
if token.Text == "0" {
return
}
case css_lexer.TPercentage:
return
case css_lexer.TDimension:
if token.DimensionUnitIsSafeLength() {
return
} else if unit := token.DimensionUnit(); t.status == unitSafe {
t.status = unitUnsafeSingle
t.unit = unit
return
} else if t.status == unitUnsafeSingle && t.unit == unit {
return
}
}
t.status = unitUnsafeMixed
}
func (box *boxTracker) updateSide(rules []css_ast.Rule, side int, new boxSide) {
if old := box.sides[side]; old.token.Kind != css_lexer.TEndOfFile &&
(!new.wasSingleRule || old.wasSingleRule) &&
old.unitSafety.status == unitSafe && new.unitSafety.status == unitSafe {
rules[old.ruleIndex] = css_ast.Rule{}
}
box.sides[side] = new
}
func (box *boxTracker) mangleSides(rules []css_ast.Rule, decl *css_ast.RDeclaration, index int, removeWhitespace bool) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
allowedIdent := ""
if box.allowAuto {
allowedIdent = "auto"
}
if quad, ok := expandTokenQuad(decl.Value, allowedIdent); ok {
// Use a single tracker for the whole rule
unitSafety := unitSafetyTracker{}
for _, t := range quad {
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
}
for side, t := range quad {
if unitSafety.status == unitSafe {
t.TurnLengthIntoNumberIfZero()
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(index),
unitSafety: unitSafety,
})
}
box.compactRules(rules, decl.KeyRange, removeWhitespace)
} else {
box.sides = [4]boxSide{}
}
}
func (box *boxTracker) mangleSide(rules []css_ast.Rule, decl *css_ast.RDeclaration, index int, removeWhitespace bool, side int) {
// Reset if we see a change in the "!important" flag
if box.important != decl.Important {
box.sides = [4]boxSide{}
box.important = decl.Important
}
if tokens := decl.Value; len(tokens) == 1 {
if t := tokens[0]; t.Kind.IsNumeric() || (t.Kind == css_lexer.TIdent && box.allowAuto && t.Text == "auto") {
unitSafety := unitSafetyTracker{}
if !box.allowAuto || t.Kind.IsNumeric() {
unitSafety.includeUnitOf(t)
}
if unitSafety.status == unitSafe && t.TurnLengthIntoNumberIfZero() {
tokens[0] = t
}
box.updateSide(rules, side, boxSide{
token: t,
ruleIndex: uint32(index),
wasSingleRule: true,
unitSafety: unitSafety,
})
box.compactRules(rules, decl.KeyRange, removeWhitespace)
return
}
}
box.sides = [4]boxSide{}
}
func (box *boxTracker) compactRules(rules []css_ast.Rule, keyRange logger.Range, removeWhitespace bool) {
// All tokens must be present
if eof := css_lexer.TEndOfFile; box.sides[0].token.Kind == eof || box.sides[1].token.Kind == eof ||
box.sides[2].token.Kind == eof || box.sides[3].token.Kind == eof {
return
}
// All tokens must have the same unit
for _, side := range box.sides[1:] {
if !side.unitSafety.isSafeWith(box.sides[0].unitSafety) {
return
}
}
// Generate the most minimal representation
tokens := compactTokenQuad(
box.sides[0].token,
box.sides[1].token,
box.sides[2].token,
box.sides[3].token,
removeWhitespace,
)
// Remove all of the existing declarations
rules[box.sides[0].ruleIndex] = css_ast.Rule{}
rules[box.sides[1].ruleIndex] = css_ast.Rule{}
rules[box.sides[2].ruleIndex] = css_ast.Rule{}
rules[box.sides[3].ruleIndex] = css_ast.Rule{}
// Insert the combined declaration where the last rule was
rules[box.sides[3].ruleIndex].Data = &css_ast.RDeclaration{
Key: box.key,
KeyText: box.keyText,
Value: tokens,
KeyRange: keyRange,
Important: box.important,
}
}

View File

@ -1,103 +0,0 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) mangleBoxShadow(tokens []css_ast.Token) []css_ast.Token {
insetCount := 0
colorCount := 0
numbersBegin := 0
numbersCount := 0
numbersDone := false
foundUnexpectedToken := false
for i, t := range tokens {
if t.Kind == css_lexer.TNumber || t.Kind == css_lexer.TDimension {
if numbersDone {
// Track if we found a non-number in between two numbers
foundUnexpectedToken = true
}
if t.TurnLengthIntoNumberIfZero() {
// "0px" => "0"
tokens[i] = t
}
if numbersCount == 0 {
// Track the index of the first number
numbersBegin = i
}
numbersCount++
} else {
if numbersCount != 0 {
// Track when we find a non-number after a number
numbersDone = true
}
if hex, ok := parseColor(t); ok {
colorCount++
tokens[i] = p.mangleColor(t, hex)
} else if t.Kind == css_lexer.TIdent && t.Text == "inset" {
insetCount++
} else {
// Track if we found a token other than a number, a color, or "inset"
foundUnexpectedToken = true
}
}
}
// If everything looks like a valid rule, trim trailing zeros off the numbers.
// There are three valid configurations of numbers:
//
// offset-x | offset-y
// offset-x | offset-y | blur-radius
// offset-x | offset-y | blur-radius | spread-radius
//
// If omitted, blur-radius and spread-radius are implied to be zero.
if insetCount <= 1 && colorCount <= 1 && numbersCount > 2 && numbersCount <= 4 && !foundUnexpectedToken {
numbersEnd := numbersBegin + numbersCount
for numbersCount > 2 && tokens[numbersBegin+numbersCount-1].IsZero() {
numbersCount--
}
tokens = append(tokens[:numbersBegin+numbersCount], tokens[numbersEnd:]...)
}
// Set the whitespace flags
for i := range tokens {
var whitespace css_ast.WhitespaceFlags
if i > 0 || !p.options.RemoveWhitespace {
whitespace |= css_ast.WhitespaceBefore
}
if i+1 < len(tokens) {
whitespace |= css_ast.WhitespaceAfter
}
tokens[i].Whitespace = whitespace
}
return tokens
}
func (p *parser) mangleBoxShadows(tokens []css_ast.Token) []css_ast.Token {
n := len(tokens)
end := 0
i := 0
for i < n {
// Find the comma or the end of the token list
comma := i
for comma < n && tokens[comma].Kind != css_lexer.TComma {
comma++
}
// Mangle this individual shadow
end += copy(tokens[end:], p.mangleBoxShadow(tokens[i:comma]))
// Skip over the comma
if comma < n {
tokens[end] = tokens[comma]
end++
comma++
}
i = comma
}
return tokens[:end]
}

View File

@ -1,669 +0,0 @@
package css_parser
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// These names are shorter than their hex codes
var shortColorName = map[uint32]string{
0x000080ff: "navy",
0x008000ff: "green",
0x008080ff: "teal",
0x4b0082ff: "indigo",
0x800000ff: "maroon",
0x800080ff: "purple",
0x808000ff: "olive",
0x808080ff: "gray",
0xa0522dff: "sienna",
0xa52a2aff: "brown",
0xc0c0c0ff: "silver",
0xcd853fff: "peru",
0xd2b48cff: "tan",
0xda70d6ff: "orchid",
0xdda0ddff: "plum",
0xee82eeff: "violet",
0xf0e68cff: "khaki",
0xf0ffffff: "azure",
0xf5deb3ff: "wheat",
0xf5f5dcff: "beige",
0xfa8072ff: "salmon",
0xfaf0e6ff: "linen",
0xff0000ff: "red",
0xff6347ff: "tomato",
0xff7f50ff: "coral",
0xffa500ff: "orange",
0xffc0cbff: "pink",
0xffd700ff: "gold",
0xffe4c4ff: "bisque",
0xfffafaff: "snow",
0xfffff0ff: "ivory",
}
var colorNameToHex = map[string]uint32{
"black": 0x000000ff,
"silver": 0xc0c0c0ff,
"gray": 0x808080ff,
"white": 0xffffffff,
"maroon": 0x800000ff,
"red": 0xff0000ff,
"purple": 0x800080ff,
"fuchsia": 0xff00ffff,
"green": 0x008000ff,
"lime": 0x00ff00ff,
"olive": 0x808000ff,
"yellow": 0xffff00ff,
"navy": 0x000080ff,
"blue": 0x0000ffff,
"teal": 0x008080ff,
"aqua": 0x00ffffff,
"orange": 0xffa500ff,
"aliceblue": 0xf0f8ffff,
"antiquewhite": 0xfaebd7ff,
"aquamarine": 0x7fffd4ff,
"azure": 0xf0ffffff,
"beige": 0xf5f5dcff,
"bisque": 0xffe4c4ff,
"blanchedalmond": 0xffebcdff,
"blueviolet": 0x8a2be2ff,
"brown": 0xa52a2aff,
"burlywood": 0xdeb887ff,
"cadetblue": 0x5f9ea0ff,
"chartreuse": 0x7fff00ff,
"chocolate": 0xd2691eff,
"coral": 0xff7f50ff,
"cornflowerblue": 0x6495edff,
"cornsilk": 0xfff8dcff,
"crimson": 0xdc143cff,
"cyan": 0x00ffffff,
"darkblue": 0x00008bff,
"darkcyan": 0x008b8bff,
"darkgoldenrod": 0xb8860bff,
"darkgray": 0xa9a9a9ff,
"darkgreen": 0x006400ff,
"darkgrey": 0xa9a9a9ff,
"darkkhaki": 0xbdb76bff,
"darkmagenta": 0x8b008bff,
"darkolivegreen": 0x556b2fff,
"darkorange": 0xff8c00ff,
"darkorchid": 0x9932ccff,
"darkred": 0x8b0000ff,
"darksalmon": 0xe9967aff,
"darkseagreen": 0x8fbc8fff,
"darkslateblue": 0x483d8bff,
"darkslategray": 0x2f4f4fff,
"darkslategrey": 0x2f4f4fff,
"darkturquoise": 0x00ced1ff,
"darkviolet": 0x9400d3ff,
"deeppink": 0xff1493ff,
"deepskyblue": 0x00bfffff,
"dimgray": 0x696969ff,
"dimgrey": 0x696969ff,
"dodgerblue": 0x1e90ffff,
"firebrick": 0xb22222ff,
"floralwhite": 0xfffaf0ff,
"forestgreen": 0x228b22ff,
"gainsboro": 0xdcdcdcff,
"ghostwhite": 0xf8f8ffff,
"gold": 0xffd700ff,
"goldenrod": 0xdaa520ff,
"greenyellow": 0xadff2fff,
"grey": 0x808080ff,
"honeydew": 0xf0fff0ff,
"hotpink": 0xff69b4ff,
"indianred": 0xcd5c5cff,
"indigo": 0x4b0082ff,
"ivory": 0xfffff0ff,
"khaki": 0xf0e68cff,
"lavender": 0xe6e6faff,
"lavenderblush": 0xfff0f5ff,
"lawngreen": 0x7cfc00ff,
"lemonchiffon": 0xfffacdff,
"lightblue": 0xadd8e6ff,
"lightcoral": 0xf08080ff,
"lightcyan": 0xe0ffffff,
"lightgoldenrodyellow": 0xfafad2ff,
"lightgray": 0xd3d3d3ff,
"lightgreen": 0x90ee90ff,
"lightgrey": 0xd3d3d3ff,
"lightpink": 0xffb6c1ff,
"lightsalmon": 0xffa07aff,
"lightseagreen": 0x20b2aaff,
"lightskyblue": 0x87cefaff,
"lightslategray": 0x778899ff,
"lightslategrey": 0x778899ff,
"lightsteelblue": 0xb0c4deff,
"lightyellow": 0xffffe0ff,
"limegreen": 0x32cd32ff,
"linen": 0xfaf0e6ff,
"magenta": 0xff00ffff,
"mediumaquamarine": 0x66cdaaff,
"mediumblue": 0x0000cdff,
"mediumorchid": 0xba55d3ff,
"mediumpurple": 0x9370dbff,
"mediumseagreen": 0x3cb371ff,
"mediumslateblue": 0x7b68eeff,
"mediumspringgreen": 0x00fa9aff,
"mediumturquoise": 0x48d1ccff,
"mediumvioletred": 0xc71585ff,
"midnightblue": 0x191970ff,
"mintcream": 0xf5fffaff,
"mistyrose": 0xffe4e1ff,
"moccasin": 0xffe4b5ff,
"navajowhite": 0xffdeadff,
"oldlace": 0xfdf5e6ff,
"olivedrab": 0x6b8e23ff,
"orangered": 0xff4500ff,
"orchid": 0xda70d6ff,
"palegoldenrod": 0xeee8aaff,
"palegreen": 0x98fb98ff,
"paleturquoise": 0xafeeeeff,
"palevioletred": 0xdb7093ff,
"papayawhip": 0xffefd5ff,
"peachpuff": 0xffdab9ff,
"peru": 0xcd853fff,
"pink": 0xffc0cbff,
"plum": 0xdda0ddff,
"powderblue": 0xb0e0e6ff,
"rosybrown": 0xbc8f8fff,
"royalblue": 0x4169e1ff,
"saddlebrown": 0x8b4513ff,
"salmon": 0xfa8072ff,
"sandybrown": 0xf4a460ff,
"seagreen": 0x2e8b57ff,
"seashell": 0xfff5eeff,
"sienna": 0xa0522dff,
"skyblue": 0x87ceebff,
"slateblue": 0x6a5acdff,
"slategray": 0x708090ff,
"slategrey": 0x708090ff,
"snow": 0xfffafaff,
"springgreen": 0x00ff7fff,
"steelblue": 0x4682b4ff,
"tan": 0xd2b48cff,
"thistle": 0xd8bfd8ff,
"tomato": 0xff6347ff,
"turquoise": 0x40e0d0ff,
"violet": 0xee82eeff,
"wheat": 0xf5deb3ff,
"whitesmoke": 0xf5f5f5ff,
"yellowgreen": 0x9acd32ff,
"rebeccapurple": 0x663399ff,
}
func parseHex(text string) (uint32, bool) {
hex := uint32(0)
for _, c := range text {
hex <<= 4
switch {
case c >= '0' && c <= '9':
hex |= uint32(c) - '0'
case c >= 'a' && c <= 'f':
hex |= uint32(c) - ('a' - 10)
case c >= 'A' && c <= 'F':
hex |= uint32(c) - ('A' - 10)
default:
return 0, false
}
}
return hex, true
}
// 0xAABBCCDD => 0xABCD
func compactHex(v uint32) uint32 {
return ((v & 0x0FF00000) >> 12) | ((v & 0x00000FF0) >> 4)
}
// 0xABCD => 0xAABBCCDD
func expandHex(v uint32) uint32 {
return ((v & 0xF000) << 16) | ((v & 0xFF00) << 12) | ((v & 0x0FF0) << 8) | ((v & 0x00FF) << 4) | (v & 0x000F)
}
func hexR(v uint32) int { return int(v >> 24) }
func hexG(v uint32) int { return int((v >> 16) & 255) }
func hexB(v uint32) int { return int((v >> 8) & 255) }
func hexA(v uint32) int { return int(v & 255) }
func floatToStringForColor(a float64) string {
text := fmt.Sprintf("%.03f", a)
for text[len(text)-1] == '0' {
text = text[:len(text)-1]
}
if text[len(text)-1] == '.' {
text = text[:len(text)-1]
}
return text
}
func degreesForAngle(token css_ast.Token) (float64, bool) {
switch token.Kind {
case css_lexer.TNumber:
if value, err := strconv.ParseFloat(token.Text, 64); err == nil {
return value, true
}
case css_lexer.TDimension:
if value, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
switch token.DimensionUnit() {
case "deg":
return value, true
case "grad":
return value * (360.0 / 400.0), true
case "rad":
return value * (180.0 / math.Pi), true
case "turn":
return value * 360.0, true
}
}
}
return 0, false
}
func lowerAlphaPercentageToNumber(token css_ast.Token) css_ast.Token {
if token.Kind == css_lexer.TPercentage {
if value, err := strconv.ParseFloat(token.Text[:len(token.Text)-1], 64); err == nil {
token.Kind = css_lexer.TNumber
token.Text = floatToStringForColor(value / 100.0)
}
}
return token
}
// Convert newer color syntax to older color syntax for older browsers
func (p *parser) lowerColor(token css_ast.Token) css_ast.Token {
text := token.Text
switch token.Kind {
case css_lexer.THash:
if p.options.UnsupportedCSSFeatures.Has(compat.HexRGBA) {
switch len(text) {
case 4:
// "#1234" => "rgba(1, 2, 3, 0.004)"
if hex, ok := parseHex(text); ok {
hex = expandHex(hex)
token.Kind = css_lexer.TFunction
token.Text = "rgba"
commaToken := p.commaToken()
token.Children = &[]css_ast.Token{
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexR(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexG(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexB(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: floatToStringForColor(float64(hexA(hex)) / 255)},
}
}
case 8:
// "#12345678" => "rgba(18, 52, 86, 0.47)"
if hex, ok := parseHex(text); ok {
token.Kind = css_lexer.TFunction
token.Text = "rgba"
commaToken := p.commaToken()
token.Children = &[]css_ast.Token{
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexR(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexG(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexB(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: floatToStringForColor(float64(hexA(hex)) / 255)},
}
}
}
}
case css_lexer.TIdent:
if text == "rebeccapurple" && p.options.UnsupportedCSSFeatures.Has(compat.RebeccaPurple) {
token.Kind = css_lexer.THash
token.Text = "663399"
}
case css_lexer.TFunction:
switch text {
case "rgb", "rgba", "hsl", "hsla":
if p.options.UnsupportedCSSFeatures.Has(compat.Modern_RGB_HSL) {
args := *token.Children
removeAlpha := false
addAlpha := false
// "hsl(1deg, 2%, 3%)" => "hsl(1, 2%, 3%)"
if (text == "hsl" || text == "hsla") && len(args) > 0 {
if degrees, ok := degreesForAngle(args[0]); ok {
args[0].Kind = css_lexer.TNumber
args[0].Text = floatToStringForColor(degrees)
}
}
// These check for "IsNumeric" to reject "var()" since a single "var()"
// can substitute for multiple tokens and that messes up pattern matching
switch len(args) {
case 3:
// "rgba(1 2 3)" => "rgb(1, 2, 3)"
// "hsla(1 2% 3%)" => "hsl(1, 2%, 3%)"
if args[0].Kind.IsNumeric() && args[1].Kind.IsNumeric() && args[2].Kind.IsNumeric() {
removeAlpha = true
args[0].Whitespace = 0
args[1].Whitespace = 0
commaToken := p.commaToken()
token.Children = &[]css_ast.Token{
args[0], commaToken,
args[1], commaToken,
args[2],
}
}
case 5:
// "rgba(1, 2, 3)" => "rgb(1, 2, 3)"
// "hsla(1, 2%, 3%)" => "hsl(1%, 2%, 3%)"
if args[0].Kind.IsNumeric() && args[1].Kind == css_lexer.TComma &&
args[2].Kind.IsNumeric() && args[3].Kind == css_lexer.TComma &&
args[4].Kind.IsNumeric() {
removeAlpha = true
break
}
// "rgb(1 2 3 / 4%)" => "rgba(1, 2, 3, 0.04)"
// "hsl(1 2% 3% / 4%)" => "hsla(1, 2%, 3%, 0.04)"
if args[0].Kind.IsNumeric() && args[1].Kind.IsNumeric() && args[2].Kind.IsNumeric() &&
args[3].Kind == css_lexer.TDelimSlash && args[4].Kind.IsNumeric() {
addAlpha = true
args[0].Whitespace = 0
args[1].Whitespace = 0
args[2].Whitespace = 0
commaToken := p.commaToken()
token.Children = &[]css_ast.Token{
args[0], commaToken,
args[1], commaToken,
args[2], commaToken,
lowerAlphaPercentageToNumber(args[4]),
}
}
case 7:
// "rgb(1%, 2%, 3%, 4%)" => "rgba(1%, 2%, 3%, 0.04)"
// "hsl(1, 2%, 3%, 4%)" => "hsla(1, 2%, 3%, 0.04)"
if args[0].Kind.IsNumeric() && args[1].Kind == css_lexer.TComma &&
args[2].Kind.IsNumeric() && args[3].Kind == css_lexer.TComma &&
args[4].Kind.IsNumeric() && args[5].Kind == css_lexer.TComma &&
args[6].Kind.IsNumeric() {
addAlpha = true
args[6] = lowerAlphaPercentageToNumber(args[6])
}
}
if removeAlpha {
if text == "rgba" {
token.Text = "rgb"
} else if text == "hsla" {
token.Text = "hsl"
}
} else if addAlpha {
if text == "rgb" {
token.Text = "rgba"
} else if text == "hsl" {
token.Text = "hsla"
}
}
}
}
}
return token
}
func parseColor(token css_ast.Token) (uint32, bool) {
text := token.Text
switch token.Kind {
case css_lexer.TIdent:
if hex, ok := colorNameToHex[strings.ToLower(text)]; ok {
return hex, true
}
case css_lexer.THash:
switch len(text) {
case 3:
// "#123"
if hex, ok := parseHex(text); ok {
return (expandHex(hex) << 8) | 0xFF, true
}
case 4:
// "#1234"
if hex, ok := parseHex(text); ok {
return expandHex(hex), true
}
case 6:
// "#112233"
if hex, ok := parseHex(text); ok {
return (hex << 8) | 0xFF, true
}
case 8:
// "#11223344"
if hex, ok := parseHex(text); ok {
return hex, true
}
}
case css_lexer.TFunction:
switch text {
case "rgb", "rgba":
args := *token.Children
var r, g, b, a css_ast.Token
switch len(args) {
case 3:
// "rgb(1 2 3)"
r, g, b = args[0], args[1], args[2]
case 5:
// "rgba(1, 2, 3)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma {
r, g, b = args[0], args[2], args[4]
break
}
// "rgb(1 2 3 / 4%)"
if args[3].Kind == css_lexer.TDelimSlash {
r, g, b, a = args[0], args[1], args[2], args[4]
}
case 7:
// "rgb(1%, 2%, 3%, 4%)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma && args[5].Kind == css_lexer.TComma {
r, g, b, a = args[0], args[2], args[4], args[6]
}
}
if r, ok := parseColorByte(r, 1); ok {
if g, ok := parseColorByte(g, 1); ok {
if b, ok := parseColorByte(b, 1); ok {
if a, ok := parseAlphaByte(a); ok {
return uint32((r << 24) | (g << 16) | (b << 8) | a), true
}
}
}
}
case "hsl", "hsla":
args := *token.Children
var h, s, l, a css_ast.Token
switch len(args) {
case 3:
// "hsl(1 2 3)"
h, s, l = args[0], args[1], args[2]
case 5:
// "hsla(1, 2, 3)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma {
h, s, l = args[0], args[2], args[4]
break
}
// "hsl(1 2 3 / 4%)"
if args[3].Kind == css_lexer.TDelimSlash {
h, s, l, a = args[0], args[1], args[2], args[4]
}
case 7:
// "hsl(1%, 2%, 3%, 4%)"
if args[1].Kind == css_lexer.TComma && args[3].Kind == css_lexer.TComma && args[5].Kind == css_lexer.TComma {
h, s, l, a = args[0], args[2], args[4], args[6]
}
}
// Convert from HSL to RGB. The algorithm is from the section
// "Converting HSL colors to sRGB colors" in the specification.
if h, ok := degreesForAngle(h); ok {
if s, ok := s.FractionForPercentage(); ok {
if l, ok := l.FractionForPercentage(); ok {
if a, ok := parseAlphaByte(a); ok {
h /= 360.0
var t2 float64
if l <= 0.5 {
t2 = l * (s + 1)
} else {
t2 = l + s - (l * s)
}
t1 := l*2 - t2
r := hueToRgb(t1, t2, h+1.0/3.0)
g := hueToRgb(t1, t2, h)
b := hueToRgb(t1, t2, h-1.0/3.0)
return uint32((r << 24) | (g << 16) | (b << 8) | a), true
}
}
}
}
}
}
return 0, false
}
func hueToRgb(t1 float64, t2 float64, hue float64) uint32 {
hue -= math.Floor(hue)
hue *= 6.0
var f float64
if hue < 1 {
f = (t2-t1)*hue + t1
} else if hue < 3 {
f = t2
} else if hue < 4 {
f = (t2-t1)*(4-hue) + t1
} else {
f = t1
}
i := int(math.Round(f * 255))
if i < 0 {
i = 0
} else if i > 255 {
i = 255
}
return uint32(i)
}
func parseAlphaByte(token css_ast.Token) (uint32, bool) {
if token.Kind == css_lexer.T(0) {
return 255, true
}
return parseColorByte(token, 255)
}
func parseColorByte(token css_ast.Token, scale float64) (uint32, bool) {
var i int
var ok bool
switch token.Kind {
case css_lexer.TNumber:
if f, err := strconv.ParseFloat(token.Text, 64); err == nil {
i = int(math.Round(f * scale))
ok = true
}
case css_lexer.TPercentage:
if f, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
i = int(math.Round(f * (255.0 / 100.0)))
ok = true
}
}
if i < 0 {
i = 0
} else if i > 255 {
i = 255
}
return uint32(i), ok
}
func (p *parser) mangleColor(token css_ast.Token, hex uint32) css_ast.Token {
// Note: Do NOT remove color information from fully transparent colors.
// Safari behaves differently than other browsers for color interpolation:
// https://css-tricks.com/thing-know-gradients-transparent-black/
if hexA(hex) == 255 {
token.Children = nil
if name, ok := shortColorName[hex]; ok {
token.Kind = css_lexer.TIdent
token.Text = name
} else {
token.Kind = css_lexer.THash
hex >>= 8
compact := compactHex(hex)
if hex == expandHex(compact) {
token.Text = fmt.Sprintf("%03x", compact)
} else {
token.Text = fmt.Sprintf("%06x", hex)
}
}
} else if !p.options.UnsupportedCSSFeatures.Has(compat.HexRGBA) {
token.Children = nil
token.Kind = css_lexer.THash
compact := compactHex(hex)
if hex == expandHex(compact) {
token.Text = fmt.Sprintf("%04x", compact)
} else {
token.Text = fmt.Sprintf("%08x", hex)
}
} else {
token.Kind = css_lexer.TFunction
token.Text = "rgba"
commaToken := p.commaToken()
index := hexA(hex) * 4
alpha := alphaFractionTable[index : index+4]
if space := strings.IndexByte(alpha, ' '); space != -1 {
alpha = alpha[:space]
}
token.Children = &[]css_ast.Token{
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexR(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexG(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: strconv.Itoa(hexB(hex))}, commaToken,
{Kind: css_lexer.TNumber, Text: alpha},
}
}
return token
}
// Every four characters in this table is the fraction for that index
const alphaFractionTable string = "" +
"0 .004.008.01 .016.02 .024.027.03 .035.04 .043.047.05 .055.06 " +
".063.067.07 .075.08 .082.086.09 .094.098.1 .106.11 .114.118.12 " +
".125.13 .133.137.14 .145.15 .153.157.16 .165.17 .173.176.18 .184" +
".19 .192.196.2 .204.208.21 .216.22 .224.227.23 .235.24 .243.247" +
".25 .255.26 .263.267.27 .275.28 .282.286.29 .294.298.3 .306.31 " +
".314.318.32 .325.33 .333.337.34 .345.35 .353.357.36 .365.37 .373" +
".376.38 .384.39 .392.396.4 .404.408.41 .416.42 .424.427.43 .435" +
".44 .443.447.45 .455.46 .463.467.47 .475.48 .482.486.49 .494.498" +
".5 .506.51 .514.518.52 .525.53 .533.537.54 .545.55 .553.557.56 " +
".565.57 .573.576.58 .584.59 .592.596.6 .604.608.61 .616.62 .624" +
".627.63 .635.64 .643.647.65 .655.66 .663.667.67 .675.68 .682.686" +
".69 .694.698.7 .706.71 .714.718.72 .725.73 .733.737.74 .745.75 " +
".753.757.76 .765.77 .773.776.78 .784.79 .792.796.8 .804.808.81 " +
".816.82 .824.827.83 .835.84 .843.847.85 .855.86 .863.867.87 .875" +
".88 .882.886.89 .894.898.9 .906.91 .914.918.92 .925.93 .933.937" +
".94 .945.95 .953.957.96 .965.97 .973.976.98 .984.99 .992.9961 "

View File

@ -1,135 +0,0 @@
package css_parser
import (
"strconv"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// Specification: https://drafts.csswg.org/css-fonts/#font-prop
// [ <font-style> || <font-variant-css2> || <font-weight> || <font-stretch-css3> ]? <font-size> [ / <line-height> ]? <font-family>
func (p *parser) mangleFont(tokens []css_ast.Token) []css_ast.Token {
var result []css_ast.Token
// Scan up to the font size
pos := 0
for ; pos < len(tokens); pos++ {
token := tokens[pos]
if isFontSize(token) {
break
}
switch token.Kind {
case css_lexer.TIdent:
switch strings.ToLower(token.Text) {
case "normal":
// "All subproperties of the font property are first reset to their initial values"
// This implies that "normal" doesn't do anything. Also all of the optional values
// contain "normal" as an option and they are unordered so it's impossible to say
// what property "normal" corresponds to. Just drop these tokens to save space.
continue
// <font-style>
case "italic":
case "oblique":
if pos+1 < len(tokens) && tokens[pos+1].IsAngle() {
result = append(result, token, tokens[pos+1])
pos++
continue
}
// <font-variant-css2>
case "small-caps":
// <font-weight>
case "bold", "bolder", "lighter":
result = append(result, p.mangleFontWeight(token))
continue
// <font-stretch-css3>
case "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
"semi-expanded", "expanded", "extra-expanded", "ultra-expanded":
default:
// All other tokens are unrecognized, so we bail if we hit one
return tokens
}
result = append(result, token)
case css_lexer.TNumber:
// "Only values greater than or equal to 1, and less than or equal to
// 1000, are valid, and all other values are invalid."
if value, err := strconv.ParseFloat(token.Text, 64); err != nil || value < 1 || value > 1000 {
return tokens
}
result = append(result, token)
default:
// All other tokens are unrecognized, so we bail if we hit one
return tokens
}
}
// <font-size>
if pos == len(tokens) {
return tokens
}
result = append(result, tokens[pos])
pos++
// / <line-height>
if pos < len(tokens) && tokens[pos].Kind == css_lexer.TDelimSlash {
if pos+1 == len(tokens) {
return tokens
}
result = append(result, tokens[pos], tokens[pos+1])
pos += 2
// Remove the whitespace around the "/" character
if p.options.RemoveWhitespace {
result[len(result)-3].Whitespace &= ^css_ast.WhitespaceAfter
result[len(result)-2].Whitespace = 0
result[len(result)-1].Whitespace &= ^css_ast.WhitespaceBefore
}
}
// <font-family>
if family, ok := p.mangleFontFamily(tokens[pos:]); ok {
return append(result, family...)
}
return tokens
}
var fontSizeKeywords = map[string]bool{
// <absolute-size>: https://drafts.csswg.org/css-fonts/#valdef-font-size-absolute-size
"xx-small": true,
"x-small": true,
"small": true,
"medium": true,
"large": true,
"x-large": true,
"xx-large": true,
"xxx-large": true,
// <relative-size>: https://drafts.csswg.org/css-fonts/#valdef-font-size-relative-size
"larger": true,
"smaller": true,
}
// Specification: https://drafts.csswg.org/css-fonts/#font-size-prop
func isFontSize(token css_ast.Token) bool {
// <length-percentage>
if token.Kind == css_lexer.TDimension || token.Kind == css_lexer.TPercentage {
return true
}
// <absolute-size> or <relative-size>
if token.Kind == css_lexer.TIdent {
_, ok := fontSizeKeywords[strings.ToLower(token.Text)]
return ok
}
return false
}

View File

@ -1,142 +0,0 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
// Specification: https://drafts.csswg.org/css-values-4/#common-keywords
var wideKeywords = map[string]bool{
"initial": true,
"inherit": true,
"unset": true,
}
// Specification: https://drafts.csswg.org/css-fonts/#generic-font-families
var genericFamilyNames = map[string]bool{
"serif": true,
"sans-serif": true,
"cursive": true,
"fantasy": true,
"monospace": true,
"system-ui": true,
"emoji": true,
"math": true,
"fangsong": true,
"ui-serif": true,
"ui-sans-serif": true,
"ui-monospace": true,
"ui-rounded": true,
}
// Specification: https://drafts.csswg.org/css-fonts/#font-family-prop
func (p *parser) mangleFontFamily(tokens []css_ast.Token) ([]css_ast.Token, bool) {
result, rest, ok := p.mangleFamilyNameOrGenericName(nil, tokens)
if !ok {
return nil, false
}
for len(rest) > 0 && rest[0].Kind == css_lexer.TComma {
result, rest, ok = p.mangleFamilyNameOrGenericName(append(result, rest[0]), rest[1:])
if !ok {
return nil, false
}
}
if len(rest) > 0 {
return nil, false
}
return result, true
}
func (p *parser) mangleFamilyNameOrGenericName(result []css_ast.Token, tokens []css_ast.Token) ([]css_ast.Token, []css_ast.Token, bool) {
if len(tokens) > 0 {
t := tokens[0]
// Handle <generic-family>
if t.Kind == css_lexer.TIdent && genericFamilyNames[t.Text] {
return append(result, t), tokens[1:], true
}
// Handle <family-name>
if t.Kind == css_lexer.TString {
// "If a sequence of identifiers is given as a <family-name>, the computed
// value is the name converted to a string by joining all the identifiers
// in the sequence by single spaces."
//
// More information: https://mathiasbynens.be/notes/unquoted-font-family
names := strings.Split(t.Text, " ")
for _, name := range names {
if !isValidCustomIdent(name, genericFamilyNames) {
return append(result, t), tokens[1:], true
}
}
for i, name := range names {
var whitespace css_ast.WhitespaceFlags
if i != 0 || !p.options.RemoveWhitespace {
whitespace = css_ast.WhitespaceBefore
}
result = append(result, css_ast.Token{
Kind: css_lexer.TIdent,
Text: name,
Whitespace: whitespace,
})
}
return result, tokens[1:], true
}
// "Font family names other than generic families must either be given
// quoted as <string>s, or unquoted as a sequence of one or more
// <custom-ident>."
if t.Kind == css_lexer.TIdent {
for {
if !isValidCustomIdent(t.Text, genericFamilyNames) {
return nil, nil, false
}
result = append(result, t)
tokens = tokens[1:]
if len(tokens) == 0 || tokens[0].Kind != css_lexer.TIdent {
break
}
t = tokens[0]
}
return result, tokens, true
}
}
// Anything other than the cases listed above causes us to bail
return nil, nil, false
}
// Specification: https://drafts.csswg.org/css-values-4/#custom-idents
func isValidCustomIdent(text string, predefinedKeywords map[string]bool) bool {
loweredText := strings.ToLower(text)
if predefinedKeywords[loweredText] {
return false
}
if wideKeywords[loweredText] {
return false
}
if loweredText == "default" {
return false
}
if loweredText == "" {
return false
}
// validate if it contains characters which needs to be escaped
if !css_lexer.WouldStartIdentifierWithoutEscapes(text) {
return false
}
for _, c := range text {
if !css_lexer.IsNameContinue(c) {
return false
}
}
return true
}

View File

@ -1,25 +0,0 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) mangleFontWeight(token css_ast.Token) css_ast.Token {
if token.Kind != css_lexer.TIdent {
return token
}
switch strings.ToLower(token.Text) {
case "normal":
token.Text = "400"
token.Kind = css_lexer.TNumber
case "bold":
token.Text = "700"
token.Kind = css_lexer.TNumber
}
return token
}

View File

@ -1,391 +0,0 @@
package css_parser
import (
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func turnPercentIntoNumberIfShorter(t *css_ast.Token) {
if t.Kind == css_lexer.TPercentage {
if shifted, ok := shiftDot(t.PercentageValue(), -2); ok && len(shifted) < len(t.Text) {
t.Kind = css_lexer.TNumber
t.Text = shifted
}
}
}
// https://www.w3.org/TR/css-transforms-1/#two-d-transform-functions
// https://drafts.csswg.org/css-transforms-2/#transform-functions
func (p *parser) mangleTransforms(tokens []css_ast.Token) []css_ast.Token {
for i := range tokens {
if token := &tokens[i]; token.Kind == css_lexer.TFunction {
if args := *token.Children; css_ast.TokensAreCommaSeparated(args) {
n := len(args)
switch strings.ToLower(token.Text) {
////////////////////////////////////////////////////////////////////////////////
// 2D transforms
case "matrix":
// specifies a 2D transformation in the form of a transformation
// matrix of the six values a, b, c, d, e, f.
if n == 11 {
// | a c 0 e |
// | b d 0 f |
// | 0 0 1 0 |
// | 0 0 0 1 |
a, b, c, d, e, f := args[0], args[2], args[4], args[6], args[8], args[10]
if b.IsZero() && c.IsZero() && e.IsZero() && f.IsZero() {
// | a 0 0 0 |
// | 0 d 0 0 |
// | 0 0 1 0 |
// | 0 0 0 1 |
if a.EqualIgnoringWhitespace(d) {
// "matrix(a, 0, 0, a, 0, 0)" => "scale(a)"
token.Text = "scale"
*token.Children = args[:1]
} else if d.IsOne() {
// "matrix(a, 0, 0, 1, 0, 0)" => "scaleX(a)"
token.Text = "scaleX"
*token.Children = args[:1]
} else if a.IsOne() {
// "matrix(1, 0, 0, d, 0, 0)" => "scaleY(d)"
token.Text = "scaleY"
*token.Children = args[6:7]
} else {
// "matrix(a, 0, 0, d, 0, 0)" => "scale(a, d)"
token.Text = "scale"
*token.Children = append(args[:2], d)
}
// Note: A "matrix" cannot be directly converted into a "translate"
// because "translate" requires units while "matrix" requires no
// units. I'm not sure exactly what the semantics are so I'm not
// sure if you can just add "px" or not. Even if that did work,
// you still couldn't substitute values containing "var()" since
// units would still not be substituted in that case.
}
}
case "translate":
// specifies a 2D translation by the vector [tx, ty], where tx is the
// first translation-value parameter and ty is the optional second
// translation-value parameter. If <ty> is not provided, ty has zero
// as a value.
if n == 1 {
args[0].TurnLengthOrPercentageIntoNumberIfZero()
} else if n == 3 {
tx, ty := &args[0], &args[2]
tx.TurnLengthOrPercentageIntoNumberIfZero()
ty.TurnLengthOrPercentageIntoNumberIfZero()
if ty.IsZero() {
// "translate(tx, 0)" => "translate(tx)"
*token.Children = args[:1]
} else if tx.IsZero() {
// "translate(0, ty)" => "translateY(ty)"
token.Text = "translateY"
*token.Children = args[2:]
}
}
case "translatex":
// specifies a translation by the given amount in the X direction.
if n == 1 {
// "translateX(tx)" => "translate(tx)"
token.Text = "translate"
args[0].TurnLengthOrPercentageIntoNumberIfZero()
}
case "translatey":
// specifies a translation by the given amount in the Y direction.
if n == 1 {
args[0].TurnLengthOrPercentageIntoNumberIfZero()
}
case "scale":
// specifies a 2D scale operation by the [sx,sy] scaling vector
// described by the 2 parameters. If the second parameter is not
// provided, it takes a value equal to the first. For example,
// scale(1, 1) would leave an element unchanged, while scale(2, 2)
// would cause it to appear twice as long in both the X and Y axes,
// or four times its typical geometric size.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
} else if n == 3 {
sx, sy := &args[0], &args[2]
turnPercentIntoNumberIfShorter(sx)
turnPercentIntoNumberIfShorter(sy)
if sx.EqualIgnoringWhitespace(*sy) {
// "scale(s, s)" => "scale(s)"
*token.Children = args[:1]
} else if sy.IsOne() {
// "scale(s, 1)" => "scaleX(s)"
token.Text = "scaleX"
*token.Children = args[:1]
} else if sx.IsOne() {
// "scale(1, s)" => "scaleY(s)"
token.Text = "scaleY"
*token.Children = args[2:]
}
}
case "scalex":
// specifies a 2D scale operation using the [sx,1] scaling vector,
// where sx is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "scaley":
// specifies a 2D scale operation using the [1,sy] scaling vector,
// where sy is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "rotate":
// specifies a 2D rotation by the angle specified in the parameter
// about the origin of the element, as defined by the
// transform-origin property. For example, rotate(90deg) would
// cause elements to appear rotated one-quarter of a turn in the
// clockwise direction.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "skew":
// specifies a 2D skew by [ax,ay] for X and Y. If the second
// parameter is not provided, it has a zero value.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
} else if n == 3 {
ax, ay := &args[0], &args[2]
ax.TurnLengthIntoNumberIfZero()
ay.TurnLengthIntoNumberIfZero()
if ay.IsZero() {
// "skew(ax, 0)" => "skew(ax)"
*token.Children = args[:1]
}
}
case "skewx":
// specifies a 2D skew transformation along the X axis by the given
// angle.
if n == 1 {
// "skewX(ax)" => "skew(ax)"
token.Text = "skew"
args[0].TurnLengthIntoNumberIfZero()
}
case "skewy":
// specifies a 2D skew transformation along the Y axis by the given
// angle.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
////////////////////////////////////////////////////////////////////////////////
// 3D transforms
case "matrix3d":
// specifies a 3D transformation as a 4x4 homogeneous matrix of 16
// values in column-major order.
if n == 31 {
// | m0 m4 m8 m12 |
// | m1 m5 m9 m13 |
// | m2 m6 m10 m14 |
// | m3 m7 m11 m15 |
mask := uint32(0)
for i := 0; i < 16; i++ {
if arg := args[i*2]; arg.IsZero() {
mask |= 1 << i
} else if arg.IsOne() {
mask |= (1 << 16) << i
}
}
const onlyScale = 0b1000_0000_0000_0000_0111_1011_1101_1110
const only2D = 0b1000_0100_0000_0000_0100_1011_1100_1100
if (mask & onlyScale) == onlyScale {
// | m0 0 0 0 |
// | 0 m5 0 0 |
// | 0 0 m10 0 |
// | 0 0 0 1 |
sx, sy, sz := args[0], args[10], args[20]
if sx.EqualIgnoringWhitespace(sy) && sz.IsOne() {
token.Text = "scale"
*token.Children = args[:1]
} else if sy.IsOne() && sz.IsOne() {
token.Text = "scaleX"
*token.Children = args[:1]
} else if sx.IsOne() && sz.IsOne() {
token.Text = "scaleY"
*token.Children = args[10:11]
} else if sx.IsOne() && sy.IsOne() {
token.Text = "scaleZ"
*token.Children = args[20:21]
} else if sz.IsOne() {
token.Text = "scale"
*token.Children = append(args[0:2], args[10])
} else {
token.Text = "scale3d"
*token.Children = append(append(args[0:2], args[10:12]...), args[20])
}
} else if (mask & only2D) == only2D {
// | m0 m4 0 m12 |
// | m1 m5 0 m13 |
// | 0 0 1 0 |
// | 0 0 0 1 |
token.Text = "matrix"
*token.Children = append(append(args[0:4], args[8:12]...), args[24:27]...)
}
// Note: A "matrix3d" cannot be directly converted into a "translate3d"
// because "translate3d" requires units while "matrix3d" requires no
// units. I'm not sure exactly what the semantics are so I'm not
// sure if you can just add "px" or not. Even if that did work,
// you still couldn't substitute values containing "var()" since
// units would still not be substituted in that case.
}
case "translate3d":
// specifies a 3D translation by the vector [tx,ty,tz], with tx,
// ty and tz being the first, second and third translation-value
// parameters respectively.
if n == 5 {
tx, ty, tz := &args[0], &args[2], &args[4]
tx.TurnLengthOrPercentageIntoNumberIfZero()
ty.TurnLengthOrPercentageIntoNumberIfZero()
tz.TurnLengthIntoNumberIfZero()
if ty.IsZero() && tz.IsZero() {
// "translate3d(tx, 0, 0)" => "translate(tx)"
token.Text = "translate"
*token.Children = args[:1]
} else if tx.IsZero() && tz.IsZero() {
// "translate3d(0, ty, 0)" => "translateY(ty)"
token.Text = "translateY"
*token.Children = args[2:3]
} else if tx.IsZero() && ty.IsZero() {
// "translate3d(0, 0, tz)" => "translateZ(tz)"
token.Text = "translateZ"
*token.Children = args[4:]
} else if tz.IsZero() {
// "translate3d(tx, ty, 0)" => "translate(tx, ty)"
token.Text = "translate"
*token.Children = args[:3]
}
}
case "translatez":
// specifies a 3D translation by the vector [0,0,tz] with the given
// amount in the Z direction.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "scale3d":
// specifies a 3D scale operation by the [sx,sy,sz] scaling vector
// described by the 3 parameters.
if n == 5 {
sx, sy, sz := &args[0], &args[2], &args[4]
turnPercentIntoNumberIfShorter(sx)
turnPercentIntoNumberIfShorter(sy)
turnPercentIntoNumberIfShorter(sz)
if sx.EqualIgnoringWhitespace(*sy) && sz.IsOne() {
// "scale3d(s, s, 1)" => "scale(s)"
token.Text = "scale"
*token.Children = args[:1]
} else if sy.IsOne() && sz.IsOne() {
// "scale3d(sx, 1, 1)" => "scaleX(sx)"
token.Text = "scaleX"
*token.Children = args[:1]
} else if sx.IsOne() && sz.IsOne() {
// "scale3d(1, sy, 1)" => "scaleY(sy)"
token.Text = "scaleY"
*token.Children = args[2:3]
} else if sx.IsOne() && sy.IsOne() {
// "scale3d(1, 1, sz)" => "scaleZ(sz)"
token.Text = "scaleZ"
*token.Children = args[4:]
} else if sz.IsOne() {
// "scale3d(sx, sy, 1)" => "scale(sx, sy)"
token.Text = "scale"
*token.Children = args[:3]
}
}
case "scalez":
// specifies a 3D scale operation using the [1,1,sz] scaling vector,
// where sz is given as the parameter.
if n == 1 {
turnPercentIntoNumberIfShorter(&args[0])
}
case "rotate3d":
// specifies a 3D rotation by the angle specified in last parameter
// about the [x,y,z] direction vector described by the first three
// parameters. A direction vector that cannot be normalized, such as
// [0,0,0], will cause the rotation to not be applied.
if n == 7 {
x, y, z, angle := &args[0], &args[2], &args[4], &args[6]
angle.TurnLengthIntoNumberIfZero()
if x.IsOne() && y.IsZero() && z.IsZero() {
// "rotate3d(1, 0, 0, angle)" => "rotateX(angle)"
token.Text = "rotateX"
*token.Children = args[6:]
} else if x.IsZero() && y.IsOne() && z.IsZero() {
// "rotate3d(0, 1, 0, angle)" => "rotateY(angle)"
token.Text = "rotateY"
*token.Children = args[6:]
} else if x.IsZero() && y.IsZero() && z.IsOne() {
// "rotate3d(0, 0, 1, angle)" => "rotate(angle)"
token.Text = "rotate"
*token.Children = args[6:]
}
}
case "rotatex":
// same as rotate3d(1, 0, 0, <angle>).
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "rotatey":
// same as rotate3d(0, 1, 0, <angle>).
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
case "rotatez":
// same as rotate3d(0, 0, 1, <angle>), which is a 3d transform
// equivalent to the 2d transform rotate(<angle>).
if n == 1 {
// "rotateZ(angle)" => "rotate(angle)"
token.Text = "rotate"
args[0].TurnLengthIntoNumberIfZero()
}
case "perspective":
// specifies a perspective projection matrix. This matrix scales
// points in X and Y based on their Z value, scaling points with
// positive Z values away from the origin, and those with negative Z
// values towards the origin. Points on the z=0 plane are unchanged.
// The parameter represents the distance of the z=0 plane from the
// viewer.
if n == 1 {
args[0].TurnLengthIntoNumberIfZero()
}
}
// Trim whitespace at the ends
if args := *token.Children; len(args) > 0 {
args[0].Whitespace &= ^css_ast.WhitespaceBefore
args[len(args)-1].Whitespace &= ^css_ast.WhitespaceAfter
}
}
}
}
return tokens
}

File diff suppressed because it is too large Load Diff

View File

@ -1,342 +0,0 @@
package css_parser
import (
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) parseSelectorList() (list []css_ast.ComplexSelector, ok bool) {
// Parse the first selector
p.eat(css_lexer.TWhitespace)
sel, good := p.parseComplexSelector()
if !good {
return
}
list = append(list, sel)
// Parse the remaining selectors
for {
p.eat(css_lexer.TWhitespace)
if !p.eat(css_lexer.TComma) {
break
}
p.eat(css_lexer.TWhitespace)
sel, good := p.parseComplexSelector()
if !good {
return
}
list = append(list, sel)
}
ok = true
return
}
func (p *parser) parseComplexSelector() (result css_ast.ComplexSelector, ok bool) {
// Parent
sel, good := p.parseCompoundSelector()
if !good {
return
}
result.Selectors = append(result.Selectors, sel)
for {
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TEndOfFile) || p.peek(css_lexer.TComma) || p.peek(css_lexer.TOpenBrace) {
break
}
// Optional combinator
combinator := p.parseCombinator()
if combinator != "" {
p.eat(css_lexer.TWhitespace)
}
// Child
sel, good := p.parseCompoundSelector()
if !good {
return
}
sel.Combinator = combinator
result.Selectors = append(result.Selectors, sel)
}
ok = true
return
}
func (p *parser) nameToken() css_ast.NameToken {
return css_ast.NameToken{
Kind: p.current().Kind,
Text: p.decoded(),
}
}
func (p *parser) parseCompoundSelector() (sel css_ast.CompoundSelector, ok bool) {
// This is an extension: https://drafts.csswg.org/css-nesting-1/
if p.eat(css_lexer.TDelimAmpersand) {
sel.HasNestPrefix = true
}
// Parse the type selector
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TIdent, css_lexer.TDelimAsterisk:
nsName := css_ast.NamespacedName{}
if !p.peek(css_lexer.TDelimBar) {
nsName.Name = p.nameToken()
p.advance()
} else {
// Hack: Create an empty "identifier" to represent this
nsName.Name.Kind = css_lexer.TIdent
}
if p.eat(css_lexer.TDelimBar) {
if !p.peek(css_lexer.TIdent) && !p.peek(css_lexer.TDelimAsterisk) {
p.expect(css_lexer.TIdent)
return
}
prefix := nsName.Name
nsName.NamespacePrefix = &prefix
nsName.Name = p.nameToken()
p.advance()
}
sel.TypeSelector = &nsName
}
// Parse the subclass selectors
subclassSelectors:
for {
switch p.current().Kind {
case css_lexer.THash:
if !p.current().IsID {
break subclassSelectors
}
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, &css_ast.SSHash{Name: name})
p.advance()
case css_lexer.TDelimDot:
p.advance()
name := p.decoded()
sel.SubclassSelectors = append(sel.SubclassSelectors, &css_ast.SSClass{Name: name})
p.expect(css_lexer.TIdent)
case css_lexer.TOpenBracket:
p.advance()
attr, good := p.parseAttributeSelector()
if !good {
return
}
sel.SubclassSelectors = append(sel.SubclassSelectors, &attr)
case css_lexer.TColon:
if p.next().Kind == css_lexer.TColon {
// Special-case the start of the pseudo-element selector section
for p.current().Kind == css_lexer.TColon {
isElement := p.next().Kind == css_lexer.TColon
if isElement {
p.advance()
}
pseudo := p.parsePseudoClassSelector()
// https://www.w3.org/TR/selectors-4/#single-colon-pseudos
// The four Level 2 pseudo-elements (::before, ::after, ::first-line,
// and ::first-letter) may, for legacy reasons, be represented using
// the <pseudo-class-selector> grammar, with only a single ":"
// character at their start.
if p.options.MangleSyntax && isElement && len(pseudo.Args) == 0 {
switch pseudo.Name {
case "before", "after", "first-line", "first-letter":
isElement = false
}
}
pseudo.IsElement = isElement
sel.SubclassSelectors = append(sel.SubclassSelectors, &pseudo)
}
break subclassSelectors
}
pseudo := p.parsePseudoClassSelector()
sel.SubclassSelectors = append(sel.SubclassSelectors, &pseudo)
default:
break subclassSelectors
}
}
// The compound selector must be non-empty
if !sel.HasNestPrefix && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0 {
p.unexpected()
return
}
ok = true
return
}
func (p *parser) parseAttributeSelector() (attr css_ast.SSAttribute, ok bool) {
// Parse the namespaced name
switch p.current().Kind {
case css_lexer.TDelimBar, css_lexer.TDelimAsterisk:
// "[|x]"
// "[*|x]"
if p.peek(css_lexer.TDelimAsterisk) {
prefix := p.nameToken()
p.advance()
attr.NamespacedName.NamespacePrefix = &prefix
} else {
// "[|attr]" is equivalent to "[attr]". From the specification:
// "In keeping with the Namespaces in the XML recommendation, default
// namespaces do not apply to attributes, therefore attribute selectors
// without a namespace component apply only to attributes that have no
// namespace (equivalent to |attr)."
}
if !p.expect(css_lexer.TDelimBar) {
return
}
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
default:
// "[x]"
// "[x|y]"
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
if p.next().Kind != css_lexer.TDelimEquals && p.eat(css_lexer.TDelimBar) {
prefix := attr.NamespacedName.Name
attr.NamespacedName.NamespacePrefix = &prefix
attr.NamespacedName.Name = p.nameToken()
if !p.expect(css_lexer.TIdent) {
return
}
}
}
// Parse the optional matcher operator
p.eat(css_lexer.TWhitespace)
if p.eat(css_lexer.TDelimEquals) {
attr.MatcherOp = "="
} else {
switch p.current().Kind {
case css_lexer.TDelimTilde:
attr.MatcherOp = "~="
case css_lexer.TDelimBar:
attr.MatcherOp = "|="
case css_lexer.TDelimCaret:
attr.MatcherOp = "^="
case css_lexer.TDelimDollar:
attr.MatcherOp = "$="
case css_lexer.TDelimAsterisk:
attr.MatcherOp = "*="
}
if attr.MatcherOp != "" {
p.advance()
p.expect(css_lexer.TDelimEquals)
}
}
// Parse the optional matcher value
if attr.MatcherOp != "" {
p.eat(css_lexer.TWhitespace)
if !p.peek(css_lexer.TString) && !p.peek(css_lexer.TIdent) {
p.unexpected()
}
attr.MatcherValue = p.decoded()
p.advance()
p.eat(css_lexer.TWhitespace)
if p.peek(css_lexer.TIdent) {
if modifier := p.decoded(); len(modifier) == 1 {
if c := modifier[0]; c == 'i' || c == 'I' || c == 's' || c == 'S' {
attr.MatcherModifier = c
p.advance()
}
}
}
}
p.expect(css_lexer.TCloseBracket)
ok = true
return
}
func (p *parser) parsePseudoClassSelector() css_ast.SSPseudoClass {
p.advance()
if p.peek(css_lexer.TFunction) {
text := p.decoded()
p.advance()
args := p.convertTokens(p.parseAnyValue())
p.expect(css_lexer.TCloseParen)
return css_ast.SSPseudoClass{Name: text, Args: args}
}
name := p.decoded()
sel := css_ast.SSPseudoClass{}
if p.expect(css_lexer.TIdent) {
sel.Name = name
}
return sel
}
func (p *parser) parseAnyValue() []css_lexer.Token {
// Reference: https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value
p.stack = p.stack[:0] // Reuse allocated memory
start := p.index
loop:
for {
switch p.current().Kind {
case css_lexer.TCloseParen, css_lexer.TCloseBracket, css_lexer.TCloseBrace:
last := len(p.stack) - 1
if last < 0 || !p.peek(p.stack[last]) {
break loop
}
p.stack = p.stack[:last]
case css_lexer.TSemicolon, css_lexer.TDelimExclamation:
if len(p.stack) == 0 {
break loop
}
case css_lexer.TOpenParen, css_lexer.TFunction:
p.stack = append(p.stack, css_lexer.TCloseParen)
case css_lexer.TOpenBracket:
p.stack = append(p.stack, css_lexer.TCloseBracket)
case css_lexer.TOpenBrace:
p.stack = append(p.stack, css_lexer.TCloseBrace)
}
p.advance()
}
tokens := p.tokens[start:p.index]
if len(tokens) == 0 {
p.unexpected()
}
return tokens
}
func (p *parser) parseCombinator() string {
switch p.current().Kind {
case css_lexer.TDelimGreaterThan:
p.advance()
return ">"
case css_lexer.TDelimPlus:
p.advance()
return "+"
case css_lexer.TDelimTilde:
p.advance()
return "~"
default:
return ""
}
}

View File

@ -1,575 +0,0 @@
package css_parser
import (
"fmt"
"math"
"strconv"
"strings"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)
func (p *parser) tryToReduceCalcExpression(token css_ast.Token) css_ast.Token {
if term := tryToParseCalcTerm(*token.Children); term != nil {
whitespace := css_ast.WhitespaceBefore | css_ast.WhitespaceAfter
if p.options.RemoveWhitespace {
whitespace = 0
}
term = term.partiallySimplify()
if result, ok := term.convertToToken(whitespace); ok {
if result.Kind == css_lexer.TOpenParen {
result.Kind = css_lexer.TFunction
result.Text = "calc"
}
return result
}
}
return token
}
// See: https://www.w3.org/TR/css-values-4/#calc-internal
type calcTerm interface {
convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool)
partiallySimplify() calcTerm
}
type calcSum struct {
terms []calcTerm
}
type calcProduct struct {
terms []calcTerm
}
type calcNegate struct {
term calcTerm
}
type calcInvert struct {
term calcTerm
}
type calcNumeric struct {
number float64
unit string
}
type calcValue struct {
token css_ast.Token
isInvalidPlusOrMinus bool
}
func floatToStringForCalc(a float64) (string, bool) {
// Handle non-finite cases
if math.IsNaN(a) || math.IsInf(a, 0) {
return "", false
}
// Print the number as a string
text := fmt.Sprintf("%.05f", a)
for text[len(text)-1] == '0' {
text = text[:len(text)-1]
}
if text[len(text)-1] == '.' {
text = text[:len(text)-1]
}
if strings.HasPrefix(text, "0.") {
text = text[1:]
} else if strings.HasPrefix(text, "-0.") {
text = "-" + text[2:]
}
// Bail if the number is not exactly represented
if number, err := strconv.ParseFloat(text, 64); err != nil || number != a {
return "", false
}
return text, true
}
func (c *calcSum) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
if product, ok := c.terms[0].(*calcProduct); ok {
token, ok := product.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, *token.Children...)
} else {
token, ok := c.terms[0].convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
}
for _, term := range c.terms[1:] {
// If child is a Negate node, append " - " to s, then serialize the Negates child and append the result to s.
if negate, ok := term.(*calcNegate); ok {
token, ok := negate.term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimMinus,
Text: "-",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
}, token)
continue
}
// If child is a negative numeric value, append " - " to s, then serialize the negation of child as normal and append the result to s.
if numeric, ok := term.(*calcNumeric); ok && numeric.number < 0 {
clone := *numeric
clone.number = -clone.number
token, ok := clone.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimMinus,
Text: "-",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
}, token)
continue
}
// Otherwise, append " + " to s, then serialize child and append the result to s.
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimPlus,
Text: "+",
Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter,
})
// ALGORITHM DEVIATION: Avoid parenthesizing product nodes inside sum nodes
if product, ok := term.(*calcProduct); ok {
token, ok := product.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, *token.Children...)
} else {
token, ok := term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
}
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &tokens,
}, true
}
func (c *calcProduct) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
tokens := make([]css_ast.Token, 0, len(c.terms)*2)
token, ok := c.terms[0].convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, token)
for _, term := range c.terms[1:] {
// If child is an Invert node, append " / " to s, then serialize the Inverts child and append the result to s.
if invert, ok := term.(*calcInvert); ok {
token, ok := invert.term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimSlash,
Text: "/",
Whitespace: whitespace,
}, token)
continue
}
// Otherwise, append " * " to s, then serialize child and append the result to s.
token, ok := term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
tokens = append(tokens, css_ast.Token{
Kind: css_lexer.TDelimAsterisk,
Text: "*",
Whitespace: whitespace,
}, token)
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &tokens,
}, true
}
func (c *calcNegate) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
token, ok := c.term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &[]css_ast.Token{
{Kind: css_lexer.TNumber, Text: "-1"},
{Kind: css_lexer.TDelimSlash, Text: "*", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
token,
},
}, true
}
func (c *calcInvert) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
// Specification: https://www.w3.org/TR/css-values-4/#calc-serialize
token, ok := c.term.convertToToken(whitespace)
if !ok {
return css_ast.Token{}, false
}
return css_ast.Token{
Kind: css_lexer.TOpenParen,
Text: "(",
Children: &[]css_ast.Token{
{Kind: css_lexer.TNumber, Text: "1"},
{Kind: css_lexer.TDelimSlash, Text: "/", Whitespace: css_ast.WhitespaceBefore | css_ast.WhitespaceAfter},
token,
},
}, true
}
func (c *calcNumeric) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
text, ok := floatToStringForCalc(c.number)
if !ok {
return css_ast.Token{}, false
}
if c.unit == "" {
return css_ast.Token{
Kind: css_lexer.TNumber,
Text: text,
}, true
} else if c.unit == "%" {
return css_ast.Token{
Kind: css_lexer.TPercentage,
Text: text + "%",
}, true
} else {
return css_ast.Token{
Kind: css_lexer.TDimension,
Text: text + c.unit,
UnitOffset: uint16(len(text)),
}, true
}
}
func (c *calcValue) convertToToken(whitespace css_ast.WhitespaceFlags) (css_ast.Token, bool) {
t := c.token
t.Whitespace = 0
return t, true
}
func (c *calcSum) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
// For each of roots children that are Sum nodes, replace them with their children.
terms := make([]calcTerm, 0, len(c.terms))
for _, term := range c.terms {
term = term.partiallySimplify()
if sum, ok := term.(*calcSum); ok {
terms = append(terms, sum.terms...)
} else {
terms = append(terms, term)
}
}
// For each set of roots children that are numeric values with identical units, remove
// those children and replace them with a single numeric value containing the sum of the
// removed nodes, and with the same unit. (E.g. combine numbers, combine percentages,
// combine px values, etc.)
for i := 0; i < len(terms); i++ {
term := terms[i]
if numeric, ok := term.(*calcNumeric); ok {
end := i + 1
for j := end; j < len(terms); j++ {
term2 := terms[j]
if numeric2, ok := term2.(*calcNumeric); ok && numeric2.unit == numeric.unit {
numeric.number += numeric2.number
} else {
terms[end] = term2
end++
}
}
terms = terms[:end]
}
}
// If root has only a single child at this point, return the child.
if len(terms) == 1 {
return terms[0]
}
// Otherwise, return root.
c.terms = terms
return c
}
func (c *calcProduct) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
// For each of roots children that are Product nodes, replace them with their children.
terms := make([]calcTerm, 0, len(c.terms))
for _, term := range c.terms {
term = term.partiallySimplify()
if product, ok := term.(*calcProduct); ok {
terms = append(terms, product.terms...)
} else {
terms = append(terms, term)
}
}
// If root has multiple children that are numbers (not percentages or dimensions), remove
// them and replace them with a single number containing the product of the removed nodes.
for i, term := range terms {
if numeric, ok := term.(*calcNumeric); ok && numeric.unit == "" {
end := i + 1
for j := end; j < len(terms); j++ {
term2 := terms[j]
if numeric2, ok := term2.(*calcNumeric); ok && numeric2.unit == "" {
numeric.number *= numeric2.number
} else {
terms[end] = term2
end++
}
}
terms = terms[:end]
break
}
}
// If root contains only numeric values and/or Invert nodes containing numeric values,
// and multiplying the types of all the children (noting that the type of an Invert
// node is the inverse of its childs type) results in a type that matches any of the
// types that a math function can resolve to, return the result of multiplying all the
// values of the children (noting that the value of an Invert node is the reciprocal
// of its childs value), expressed in the results canonical unit.
if len(terms) == 2 {
// Right now, only handle the case of two numbers, one of which has no unit
if first, ok := terms[0].(*calcNumeric); ok {
if second, ok := terms[1].(*calcNumeric); ok {
if first.unit == "" {
second.number *= first.number
return second
}
if second.unit == "" {
first.number *= second.number
return first
}
}
}
}
// ALGORITHM DEVIATION: Divide instead of multiply if the reciprocal is shorter
for i := 1; i < len(terms); i++ {
if numeric, ok := terms[i].(*calcNumeric); ok {
reciprocal := 1 / numeric.number
if multiply, ok := floatToStringForCalc(numeric.number); ok {
if divide, ok := floatToStringForCalc(reciprocal); ok && len(divide) < len(multiply) {
numeric.number = reciprocal
terms[i] = &calcInvert{term: numeric}
}
}
}
}
// If root has only a single child at this point, return the child.
if len(terms) == 1 {
return terms[0]
}
// Otherwise, return root.
c.terms = terms
return c
}
func (c *calcNegate) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
c.term = c.term.partiallySimplify()
// If roots child is a numeric value, return an equivalent numeric value, but with the value negated (0 - value).
if numeric, ok := c.term.(*calcNumeric); ok {
numeric.number = -numeric.number
return numeric
}
// If roots child is a Negate node, return the childs child.
if negate, ok := c.term.(*calcNegate); ok {
return negate.term
}
return c
}
func (c *calcInvert) partiallySimplify() calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-simplification
c.term = c.term.partiallySimplify()
// If roots child is a number (not a percentage or dimension) return the reciprocal of the childs value.
if numeric, ok := c.term.(*calcNumeric); ok && numeric.unit == "" {
numeric.number = 1 / numeric.number
return numeric
}
// If roots child is an Invert node, return the childs child.
if invert, ok := c.term.(*calcInvert); ok {
return invert.term
}
return c
}
func (c *calcNumeric) partiallySimplify() calcTerm {
return c
}
func (c *calcValue) partiallySimplify() calcTerm {
return c
}
func tryToParseCalcTerm(tokens []css_ast.Token) calcTerm {
// Specification: https://www.w3.org/TR/css-values-4/#calc-internal
terms := make([]calcTerm, len(tokens))
for i, token := range tokens {
var term calcTerm
if token.Kind == css_lexer.TFunction && token.Text == "var" {
// Using "var()" should bail because it can expand to any number of tokens
return nil
} else if token.Kind == css_lexer.TOpenParen || (token.Kind == css_lexer.TFunction && token.Text == "calc") {
term = tryToParseCalcTerm(*token.Children)
if term == nil {
return nil
}
} else if token.Kind == css_lexer.TNumber {
if number, err := strconv.ParseFloat(token.Text, 64); err == nil {
term = &calcNumeric{number: number}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TPercentage {
if number, err := strconv.ParseFloat(token.PercentageValue(), 64); err == nil {
term = &calcNumeric{number: number, unit: "%"}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TDimension {
if number, err := strconv.ParseFloat(token.DimensionValue(), 64); err == nil {
term = &calcNumeric{number: number, unit: token.DimensionUnit()}
} else {
term = &calcValue{token: token}
}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "Infinity") {
term = &calcNumeric{number: math.Inf(1)}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "-Infinity") {
term = &calcNumeric{number: math.Inf(-1)}
} else if token.Kind == css_lexer.TIdent && strings.EqualFold(token.Text, "NaN") {
term = &calcNumeric{number: math.NaN()}
} else {
term = &calcValue{
token: token,
// From the specification: "In addition, whitespace is required on both sides of the
// + and - operators. (The * and / operators can be used without white space around them.)"
isInvalidPlusOrMinus: i > 0 && i+1 < len(tokens) &&
(token.Kind == css_lexer.TDelimPlus || token.Kind == css_lexer.TDelimMinus) &&
(((token.Whitespace&css_ast.WhitespaceBefore) == 0 && (tokens[i-1].Whitespace&css_ast.WhitespaceAfter) == 0) ||
(token.Whitespace&css_ast.WhitespaceAfter) == 0 && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) == 0),
}
}
terms[i] = term
}
// Collect children into Product and Invert nodes
first := 1
for first+1 < len(terms) {
// If this is a "*" or "/" operator
if value, ok := terms[first].(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
// Scan over the run
last := first
for last+3 < len(terms) {
if value, ok := terms[last+2].(*calcValue); ok && (value.token.Kind == css_lexer.TDelimAsterisk || value.token.Kind == css_lexer.TDelimSlash) {
last += 2
} else {
break
}
}
// Generate a node for the run
product := calcProduct{terms: make([]calcTerm, (last-first)/2+2)}
for i := range product.terms {
term := terms[first+i*2-1]
if i > 0 && terms[first+i*2-2].(*calcValue).token.Kind == css_lexer.TDelimSlash {
term = &calcInvert{term: term}
}
product.terms[i] = term
}
// Replace the run with a single node
terms[first-1] = &product
terms = append(terms[:first], terms[last+2:]...)
continue
}
first++
}
// Collect children into Sum and Negate nodes
first = 1
for first+1 < len(terms) {
// If this is a "+" or "-" operator
if value, ok := terms[first].(*calcValue); ok && !value.isInvalidPlusOrMinus &&
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
// Scan over the run
last := first
for last+3 < len(terms) {
if value, ok := terms[last+2].(*calcValue); ok && !value.isInvalidPlusOrMinus &&
(value.token.Kind == css_lexer.TDelimPlus || value.token.Kind == css_lexer.TDelimMinus) {
last += 2
} else {
break
}
}
// Generate a node for the run
sum := calcSum{terms: make([]calcTerm, (last-first)/2+2)}
for i := range sum.terms {
term := terms[first+i*2-1]
if i > 0 && terms[first+i*2-2].(*calcValue).token.Kind == css_lexer.TDelimMinus {
term = &calcNegate{term: term}
}
sum.terms[i] = term
}
// Replace the run with a single node
terms[first-1] = &sum
terms = append(terms[:first], terms[last+2:]...)
continue
}
first++
}
// This only succeeds if everything reduces to a single term
if len(terms) == 1 {
return terms[0]
}
return nil
}

View File

@ -1,725 +0,0 @@
package css_printer
import (
"fmt"
"strings"
"unicode/utf8"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/sourcemap"
)
const quoteForURL byte = 0
type printer struct {
options Options
importRecords []ast.ImportRecord
css []byte
extractedLegalComments map[string]bool
builder sourcemap.ChunkBuilder
}
type Options struct {
RemoveWhitespace bool
ASCIIOnly bool
AddSourceMappings bool
LegalComments config.LegalComments
// If we're writing out a source map, this table of line start indices lets
// us do binary search on to figure out what line a given AST node came from
LineOffsetTables []sourcemap.LineOffsetTable
// This will be present if the input file had a source map. In that case we
// want to map all the way back to the original input file(s).
InputSourceMap *sourcemap.SourceMap
}
type PrintResult struct {
CSS []byte
ExtractedLegalComments map[string]bool
SourceMapChunk sourcemap.Chunk
}
func Print(tree css_ast.AST, options Options) PrintResult {
p := printer{
options: options,
importRecords: tree.ImportRecords,
builder: sourcemap.MakeChunkBuilder(options.InputSourceMap, options.LineOffsetTables),
}
for _, rule := range tree.Rules {
p.printRule(rule, 0, false)
}
return PrintResult{
CSS: p.css,
ExtractedLegalComments: p.extractedLegalComments,
SourceMapChunk: p.builder.GenerateChunk(p.css),
}
}
func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicolon bool) {
if r, ok := rule.Data.(*css_ast.RComment); ok {
switch p.options.LegalComments {
case config.LegalCommentsNone:
return
case config.LegalCommentsEndOfFile,
config.LegalCommentsLinkedWithComment,
config.LegalCommentsExternalWithoutComment:
if p.extractedLegalComments == nil {
p.extractedLegalComments = make(map[string]bool)
}
p.extractedLegalComments[r.Text] = true
return
}
}
if p.options.AddSourceMappings {
p.builder.AddSourceMapping(rule.Loc, p.css)
}
if !p.options.RemoveWhitespace {
p.printIndent(indent)
}
switch r := rule.Data.(type) {
case *css_ast.RAtCharset:
// It's not valid to remove the space in between these two tokens
p.print("@charset ")
// It's not valid to print the string with single quotes
p.printQuotedWithQuote(r.Encoding, '"')
p.print(";")
case *css_ast.RAtImport:
if p.options.RemoveWhitespace {
p.print("@import")
} else {
p.print("@import ")
}
p.printQuoted(p.importRecords[r.ImportRecordIndex].Path.Text)
p.printTokens(r.ImportConditions, printTokensOpts{})
p.print(";")
case *css_ast.RAtKeyframes:
p.print("@")
p.printIdent(r.AtToken, identNormal, mayNeedWhitespaceAfter)
p.print(" ")
if r.Name == "" {
p.print("\"\"")
} else {
p.printIdent(r.Name, identNormal, canDiscardWhitespaceAfter)
}
if !p.options.RemoveWhitespace {
p.print(" ")
}
if p.options.RemoveWhitespace {
p.print("{")
} else {
p.print("{\n")
}
indent++
for _, block := range r.Blocks {
if !p.options.RemoveWhitespace {
p.printIndent(indent)
}
for i, sel := range block.Selectors {
if i > 0 {
if p.options.RemoveWhitespace {
p.print(",")
} else {
p.print(", ")
}
}
p.print(sel)
}
if !p.options.RemoveWhitespace {
p.print(" ")
}
p.printRuleBlock(block.Rules, indent)
if !p.options.RemoveWhitespace {
p.print("\n")
}
}
indent--
if !p.options.RemoveWhitespace {
p.printIndent(indent)
}
p.print("}")
case *css_ast.RKnownAt:
p.print("@")
whitespace := mayNeedWhitespaceAfter
if len(r.Prelude) == 0 {
whitespace = canDiscardWhitespaceAfter
}
p.printIdent(r.AtToken, identNormal, whitespace)
if !p.options.RemoveWhitespace || len(r.Prelude) > 0 {
p.print(" ")
}
p.printTokens(r.Prelude, printTokensOpts{})
if !p.options.RemoveWhitespace && len(r.Prelude) > 0 {
p.print(" ")
}
p.printRuleBlock(r.Rules, indent)
case *css_ast.RUnknownAt:
p.print("@")
whitespace := mayNeedWhitespaceAfter
if len(r.Prelude) == 0 {
whitespace = canDiscardWhitespaceAfter
}
p.printIdent(r.AtToken, identNormal, whitespace)
if (!p.options.RemoveWhitespace && r.Block != nil) || len(r.Prelude) > 0 {
p.print(" ")
}
p.printTokens(r.Prelude, printTokensOpts{})
if !p.options.RemoveWhitespace && r.Block != nil && len(r.Prelude) > 0 {
p.print(" ")
}
if r.Block == nil {
p.print(";")
} else {
p.printTokens(r.Block, printTokensOpts{})
}
case *css_ast.RSelector:
p.printComplexSelectors(r.Selectors, indent)
if !p.options.RemoveWhitespace {
p.print(" ")
}
p.printRuleBlock(r.Rules, indent)
case *css_ast.RQualified:
hasWhitespaceAfter := p.printTokens(r.Prelude, printTokensOpts{})
if !hasWhitespaceAfter && !p.options.RemoveWhitespace {
p.print(" ")
}
p.printRuleBlock(r.Rules, indent)
case *css_ast.RDeclaration:
p.printIdent(r.KeyText, identNormal, canDiscardWhitespaceAfter)
p.print(":")
hasWhitespaceAfter := p.printTokens(r.Value, printTokensOpts{
indent: indent,
isDeclaration: true,
})
if r.Important {
if !hasWhitespaceAfter && !p.options.RemoveWhitespace && len(r.Value) > 0 {
p.print(" ")
}
p.print("!important")
}
if !omitTrailingSemicolon {
p.print(";")
}
case *css_ast.RBadDeclaration:
p.printTokens(r.Tokens, printTokensOpts{})
if !omitTrailingSemicolon {
p.print(";")
}
case *css_ast.RComment:
p.printIndentedComment(indent, r.Text)
default:
panic("Internal error")
}
if !p.options.RemoveWhitespace {
p.print("\n")
}
}
func (p *printer) printIndentedComment(indent int32, text string) {
// Avoid generating a comment containing the character sequence "</style"
text = helpers.EscapeClosingTag(text, "/style")
// Re-indent multi-line comments
for {
newline := strings.IndexByte(text, '\n')
if newline == -1 {
break
}
p.print(text[:newline+1])
if !p.options.RemoveWhitespace {
p.printIndent(indent)
}
text = text[newline+1:]
}
p.print(text)
}
func (p *printer) printRuleBlock(rules []css_ast.Rule, indent int32) {
if p.options.RemoveWhitespace {
p.print("{")
} else {
p.print("{\n")
}
for i, decl := range rules {
omitTrailingSemicolon := p.options.RemoveWhitespace && i+1 == len(rules)
p.printRule(decl, indent+1, omitTrailingSemicolon)
}
if !p.options.RemoveWhitespace {
p.printIndent(indent)
}
p.print("}")
}
func (p *printer) printComplexSelectors(selectors []css_ast.ComplexSelector, indent int32) {
for i, complex := range selectors {
if i > 0 {
if p.options.RemoveWhitespace {
p.print(",")
} else {
p.print(",\n")
p.printIndent(indent)
}
}
for j, compound := range complex.Selectors {
p.printCompoundSelector(compound, j == 0, j+1 == len(complex.Selectors))
}
}
}
func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bool, isLast bool) {
if !isFirst && sel.Combinator == "" {
// A space is required in between compound selectors if there is no
// combinator in the middle. It's fine to convert "a + b" into "a+b"
// but not to convert "a b" into "ab".
p.print(" ")
}
if sel.HasNestPrefix {
p.print("&")
}
if sel.Combinator != "" {
if !p.options.RemoveWhitespace {
p.print(" ")
}
p.print(sel.Combinator)
if !p.options.RemoveWhitespace {
p.print(" ")
}
}
if sel.TypeSelector != nil {
whitespace := mayNeedWhitespaceAfter
if len(sel.SubclassSelectors) > 0 {
// There is no chance of whitespace before a subclass selector or pseudo
// class selector
whitespace = canDiscardWhitespaceAfter
}
p.printNamespacedName(*sel.TypeSelector, whitespace)
}
for i, sub := range sel.SubclassSelectors {
whitespace := mayNeedWhitespaceAfter
// There is no chance of whitespace between subclass selectors
if i+1 < len(sel.SubclassSelectors) {
whitespace = canDiscardWhitespaceAfter
}
switch s := sub.(type) {
case *css_ast.SSHash:
p.print("#")
// This deliberately does not use identHash. From the specification:
// "In <id-selector>, the <hash-token>'s value must be an identifier."
p.printIdent(s.Name, identNormal, whitespace)
case *css_ast.SSClass:
p.print(".")
p.printIdent(s.Name, identNormal, whitespace)
case *css_ast.SSAttribute:
p.print("[")
p.printNamespacedName(s.NamespacedName, canDiscardWhitespaceAfter)
if s.MatcherOp != "" {
p.print(s.MatcherOp)
printAsIdent := false
// Print the value as an identifier if it's possible
if css_lexer.WouldStartIdentifierWithoutEscapes(s.MatcherValue) {
printAsIdent = true
for _, c := range s.MatcherValue {
if !css_lexer.IsNameContinue(c) {
printAsIdent = false
break
}
}
}
if printAsIdent {
p.printIdent(s.MatcherValue, identNormal, canDiscardWhitespaceAfter)
} else {
p.printQuoted(s.MatcherValue)
}
}
if s.MatcherModifier != 0 {
p.print(" ")
p.print(string(rune(s.MatcherModifier)))
}
p.print("]")
case *css_ast.SSPseudoClass:
p.printPseudoClassSelector(*s, whitespace)
}
}
}
func (p *printer) printNamespacedName(nsName css_ast.NamespacedName, whitespace trailingWhitespace) {
if nsName.NamespacePrefix != nil {
switch nsName.NamespacePrefix.Kind {
case css_lexer.TIdent:
p.printIdent(nsName.NamespacePrefix.Text, identNormal, canDiscardWhitespaceAfter)
case css_lexer.TDelimAsterisk:
p.print("*")
default:
panic("Internal error")
}
p.print("|")
}
switch nsName.Name.Kind {
case css_lexer.TIdent:
p.printIdent(nsName.Name.Text, identNormal, whitespace)
case css_lexer.TDelimAsterisk:
p.print("*")
case css_lexer.TDelimAmpersand:
p.print("&")
default:
panic("Internal error")
}
}
func (p *printer) printPseudoClassSelector(pseudo css_ast.SSPseudoClass, whitespace trailingWhitespace) {
if pseudo.IsElement {
p.print("::")
} else {
p.print(":")
}
if len(pseudo.Args) > 0 {
p.printIdent(pseudo.Name, identNormal, canDiscardWhitespaceAfter)
p.print("(")
p.printTokens(pseudo.Args, printTokensOpts{})
p.print(")")
} else {
p.printIdent(pseudo.Name, identNormal, whitespace)
}
}
func (p *printer) print(text string) {
p.css = append(p.css, text...)
}
func bestQuoteCharForString(text string, forURL bool) byte {
forURLCost := 0
singleCost := 2
doubleCost := 2
for _, c := range text {
switch c {
case '\'':
forURLCost++
singleCost++
case '"':
forURLCost++
doubleCost++
case '(', ')', ' ', '\t':
forURLCost++
case '\\', '\n', '\r', '\f':
forURLCost++
singleCost++
doubleCost++
}
}
// Quotes can sometimes be omitted for URL tokens
if forURL && forURLCost < singleCost && forURLCost < doubleCost {
return quoteForURL
}
// Prefer double quotes to single quotes if there is no cost difference
if singleCost < doubleCost {
return '\''
}
return '"'
}
func (p *printer) printQuoted(text string) {
p.printQuotedWithQuote(text, bestQuoteCharForString(text, false))
}
type escapeKind uint8
const (
escapeNone escapeKind = iota
escapeBackslash
escapeHex
)
func (p *printer) printWithEscape(c rune, escape escapeKind, remainingText string, mayNeedWhitespaceAfter bool) {
var temp [utf8.UTFMax]byte
if escape == escapeBackslash && ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
// Hexadecimal characters cannot use a plain backslash escape
escape = escapeHex
}
switch escape {
case escapeNone:
width := utf8.EncodeRune(temp[:], c)
p.css = append(p.css, temp[:width]...)
case escapeBackslash:
p.css = append(p.css, '\\')
width := utf8.EncodeRune(temp[:], c)
p.css = append(p.css, temp[:width]...)
case escapeHex:
text := fmt.Sprintf("\\%x", c)
p.css = append(p.css, text...)
// Make sure the next character is not interpreted as part of the escape sequence
if len(text) < 1+6 {
if next := utf8.RuneLen(c); next < len(remainingText) {
c = rune(remainingText[next])
if c == ' ' || c == '\t' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') {
p.css = append(p.css, ' ')
}
} else if mayNeedWhitespaceAfter {
// If the last character is a hexadecimal escape, print a space afterwards
// for the escape sequence to consume. That way we're sure it won't
// accidentally consume a semantically significant space afterward.
p.css = append(p.css, ' ')
}
}
}
}
func (p *printer) printQuotedWithQuote(text string, quote byte) {
if quote != quoteForURL {
p.css = append(p.css, quote)
}
for i, c := range text {
escape := escapeNone
switch c {
case '\x00', '\r', '\n', '\f':
// Use a hexadecimal escape for characters that would be invalid escapes
escape = escapeHex
case '\\', rune(quote):
escape = escapeBackslash
case '(', ')', ' ', '\t', '"', '\'':
// These characters must be escaped in URL tokens
if quote == quoteForURL {
escape = escapeBackslash
}
case '/':
// Avoid generating the sequence "</style" in CSS code
if i >= 1 && text[i-1] == '<' && i+6 <= len(text) && strings.EqualFold(text[i+1:i+6], "style") {
escape = escapeBackslash
}
default:
if (p.options.ASCIIOnly && c >= 0x80) || c == '\uFEFF' {
escape = escapeHex
}
}
p.printWithEscape(c, escape, text[i:], false)
}
if quote != quoteForURL {
p.css = append(p.css, quote)
}
}
type identMode uint8
const (
identNormal identMode = iota
identHash
identDimensionUnit
)
type trailingWhitespace uint8
const (
mayNeedWhitespaceAfter trailingWhitespace = iota
canDiscardWhitespaceAfter
)
func (p *printer) printIdent(text string, mode identMode, whitespace trailingWhitespace) {
for i, c := range text {
escape := escapeNone
if p.options.ASCIIOnly && c >= 0x80 {
escape = escapeHex
} else if c == '\r' || c == '\n' || c == '\f' || c == '\uFEFF' {
// Use a hexadecimal escape for characters that would be invalid escapes
escape = escapeHex
} else {
// Escape non-identifier characters
if !css_lexer.IsNameContinue(c) {
escape = escapeBackslash
}
// Special escape behavior for the first character
if i == 0 {
switch mode {
case identNormal:
if !css_lexer.WouldStartIdentifierWithoutEscapes(text) {
escape = escapeBackslash
}
case identDimensionUnit:
if !css_lexer.WouldStartIdentifierWithoutEscapes(text) {
escape = escapeBackslash
} else if c >= '0' && c <= '9' {
// Unit: "2x"
escape = escapeHex
} else if c == 'e' || c == 'E' {
if len(text) >= 2 && text[1] >= '0' && text[1] <= '9' {
// Unit: "e2x"
escape = escapeBackslash
} else if len(text) >= 3 && text[1] == '-' && text[2] >= '0' && text[2] <= '9' {
// Unit: "e-2x"
escape = escapeBackslash
}
}
}
}
}
// If the last character is a hexadecimal escape, print a space afterwards
// for the escape sequence to consume. That way we're sure it won't
// accidentally consume a semantically significant space afterward.
mayNeedWhitespaceAfter := whitespace == mayNeedWhitespaceAfter && escape != escapeNone && i+utf8.RuneLen(c) == len(text)
p.printWithEscape(c, escape, text[i:], mayNeedWhitespaceAfter)
}
}
func (p *printer) printIndent(indent int32) {
for i, n := 0, int(indent); i < n; i++ {
p.css = append(p.css, " "...)
}
}
type printTokensOpts struct {
indent int32
isDeclaration bool
}
func (p *printer) printTokens(tokens []css_ast.Token, opts printTokensOpts) bool {
hasWhitespaceAfter := len(tokens) > 0 && (tokens[0].Whitespace&css_ast.WhitespaceBefore) != 0
// Pretty-print long comma-separated declarations of 3 or more items
isMultiLineValue := false
if !p.options.RemoveWhitespace && opts.isDeclaration {
commaCount := 0
for _, t := range tokens {
if t.Kind == css_lexer.TComma {
commaCount++
}
}
isMultiLineValue = commaCount >= 2
}
for i, t := range tokens {
if t.Kind == css_lexer.TWhitespace {
hasWhitespaceAfter = true
continue
}
if hasWhitespaceAfter {
if isMultiLineValue && (i == 0 || tokens[i-1].Kind == css_lexer.TComma) {
p.print("\n")
p.printIndent(opts.indent + 1)
} else {
p.print(" ")
}
}
hasWhitespaceAfter = (t.Whitespace&css_ast.WhitespaceAfter) != 0 ||
(i+1 < len(tokens) && (tokens[i+1].Whitespace&css_ast.WhitespaceBefore) != 0)
whitespace := mayNeedWhitespaceAfter
if !hasWhitespaceAfter {
whitespace = canDiscardWhitespaceAfter
}
switch t.Kind {
case css_lexer.TIdent:
p.printIdent(t.Text, identNormal, whitespace)
case css_lexer.TFunction:
p.printIdent(t.Text, identNormal, whitespace)
p.print("(")
case css_lexer.TDimension:
p.print(t.DimensionValue())
p.printIdent(t.DimensionUnit(), identDimensionUnit, whitespace)
case css_lexer.TAtKeyword:
p.print("@")
p.printIdent(t.Text, identNormal, whitespace)
case css_lexer.THash:
p.print("#")
p.printIdent(t.Text, identHash, whitespace)
case css_lexer.TString:
p.printQuoted(t.Text)
case css_lexer.TURL:
text := p.importRecords[t.ImportRecordIndex].Path.Text
p.print("url(")
p.printQuotedWithQuote(text, bestQuoteCharForString(text, true))
p.print(")")
default:
p.print(t.Text)
}
if t.Children != nil {
p.printTokens(*t.Children, printTokensOpts{})
switch t.Kind {
case css_lexer.TFunction:
p.print(")")
case css_lexer.TOpenParen:
p.print(")")
case css_lexer.TOpenBrace:
p.print("}")
case css_lexer.TOpenBracket:
p.print("]")
}
}
}
if hasWhitespaceAfter {
p.print(" ")
}
return hasWhitespaceAfter
}

View File

@ -1,649 +0,0 @@
// Code in this file has been forked from the "filepath" module in the Go
// source code to work around bugs with the WebAssembly build target. More
// information about why here: https://github.com/golang/go/issues/43768.
////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package fs
import (
"errors"
"os"
"strings"
"syscall"
)
type goFilepath struct {
cwd string
isWindows bool
pathSeparator byte
}
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
// reservedNames lists reserved Windows names. Search for PRN in
// https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
// for details.
var reservedNames = []string{
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
}
// isReservedName returns true, if path is Windows reserved name.
// See reservedNames for the full list.
func isReservedName(path string) bool {
if len(path) == 0 {
return false
}
for _, reserved := range reservedNames {
if strings.EqualFold(path, reserved) {
return true
}
}
return false
}
// IsAbs reports whether the path is absolute.
func (fp goFilepath) isAbs(path string) bool {
if !fp.isWindows {
return strings.HasPrefix(path, "/")
}
if isReservedName(path) {
return true
}
l := fp.volumeNameLen(path)
if l == 0 {
return false
}
path = path[l:]
if path == "" {
return false
}
return isSlash(path[0])
}
// Abs returns an absolute representation of path.
// If the path is not absolute it will be joined with the current
// working directory to turn it into an absolute path. The absolute
// path name for a given file is not guaranteed to be unique.
// Abs calls Clean on the result.
func (fp goFilepath) abs(path string) (string, error) {
if fp.isAbs(path) {
return fp.clean(path), nil
}
return fp.join([]string{fp.cwd, path}), nil
}
// IsPathSeparator reports whether c is a directory separator character.
func (fp goFilepath) isPathSeparator(c uint8) bool {
return c == '/' || (fp.isWindows && c == '\\')
}
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
func (fp goFilepath) volumeNameLen(path string) int {
if !fp.isWindows {
return 0
}
if len(path) < 2 {
return 0
}
// with drive letter
c := path[0]
if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
return 2
}
// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
!isSlash(path[2]) && path[2] != '.' {
// first, leading `\\` and next shouldn't be `\`. its server name.
for n := 3; n < l-1; n++ {
// second, next '\' shouldn't be repeated.
if isSlash(path[n]) {
n++
// third, following something characters. its share name.
if !isSlash(path[n]) {
if path[n] == '.' {
break
}
for ; n < l; n++ {
if isSlash(path[n]) {
break
}
}
return n
}
break
}
}
}
return 0
}
// EvalSymlinks returns the path name after the evaluation of any symbolic
// links.
// If path is relative the result will be relative to the current directory,
// unless one of the components is an absolute symbolic link.
// EvalSymlinks calls Clean on the result.
func (fp goFilepath) evalSymlinks(path string) (string, error) {
volLen := fp.volumeNameLen(path)
pathSeparator := string(fp.pathSeparator)
if volLen < len(path) && fp.isPathSeparator(path[volLen]) {
volLen++
}
vol := path[:volLen]
dest := vol
linksWalked := 0
for start, end := volLen, volLen; start < len(path); start = end {
for start < len(path) && fp.isPathSeparator(path[start]) {
start++
}
end = start
for end < len(path) && !fp.isPathSeparator(path[end]) {
end++
}
// On Windows, "." can be a symlink.
// We look it up, and use the value if it is absolute.
// If not, we just return ".".
isWindowsDot := fp.isWindows && path[fp.volumeNameLen(path):] == "."
// The next path component is in path[start:end].
if end == start {
// No more path components.
break
} else if path[start:end] == "." && !isWindowsDot {
// Ignore path component ".".
continue
} else if path[start:end] == ".." {
// Back up to previous component if possible.
// Note that volLen includes any leading slash.
// Set r to the index of the last slash in dest,
// after the volume.
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if fp.isPathSeparator(dest[r]) {
break
}
}
if r < volLen || dest[r+1:] == ".." {
// Either path has no slashes
// (it's empty or just "C:")
// or it ends in a ".." we had to keep.
// Either way, keep this "..".
if len(dest) > volLen {
dest += pathSeparator
}
dest += ".."
} else {
// Discard everything since the last slash.
dest = dest[:r]
}
continue
}
// Ordinary path component. Add it to result.
if len(dest) > fp.volumeNameLen(dest) && !fp.isPathSeparator(dest[len(dest)-1]) {
dest += pathSeparator
}
dest += path[start:end]
// Resolve symlink.
fi, err := os.Lstat(dest)
if err != nil {
return "", err
}
if fi.Mode()&os.ModeSymlink == 0 {
if !fi.Mode().IsDir() && end < len(path) {
return "", syscall.ENOTDIR
}
continue
}
// Found symlink.
linksWalked++
if linksWalked > 255 {
return "", errors.New("EvalSymlinks: too many links")
}
link, err := os.Readlink(dest)
if err != nil {
return "", err
}
if isWindowsDot && !fp.isAbs(link) {
// On Windows, if "." is a relative symlink,
// just return ".".
break
}
path = link + path[end:]
v := fp.volumeNameLen(link)
if v > 0 {
// Symlink to drive name is an absolute path.
if v < len(link) && fp.isPathSeparator(link[v]) {
v++
}
vol = link[:v]
dest = vol
end = len(vol)
} else if len(link) > 0 && fp.isPathSeparator(link[0]) {
// Symlink to absolute path.
dest = link[:1]
end = 1
} else {
// Symlink to relative path; replace last
// path component in dest.
var r int
for r = len(dest) - 1; r >= volLen; r-- {
if fp.isPathSeparator(dest[r]) {
break
}
}
if r < volLen {
dest = vol
} else {
dest = dest[:r]
}
end = 0
}
}
return fp.clean(dest), nil
}
// A lazybuf is a lazily constructed path buffer.
// It supports append, reading previously appended bytes,
// and retrieving the final string. It does not allocate a buffer
// to hold the output until that output diverges from s.
type lazybuf struct {
path string
buf []byte
w int
volAndPath string
volLen int
}
func (b *lazybuf) index(i int) byte {
if b.buf != nil {
return b.buf[i]
}
return b.path[i]
}
func (b *lazybuf) append(c byte) {
if b.buf == nil {
if b.w < len(b.path) && b.path[b.w] == c {
b.w++
return
}
b.buf = make([]byte, len(b.path))
copy(b.buf, b.path[:b.w])
}
b.buf[b.w] = c
b.w++
}
func (b *lazybuf) string() string {
if b.buf == nil {
return b.volAndPath[:b.volLen+b.w]
}
return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
}
// FromSlash returns the result of replacing each slash ('/') character
// in path with a separator character. Multiple slashes are replaced
// by multiple separators.
func (fp goFilepath) fromSlash(path string) string {
if !fp.isWindows {
return path
}
return strings.ReplaceAll(path, "/", "\\")
}
// Clean returns the shortest path name equivalent to path
// by purely lexical processing. It applies the following rules
// iteratively until no further processing can be done:
//
// 1. Replace multiple Separator elements with a single one.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path,
// assuming Separator is '/'.
//
// The returned path ends in a slash only if it represents a root directory,
// such as "/" on Unix or `C:\` on Windows.
//
// Finally, any occurrences of slash are replaced by Separator.
//
// If the result of this process is an empty string, Clean
// returns the string ".".
//
// See also Rob Pike, ``Lexical File Names in Plan 9 or
// Getting Dot-Dot Right,''
// https://9p.io/sys/doc/lexnames.html
func (fp goFilepath) clean(path string) string {
originalPath := path
volLen := fp.volumeNameLen(path)
path = path[volLen:]
if path == "" {
if volLen > 1 && originalPath[1] != ':' {
// should be UNC
return fp.fromSlash(originalPath)
}
return originalPath + "."
}
rooted := fp.isPathSeparator(path[0])
// Invariants:
// reading from path; r is index of next byte to process.
// writing to buf; w is index of next byte to write.
// dotdot is index in buf where .. must stop, either because
// it is the leading slash or it is a leading ../../.. prefix.
n := len(path)
out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
r, dotdot := 0, 0
if rooted {
out.append(fp.pathSeparator)
r, dotdot = 1, 1
}
for r < n {
switch {
case fp.isPathSeparator(path[r]):
// empty path element
r++
case path[r] == '.' && (r+1 == n || fp.isPathSeparator(path[r+1])):
// . element
r++
case path[r] == '.' && path[r+1] == '.' && (r+2 == n || fp.isPathSeparator(path[r+2])):
// .. element: remove to last separator
r += 2
switch {
case out.w > dotdot:
// can backtrack
out.w--
for out.w > dotdot && !fp.isPathSeparator(out.index(out.w)) {
out.w--
}
case !rooted:
// cannot backtrack, but not rooted, so append .. element.
if out.w > 0 {
out.append(fp.pathSeparator)
}
out.append('.')
out.append('.')
dotdot = out.w
}
default:
// real path element.
// add slash if needed
if rooted && out.w != 1 || !rooted && out.w != 0 {
out.append(fp.pathSeparator)
}
// copy element
for ; r < n && !fp.isPathSeparator(path[r]); r++ {
out.append(path[r])
}
}
}
// Turn empty string into "."
if out.w == 0 {
out.append('.')
}
return fp.fromSlash(out.string())
}
// VolumeName returns leading volume name.
// Given "C:\foo\bar" it returns "C:" on Windows.
// Given "\\host\share\foo" it returns "\\host\share".
// On other platforms it returns "".
func (fp goFilepath) volumeName(path string) string {
return path[:fp.volumeNameLen(path)]
}
// Base returns the last element of path.
// Trailing path separators are removed before extracting the last element.
// If the path is empty, Base returns ".".
// If the path consists entirely of separators, Base returns a single separator.
func (fp goFilepath) base(path string) string {
if path == "" {
return "."
}
// Strip trailing slashes.
for len(path) > 0 && fp.isPathSeparator(path[len(path)-1]) {
path = path[0 : len(path)-1]
}
// Throw away volume name
path = path[len(fp.volumeName(path)):]
// Find the last element
i := len(path) - 1
for i >= 0 && !fp.isPathSeparator(path[i]) {
i--
}
if i >= 0 {
path = path[i+1:]
}
// If empty now, it had only slashes.
if path == "" {
return string(fp.pathSeparator)
}
return path
}
// Dir returns all but the last element of path, typically the path's directory.
// After dropping the final element, Dir calls Clean on the path and trailing
// slashes are removed.
// If the path is empty, Dir returns ".".
// If the path consists entirely of separators, Dir returns a single separator.
// The returned path does not end in a separator unless it is the root directory.
func (fp goFilepath) dir(path string) string {
vol := fp.volumeName(path)
i := len(path) - 1
for i >= len(vol) && !fp.isPathSeparator(path[i]) {
i--
}
dir := fp.clean(path[len(vol) : i+1])
if dir == "." && len(vol) > 2 {
// must be UNC
return vol
}
return vol + dir
}
// Ext returns the file name extension used by path.
// The extension is the suffix beginning at the final dot
// in the final element of path; it is empty if there is
// no dot.
func (fp goFilepath) ext(path string) string {
for i := len(path) - 1; i >= 0 && !fp.isPathSeparator(path[i]); i-- {
if path[i] == '.' {
return path[i:]
}
}
return ""
}
// Join joins any number of path elements into a single path,
// separating them with an OS specific Separator. Empty elements
// are ignored. The result is Cleaned. However, if the argument
// list is empty or all its elements are empty, Join returns
// an empty string.
// On Windows, the result will only be a UNC path if the first
// non-empty element is a UNC path.
func (fp goFilepath) join(elem []string) string {
for i, e := range elem {
if e != "" {
if fp.isWindows {
return fp.joinNonEmpty(elem[i:])
}
return fp.clean(strings.Join(elem[i:], string(fp.pathSeparator)))
}
}
return ""
}
// joinNonEmpty is like join, but it assumes that the first element is non-empty.
func (fp goFilepath) joinNonEmpty(elem []string) string {
if len(elem[0]) == 2 && elem[0][1] == ':' {
// First element is drive letter without terminating slash.
// Keep path relative to current directory on that drive.
// Skip empty elements.
i := 1
for ; i < len(elem); i++ {
if elem[i] != "" {
break
}
}
return fp.clean(elem[0] + strings.Join(elem[i:], string(fp.pathSeparator)))
}
// The following logic prevents Join from inadvertently creating a
// UNC path on Windows. Unless the first element is a UNC path, Join
// shouldn't create a UNC path. See golang.org/issue/9167.
p := fp.clean(strings.Join(elem, string(fp.pathSeparator)))
if !fp.isUNC(p) {
return p
}
// p == UNC only allowed when the first element is a UNC path.
head := fp.clean(elem[0])
if fp.isUNC(head) {
return p
}
// head + tail == UNC, but joining two non-UNC paths should not result
// in a UNC path. Undo creation of UNC path.
tail := fp.clean(strings.Join(elem[1:], string(fp.pathSeparator)))
if head[len(head)-1] == fp.pathSeparator {
return head + tail
}
return head + string(fp.pathSeparator) + tail
}
// isUNC reports whether path is a UNC path.
func (fp goFilepath) isUNC(path string) bool {
return fp.volumeNameLen(path) > 2
}
// Rel returns a relative path that is lexically equivalent to targpath when
// joined to basepath with an intervening separator. That is,
// Join(basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
// On success, the returned path will always be relative to basepath,
// even if basepath and targpath share no elements.
// An error is returned if targpath can't be made relative to basepath or if
// knowing the current working directory would be necessary to compute it.
// Rel calls Clean on the result.
func (fp goFilepath) rel(basepath, targpath string) (string, error) {
baseVol := fp.volumeName(basepath)
targVol := fp.volumeName(targpath)
base := fp.clean(basepath)
targ := fp.clean(targpath)
if fp.sameWord(targ, base) {
return ".", nil
}
base = base[len(baseVol):]
targ = targ[len(targVol):]
if base == "." {
base = ""
}
// Can't use IsAbs - `\a` and `a` are both relative in Windows.
baseSlashed := len(base) > 0 && base[0] == fp.pathSeparator
targSlashed := len(targ) > 0 && targ[0] == fp.pathSeparator
if baseSlashed != targSlashed || !fp.sameWord(baseVol, targVol) {
return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
}
// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
bl := len(base)
tl := len(targ)
var b0, bi, t0, ti int
for {
for bi < bl && base[bi] != fp.pathSeparator {
bi++
}
for ti < tl && targ[ti] != fp.pathSeparator {
ti++
}
if !fp.sameWord(targ[t0:ti], base[b0:bi]) {
break
}
if bi < bl {
bi++
}
if ti < tl {
ti++
}
b0 = bi
t0 = ti
}
if base[b0:bi] == ".." {
return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
}
if b0 != bl {
// Base elements left. Must go up before going down.
seps := strings.Count(base[b0:bl], string(fp.pathSeparator))
size := 2 + seps*3
if tl != t0 {
size += 1 + tl - t0
}
buf := make([]byte, size)
n := copy(buf, "..")
for i := 0; i < seps; i++ {
buf[n] = fp.pathSeparator
copy(buf[n+1:], "..")
n += 3
}
if t0 != tl {
buf[n] = fp.pathSeparator
copy(buf[n+1:], targ[t0:])
}
return string(buf), nil
}
return targ[t0:], nil
}
func (fp goFilepath) sameWord(a, b string) bool {
if !fp.isWindows {
return a == b
}
return strings.EqualFold(a, b)
}

View File

@ -1,270 +0,0 @@
package fs
import (
"errors"
"os"
"sort"
"strings"
"sync"
"syscall"
)
type EntryKind uint8
const (
DirEntry EntryKind = 1
FileEntry EntryKind = 2
)
type Entry struct {
symlink string
dir string
base string
mutex sync.Mutex
kind EntryKind
needStat bool
}
func (e *Entry) Kind(fs FS) EntryKind {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.needStat {
e.needStat = false
e.symlink, e.kind = fs.kind(e.dir, e.base)
}
return e.kind
}
func (e *Entry) Symlink(fs FS) string {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.needStat {
e.needStat = false
e.symlink, e.kind = fs.kind(e.dir, e.base)
}
return e.symlink
}
type accessedEntries struct {
mutex sync.Mutex
wasPresent map[string]bool
// If this is nil, "SortedKeys()" was not accessed. This means we should
// check for whether this directory has changed or not by seeing if any of
// the entries in the "wasPresent" map have changed in "present or not"
// status, since the only access was to individual entries via "Get()".
//
// If this is non-nil, "SortedKeys()" was accessed. This means we should
// check for whether this directory has changed or not by checking the
// "allEntries" array for equality with the existing entries list, since the
// code asked for all entries and may have used the presence or absence of
// entries in that list.
//
// The goal of having these two checks is to be as narrow as possible to
// avoid unnecessary rebuilds. If only "Get()" is called on a few entries,
// then we won't invalidate the build if random unrelated entries are added
// or removed. But if "SortedKeys()" is called, we need to invalidate the
// build if anything about the set of entries in this directory is changed.
allEntries []string
}
type DirEntries struct {
dir string
data map[string]*Entry
accessedEntries *accessedEntries
}
func MakeEmptyDirEntries(dir string) DirEntries {
return DirEntries{dir, make(map[string]*Entry), nil}
}
type DifferentCase struct {
Dir string
Query string
Actual string
}
func (entries DirEntries) Get(query string) (*Entry, *DifferentCase) {
if entries.data != nil {
key := strings.ToLower(query)
entry := entries.data[key]
// Track whether this specific entry was present or absent for watch mode
if accessed := entries.accessedEntries; accessed != nil {
accessed.mutex.Lock()
accessed.wasPresent[key] = entry != nil
accessed.mutex.Unlock()
}
if entry != nil {
if entry.base != query {
return entry, &DifferentCase{
Dir: entries.dir,
Query: query,
Actual: entry.base,
}
}
return entry, nil
}
}
return nil, nil
}
func (entries DirEntries) SortedKeys() (keys []string) {
if entries.data != nil {
keys = make([]string, 0, len(entries.data))
for _, entry := range entries.data {
keys = append(keys, entry.base)
}
sort.Strings(keys)
// Track the exact set of all entries for watch mode
if entries.accessedEntries != nil {
entries.accessedEntries.mutex.Lock()
entries.accessedEntries.allEntries = keys
entries.accessedEntries.mutex.Unlock()
}
return keys
}
return
}
type OpenedFile interface {
Len() int
Read(start int, end int) ([]byte, error)
Close() error
}
type InMemoryOpenedFile struct {
Contents []byte
}
func (f *InMemoryOpenedFile) Len() int {
return len(f.Contents)
}
func (f *InMemoryOpenedFile) Read(start int, end int) ([]byte, error) {
return []byte(f.Contents[start:end]), nil
}
func (f *InMemoryOpenedFile) Close() error {
return nil
}
type FS interface {
// The returned map is immutable and is cached across invocations. Do not
// mutate it.
ReadDirectory(path string) (entries DirEntries, canonicalError error, originalError error)
ReadFile(path string) (contents string, canonicalError error, originalError error)
OpenFile(path string) (result OpenedFile, canonicalError error, originalError error)
// This is a key made from the information returned by "stat". It is intended
// to be different if the file has been edited, and to otherwise be equal if
// the file has not been edited. It should usually work, but no guarantees.
//
// See https://apenwarr.ca/log/20181113 for more information about why this
// can be broken. For example, writing to a file with mmap on WSL on Windows
// won't change this key. Hopefully this isn't too much of an issue.
//
// Additional reading:
// - https://github.com/npm/npm/pull/20027
// - https://github.com/golang/go/commit/7dea509703eb5ad66a35628b12a678110fbb1f72
ModKey(path string) (ModKey, error)
// This is part of the interface because the mock interface used for tests
// should not depend on file system behavior (i.e. different slashes for
// Windows) while the real interface should.
IsAbs(path string) bool
Abs(path string) (string, bool)
Dir(path string) string
Base(path string) string
Ext(path string) string
Join(parts ...string) string
Cwd() string
Rel(base string, target string) (string, bool)
// This is used in the implementation of "Entry"
kind(dir string, base string) (symlink string, kind EntryKind)
// This is a set of all files used and all directories checked. The build
// must be invalidated if any of these watched files change.
WatchData() WatchData
}
type WatchData struct {
// These functions return a non-empty path as a string if the file system
// entry has been modified. For files, the returned path is the same as the
// file path. For directories, the returned path is either the directory
// itself or a file in the directory that was changed.
Paths map[string]func() string
}
type ModKey struct {
// What gets filled in here is OS-dependent
inode uint64
size int64
mtime_sec int64
mtime_nsec int64
mode uint32
uid uint32
}
// Some file systems have a time resolution of only a few seconds. If a mtime
// value is too new, we won't be able to tell if it has been recently modified
// or not. So we only use mtimes for comparison if they are sufficiently old.
// Apparently the FAT file system has a resolution of two seconds according to
// this article: https://en.wikipedia.org/wiki/Stat_(system_call).
const modKeySafetyGap = 3 // In seconds
var modKeyUnusable = errors.New("The modification key is unusable")
// Limit the number of files open simultaneously to avoid ulimit issues
var fileOpenLimit = make(chan bool, 32)
func BeforeFileOpen() {
// This will block if the number of open files is already at the limit
fileOpenLimit <- false
}
func AfterFileClose() {
<-fileOpenLimit
}
// This is a fork of "os.MkdirAll" to work around bugs with the WebAssembly
// build target. More information here: https://github.com/golang/go/issues/43768.
func MkdirAll(fs FS, path string, perm os.FileMode) error {
// Run "Join" once to run "Clean" on the path, which removes trailing slashes
return mkdirAll(fs, fs.Join(path), perm)
}
func mkdirAll(fs FS, path string, perm os.FileMode) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
if dir, err := os.Stat(path); err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}
// Slow path: make sure parent exists and then call Mkdir for path.
if parent := fs.Dir(path); parent != path {
// Create parent.
if err := mkdirAll(fs, parent, perm); err != nil {
return err
}
}
// Parent now exists; invoke Mkdir and use its result.
if err := os.Mkdir(path, perm); err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := os.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}

View File

@ -1,157 +0,0 @@
// This is a mock implementation of the "fs" module for use with tests. It does
// not actually read from the file system. Instead, it reads from a pre-specified
// map of file paths to files.
package fs
import (
"errors"
"path"
"strings"
"syscall"
)
type mockFS struct {
dirs map[string]DirEntries
files map[string]string
}
func MockFS(input map[string]string) FS {
dirs := make(map[string]DirEntries)
files := make(map[string]string)
for k, v := range input {
files[k] = v
original := k
// Build the directory map
for {
kDir := path.Dir(k)
dir, ok := dirs[kDir]
if !ok {
dir = DirEntries{kDir, make(map[string]*Entry), nil}
dirs[kDir] = dir
}
if kDir == k {
break
}
base := path.Base(k)
if k == original {
dir.data[strings.ToLower(base)] = &Entry{kind: FileEntry, base: base}
} else {
dir.data[strings.ToLower(base)] = &Entry{kind: DirEntry, base: base}
}
k = kDir
}
}
return &mockFS{dirs, files}
}
func (fs *mockFS) ReadDirectory(path string) (DirEntries, error, error) {
if dir, ok := fs.dirs[path]; ok {
return dir, nil, nil
}
return DirEntries{}, syscall.ENOENT, syscall.ENOENT
}
func (fs *mockFS) ReadFile(path string) (string, error, error) {
if contents, ok := fs.files[path]; ok {
return contents, nil, nil
}
return "", syscall.ENOENT, syscall.ENOENT
}
func (fs *mockFS) OpenFile(path string) (OpenedFile, error, error) {
if contents, ok := fs.files[path]; ok {
return &InMemoryOpenedFile{Contents: []byte(contents)}, nil, nil
}
return nil, syscall.ENOENT, syscall.ENOENT
}
func (fs *mockFS) ModKey(path string) (ModKey, error) {
return ModKey{}, errors.New("This is not available during tests")
}
func (*mockFS) IsAbs(p string) bool {
return path.IsAbs(p)
}
func (*mockFS) Abs(p string) (string, bool) {
return path.Clean(path.Join("/", p)), true
}
func (*mockFS) Dir(p string) string {
return path.Dir(p)
}
func (*mockFS) Base(p string) string {
return path.Base(p)
}
func (*mockFS) Ext(p string) string {
return path.Ext(p)
}
func (*mockFS) Join(parts ...string) string {
return path.Clean(path.Join(parts...))
}
func (*mockFS) Cwd() string {
return "/"
}
func splitOnSlash(path string) (string, string) {
if slash := strings.IndexByte(path, '/'); slash != -1 {
return path[:slash], path[slash+1:]
}
return path, ""
}
func (*mockFS) Rel(base string, target string) (string, bool) {
base = path.Clean(base)
target = path.Clean(target)
// Base cases
if base == "" || base == "." {
return target, true
}
if base == target {
return ".", true
}
// Find the common parent directory
for {
bHead, bTail := splitOnSlash(base)
tHead, tTail := splitOnSlash(target)
if bHead != tHead {
break
}
base = bTail
target = tTail
}
// Stop now if base is a subpath of target
if base == "" {
return target, true
}
// Traverse up to the common parent
commonParent := strings.Repeat("../", strings.Count(base, "/")+1)
// Stop now if target is a subpath of base
if target == "" {
return commonParent[:len(commonParent)-1], true
}
// Otherwise, down to the parent
return commonParent + target, true
}
func (fs *mockFS) kind(dir string, base string) (symlink string, kind EntryKind) {
panic("This should never be called")
}
func (fs *mockFS) WatchData() WatchData {
panic("This should never be called")
}

View File

@ -1,529 +0,0 @@
package fs
import (
"fmt"
"io"
"io/ioutil"
"os"
"sort"
"strings"
"sync"
"syscall"
)
type realFS struct {
// Stores the file entries for directories we've listed before
entriesMutex sync.Mutex
entries map[string]entriesOrErr
// If true, do not use the "entries" cache
doNotCacheEntries bool
// This stores data that will end up being returned by "WatchData()"
watchMutex sync.Mutex
watchData map[string]privateWatchData
// When building with WebAssembly, the Go compiler doesn't correctly handle
// platform-specific path behavior. Hack around these bugs by compiling
// support for both Unix and Windows paths into all executables and switch
// between them at run-time instead.
fp goFilepath
}
type entriesOrErr struct {
entries DirEntries
canonicalError error
originalError error
}
type watchState uint8
const (
stateNone watchState = iota
stateDirHasAccessedEntries // Compare "accessedEntries"
stateDirMissing // Compare directory presence
stateFileHasModKey // Compare "modKey"
stateFileNeedModKey // Need to transition to "stateFileHasModKey" or "stateFileUnusableModKey" before "WatchData()" returns
stateFileMissing // Compare file presence
stateFileUnusableModKey // Compare "fileContents"
)
type privateWatchData struct {
accessedEntries *accessedEntries
fileContents string
modKey ModKey
state watchState
}
type RealFSOptions struct {
WantWatchData bool
AbsWorkingDir string
DoNotCache bool
}
func RealFS(options RealFSOptions) (FS, error) {
var fp goFilepath
if CheckIfWindows() {
fp.isWindows = true
fp.pathSeparator = '\\'
} else {
fp.isWindows = false
fp.pathSeparator = '/'
}
// Come up with a default working directory if one was not specified
fp.cwd = options.AbsWorkingDir
if fp.cwd == "" {
if cwd, err := os.Getwd(); err == nil {
fp.cwd = cwd
} else if fp.isWindows {
fp.cwd = "C:\\"
} else {
fp.cwd = "/"
}
} else if !fp.isAbs(fp.cwd) {
return nil, fmt.Errorf("The working directory %q is not an absolute path", fp.cwd)
}
// Resolve symlinks in the current working directory. Symlinks are resolved
// when input file paths are converted to absolute paths because we need to
// recognize an input file as unique even if it has multiple symlinks
// pointing to it. The build will generate relative paths from the current
// working directory to the absolute input file paths for error messages,
// so the current working directory should be processed the same way. Not
// doing this causes test failures with esbuild when run from inside a
// symlinked directory.
//
// This deliberately ignores errors due to e.g. infinite loops. If there is
// an error, we will just use the original working directory and likely
// encounter an error later anyway. And if we don't encounter an error
// later, then the current working directory didn't even matter and the
// error is unimportant.
if path, err := fp.evalSymlinks(fp.cwd); err == nil {
fp.cwd = path
}
// Only allocate memory for watch data if necessary
var watchData map[string]privateWatchData
if options.WantWatchData {
watchData = make(map[string]privateWatchData)
}
return &realFS{
entries: make(map[string]entriesOrErr),
fp: fp,
watchData: watchData,
doNotCacheEntries: options.DoNotCache,
}, nil
}
func (fs *realFS) ReadDirectory(dir string) (entries DirEntries, canonicalError error, originalError error) {
if !fs.doNotCacheEntries {
// First, check the cache
cached, ok := func() (cached entriesOrErr, ok bool) {
fs.entriesMutex.Lock()
defer fs.entriesMutex.Unlock()
cached, ok = fs.entries[dir]
return
}()
if ok {
// Cache hit: stop now
return cached.entries, cached.canonicalError, cached.originalError
}
}
// Cache miss: read the directory entries
names, canonicalError, originalError := fs.readdir(dir)
entries = DirEntries{dir, make(map[string]*Entry), nil}
// Unwrap to get the underlying error
if pathErr, ok := canonicalError.(*os.PathError); ok {
canonicalError = pathErr.Unwrap()
}
if canonicalError == nil {
for _, name := range names {
// Call "stat" lazily for performance. The "@material-ui/icons" package
// contains a directory with over 11,000 entries in it and running "stat"
// for each entry was a big performance issue for that package.
entries.data[strings.ToLower(name)] = &Entry{
dir: dir,
base: name,
needStat: true,
}
}
}
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
state := stateDirHasAccessedEntries
if canonicalError != nil {
state = stateDirMissing
}
entries.accessedEntries = &accessedEntries{wasPresent: make(map[string]bool)}
fs.watchData[dir] = privateWatchData{
accessedEntries: entries.accessedEntries,
state: state,
}
}
// Update the cache unconditionally. Even if the read failed, we don't want to
// retry again later. The directory is inaccessible so trying again is wasted.
if canonicalError != nil {
entries.data = nil
}
if !fs.doNotCacheEntries {
fs.entriesMutex.Lock()
defer fs.entriesMutex.Unlock()
fs.entries[dir] = entriesOrErr{
entries: entries,
canonicalError: canonicalError,
originalError: originalError,
}
}
return entries, canonicalError, originalError
}
func (fs *realFS) ReadFile(path string) (contents string, canonicalError error, originalError error) {
BeforeFileOpen()
defer AfterFileClose()
buffer, originalError := ioutil.ReadFile(path)
canonicalError = fs.canonicalizeError(originalError)
// Allocate the string once
fileContents := string(buffer)
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
data, ok := fs.watchData[path]
if canonicalError != nil {
data.state = stateFileMissing
} else if !ok {
data.state = stateFileNeedModKey
}
data.fileContents = fileContents
fs.watchData[path] = data
}
return fileContents, canonicalError, originalError
}
type realOpenedFile struct {
handle *os.File
len int
}
func (f *realOpenedFile) Len() int {
return f.len
}
func (f *realOpenedFile) Read(start int, end int) ([]byte, error) {
bytes := make([]byte, end-start)
remaining := bytes
_, err := f.handle.Seek(int64(start), io.SeekStart)
if err != nil {
return nil, err
}
for len(remaining) > 0 {
n, err := f.handle.Read(remaining)
if err != nil && n <= 0 {
return nil, err
}
remaining = remaining[n:]
}
return bytes, nil
}
func (f *realOpenedFile) Close() error {
return f.handle.Close()
}
func (fs *realFS) OpenFile(path string) (OpenedFile, error, error) {
BeforeFileOpen()
defer AfterFileClose()
f, err := os.Open(path)
if err != nil {
return nil, fs.canonicalizeError(err), err
}
info, err := f.Stat()
if err != nil {
f.Close()
return nil, fs.canonicalizeError(err), err
}
return &realOpenedFile{f, int(info.Size())}, nil, nil
}
func (fs *realFS) ModKey(path string) (ModKey, error) {
BeforeFileOpen()
defer AfterFileClose()
key, err := modKey(path)
// Store data for watch mode
if fs.watchData != nil {
defer fs.watchMutex.Unlock()
fs.watchMutex.Lock()
data, ok := fs.watchData[path]
if !ok {
if err == modKeyUnusable {
data.state = stateFileUnusableModKey
} else if err != nil {
data.state = stateFileMissing
} else {
data.state = stateFileHasModKey
}
} else if data.state == stateFileNeedModKey {
data.state = stateFileHasModKey
}
data.modKey = key
fs.watchData[path] = data
}
return key, err
}
func (fs *realFS) IsAbs(p string) bool {
return fs.fp.isAbs(p)
}
func (fs *realFS) Abs(p string) (string, bool) {
abs, err := fs.fp.abs(p)
return abs, err == nil
}
func (fs *realFS) Dir(p string) string {
return fs.fp.dir(p)
}
func (fs *realFS) Base(p string) string {
return fs.fp.base(p)
}
func (fs *realFS) Ext(p string) string {
return fs.fp.ext(p)
}
func (fs *realFS) Join(parts ...string) string {
return fs.fp.clean(fs.fp.join(parts))
}
func (fs *realFS) Cwd() string {
return fs.fp.cwd
}
func (fs *realFS) Rel(base string, target string) (string, bool) {
if rel, err := fs.fp.rel(base, target); err == nil {
return rel, true
}
return "", false
}
func (fs *realFS) readdir(dirname string) (entries []string, canonicalError error, originalError error) {
BeforeFileOpen()
defer AfterFileClose()
f, originalError := os.Open(dirname)
canonicalError = fs.canonicalizeError(originalError)
// Stop now if there was an error
if canonicalError != nil {
return nil, canonicalError, originalError
}
defer f.Close()
entries, err := f.Readdirnames(-1)
// Unwrap to get the underlying error
if syscallErr, ok := err.(*os.SyscallError); ok {
err = syscallErr.Unwrap()
}
// Don't convert ENOTDIR to ENOENT here. ENOTDIR is a legitimate error
// condition for Readdirnames() on non-Windows platforms.
return entries, canonicalError, originalError
}
func (fs *realFS) canonicalizeError(err error) error {
// Unwrap to get the underlying error
if pathErr, ok := err.(*os.PathError); ok {
err = pathErr.Unwrap()
}
// This has been copied from golang.org/x/sys/windows
const ERROR_INVALID_NAME syscall.Errno = 123
// Windows is much more restrictive than Unix about file names. If a file name
// is invalid, it will return ERROR_INVALID_NAME. Treat this as ENOENT (i.e.
// "the file does not exist") so that the resolver continues trying to resolve
// the path on this failure instead of aborting with an error.
if fs.fp.isWindows && err == ERROR_INVALID_NAME {
err = syscall.ENOENT
}
// Windows returns ENOTDIR here even though nothing we've done yet has asked
// for a directory. This really means ENOENT on Windows. Return ENOENT here
// so callers that check for ENOENT will successfully detect this file as
// missing.
if err == syscall.ENOTDIR {
err = syscall.ENOENT
}
return err
}
func (fs *realFS) kind(dir string, base string) (symlink string, kind EntryKind) {
entryPath := fs.fp.join([]string{dir, base})
// Use "lstat" since we want information about symbolic links
BeforeFileOpen()
defer AfterFileClose()
stat, err := os.Lstat(entryPath)
if err != nil {
return
}
mode := stat.Mode()
// Follow symlinks now so the cache contains the translation
if (mode & os.ModeSymlink) != 0 {
symlink = entryPath
linksWalked := 0
for {
linksWalked++
if linksWalked > 255 {
return // Error: too many links
}
link, err := os.Readlink(symlink)
if err != nil {
return // Skip over this entry
}
if !fs.fp.isAbs(link) {
link = fs.fp.join([]string{dir, link})
}
symlink = fs.fp.clean(link)
// Re-run "lstat" on the symlink target
stat2, err2 := os.Lstat(symlink)
if err2 != nil {
return // Skip over this entry
}
mode = stat2.Mode()
if (mode & os.ModeSymlink) == 0 {
break
}
dir = fs.fp.dir(symlink)
}
}
// We consider the entry either a directory or a file
if (mode & os.ModeDir) != 0 {
kind = DirEntry
} else {
kind = FileEntry
}
return
}
func (fs *realFS) WatchData() WatchData {
paths := make(map[string]func() string)
for path, data := range fs.watchData {
// Each closure below needs its own copy of these loop variables
path := path
data := data
// Each function should return true if the state has been changed
if data.state == stateFileNeedModKey {
key, err := modKey(path)
if err == modKeyUnusable {
data.state = stateFileUnusableModKey
} else if err != nil {
data.state = stateFileMissing
} else {
data.state = stateFileHasModKey
data.modKey = key
}
}
switch data.state {
case stateDirMissing:
paths[path] = func() string {
info, err := os.Stat(path)
if err == nil && info.IsDir() {
return path
}
return ""
}
case stateDirHasAccessedEntries:
paths[path] = func() string {
names, err, _ := fs.readdir(path)
if err != nil {
return path
}
data.accessedEntries.mutex.Lock()
defer data.accessedEntries.mutex.Unlock()
if allEntries := data.accessedEntries.allEntries; allEntries != nil {
// Check all entries
if len(names) != len(allEntries) {
return path
}
sort.Strings(names)
for i, s := range names {
if s != allEntries[i] {
return path
}
}
} else {
// Check individual entries
isPresent := make(map[string]bool, len(names))
for _, name := range names {
isPresent[strings.ToLower(name)] = true
}
for name, wasPresent := range data.accessedEntries.wasPresent {
if wasPresent != isPresent[name] {
return fs.Join(path, name)
}
}
}
return ""
}
case stateFileMissing:
paths[path] = func() string {
if info, err := os.Stat(path); err == nil && !info.IsDir() {
return path
}
return ""
}
case stateFileHasModKey:
paths[path] = func() string {
if key, err := modKey(path); err != nil || key != data.modKey {
return path
}
return ""
}
case stateFileUnusableModKey:
paths[path] = func() string {
if buffer, err := ioutil.ReadFile(path); err != nil || string(buffer) != data.fileContents {
return path
}
return ""
}
}
}
return WatchData{
Paths: paths,
}
}

View File

@ -1,9 +0,0 @@
//go:build (!js || !wasm) && !windows
// +build !js !wasm
// +build !windows
package fs
func CheckIfWindows() bool {
return false
}

View File

@ -1,25 +0,0 @@
//go:build js && wasm
// +build js,wasm
package fs
import (
"os"
)
var checkedIfWindows bool
var cachedIfWindows bool
func CheckIfWindows() bool {
if !checkedIfWindows {
checkedIfWindows = true
// Hack: Assume that we're on Windows if we're running WebAssembly and
// the "C:\\" directory exists. This is a workaround for a bug in Go's
// WebAssembly support: https://github.com/golang/go/issues/43768.
_, err := os.Stat("C:\\")
cachedIfWindows = err == nil
}
return cachedIfWindows
}

View File

@ -1,8 +0,0 @@
//go:build windows
// +build windows
package fs
func CheckIfWindows() bool {
return true
}

View File

@ -1,35 +0,0 @@
//go:build !darwin && !freebsd && !linux
// +build !darwin,!freebsd,!linux
package fs
import (
"os"
"time"
)
var zeroTime time.Time
func modKey(path string) (ModKey, error) {
info, err := os.Stat(path)
if err != nil {
return ModKey{}, err
}
// We can't detect changes if the file system zeros out the modification time
mtime := info.ModTime()
if mtime == zeroTime || mtime.Unix() == 0 {
return ModKey{}, modKeyUnusable
}
// Don't generate a modification key if the file is too new
if mtime.Add(modKeySafetyGap * time.Second).After(time.Now()) {
return ModKey{}, modKeyUnusable
}
return ModKey{
size: info.Size(),
mtime_sec: mtime.Unix(),
mode: uint32(info.Mode()),
}, nil
}

View File

@ -1,41 +0,0 @@
//go:build darwin || freebsd || linux
// +build darwin freebsd linux
package fs
import (
"time"
"golang.org/x/sys/unix"
)
func modKey(path string) (ModKey, error) {
stat := unix.Stat_t{}
if err := unix.Stat(path, &stat); err != nil {
return ModKey{}, err
}
// We can't detect changes if the file system zeros out the modification time
if stat.Mtim.Sec == 0 && stat.Mtim.Nsec == 0 {
return ModKey{}, modKeyUnusable
}
// Don't generate a modification key if the file is too new
now, err := unix.TimeToTimespec(time.Now())
if err != nil {
return ModKey{}, err
}
mtimeSec := stat.Mtim.Sec + modKeySafetyGap
if mtimeSec > now.Sec || (mtimeSec == now.Sec && stat.Mtim.Nsec > now.Nsec) {
return ModKey{}, modKeyUnusable
}
return ModKey{
inode: stat.Ino,
size: stat.Size,
mtime_sec: int64(stat.Mtim.Sec),
mtime_nsec: int64(stat.Mtim.Nsec),
mode: uint32(stat.Mode),
uid: stat.Uid,
}, nil
}

View File

@ -1,385 +0,0 @@
package graph
// This graph represents the set of files that the linker operates on. Each
// linker has a separate one of these graphs (there is one linker when code
// splitting is on, but one linker per entry point when code splitting is off).
//
// The input data to the linker constructor must be considered immutable because
// it's shared between linker invocations and is also stored in the cache for
// incremental builds.
//
// The linker constructor makes a shallow clone of the input data and is careful
// to pre-clone ahead of time the AST fields that it may modify. The Go language
// doesn't have any type system features for immutability so this has to be
// manually enforced. Please be careful.
import (
"sort"
"sync"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/runtime"
)
type entryPointKind uint8
const (
entryPointNone entryPointKind = iota
entryPointUserSpecified
entryPointDynamicImport
)
type LinkerFile struct {
InputFile InputFile
// This holds all entry points that can reach this file. It will be used to
// assign the parts in this file to a chunk.
EntryBits helpers.BitSet
// This is lazily-allocated because it's only needed if there are warnings
// logged, which should be relatively rare.
lazyLineColumnTracker *logger.LineColumnTracker
// The minimum number of links in the module graph to get from an entry point
// to this file
DistanceFromEntryPoint uint32
// If "entryPointKind" is not "entryPointNone", this is the index of the
// corresponding entry point chunk.
EntryPointChunkIndex uint32
// This file is an entry point if and only if this is not "entryPointNone".
// Note that dynamically-imported files are allowed to also be specified by
// the user as top-level entry points, so some dynamically-imported files
// may be "entryPointUserSpecified" instead of "entryPointDynamicImport".
entryPointKind entryPointKind
// This is true if this file has been marked as live by the tree shaking
// algorithm.
IsLive bool
}
func (f *LinkerFile) IsEntryPoint() bool {
return f.entryPointKind != entryPointNone
}
func (f *LinkerFile) IsUserSpecifiedEntryPoint() bool {
return f.entryPointKind == entryPointUserSpecified
}
// Note: This is not guarded by a mutex. Make sure this isn't called from a
// parallel part of the code.
func (f *LinkerFile) LineColumnTracker() *logger.LineColumnTracker {
if f.lazyLineColumnTracker == nil {
tracker := logger.MakeLineColumnTracker(&f.InputFile.Source)
f.lazyLineColumnTracker = &tracker
}
return f.lazyLineColumnTracker
}
type EntryPoint struct {
// This may be an absolute path or a relative path. If absolute, it will
// eventually be turned into a relative path by computing the path relative
// to the "outbase" directory. Then this relative path will be joined onto
// the "outdir" directory to form the final output path for this entry point.
OutputPath string
// This is the source index of the entry point. This file must have a valid
// entry point kind (i.e. not "none").
SourceIndex uint32
// Manually specified output paths are ignored when computing the default
// "outbase" directory, which is computed as the lowest common ancestor of
// all automatically generated output paths.
OutputPathWasAutoGenerated bool
}
type LinkerGraph struct {
Files []LinkerFile
entryPoints []EntryPoint
Symbols js_ast.SymbolMap
// We should avoid traversing all files in the bundle, because the linker
// should be able to run a linking operation on a large bundle where only
// a few files are needed (e.g. an incremental compilation scenario). This
// holds all files that could possibly be reached through the entry points.
// If you need to iterate over all files in the linking operation, iterate
// over this array. This array is also sorted in a deterministic ordering
// to help ensure deterministic builds (source indices are random).
ReachableFiles []uint32
// This maps from unstable source index to stable reachable file index. This
// is useful as a deterministic key for sorting if you need to sort something
// containing a source index (such as "js_ast.Ref" symbol references).
StableSourceIndices []uint32
}
func CloneLinkerGraph(
inputFiles []InputFile,
reachableFiles []uint32,
originalEntryPoints []EntryPoint,
codeSplitting bool,
) LinkerGraph {
entryPoints := append([]EntryPoint{}, originalEntryPoints...)
symbols := js_ast.NewSymbolMap(len(inputFiles))
files := make([]LinkerFile, len(inputFiles))
// Mark all entry points so we don't add them again for import() expressions
for _, entryPoint := range entryPoints {
files[entryPoint.SourceIndex].entryPointKind = entryPointUserSpecified
}
// Clone various things since we may mutate them later. Do this in parallel
// for a speedup (around ~2x faster for this function in the three.js
// benchmark on a 6-core laptop).
var dynamicImportEntryPoints []uint32
var dynamicImportEntryPointsMutex sync.Mutex
waitGroup := sync.WaitGroup{}
waitGroup.Add(len(reachableFiles))
stableSourceIndices := make([]uint32, len(inputFiles))
for stableIndex, sourceIndex := range reachableFiles {
// Create a way to convert source indices to a stable ordering
stableSourceIndices[sourceIndex] = uint32(stableIndex)
go func(sourceIndex uint32) {
file := &files[sourceIndex]
file.InputFile = inputFiles[sourceIndex]
switch repr := file.InputFile.Repr.(type) {
case *JSRepr:
// Clone the representation
{
clone := *repr
repr = &clone
file.InputFile.Repr = repr
}
// Clone the symbol map
fileSymbols := append([]js_ast.Symbol{}, repr.AST.Symbols...)
symbols.SymbolsForSource[sourceIndex] = fileSymbols
repr.AST.Symbols = nil
// Clone the parts
repr.AST.Parts = append([]js_ast.Part{}, repr.AST.Parts...)
for i := range repr.AST.Parts {
part := &repr.AST.Parts[i]
clone := make(map[js_ast.Ref]js_ast.SymbolUse, len(part.SymbolUses))
for ref, uses := range part.SymbolUses {
clone[ref] = uses
}
part.SymbolUses = clone
part.Dependencies = append([]js_ast.Dependency{}, part.Dependencies...)
}
// Clone the import records
repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...)
// Add dynamic imports as additional entry points if code splitting is active
if codeSplitting {
for importRecordIndex := range repr.AST.ImportRecords {
if record := &repr.AST.ImportRecords[importRecordIndex]; record.SourceIndex.IsValid() && record.Kind == ast.ImportDynamic {
dynamicImportEntryPointsMutex.Lock()
dynamicImportEntryPoints = append(dynamicImportEntryPoints, record.SourceIndex.GetIndex())
dynamicImportEntryPointsMutex.Unlock()
}
}
}
// Clone the import map
namedImports := make(map[js_ast.Ref]js_ast.NamedImport, len(repr.AST.NamedImports))
for k, v := range repr.AST.NamedImports {
namedImports[k] = v
}
repr.AST.NamedImports = namedImports
// Clone the export map
resolvedExports := make(map[string]ExportData)
for alias, name := range repr.AST.NamedExports {
resolvedExports[alias] = ExportData{
Ref: name.Ref,
SourceIndex: sourceIndex,
NameLoc: name.AliasLoc,
}
}
// Clone the top-level scope so we can generate more variables
{
new := &js_ast.Scope{}
*new = *repr.AST.ModuleScope
new.Generated = append([]js_ast.Ref{}, new.Generated...)
repr.AST.ModuleScope = new
}
// Also associate some default metadata with the file
repr.Meta.ResolvedExports = resolvedExports
repr.Meta.IsProbablyTypeScriptType = make(map[js_ast.Ref]bool)
repr.Meta.ImportsToBind = make(map[js_ast.Ref]ImportData)
case *CSSRepr:
// Clone the representation
{
clone := *repr
repr = &clone
file.InputFile.Repr = repr
}
// Clone the import records
repr.AST.ImportRecords = append([]ast.ImportRecord{}, repr.AST.ImportRecords...)
}
// All files start off as far as possible from an entry point
file.DistanceFromEntryPoint = ^uint32(0)
waitGroup.Done()
}(sourceIndex)
}
waitGroup.Wait()
// Process dynamic entry points after merging control flow again
stableEntryPoints := make([]int, 0, len(dynamicImportEntryPoints))
for _, sourceIndex := range dynamicImportEntryPoints {
if otherFile := &files[sourceIndex]; otherFile.entryPointKind == entryPointNone {
stableEntryPoints = append(stableEntryPoints, int(stableSourceIndices[sourceIndex]))
otherFile.entryPointKind = entryPointDynamicImport
}
}
// Make sure to add dynamic entry points in a deterministic order
sort.Ints(stableEntryPoints)
for _, stableIndex := range stableEntryPoints {
entryPoints = append(entryPoints, EntryPoint{SourceIndex: reachableFiles[stableIndex]})
}
// Allocate the entry bit set now that the number of entry points is known
bitCount := uint(len(entryPoints))
for _, sourceIndex := range reachableFiles {
files[sourceIndex].EntryBits = helpers.NewBitSet(bitCount)
}
return LinkerGraph{
Symbols: symbols,
entryPoints: entryPoints,
Files: files,
ReachableFiles: reachableFiles,
StableSourceIndices: stableSourceIndices,
}
}
// Prevent packages that depend on us from adding or removing entry points
func (g *LinkerGraph) EntryPoints() []EntryPoint {
return g.entryPoints
}
func (g *LinkerGraph) AddPartToFile(sourceIndex uint32, part js_ast.Part) uint32 {
// Invariant: this map is never null
if part.SymbolUses == nil {
part.SymbolUses = make(map[js_ast.Ref]js_ast.SymbolUse)
}
repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr)
partIndex := uint32(len(repr.AST.Parts))
repr.AST.Parts = append(repr.AST.Parts, part)
// Invariant: the parts for all top-level symbols can be found in the file-level map
for _, declaredSymbol := range part.DeclaredSymbols {
if declaredSymbol.IsTopLevel {
// Check for an existing overlay
partIndices, ok := repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref]
// If missing, initialize using the original values from the parser
if !ok {
partIndices = append(partIndices, repr.AST.TopLevelSymbolToPartsFromParser[declaredSymbol.Ref]...)
}
// Add this part to the overlay
partIndices = append(partIndices, partIndex)
if repr.Meta.TopLevelSymbolToPartsOverlay == nil {
repr.Meta.TopLevelSymbolToPartsOverlay = make(map[js_ast.Ref][]uint32)
}
repr.Meta.TopLevelSymbolToPartsOverlay[declaredSymbol.Ref] = partIndices
}
}
return partIndex
}
func (g *LinkerGraph) GenerateNewSymbol(sourceIndex uint32, kind js_ast.SymbolKind, originalName string) js_ast.Ref {
sourceSymbols := &g.Symbols.SymbolsForSource[sourceIndex]
ref := js_ast.Ref{
SourceIndex: sourceIndex,
InnerIndex: uint32(len(*sourceSymbols)),
}
*sourceSymbols = append(*sourceSymbols, js_ast.Symbol{
Kind: kind,
OriginalName: originalName,
Link: js_ast.InvalidRef,
})
generated := &g.Files[sourceIndex].InputFile.Repr.(*JSRepr).AST.ModuleScope.Generated
*generated = append(*generated, ref)
return ref
}
func (g *LinkerGraph) GenerateSymbolImportAndUse(
sourceIndex uint32,
partIndex uint32,
ref js_ast.Ref,
useCount uint32,
sourceIndexToImportFrom uint32,
) {
if useCount == 0 {
return
}
repr := g.Files[sourceIndex].InputFile.Repr.(*JSRepr)
part := &repr.AST.Parts[partIndex]
// Mark this symbol as used by this part
use := part.SymbolUses[ref]
use.CountEstimate += useCount
part.SymbolUses[ref] = use
// Uphold invariants about the CommonJS "exports" and "module" symbols
if ref == repr.AST.ExportsRef {
repr.AST.UsesExportsRef = true
}
if ref == repr.AST.ModuleRef {
repr.AST.UsesModuleRef = true
}
// Track that this specific symbol was imported
if sourceIndexToImportFrom != sourceIndex {
repr.Meta.ImportsToBind[ref] = ImportData{
SourceIndex: sourceIndexToImportFrom,
Ref: ref,
}
}
// Pull in all parts that declare this symbol
targetRepr := g.Files[sourceIndexToImportFrom].InputFile.Repr.(*JSRepr)
for _, partIndex := range targetRepr.TopLevelSymbolToParts(ref) {
part.Dependencies = append(part.Dependencies, js_ast.Dependency{
SourceIndex: sourceIndexToImportFrom,
PartIndex: partIndex,
})
}
}
func (g *LinkerGraph) GenerateRuntimeSymbolImportAndUse(
sourceIndex uint32,
partIndex uint32,
name string,
useCount uint32,
) {
if useCount == 0 {
return
}
runtimeRepr := g.Files[runtime.SourceIndex].InputFile.Repr.(*JSRepr)
ref := runtimeRepr.AST.NamedExports[name].Ref
g.GenerateSymbolImportAndUse(sourceIndex, partIndex, ref, useCount, runtime.SourceIndex)
}

View File

@ -1,117 +0,0 @@
package graph
// The code in this file mainly represents data that passes from the scan phase
// to the compile phase of the bundler. There is currently one exception: the
// "meta" member of the JavaScript file representation. That could have been
// stored separately but is stored together for convenience and to avoid an
// extra level of indirection. Instead it's kept in a separate type to keep
// things organized.
import (
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/resolver"
"github.com/evanw/esbuild/internal/sourcemap"
)
type InputFile struct {
Source logger.Source
Repr InputFileRepr
InputSourceMap *sourcemap.SourceMap
// If this file ends up being used in the bundle, these are additional files
// that must be written to the output directory. It's used by the "file"
// loader.
AdditionalFiles []OutputFile
UniqueKeyForFileLoader string
SideEffects SideEffects
Loader config.Loader
}
type OutputFile struct {
AbsPath string
Contents []byte
// If "AbsMetadataFile" is present, this will be filled out with information
// about this file in JSON format. This is a partial JSON file that will be
// fully assembled later.
JSONMetadataChunk string
IsExecutable bool
}
type SideEffects struct {
// This is optional additional information for use in error messages
Data *resolver.SideEffectsData
Kind SideEffectsKind
}
type SideEffectsKind uint8
const (
// The default value conservatively considers all files to have side effects.
HasSideEffects SideEffectsKind = iota
// This file was listed as not having side effects by a "package.json"
// file in one of our containing directories with a "sideEffects" field.
NoSideEffects_PackageJSON
// This file is considered to have no side effects because the AST was empty
// after parsing finished. This should be the case for ".d.ts" files.
NoSideEffects_EmptyAST
// This file was loaded using a data-oriented loader (e.g. "text") that is
// known to not have side effects.
NoSideEffects_PureData
// Same as above but it came from a plugin. We don't want to warn about
// unused imports to these files since running the plugin is a side effect.
// Removing the import would not call the plugin which is observable.
NoSideEffects_PureData_FromPlugin
)
type InputFileRepr interface {
ImportRecords() *[]ast.ImportRecord
}
type JSRepr struct {
AST js_ast.AST
Meta JSReprMeta
// If present, this is the CSS file that this JavaScript stub corresponds to.
// A JavaScript stub is automatically generated for a CSS file when it's
// imported from a JavaScript file.
CSSSourceIndex ast.Index32
}
func (repr *JSRepr) ImportRecords() *[]ast.ImportRecord {
return &repr.AST.ImportRecords
}
func (repr *JSRepr) TopLevelSymbolToParts(ref js_ast.Ref) []uint32 {
// Overlay the mutable map from the linker
if parts, ok := repr.Meta.TopLevelSymbolToPartsOverlay[ref]; ok {
return parts
}
// Fall back to the immutable map from the parser
return repr.AST.TopLevelSymbolToPartsFromParser[ref]
}
type CSSRepr struct {
AST css_ast.AST
// If present, this is the JavaScript stub corresponding to this CSS file.
// A JavaScript stub is automatically generated for a CSS file when it's
// imported from a JavaScript file.
JSSourceIndex ast.Index32
}
func (repr *CSSRepr) ImportRecords() *[]ast.ImportRecord {
return &repr.AST.ImportRecords
}

View File

@ -1,203 +0,0 @@
package graph
// The code in this file represents data that is required by the compile phase
// of the bundler but that is not required by the scan phase.
import (
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/logger"
)
type WrapKind uint8
const (
WrapNone WrapKind = iota
// The module will be bundled CommonJS-style like this:
//
// // foo.ts
// let require_foo = __commonJS((exports, module) => {
// exports.foo = 123;
// });
//
// // bar.ts
// let foo = flag ? require_foo() : null;
//
WrapCJS
// The module will be bundled ESM-style like this:
//
// // foo.ts
// var foo, foo_exports = {};
// __export(foo_exports, {
// foo: () => foo
// });
// let init_foo = __esm(() => {
// foo = 123;
// });
//
// // bar.ts
// let foo = flag ? (init_foo(), __toCommonJS(foo_exports)) : null;
//
WrapESM
)
// This contains linker-specific metadata corresponding to a "file" struct
// from the initial scan phase of the bundler. It's separated out because it's
// conceptually only used for a single linking operation and because multiple
// linking operations may be happening in parallel with different metadata for
// the same file.
type JSReprMeta struct {
// This is only for TypeScript files. If an import symbol is in this map, it
// means the import couldn't be found and doesn't actually exist. This is not
// an error in TypeScript because the import is probably just a type.
//
// Normally we remove all unused imports for TypeScript files during parsing,
// which automatically removes type-only imports. But there are certain re-
// export situations where it's impossible to tell if an import is a type or
// not:
//
// import {typeOrNotTypeWhoKnows} from 'path';
// export {typeOrNotTypeWhoKnows};
//
// Really people should be using the TypeScript "isolatedModules" flag with
// bundlers like this one that compile TypeScript files independently without
// type checking. That causes the TypeScript type checker to emit the error
// "Re-exporting a type when the '--isolatedModules' flag is provided requires
// using 'export type'." But we try to be robust to such code anyway.
IsProbablyTypeScriptType map[js_ast.Ref]bool
// Imports are matched with exports in a separate pass from when the matched
// exports are actually bound to the imports. Here "binding" means adding non-
// local dependencies on the parts in the exporting file that declare the
// exported symbol to all parts in the importing file that use the imported
// symbol.
//
// This must be a separate pass because of the "probably TypeScript type"
// check above. We can't generate the part for the export namespace until
// we've matched imports with exports because the generated code must omit
// type-only imports in the export namespace code. And we can't bind exports
// to imports until the part for the export namespace is generated since that
// part needs to participate in the binding.
//
// This array holds the deferred imports to bind so the pass can be split
// into two separate passes.
ImportsToBind map[js_ast.Ref]ImportData
// This includes both named exports and re-exports.
//
// Named exports come from explicit export statements in the original file,
// and are copied from the "NamedExports" field in the AST.
//
// Re-exports come from other files and are the result of resolving export
// star statements (i.e. "export * from 'foo'").
ResolvedExports map[string]ExportData
ResolvedExportStar *ExportData
// Never iterate over "resolvedExports" directly. Instead, iterate over this
// array. Some exports in that map aren't meant to end up in generated code.
// This array excludes these exports and is also sorted, which avoids non-
// determinism due to random map iteration order.
SortedAndFilteredExportAliases []string
// If this is an entry point, this array holds a reference to one free
// temporary symbol for each entry in "sortedAndFilteredExportAliases".
// These may be needed to store copies of CommonJS re-exports in ESM.
CJSExportCopies []js_ast.Ref
// This is merged on top of the corresponding map from the parser in the AST.
// You should call "TopLevelSymbolToParts" to access this instead of accessing
// it directly.
TopLevelSymbolToPartsOverlay map[js_ast.Ref][]uint32
// The index of the automatically-generated part used to represent the
// CommonJS or ESM wrapper. This part is empty and is only useful for tree
// shaking and code splitting. The wrapper can't be inserted into the part
// because the wrapper contains other parts, which can't be represented by
// the current part system. Only wrapped files have one of these.
WrapperPartIndex ast.Index32
// The index of the automatically-generated part used to handle entry point
// specific stuff. If a certain part is needed by the entry point, it's added
// as a dependency of this part. This is important for parts that are marked
// as removable when unused and that are not used by anything else. Only
// entry point files have one of these.
EntryPointPartIndex ast.Index32
// This is true if this file is affected by top-level await, either by having
// a top-level await inside this file or by having an import/export statement
// that transitively imports such a file. It is forbidden to call "require()"
// on these files since they are evaluated asynchronously.
IsAsyncOrHasAsyncDependency bool
Wrap WrapKind
// If true, we need to insert "var exports = {};". This is the case for ESM
// files when the import namespace is captured via "import * as" and also
// when they are the target of a "require()" call.
NeedsExportsVariable bool
// If true, the "__export(exports, { ... })" call will be force-included even
// if there are no parts that reference "exports". Otherwise this call will
// be removed due to the tree shaking pass. This is used when for entry point
// files when code related to the current output format needs to reference
// the "exports" variable.
ForceIncludeExportsForEntryPoint bool
// This is set when we need to pull in the "__export" symbol in to the part
// at "nsExportPartIndex". This can't be done in "createExportsForFile"
// because of concurrent map hazards. Instead, it must be done later.
NeedsExportSymbolFromRuntime bool
// Wrapped files must also ensure that their dependencies are wrapped. This
// flag is used during the traversal that enforces this invariant, and is used
// to detect when the fixed point has been reached.
DidWrapDependencies bool
}
type ImportData struct {
// This is an array of intermediate statements that re-exported this symbol
// in a chain before getting to the final symbol. This can be done either with
// "export * from" or "export {} from". If this is done with "export * from"
// then this may not be the result of a single chain but may instead form
// a diamond shape if this same symbol was re-exported multiple times from
// different files.
ReExports []js_ast.Dependency
NameLoc logger.Loc // Optional, goes with sourceIndex, ignore if zero
Ref js_ast.Ref
SourceIndex uint32
}
type ExportData struct {
Ref js_ast.Ref
// Export star resolution happens first before import resolution. That means
// it cannot yet determine if duplicate names from export star resolution are
// ambiguous (point to different symbols) or not (point to the same symbol).
// This issue can happen in the following scenario:
//
// // entry.js
// export * from './a'
// export * from './b'
//
// // a.js
// export * from './c'
//
// // b.js
// export {x} from './c'
//
// // c.js
// export let x = 1, y = 2
//
// In this case "entry.js" should have two exports "x" and "y", neither of
// which are ambiguous. To handle this case, ambiguity resolution must be
// deferred until import resolution time. That is done using this array.
PotentiallyAmbiguousExportStarRefs []ImportData
// This is the file that the named export above came from. This will be
// different from the file that contains this object if this is a re-export.
NameLoc logger.Loc // Optional, goes with sourceIndex, ignore if zero
SourceIndex uint32
}

View File

@ -1,27 +0,0 @@
package helpers
import "bytes"
type BitSet struct {
entries []byte
}
func NewBitSet(bitCount uint) BitSet {
return BitSet{make([]byte, (bitCount+7)/8)}
}
func (bs BitSet) HasBit(bit uint) bool {
return (bs.entries[bit/8] & (1 << (bit & 7))) != 0
}
func (bs BitSet) SetBit(bit uint) {
bs.entries[bit/8] |= 1 << (bit & 7)
}
func (bs BitSet) Equals(other BitSet) bool {
return bytes.Equal(bs.entries, other.entries)
}
func (bs BitSet) String() string {
return string(bs.entries)
}

View File

@ -1,89 +0,0 @@
package helpers
import (
"strings"
"unicode/utf8"
)
func RemoveMultiLineCommentIndent(prefix string, text string) string {
// Figure out the initial indent
indent := 0
seekBackwardToNewline:
for len(prefix) > 0 {
c, size := utf8.DecodeLastRuneInString(prefix)
switch c {
case '\r', '\n', '\u2028', '\u2029':
break seekBackwardToNewline
}
prefix = prefix[:len(prefix)-size]
indent++
}
// Split the comment into lines
var lines []string
start := 0
for i, c := range text {
switch c {
case '\r', '\n':
// Don't double-append for Windows style "\r\n" newlines
if start <= i {
lines = append(lines, text[start:i])
}
start = i + 1
// Ignore the second part of Windows style "\r\n" newlines
if c == '\r' && start < len(text) && text[start] == '\n' {
start++
}
case '\u2028', '\u2029':
lines = append(lines, text[start:i])
start = i + 3
}
}
lines = append(lines, text[start:])
// Find the minimum indent over all lines after the first line
for _, line := range lines[1:] {
lineIndent := 0
for _, c := range line {
if c != ' ' && c != '\t' {
break
}
lineIndent++
}
if indent > lineIndent {
indent = lineIndent
}
}
// Trim the indent off of all lines after the first line
for i, line := range lines {
if i > 0 {
lines[i] = line[indent:]
}
}
return strings.Join(lines, "\n")
}
func EscapeClosingTag(text string, slashTag string) string {
i := strings.Index(text, "</")
if i < 0 {
return text
}
var b strings.Builder
for {
b.WriteString(text[:i+1])
text = text[i+1:]
if len(text) >= len(slashTag) && strings.EqualFold(text[:len(slashTag)], slashTag) {
b.WriteByte('\\')
}
i = strings.Index(text, "</")
if i < 0 {
break
}
}
b.WriteString(text)
return b.String()
}

View File

@ -1,14 +0,0 @@
package helpers
// From: http://boost.sourceforge.net/doc/html/boost/hash_combine.html
func HashCombine(seed uint32, hash uint32) uint32 {
return seed ^ (hash + 0x9e3779b9 + (seed << 6) + (seed >> 2))
}
func HashCombineString(seed uint32, text string) uint32 {
seed = HashCombine(seed, uint32(len(text)))
for _, c := range text {
seed = HashCombine(seed, uint32(c))
}
return seed
}

View File

@ -1,86 +0,0 @@
package helpers
import (
"bytes"
"strings"
)
// This provides an efficient way to join lots of big string and byte slices
// together. It avoids the cost of repeatedly reallocating as the buffer grows
// by measuring exactly how big the buffer should be and then allocating once.
// This is a measurable speedup.
type Joiner struct {
lastByte byte
strings []joinerString
bytes []joinerBytes
length uint32
}
type joinerString struct {
data string
offset uint32
}
type joinerBytes struct {
data []byte
offset uint32
}
func (j *Joiner) AddString(data string) {
if len(data) > 0 {
j.lastByte = data[len(data)-1]
}
j.strings = append(j.strings, joinerString{data, j.length})
j.length += uint32(len(data))
}
func (j *Joiner) AddBytes(data []byte) {
if len(data) > 0 {
j.lastByte = data[len(data)-1]
}
j.bytes = append(j.bytes, joinerBytes{data, j.length})
j.length += uint32(len(data))
}
func (j *Joiner) LastByte() byte {
return j.lastByte
}
func (j *Joiner) Length() uint32 {
return j.length
}
func (j *Joiner) EnsureNewlineAtEnd() {
if j.length > 0 && j.lastByte != '\n' {
j.AddString("\n")
}
}
func (j *Joiner) Done() []byte {
if len(j.strings) == 0 && len(j.bytes) == 1 && j.bytes[0].offset == 0 {
// No need to allocate if there was only a single byte array written
return j.bytes[0].data
}
buffer := make([]byte, j.length)
for _, item := range j.strings {
copy(buffer[item.offset:], item.data)
}
for _, item := range j.bytes {
copy(buffer[item.offset:], item.data)
}
return buffer
}
func (j *Joiner) Contains(s string, b []byte) bool {
for _, item := range j.strings {
if strings.Contains(item.data, s) {
return true
}
}
for _, item := range j.bytes {
if bytes.Contains(item.data, b) {
return true
}
}
return false
}

View File

@ -1,31 +0,0 @@
package helpers
import "strings"
var builtinTypesLower = map[string]string{
".css": "text/css; charset=utf-8",
".gif": "image/gif",
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
".json": "application/json",
".mjs": "text/javascript; charset=utf-8",
".pdf": "application/pdf",
".png": "image/png",
".svg": "image/svg+xml",
".wasm": "application/wasm",
".webp": "image/webp",
".xml": "text/xml; charset=utf-8",
}
// This is used instead of Go's built-in "mime.TypeByExtension" function because
// that function is broken on Windows: https://github.com/golang/go/issues/32350.
func MimeTypeByExtension(ext string) string {
contentType := builtinTypesLower[ext]
if contentType == "" {
contentType = builtinTypesLower[strings.ToLower(ext)]
}
return contentType
}

View File

@ -1,22 +0,0 @@
package helpers
import "strings"
func IsInsideNodeModules(path string) bool {
for {
// This is written in a platform-independent manner because it's run on
// user-specified paths which can be arbitrary non-file-system things. So
// for example Windows paths may end up being used on Unix or URLs may end
// up being used on Windows. Be consistently agnostic to which kind of
// slash is used on all platforms.
slash := strings.LastIndexAny(path, "/\\")
if slash == -1 {
return false
}
dir, base := path[:slash], path[slash+1:]
if base == "node_modules" {
return true
}
path = dir
}
}

View File

@ -1,50 +0,0 @@
package helpers
import (
"runtime/debug"
"strings"
)
func PrettyPrintedStack() string {
lines := strings.Split(strings.TrimSpace(string(debug.Stack())), "\n")
// Strip the first "goroutine" line
if len(lines) > 0 {
if first := lines[0]; strings.HasPrefix(first, "goroutine ") && strings.HasSuffix(first, ":") {
lines = lines[1:]
}
}
sb := strings.Builder{}
for _, line := range lines {
// Indented lines are source locations
if strings.HasPrefix(line, "\t") {
line = line[1:]
line = strings.TrimPrefix(line, "github.com/evanw/esbuild/")
if offset := strings.LastIndex(line, " +0x"); offset != -1 {
line = line[:offset]
}
sb.WriteString(" (")
sb.WriteString(line)
sb.WriteString(")")
continue
}
// Other lines are function calls
if sb.Len() > 0 {
sb.WriteByte('\n')
}
if strings.HasSuffix(line, ")") {
if paren := strings.LastIndexByte(line, '('); paren != -1 {
line = line[:paren]
}
}
if slash := strings.LastIndexByte(line, '/'); slash != -1 {
line = line[slash+1:]
}
sb.WriteString(line)
}
return sb.String()
}

View File

@ -1,94 +0,0 @@
package helpers
import (
"fmt"
"strings"
"sync"
"time"
"github.com/evanw/esbuild/internal/logger"
)
type Timer struct {
mutex sync.Mutex
data []timerData
}
type timerData struct {
name string
time time.Time
isEnd bool
}
func (t *Timer) Begin(name string) {
if t != nil {
t.data = append(t.data, timerData{
name: name,
time: time.Now(),
})
}
}
func (t *Timer) End(name string) {
if t != nil {
t.data = append(t.data, timerData{
name: name,
time: time.Now(),
isEnd: true,
})
}
}
func (t *Timer) Fork() *Timer {
if t != nil {
return &Timer{}
}
return nil
}
func (t *Timer) Join(other *Timer) {
if t != nil && other != nil {
t.mutex.Lock()
defer t.mutex.Unlock()
t.data = append(t.data, other.data...)
}
}
func (t *Timer) Log(log logger.Log) {
if t == nil {
return
}
type pair struct {
timerData
index uint32
}
var notes []logger.MsgData
var stack []pair
indent := 0
for _, item := range t.data {
if !item.isEnd {
top := pair{timerData: item, index: uint32(len(notes))}
notes = append(notes, logger.MsgData{})
stack = append(stack, top)
indent++
} else {
indent--
last := len(stack) - 1
top := stack[last]
stack = stack[:last]
if item.name != top.name {
panic("Internal error")
}
notes[top.index].Text = fmt.Sprintf("%s%s: %dms",
strings.Repeat(" ", indent),
top.name,
item.time.Sub(top.time).Milliseconds())
}
}
log.AddWithNotes(logger.Info, nil, logger.Range{},
"Timing information (times may not nest hierarchically due to parallelism)", notes)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,382 +0,0 @@
package js_lexer
var tokenToString = map[T]string{
TEndOfFile: "end of file",
TSyntaxError: "syntax error",
THashbang: "hashbang comment",
// Literals
TNoSubstitutionTemplateLiteral: "template literal",
TNumericLiteral: "number",
TStringLiteral: "string",
TBigIntegerLiteral: "bigint",
// Pseudo-literals
TTemplateHead: "template literal",
TTemplateMiddle: "template literal",
TTemplateTail: "template literal",
// Punctuation
TAmpersand: "\"&\"",
TAmpersandAmpersand: "\"&&\"",
TAsterisk: "\"*\"",
TAsteriskAsterisk: "\"**\"",
TAt: "\"@\"",
TBar: "\"|\"",
TBarBar: "\"||\"",
TCaret: "\"^\"",
TCloseBrace: "\"}\"",
TCloseBracket: "\"]\"",
TCloseParen: "\")\"",
TColon: "\":\"",
TComma: "\",\"",
TDot: "\".\"",
TDotDotDot: "\"...\"",
TEqualsEquals: "\"==\"",
TEqualsEqualsEquals: "\"===\"",
TEqualsGreaterThan: "\"=>\"",
TExclamation: "\"!\"",
TExclamationEquals: "\"!=\"",
TExclamationEqualsEquals: "\"!==\"",
TGreaterThan: "\">\"",
TGreaterThanEquals: "\">=\"",
TGreaterThanGreaterThan: "\">>\"",
TGreaterThanGreaterThanGreaterThan: "\">>>\"",
TLessThan: "\"<\"",
TLessThanEquals: "\"<=\"",
TLessThanLessThan: "\"<<\"",
TMinus: "\"-\"",
TMinusMinus: "\"--\"",
TOpenBrace: "\"{\"",
TOpenBracket: "\"[\"",
TOpenParen: "\"(\"",
TPercent: "\"%\"",
TPlus: "\"+\"",
TPlusPlus: "\"++\"",
TQuestion: "\"?\"",
TQuestionDot: "\"?.\"",
TQuestionQuestion: "\"??\"",
TSemicolon: "\";\"",
TSlash: "\"/\"",
TTilde: "\"~\"",
// Assignments
TAmpersandAmpersandEquals: "\"&&=\"",
TAmpersandEquals: "\"&=\"",
TAsteriskAsteriskEquals: "\"**=\"",
TAsteriskEquals: "\"*=\"",
TBarBarEquals: "\"||=\"",
TBarEquals: "\"|=\"",
TCaretEquals: "\"^=\"",
TEquals: "\"=\"",
TGreaterThanGreaterThanEquals: "\">>=\"",
TGreaterThanGreaterThanGreaterThanEquals: "\">>>=\"",
TLessThanLessThanEquals: "\"<<=\"",
TMinusEquals: "\"-=\"",
TPercentEquals: "\"%=\"",
TPlusEquals: "\"+=\"",
TQuestionQuestionEquals: "\"??=\"",
TSlashEquals: "\"/=\"",
// Class-private fields and methods
TPrivateIdentifier: "private identifier",
// Identifiers
TIdentifier: "identifier",
TEscapedKeyword: "escaped keyword",
// Reserved words
TBreak: "\"break\"",
TCase: "\"case\"",
TCatch: "\"catch\"",
TClass: "\"class\"",
TConst: "\"const\"",
TContinue: "\"continue\"",
TDebugger: "\"debugger\"",
TDefault: "\"default\"",
TDelete: "\"delete\"",
TDo: "\"do\"",
TElse: "\"else\"",
TEnum: "\"enum\"",
TExport: "\"export\"",
TExtends: "\"extends\"",
TFalse: "\"false\"",
TFinally: "\"finally\"",
TFor: "\"for\"",
TFunction: "\"function\"",
TIf: "\"if\"",
TImport: "\"import\"",
TIn: "\"in\"",
TInstanceof: "\"instanceof\"",
TNew: "\"new\"",
TNull: "\"null\"",
TReturn: "\"return\"",
TSuper: "\"super\"",
TSwitch: "\"switch\"",
TThis: "\"this\"",
TThrow: "\"throw\"",
TTrue: "\"true\"",
TTry: "\"try\"",
TTypeof: "\"typeof\"",
TVar: "\"var\"",
TVoid: "\"void\"",
TWhile: "\"while\"",
TWith: "\"with\"",
}
// This is from https://github.com/microsoft/TypeScript/blob/master/src/compiler/transformers/jsx.ts
var jsxEntity = map[string]rune{
"quot": 0x0022,
"amp": 0x0026,
"apos": 0x0027,
"lt": 0x003C,
"gt": 0x003E,
"nbsp": 0x00A0,
"iexcl": 0x00A1,
"cent": 0x00A2,
"pound": 0x00A3,
"curren": 0x00A4,
"yen": 0x00A5,
"brvbar": 0x00A6,
"sect": 0x00A7,
"uml": 0x00A8,
"copy": 0x00A9,
"ordf": 0x00AA,
"laquo": 0x00AB,
"not": 0x00AC,
"shy": 0x00AD,
"reg": 0x00AE,
"macr": 0x00AF,
"deg": 0x00B0,
"plusmn": 0x00B1,
"sup2": 0x00B2,
"sup3": 0x00B3,
"acute": 0x00B4,
"micro": 0x00B5,
"para": 0x00B6,
"middot": 0x00B7,
"cedil": 0x00B8,
"sup1": 0x00B9,
"ordm": 0x00BA,
"raquo": 0x00BB,
"frac14": 0x00BC,
"frac12": 0x00BD,
"frac34": 0x00BE,
"iquest": 0x00BF,
"Agrave": 0x00C0,
"Aacute": 0x00C1,
"Acirc": 0x00C2,
"Atilde": 0x00C3,
"Auml": 0x00C4,
"Aring": 0x00C5,
"AElig": 0x00C6,
"Ccedil": 0x00C7,
"Egrave": 0x00C8,
"Eacute": 0x00C9,
"Ecirc": 0x00CA,
"Euml": 0x00CB,
"Igrave": 0x00CC,
"Iacute": 0x00CD,
"Icirc": 0x00CE,
"Iuml": 0x00CF,
"ETH": 0x00D0,
"Ntilde": 0x00D1,
"Ograve": 0x00D2,
"Oacute": 0x00D3,
"Ocirc": 0x00D4,
"Otilde": 0x00D5,
"Ouml": 0x00D6,
"times": 0x00D7,
"Oslash": 0x00D8,
"Ugrave": 0x00D9,
"Uacute": 0x00DA,
"Ucirc": 0x00DB,
"Uuml": 0x00DC,
"Yacute": 0x00DD,
"THORN": 0x00DE,
"szlig": 0x00DF,
"agrave": 0x00E0,
"aacute": 0x00E1,
"acirc": 0x00E2,
"atilde": 0x00E3,
"auml": 0x00E4,
"aring": 0x00E5,
"aelig": 0x00E6,
"ccedil": 0x00E7,
"egrave": 0x00E8,
"eacute": 0x00E9,
"ecirc": 0x00EA,
"euml": 0x00EB,
"igrave": 0x00EC,
"iacute": 0x00ED,
"icirc": 0x00EE,
"iuml": 0x00EF,
"eth": 0x00F0,
"ntilde": 0x00F1,
"ograve": 0x00F2,
"oacute": 0x00F3,
"ocirc": 0x00F4,
"otilde": 0x00F5,
"ouml": 0x00F6,
"divide": 0x00F7,
"oslash": 0x00F8,
"ugrave": 0x00F9,
"uacute": 0x00FA,
"ucirc": 0x00FB,
"uuml": 0x00FC,
"yacute": 0x00FD,
"thorn": 0x00FE,
"yuml": 0x00FF,
"OElig": 0x0152,
"oelig": 0x0153,
"Scaron": 0x0160,
"scaron": 0x0161,
"Yuml": 0x0178,
"fnof": 0x0192,
"circ": 0x02C6,
"tilde": 0x02DC,
"Alpha": 0x0391,
"Beta": 0x0392,
"Gamma": 0x0393,
"Delta": 0x0394,
"Epsilon": 0x0395,
"Zeta": 0x0396,
"Eta": 0x0397,
"Theta": 0x0398,
"Iota": 0x0399,
"Kappa": 0x039A,
"Lambda": 0x039B,
"Mu": 0x039C,
"Nu": 0x039D,
"Xi": 0x039E,
"Omicron": 0x039F,
"Pi": 0x03A0,
"Rho": 0x03A1,
"Sigma": 0x03A3,
"Tau": 0x03A4,
"Upsilon": 0x03A5,
"Phi": 0x03A6,
"Chi": 0x03A7,
"Psi": 0x03A8,
"Omega": 0x03A9,
"alpha": 0x03B1,
"beta": 0x03B2,
"gamma": 0x03B3,
"delta": 0x03B4,
"epsilon": 0x03B5,
"zeta": 0x03B6,
"eta": 0x03B7,
"theta": 0x03B8,
"iota": 0x03B9,
"kappa": 0x03BA,
"lambda": 0x03BB,
"mu": 0x03BC,
"nu": 0x03BD,
"xi": 0x03BE,
"omicron": 0x03BF,
"pi": 0x03C0,
"rho": 0x03C1,
"sigmaf": 0x03C2,
"sigma": 0x03C3,
"tau": 0x03C4,
"upsilon": 0x03C5,
"phi": 0x03C6,
"chi": 0x03C7,
"psi": 0x03C8,
"omega": 0x03C9,
"thetasym": 0x03D1,
"upsih": 0x03D2,
"piv": 0x03D6,
"ensp": 0x2002,
"emsp": 0x2003,
"thinsp": 0x2009,
"zwnj": 0x200C,
"zwj": 0x200D,
"lrm": 0x200E,
"rlm": 0x200F,
"ndash": 0x2013,
"mdash": 0x2014,
"lsquo": 0x2018,
"rsquo": 0x2019,
"sbquo": 0x201A,
"ldquo": 0x201C,
"rdquo": 0x201D,
"bdquo": 0x201E,
"dagger": 0x2020,
"Dagger": 0x2021,
"bull": 0x2022,
"hellip": 0x2026,
"permil": 0x2030,
"prime": 0x2032,
"Prime": 0x2033,
"lsaquo": 0x2039,
"rsaquo": 0x203A,
"oline": 0x203E,
"frasl": 0x2044,
"euro": 0x20AC,
"image": 0x2111,
"weierp": 0x2118,
"real": 0x211C,
"trade": 0x2122,
"alefsym": 0x2135,
"larr": 0x2190,
"uarr": 0x2191,
"rarr": 0x2192,
"darr": 0x2193,
"harr": 0x2194,
"crarr": 0x21B5,
"lArr": 0x21D0,
"uArr": 0x21D1,
"rArr": 0x21D2,
"dArr": 0x21D3,
"hArr": 0x21D4,
"forall": 0x2200,
"part": 0x2202,
"exist": 0x2203,
"empty": 0x2205,
"nabla": 0x2207,
"isin": 0x2208,
"notin": 0x2209,
"ni": 0x220B,
"prod": 0x220F,
"sum": 0x2211,
"minus": 0x2212,
"lowast": 0x2217,
"radic": 0x221A,
"prop": 0x221D,
"infin": 0x221E,
"ang": 0x2220,
"and": 0x2227,
"or": 0x2228,
"cap": 0x2229,
"cup": 0x222A,
"int": 0x222B,
"there4": 0x2234,
"sim": 0x223C,
"cong": 0x2245,
"asymp": 0x2248,
"ne": 0x2260,
"equiv": 0x2261,
"le": 0x2264,
"ge": 0x2265,
"sub": 0x2282,
"sup": 0x2283,
"nsub": 0x2284,
"sube": 0x2286,
"supe": 0x2287,
"oplus": 0x2295,
"otimes": 0x2297,
"perp": 0x22A5,
"sdot": 0x22C5,
"lceil": 0x2308,
"rceil": 0x2309,
"lfloor": 0x230A,
"rfloor": 0x230B,
"lang": 0x2329,
"rang": 0x232A,
"loz": 0x25CA,
"spades": 0x2660,
"clubs": 0x2663,
"hearts": 0x2665,
"diams": 0x2666,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +0,0 @@
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
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,187 +0,0 @@
package js_parser
import (
"fmt"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/logger"
)
type jsonParser struct {
log logger.Log
source logger.Source
tracker logger.LineColumnTracker
lexer js_lexer.Lexer
options JSONOptions
suppressWarningsAboutWeirdCode bool
}
func (p *jsonParser) parseMaybeTrailingComma(closeToken js_lexer.T) bool {
commaRange := p.lexer.Range()
p.lexer.Expect(js_lexer.TComma)
if p.lexer.Token == closeToken {
if !p.options.AllowTrailingCommas {
p.log.Add(logger.Error, &p.tracker, commaRange, "JSON does not support trailing commas")
}
return false
}
return true
}
func (p *jsonParser) parseExpr() js_ast.Expr {
loc := p.lexer.Loc()
switch p.lexer.Token {
case js_lexer.TFalse:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: false}}
case js_lexer.TTrue:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EBoolean{Value: true}}
case js_lexer.TNull:
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: js_ast.ENullShared}
case js_lexer.TStringLiteral:
value := p.lexer.StringLiteral()
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.EString{Value: value}}
case js_lexer.TNumericLiteral:
value := p.lexer.Number
p.lexer.Next()
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: value}}
case js_lexer.TMinus:
p.lexer.Next()
value := p.lexer.Number
p.lexer.Expect(js_lexer.TNumericLiteral)
return js_ast.Expr{Loc: loc, Data: &js_ast.ENumber{Value: -value}}
case js_lexer.TOpenBracket:
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
items := []js_ast.Expr{}
for p.lexer.Token != js_lexer.TCloseBracket {
if len(items) > 0 {
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
if !p.parseMaybeTrailingComma(js_lexer.TCloseBracket) {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
item := p.parseExpr()
items = append(items, item)
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Expect(js_lexer.TCloseBracket)
return js_ast.Expr{Loc: loc, Data: &js_ast.EArray{
Items: items,
IsSingleLine: isSingleLine,
}}
case js_lexer.TOpenBrace:
p.lexer.Next()
isSingleLine := !p.lexer.HasNewlineBefore
properties := []js_ast.Property{}
duplicates := make(map[string]logger.Range)
for p.lexer.Token != js_lexer.TCloseBrace {
if len(properties) > 0 {
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
if !p.parseMaybeTrailingComma(js_lexer.TCloseBrace) {
break
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
}
keyString := p.lexer.StringLiteral()
keyRange := p.lexer.Range()
key := js_ast.Expr{Loc: keyRange.Loc, Data: &js_ast.EString{Value: keyString}}
p.lexer.Expect(js_lexer.TStringLiteral)
// Warn about duplicate keys
if !p.suppressWarningsAboutWeirdCode {
keyText := js_lexer.UTF16ToString(keyString)
if prevRange, ok := duplicates[keyText]; ok {
p.log.AddWithNotes(logger.Warning, &p.tracker, keyRange, fmt.Sprintf("Duplicate key %q in object literal", keyText),
[]logger.MsgData{p.tracker.MsgData(prevRange, fmt.Sprintf("The original key %q is here:", keyText))})
} else {
duplicates[keyText] = keyRange
}
}
p.lexer.Expect(js_lexer.TColon)
value := p.parseExpr()
property := js_ast.Property{
Kind: js_ast.PropertyNormal,
Key: key,
ValueOrNil: value,
}
properties = append(properties, property)
}
if p.lexer.HasNewlineBefore {
isSingleLine = false
}
p.lexer.Expect(js_lexer.TCloseBrace)
return js_ast.Expr{Loc: loc, Data: &js_ast.EObject{
Properties: properties,
IsSingleLine: isSingleLine,
}}
default:
p.lexer.Unexpected()
return js_ast.Expr{}
}
}
type JSONOptions struct {
AllowComments bool
AllowTrailingCommas bool
}
func ParseJSON(log logger.Log, source logger.Source, options JSONOptions) (result js_ast.Expr, ok bool) {
ok = true
defer func() {
r := recover()
if _, isLexerPanic := r.(js_lexer.LexerPanic); isLexerPanic {
ok = false
} else if r != nil {
panic(r)
}
}()
p := &jsonParser{
log: log,
source: source,
tracker: logger.MakeLineColumnTracker(&source),
options: options,
lexer: js_lexer.NewLexerJSON(log, source, options.AllowComments),
suppressWarningsAboutWeirdCode: helpers.IsInsideNodeModules(source.KeyPath.Text),
}
result = p.parseExpr()
p.lexer.Expect(js_lexer.TEndOfFile)
return
}

View File

@ -1,251 +0,0 @@
package js_parser
import (
"fmt"
"sort"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/logger"
"github.com/evanw/esbuild/internal/sourcemap"
)
// Specification: https://sourcemaps.info/spec.html
func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
expr, ok := ParseJSON(log, source, JSONOptions{})
if !ok {
return nil
}
obj, ok := expr.Data.(*js_ast.EObject)
tracker := logger.MakeLineColumnTracker(&source)
if !ok {
log.Add(logger.Error, &tracker, logger.Range{Loc: expr.Loc}, "Invalid source map")
return nil
}
var sources []string
var sourcesContent []sourcemap.SourceContent
var mappingsRaw []uint16
var mappingsStart int32
hasVersion := false
for _, prop := range obj.Properties {
keyRange := source.RangeOfString(prop.Key.Loc)
switch js_lexer.UTF16ToString(prop.Key.Data.(*js_ast.EString).Value) {
case "sections":
log.Add(logger.Warning, &tracker, keyRange, "Source maps with \"sections\" are not supported")
return nil
case "version":
if value, ok := prop.ValueOrNil.Data.(*js_ast.ENumber); ok && value.Value == 3 {
hasVersion = true
}
case "mappings":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EString); ok {
mappingsRaw = value.Value
mappingsStart = prop.ValueOrNil.Loc.Start + 1
}
case "sources":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sources = nil
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EString); ok {
sources = append(sources, js_lexer.UTF16ToString(element.Value))
} else {
sources = append(sources, "")
}
}
}
case "sourcesContent":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sourcesContent = nil
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EString); ok {
sourcesContent = append(sourcesContent, sourcemap.SourceContent{
Value: element.Value,
Quoted: source.TextForRange(source.RangeOfString(item.Loc)),
})
} else {
sourcesContent = append(sourcesContent, sourcemap.SourceContent{})
}
}
}
}
}
// Silently fail if the version was missing or incorrect
if !hasVersion {
return nil
}
// Silently fail if the source map is pointless (i.e. empty)
if len(sources) == 0 || len(mappingsRaw) == 0 {
return nil
}
var mappings mappingArray
mappingsLen := len(mappingsRaw)
sourcesLen := len(sources)
generatedLine := 0
generatedColumn := 0
sourceIndex := 0
originalLine := 0
originalColumn := 0
current := 0
errorText := ""
errorLen := 0
needSort := false
// Parse the mappings
for current < mappingsLen {
// Handle a line break
if mappingsRaw[current] == ';' {
generatedLine++
generatedColumn = 0
current++
continue
}
// Read the generated column
generatedColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing generated column"
errorLen = i
break
}
if generatedColumnDelta < 0 {
// This would mess up binary search
needSort = true
}
generatedColumn += generatedColumnDelta
if generatedColumn < 0 {
errorText = fmt.Sprintf("Invalid generated column value: %d", generatedColumn)
errorLen = i
break
}
current += i
// According to the specification, it's valid for a mapping to have 1,
// 4, or 5 variable-length fields. Having one field means there's no
// original location information, which is pretty useless. Just ignore
// those entries.
if current == mappingsLen {
break
}
switch mappingsRaw[current] {
case ',':
current++
continue
case ';':
continue
}
// Read the original source
sourceIndexDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing source index"
errorLen = i
break
}
sourceIndex += sourceIndexDelta
if sourceIndex < 0 || sourceIndex >= sourcesLen {
errorText = fmt.Sprintf("Invalid source index value: %d", sourceIndex)
errorLen = i
break
}
current += i
// Read the original line
originalLineDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing original line"
errorLen = i
break
}
originalLine += originalLineDelta
if originalLine < 0 {
errorText = fmt.Sprintf("Invalid original line value: %d", originalLine)
errorLen = i
break
}
current += i
// Read the original column
originalColumnDelta, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:])
if !ok {
errorText = "Missing original column"
errorLen = i
break
}
originalColumn += originalColumnDelta
if originalColumn < 0 {
errorText = fmt.Sprintf("Invalid original column value: %d", originalColumn)
errorLen = i
break
}
current += i
// Ignore the optional name index
if _, i, ok := sourcemap.DecodeVLQUTF16(mappingsRaw[current:]); ok {
current += i
}
// Handle the next character
if current < mappingsLen {
if c := mappingsRaw[current]; c == ',' {
current++
} else if c != ';' {
errorText = fmt.Sprintf("Invalid character after mapping: %q",
js_lexer.UTF16ToString(mappingsRaw[current:current+1]))
errorLen = 1
break
}
}
mappings = append(mappings, sourcemap.Mapping{
GeneratedLine: int32(generatedLine),
GeneratedColumn: int32(generatedColumn),
SourceIndex: int32(sourceIndex),
OriginalLine: int32(originalLine),
OriginalColumn: int32(originalColumn),
})
}
if errorText != "" {
r := logger.Range{Loc: logger.Loc{Start: mappingsStart + int32(current)}, Len: int32(errorLen)}
log.Add(logger.Warning, &tracker, r,
fmt.Sprintf("Bad \"mappings\" data in source map at character %d: %s", current, errorText))
return nil
}
if needSort {
// If we get here, some mappings are out of order. Lines can't be out of
// order by construction but columns can. This is a pretty rare situation
// because almost all source map generators always write out mappings in
// order as they write the output instead of scrambling the order.
sort.Stable(mappings)
}
return &sourcemap.SourceMap{
Sources: sources,
SourcesContent: sourcesContent,
Mappings: mappings,
}
}
// This type is just so we can use Go's native sort function
type mappingArray []sourcemap.Mapping
func (a mappingArray) Len() int { return len(a) }
func (a mappingArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a mappingArray) Less(i int, j int) bool {
ai := a[i]
aj := a[j]
return ai.GeneratedLine < aj.GeneratedLine || (ai.GeneratedLine == aj.GeneratedLine && ai.GeneratedColumn <= aj.GeneratedColumn)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +0,0 @@
//go:build darwin
// +build darwin
package logger
import (
"os"
"golang.org/x/sys/unix"
)
const SupportsColorEscapes = true
func GetTerminalInfo(file *os.File) (info TerminalInfo) {
fd := file.Fd()
// Is this file descriptor a terminal?
if _, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA); err == nil {
info.IsTTY = true
info.UseColorEscapes = !hasNoColorEnvironmentVariable()
// Get the width of the window
if w, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ); err == nil {
info.Width = int(w.Col)
info.Height = int(w.Row)
}
}
return
}
func writeStringWithColor(file *os.File, text string) {
file.WriteString(text)
}

View File

@ -1,34 +0,0 @@
//go:build linux
// +build linux
package logger
import (
"os"
"golang.org/x/sys/unix"
)
const SupportsColorEscapes = true
func GetTerminalInfo(file *os.File) (info TerminalInfo) {
fd := file.Fd()
// Is this file descriptor a terminal?
if _, err := unix.IoctlGetTermios(int(fd), unix.TCGETS); err == nil {
info.IsTTY = true
info.UseColorEscapes = !hasNoColorEnvironmentVariable()
// Get the width of the window
if w, err := unix.IoctlGetWinsize(int(fd), unix.TIOCGWINSZ); err == nil {
info.Width = int(w.Col)
info.Height = int(w.Row)
}
}
return
}
func writeStringWithColor(file *os.File, text string) {
file.WriteString(text)
}

View File

@ -1,16 +0,0 @@
//go:build !darwin && !linux && !windows
// +build !darwin,!linux,!windows
package logger
import "os"
const SupportsColorEscapes = false
func GetTerminalInfo(*os.File) TerminalInfo {
return TerminalInfo{}
}
func writeStringWithColor(file *os.File, text string) {
file.WriteString(text)
}

View File

@ -1,136 +0,0 @@
//go:build windows
// +build windows
package logger
import (
"os"
"strings"
"syscall"
"unsafe"
)
const SupportsColorEscapes = true
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var getConsoleMode = kernel32.NewProc("GetConsoleMode")
var setConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
var getConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
type consoleScreenBufferInfo struct {
dwSizeX int16
dwSizeY int16
dwCursorPositionX int16
dwCursorPositionY int16
wAttributes uint16
srWindowLeft int16
srWindowTop int16
srWindowRight int16
srWindowBottom int16
dwMaximumWindowSizeX int16
dwMaximumWindowSizeY int16
}
func GetTerminalInfo(file *os.File) TerminalInfo {
fd := file.Fd()
// Is this file descriptor a terminal?
var unused uint32
isTTY, _, _ := syscall.Syscall(getConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&unused)), 0)
// Get the width of the window
var info consoleScreenBufferInfo
syscall.Syscall(getConsoleScreenBufferInfo.Addr(), 2, fd, uintptr(unsafe.Pointer(&info)), 0)
return TerminalInfo{
IsTTY: isTTY != 0,
Width: int(info.dwSizeX) - 1,
Height: int(info.dwSizeY) - 1,
UseColorEscapes: !hasNoColorEnvironmentVariable(),
}
}
const (
FOREGROUND_BLUE uint8 = 1 << iota
FOREGROUND_GREEN
FOREGROUND_RED
FOREGROUND_INTENSITY
BACKGROUND_BLUE
BACKGROUND_GREEN
BACKGROUND_RED
BACKGROUND_INTENSITY
)
var windowsEscapeSequenceMap = map[string]uint8{
TerminalColors.Reset: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
TerminalColors.Dim: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
TerminalColors.Bold: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY,
// Apparently underlines only work with the CJK locale on Windows :(
TerminalColors.Underline: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE,
TerminalColors.Red: FOREGROUND_RED,
TerminalColors.Green: FOREGROUND_GREEN,
TerminalColors.Blue: FOREGROUND_BLUE,
TerminalColors.Cyan: FOREGROUND_GREEN | FOREGROUND_BLUE,
TerminalColors.Magenta: FOREGROUND_RED | FOREGROUND_BLUE,
TerminalColors.Yellow: FOREGROUND_RED | FOREGROUND_GREEN,
TerminalColors.RedBgRed: FOREGROUND_RED | BACKGROUND_RED,
TerminalColors.RedBgWhite: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | BACKGROUND_RED,
TerminalColors.GreenBgGreen: FOREGROUND_GREEN | BACKGROUND_GREEN,
TerminalColors.GreenBgWhite: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | BACKGROUND_GREEN,
TerminalColors.BlueBgBlue: FOREGROUND_BLUE | BACKGROUND_BLUE,
TerminalColors.BlueBgWhite: FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | BACKGROUND_BLUE,
TerminalColors.CyanBgCyan: FOREGROUND_GREEN | FOREGROUND_BLUE | BACKGROUND_GREEN | BACKGROUND_BLUE,
TerminalColors.CyanBgBlack: BACKGROUND_GREEN | BACKGROUND_BLUE,
TerminalColors.MagentaBgMagenta: FOREGROUND_RED | FOREGROUND_BLUE | BACKGROUND_RED | BACKGROUND_BLUE,
TerminalColors.MagentaBgBlack: BACKGROUND_RED | BACKGROUND_BLUE,
TerminalColors.YellowBgYellow: FOREGROUND_RED | FOREGROUND_GREEN | BACKGROUND_RED | BACKGROUND_GREEN,
TerminalColors.YellowBgBlack: BACKGROUND_RED | BACKGROUND_GREEN,
}
func writeStringWithColor(file *os.File, text string) {
fd := file.Fd()
i := 0
for i < len(text) {
// Find the escape
if text[i] != 033 {
i++
continue
}
// Find the 'm'
window := text[i:]
if len(window) > 8 {
window = window[:8]
}
m := strings.IndexByte(window, 'm')
if m == -1 {
i++
continue
}
m += i + 1
// Find the escape sequence
attributes, ok := windowsEscapeSequenceMap[text[i:m]]
if !ok {
i++
continue
}
// Write out the text before the escape sequence
file.WriteString(text[:i])
// Apply the escape sequence
text = text[m:]
i = 0
setConsoleTextAttribute.Call(fd, uintptr(attributes))
}
// Write out the remaining text
file.WriteString(text)
}

View File

@ -1,624 +0,0 @@
package renamer
import (
"fmt"
"sort"
"strconv"
"sync"
"sync/atomic"
"github.com/evanw/esbuild/internal/ast"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
)
func ComputeReservedNames(moduleScopes []*js_ast.Scope, symbols js_ast.SymbolMap) map[string]uint32 {
names := make(map[string]uint32)
// All keywords and strict mode reserved words are reserved names
for k := range js_lexer.Keywords {
names[k] = 1
}
for k := range js_lexer.StrictModeReservedWords {
names[k] = 1
}
// All unbound symbols must be reserved names
for _, scope := range moduleScopes {
computeReservedNamesForScope(scope, symbols, names)
}
return names
}
func computeReservedNamesForScope(scope *js_ast.Scope, symbols js_ast.SymbolMap, names map[string]uint32) {
for _, member := range scope.Members {
symbol := symbols.Get(member.Ref)
if symbol.Kind == js_ast.SymbolUnbound || symbol.MustNotBeRenamed {
names[symbol.OriginalName] = 1
}
}
for _, ref := range scope.Generated {
symbol := symbols.Get(ref)
if symbol.Kind == js_ast.SymbolUnbound || symbol.MustNotBeRenamed {
names[symbol.OriginalName] = 1
}
}
// If there's a direct "eval" somewhere inside the current scope, continue
// traversing down the scope tree until we find it to get all reserved names
if scope.ContainsDirectEval {
for _, child := range scope.Children {
if child.ContainsDirectEval {
computeReservedNamesForScope(child, symbols, names)
}
}
}
}
type Renamer interface {
NameForSymbol(ref js_ast.Ref) string
}
////////////////////////////////////////////////////////////////////////////////
// noOpRenamer
type noOpRenamer struct {
symbols js_ast.SymbolMap
}
func NewNoOpRenamer(symbols js_ast.SymbolMap) Renamer {
return &noOpRenamer{
symbols: symbols,
}
}
func (r *noOpRenamer) NameForSymbol(ref js_ast.Ref) string {
ref = js_ast.FollowSymbols(r.symbols, ref)
return r.symbols.Get(ref).OriginalName
}
////////////////////////////////////////////////////////////////////////////////
// MinifyRenamer
type symbolSlot struct {
name string
count uint32
needsCapitalForJSX uint32 // This is really a bool but needs to be atomic
}
type MinifyRenamer struct {
symbols js_ast.SymbolMap
reservedNames map[string]uint32
slots [3][]symbolSlot
topLevelSymbolToSlot map[js_ast.Ref]uint32
}
func NewMinifyRenamer(symbols js_ast.SymbolMap, firstTopLevelSlots js_ast.SlotCounts, reservedNames map[string]uint32) *MinifyRenamer {
return &MinifyRenamer{
symbols: symbols,
reservedNames: reservedNames,
slots: [3][]symbolSlot{
make([]symbolSlot, firstTopLevelSlots[0]),
make([]symbolSlot, firstTopLevelSlots[1]),
make([]symbolSlot, firstTopLevelSlots[2]),
},
topLevelSymbolToSlot: make(map[js_ast.Ref]uint32),
}
}
func (r *MinifyRenamer) NameForSymbol(ref js_ast.Ref) string {
// Follow links to get to the underlying symbol
ref = js_ast.FollowSymbols(r.symbols, ref)
symbol := r.symbols.Get(ref)
// Skip this symbol if the name is pinned
ns := symbol.SlotNamespace()
if ns == js_ast.SlotMustNotBeRenamed {
return symbol.OriginalName
}
// Check if it's a nested scope symbol
i := symbol.NestedScopeSlot
// If it's not (i.e. it's in a top-level scope), look up the slot
if !i.IsValid() {
index, ok := r.topLevelSymbolToSlot[ref]
if !ok {
// If we get here, then we're printing a symbol that never had any
// recorded uses. This is odd but can happen in certain scenarios.
// For example, code in a branch with dead control flow won't mark
// any uses but may still be printed. In that case it doesn't matter
// what name we use since it's dead code.
return symbol.OriginalName
}
i = ast.MakeIndex32(index)
}
return r.slots[ns][i.GetIndex()].name
}
// The sort order here is arbitrary but needs to be consistent between builds.
// The InnerIndex should be stable because the parser for a single file is
// single-threaded and deterministically assigns out InnerIndex values
// sequentially. But the SourceIndex should be unstable because the main thread
// assigns out source index values sequentially to newly-discovered dependencies
// in a multi-threaded producer/consumer relationship. So instead we use the
// index of the source in the DFS order over all entry points for stability.
type DeferredTopLevelSymbol struct {
StableSourceIndex uint32
Ref js_ast.Ref
Count uint32
}
// This type is just so we can use Go's native sort function
type DeferredTopLevelSymbolArray []DeferredTopLevelSymbol
func (a DeferredTopLevelSymbolArray) Len() int { return len(a) }
func (a DeferredTopLevelSymbolArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a DeferredTopLevelSymbolArray) Less(i int, j int) bool {
ai, aj := a[i], a[j]
if ai.StableSourceIndex < aj.StableSourceIndex {
return true
}
if ai.StableSourceIndex > aj.StableSourceIndex {
return false
}
if ai.Ref.InnerIndex < aj.Ref.InnerIndex {
return true
}
if ai.Ref.InnerIndex > aj.Ref.InnerIndex {
return false
}
return ai.Count < aj.Count
}
func (r *MinifyRenamer) AccumulateSymbolUseCounts(
topLevelSymbols *DeferredTopLevelSymbolArray,
symbolUses map[js_ast.Ref]js_ast.SymbolUse,
stableSourceIndices []uint32,
) {
// NOTE: This function is run in parallel. Make sure to avoid data races.
for ref, use := range symbolUses {
r.AccumulateSymbolCount(topLevelSymbols, ref, use.CountEstimate, stableSourceIndices)
}
}
func (r *MinifyRenamer) AccumulateSymbolCount(
topLevelSymbols *DeferredTopLevelSymbolArray,
ref js_ast.Ref,
count uint32,
stableSourceIndices []uint32,
) {
// NOTE: This function is run in parallel. Make sure to avoid data races.
// Follow links to get to the underlying symbol
ref = js_ast.FollowSymbols(r.symbols, ref)
symbol := r.symbols.Get(ref)
for symbol.NamespaceAlias != nil {
ref = js_ast.FollowSymbols(r.symbols, symbol.NamespaceAlias.NamespaceRef)
symbol = r.symbols.Get(ref)
}
// Skip this symbol if the name is pinned
ns := symbol.SlotNamespace()
if ns == js_ast.SlotMustNotBeRenamed {
return
}
// Check if it's a nested scope symbol
if i := symbol.NestedScopeSlot; i.IsValid() {
// If it is, accumulate the count using a parallel-safe atomic increment
slot := &r.slots[ns][i.GetIndex()]
atomic.AddUint32(&slot.count, count)
if symbol.MustStartWithCapitalLetterForJSX {
atomic.StoreUint32(&slot.needsCapitalForJSX, 1)
}
return
}
// If it's a top-level symbol, defer it to later since we have
// to allocate slots for these in serial instead of in parallel
*topLevelSymbols = append(*topLevelSymbols, DeferredTopLevelSymbol{
StableSourceIndex: stableSourceIndices[ref.SourceIndex],
Ref: ref,
Count: count,
})
}
// The parallel part of the symbol count accumulation algorithm above processes
// nested symbols and generates on an array of top-level symbols to process later.
// After the parallel part has finished, that array of top-level symbols is passed
// to this function which processes them in serial.
func (r *MinifyRenamer) AllocateTopLevelSymbolSlots(topLevelSymbols DeferredTopLevelSymbolArray) {
for _, stable := range topLevelSymbols {
symbol := r.symbols.Get(stable.Ref)
slots := &r.slots[symbol.SlotNamespace()]
if i, ok := r.topLevelSymbolToSlot[stable.Ref]; ok {
slot := &(*slots)[i]
slot.count += stable.Count
if symbol.MustStartWithCapitalLetterForJSX {
slot.needsCapitalForJSX = 1
}
} else {
needsCapitalForJSX := uint32(0)
if symbol.MustStartWithCapitalLetterForJSX {
needsCapitalForJSX = 1
}
i = uint32(len(*slots))
*slots = append(*slots, symbolSlot{
count: stable.Count,
needsCapitalForJSX: needsCapitalForJSX,
})
r.topLevelSymbolToSlot[stable.Ref] = i
}
}
}
func (r *MinifyRenamer) AssignNamesByFrequency(minifier *js_ast.NameMinifier) {
for ns, slots := range r.slots {
// Sort symbols by count
sorted := make(slotAndCountArray, len(slots))
for i, item := range slots {
sorted[i] = slotAndCount{slot: uint32(i), count: item.count}
}
sort.Sort(sorted)
// Assign names to symbols
nextName := 0
for _, data := range sorted {
slot := &slots[data.slot]
name := minifier.NumberToMinifiedName(nextName)
nextName++
// Make sure we never generate a reserved name. We only have to worry
// about collisions with reserved identifiers for normal symbols, and we
// only have to worry about collisions with keywords for labels. We do
// not have to worry about either for private names because they start
// with a "#" character.
switch js_ast.SlotNamespace(ns) {
case js_ast.SlotDefault:
for r.reservedNames[name] != 0 {
name = minifier.NumberToMinifiedName(nextName)
nextName++
}
// Make sure names of symbols used in JSX elements start with a capital letter
if slot.needsCapitalForJSX != 0 {
for name[0] >= 'a' && name[0] <= 'z' {
name = minifier.NumberToMinifiedName(nextName)
nextName++
}
}
case js_ast.SlotLabel:
for js_lexer.Keywords[name] != 0 {
name = minifier.NumberToMinifiedName(nextName)
nextName++
}
}
// Private names must be prefixed with "#"
if js_ast.SlotNamespace(ns) == js_ast.SlotPrivateName {
name = "#" + name
}
slot.name = name
}
}
}
// Returns the number of nested slots
func AssignNestedScopeSlots(moduleScope *js_ast.Scope, symbols []js_ast.Symbol) (slotCounts js_ast.SlotCounts) {
// Temporarily set the nested scope slots of top-level symbols to valid so
// they aren't renamed in nested scopes. This prevents us from accidentally
// assigning nested scope slots to variables declared using "var" in a nested
// scope that are actually hoisted up to the module scope to become a top-
// level symbol.
validSlot := ast.MakeIndex32(1)
for _, member := range moduleScope.Members {
symbols[member.Ref.InnerIndex].NestedScopeSlot = validSlot
}
for _, ref := range moduleScope.Generated {
symbols[ref.InnerIndex].NestedScopeSlot = validSlot
}
// Assign nested scope slots independently for each nested scope
for _, child := range moduleScope.Children {
slotCounts.UnionMax(assignNestedScopeSlotsHelper(child, symbols, js_ast.SlotCounts{}))
}
// Then set the nested scope slots of top-level symbols back to zero. Top-
// level symbols are not supposed to have nested scope slots.
for _, member := range moduleScope.Members {
symbols[member.Ref.InnerIndex].NestedScopeSlot = ast.Index32{}
}
for _, ref := range moduleScope.Generated {
symbols[ref.InnerIndex].NestedScopeSlot = ast.Index32{}
}
return
}
func assignNestedScopeSlotsHelper(scope *js_ast.Scope, symbols []js_ast.Symbol, slot js_ast.SlotCounts) js_ast.SlotCounts {
// Sort member map keys for determinism
sortedMembers := make([]int, 0, len(scope.Members))
for _, member := range scope.Members {
sortedMembers = append(sortedMembers, int(member.Ref.InnerIndex))
}
sort.Ints(sortedMembers)
// Assign slots for this scope's symbols. Only do this if the slot is
// not already assigned. Nested scopes have copies of symbols from parent
// scopes and we want to use the slot from the parent scope, not child scopes.
for _, innerIndex := range sortedMembers {
symbol := &symbols[innerIndex]
if ns := symbol.SlotNamespace(); ns != js_ast.SlotMustNotBeRenamed && !symbol.NestedScopeSlot.IsValid() {
symbol.NestedScopeSlot = ast.MakeIndex32(slot[ns])
slot[ns]++
}
}
for _, ref := range scope.Generated {
symbol := &symbols[ref.InnerIndex]
if ns := symbol.SlotNamespace(); ns != js_ast.SlotMustNotBeRenamed && !symbol.NestedScopeSlot.IsValid() {
symbol.NestedScopeSlot = ast.MakeIndex32(slot[ns])
slot[ns]++
}
}
// Labels are always declared in a nested scope, so we don't need to check.
if scope.Label.Ref != js_ast.InvalidRef {
symbol := &symbols[scope.Label.Ref.InnerIndex]
symbol.NestedScopeSlot = ast.MakeIndex32(slot[js_ast.SlotLabel])
slot[js_ast.SlotLabel]++
}
// Assign slots for the symbols of child scopes
slotCounts := slot
for _, child := range scope.Children {
slotCounts.UnionMax(assignNestedScopeSlotsHelper(child, symbols, slot))
}
return slotCounts
}
type slotAndCount struct {
slot uint32
count uint32
}
// This type is just so we can use Go's native sort function
type slotAndCountArray []slotAndCount
func (a slotAndCountArray) Len() int { return len(a) }
func (a slotAndCountArray) Swap(i int, j int) { a[i], a[j] = a[j], a[i] }
func (a slotAndCountArray) Less(i int, j int) bool {
ai, aj := a[i], a[j]
return ai.count > aj.count || (ai.count == aj.count && ai.slot < aj.slot)
}
////////////////////////////////////////////////////////////////////////////////
// NumberRenamer
type NumberRenamer struct {
symbols js_ast.SymbolMap
names [][]string
root numberScope
}
func NewNumberRenamer(symbols js_ast.SymbolMap, reservedNames map[string]uint32) *NumberRenamer {
return &NumberRenamer{
symbols: symbols,
names: make([][]string, len(symbols.SymbolsForSource)),
root: numberScope{nameCounts: reservedNames},
}
}
func (r *NumberRenamer) NameForSymbol(ref js_ast.Ref) string {
ref = js_ast.FollowSymbols(r.symbols, ref)
if inner := r.names[ref.SourceIndex]; inner != nil {
if name := inner[ref.InnerIndex]; name != "" {
return name
}
}
return r.symbols.Get(ref).OriginalName
}
func (r *NumberRenamer) AddTopLevelSymbol(ref js_ast.Ref) {
r.assignName(&r.root, ref)
}
func (r *NumberRenamer) assignName(scope *numberScope, ref js_ast.Ref) {
ref = js_ast.FollowSymbols(r.symbols, ref)
// Don't rename the same symbol more than once
inner := r.names[ref.SourceIndex]
if inner != nil && inner[ref.InnerIndex] != "" {
return
}
// Don't rename unbound symbols, symbols marked as reserved names, labels, or private names
symbol := r.symbols.Get(ref)
if symbol.SlotNamespace() != js_ast.SlotDefault {
return
}
// Make sure names of symbols used in JSX elements start with a capital letter
originalName := symbol.OriginalName
if symbol.MustStartWithCapitalLetterForJSX {
if first := rune(originalName[0]); first >= 'a' && first <= 'z' {
originalName = fmt.Sprintf("%c%s", first+('A'-'a'), originalName[1:])
}
}
// Compute a new name
name := scope.findUnusedName(originalName)
// Store the new name
if inner == nil {
// Note: This should not be a data race even though this method is run from
// multiple threads. The parallel part only looks at symbols defined in
// nested scopes, and those can only ever be accessed from within the file.
// References to those symbols should never spread across files.
//
// While we could avoid the data race by densely preallocating the entire
// "names" array ahead of time, that will waste a lot more memory for
// builds that make heavy use of code splitting and have many chunks. Doing
// things lazily like this means we use less memory but still stay safe.
inner = make([]string, len(r.symbols.SymbolsForSource[ref.SourceIndex]))
r.names[ref.SourceIndex] = inner
}
inner[ref.InnerIndex] = name
}
func (r *NumberRenamer) assignNamesRecursive(scope *js_ast.Scope, sourceIndex uint32, parent *numberScope, sorted *[]int) {
s := &numberScope{parent: parent, nameCounts: make(map[string]uint32)}
// Sort member map keys for determinism, reusing a shared memory buffer
*sorted = (*sorted)[:0]
for _, member := range scope.Members {
*sorted = append(*sorted, int(member.Ref.InnerIndex))
}
sort.Ints(*sorted)
// Rename all symbols in this scope
for _, innerIndex := range *sorted {
r.assignName(s, js_ast.Ref{SourceIndex: sourceIndex, InnerIndex: uint32(innerIndex)})
}
for _, ref := range scope.Generated {
r.assignName(s, ref)
}
// Symbols in child scopes may also have to be renamed to avoid conflicts
for _, child := range scope.Children {
r.assignNamesRecursive(child, sourceIndex, s, sorted)
}
}
func (r *NumberRenamer) AssignNamesByScope(nestedScopes map[uint32][]*js_ast.Scope) {
waitGroup := sync.WaitGroup{}
waitGroup.Add(len(nestedScopes))
// Rename nested scopes from separate files in parallel
for sourceIndex, scopes := range nestedScopes {
go func(sourceIndex uint32, scopes []*js_ast.Scope) {
var sorted []int
for _, scope := range scopes {
r.assignNamesRecursive(scope, sourceIndex, &r.root, &sorted)
}
waitGroup.Done()
}(sourceIndex, scopes)
}
waitGroup.Wait()
}
type numberScope struct {
parent *numberScope
// This is used as a set of used names in this scope. This also maps the name
// to the number of times the name has experienced a collision. When a name
// collides with an already-used name, we need to rename it. This is done by
// incrementing a number at the end until the name is unused. We save the
// count here so that subsequent collisions can start counting from where the
// previous collision ended instead of having to start counting from 1.
nameCounts map[string]uint32
}
type nameUse uint8
const (
nameUnused nameUse = iota
nameUsed
nameUsedInSameScope
)
func (s *numberScope) findNameUse(name string) nameUse {
original := s
for {
if _, ok := s.nameCounts[name]; ok {
if s == original {
return nameUsedInSameScope
}
return nameUsed
}
s = s.parent
if s == nil {
return nameUnused
}
}
}
func (s *numberScope) findUnusedName(name string) string {
name = js_lexer.ForceValidIdentifier(name)
if use := s.findNameUse(name); use != nameUnused {
// If the name is already in use, generate a new name by appending a number
tries := uint32(1)
if use == nameUsedInSameScope {
// To avoid O(n^2) behavior, the number must start off being the number
// that we used last time there was a collision with this name. Otherwise
// if there are many collisions with the same name, each name collision
// would have to increment the counter past all previous name collisions
// which is a O(n^2) time algorithm. Only do this if this symbol comes
// from the same scope as the previous one since sibling scopes can reuse
// the same name without problems.
tries = s.nameCounts[name]
}
prefix := name
// Keep incrementing the number until the name is unused
for {
tries++
name = prefix + strconv.Itoa(int(tries))
// Make sure this new name is unused
if s.findNameUse(name) == nameUnused {
// Store the count so we can start here next time instead of starting
// from 1. This means we avoid O(n^2) behavior.
if use == nameUsedInSameScope {
s.nameCounts[prefix] = tries
}
break
}
}
}
// Each name starts off with a count of 1 so that the first collision with
// "name" is called "name2"
s.nameCounts[name] = 1
return name
}
////////////////////////////////////////////////////////////////////////////////
// ExportRenamer
type ExportRenamer struct {
count int
used map[string]uint32
}
func (r *ExportRenamer) NextRenamedName(name string) string {
if r.used == nil {
r.used = make(map[string]uint32)
}
if tries, ok := r.used[name]; ok {
prefix := name
for {
tries++
name = prefix + strconv.Itoa(int(tries))
if _, ok := r.used[name]; !ok {
break
}
}
r.used[name] = tries
} else {
r.used[name] = 1
}
return name
}
func (r *ExportRenamer) NextMinifiedName() string {
name := js_ast.DefaultNameMinifier.NumberToMinifiedName(r.count)
r.count++
return name
}

View File

@ -1,76 +0,0 @@
package resolver
import (
"encoding/base64"
"fmt"
"net/url"
"strings"
)
type DataURL struct {
mimeType string
data string
isBase64 bool
}
func ParseDataURL(url string) (parsed DataURL, ok bool) {
if strings.HasPrefix(url, "data:") {
if comma := strings.IndexByte(url, ','); comma != -1 {
parsed.mimeType = url[len("data:"):comma]
parsed.data = url[comma+1:]
if strings.HasSuffix(parsed.mimeType, ";base64") {
parsed.mimeType = parsed.mimeType[:len(parsed.mimeType)-len(";base64")]
parsed.isBase64 = true
}
ok = true
}
}
return
}
type MIMEType uint8
const (
MIMETypeUnsupported MIMEType = iota
MIMETypeTextCSS
MIMETypeTextJavaScript
MIMETypeApplicationJSON
)
func (parsed DataURL) DecodeMIMEType() MIMEType {
// Remove things like ";charset=utf-8"
mimeType := parsed.mimeType
if semicolon := strings.IndexByte(mimeType, ';'); semicolon != -1 {
mimeType = mimeType[:semicolon]
}
// Hard-code a few supported types
switch mimeType {
case "text/css":
return MIMETypeTextCSS
case "text/javascript":
return MIMETypeTextJavaScript
case "application/json":
return MIMETypeApplicationJSON
default:
return MIMETypeUnsupported
}
}
func (parsed DataURL) DecodeData() (string, error) {
// Try to read base64 data
if parsed.isBase64 {
bytes, err := base64.StdEncoding.DecodeString(parsed.data)
if err != nil {
return "", fmt.Errorf("could not decode base64 data: %s", err.Error())
}
return string(bytes), nil
}
// Try to read percent-escaped data
content, err := url.PathUnescape(parsed.data)
if err != nil {
return "", fmt.Errorf("could not decode percent-escaped data: %s", err.Error())
}
return content, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,319 +0,0 @@
package resolver
import (
"fmt"
"strings"
"github.com/evanw/esbuild/internal/cache"
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/js_ast"
"github.com/evanw/esbuild/internal/js_lexer"
"github.com/evanw/esbuild/internal/js_parser"
"github.com/evanw/esbuild/internal/logger"
)
type TSConfigJSON struct {
AbsPath string
// The absolute path of "compilerOptions.baseUrl"
BaseURL *string
// This is used if "Paths" is non-nil. It's equal to "BaseURL" except if
// "BaseURL" is missing, in which case it is as if "BaseURL" was ".". This
// is to implement the "paths without baseUrl" feature from TypeScript 4.1.
// More info: https://github.com/microsoft/TypeScript/issues/31869
BaseURLForPaths string
// The verbatim values of "compilerOptions.paths". The keys are patterns to
// match and the values are arrays of fallback paths to search. Each key and
// each fallback path can optionally have a single "*" wildcard character.
// If both the key and the value have a wildcard, the substring matched by
// the wildcard is substituted into the fallback path. The keys represent
// module-style path names and the fallback paths are relative to the
// "baseUrl" value in the "tsconfig.json" file.
Paths map[string][]string
JSXFactory []string
JSXFragmentFactory []string
TSTarget *config.TSTarget
UseDefineForClassFields config.MaybeBool
PreserveImportsNotUsedAsValues bool
PreserveValueImports bool
}
func ParseTSConfigJSON(
log logger.Log,
source logger.Source,
jsonCache *cache.JSONCache,
extends func(string, logger.Range) *TSConfigJSON,
) *TSConfigJSON {
// Unfortunately "tsconfig.json" isn't actually JSON. It's some other
// format that appears to be defined by the implementation details of the
// TypeScript compiler.
//
// Attempt to parse it anyway by modifying the JSON parser, but just for
// these particular files. This is likely not a completely accurate
// emulation of what the TypeScript compiler does (e.g. string escape
// behavior may also be different).
json, ok := jsonCache.Parse(log, source, js_parser.JSONOptions{
AllowComments: true, // https://github.com/microsoft/TypeScript/issues/4987
AllowTrailingCommas: true,
})
if !ok {
return nil
}
var result TSConfigJSON
result.AbsPath = source.KeyPath.Text
tracker := logger.MakeLineColumnTracker(&source)
// Parse "extends"
if extends != nil {
if valueJSON, _, ok := getProperty(json, "extends"); ok {
if value, ok := getString(valueJSON); ok {
if base := extends(value, source.RangeOfString(valueJSON.Loc)); base != nil {
result = *base
}
}
}
}
// Parse "compilerOptions"
if compilerOptionsJSON, _, ok := getProperty(json, "compilerOptions"); ok {
// Parse "baseUrl"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "baseUrl"); ok {
if value, ok := getString(valueJSON); ok {
result.BaseURL = &value
}
}
// Parse "jsxFactory"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFactory"); ok {
if value, ok := getString(valueJSON); ok {
result.JSXFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
}
}
// Parse "jsxFragmentFactory"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "jsxFragmentFactory"); ok {
if value, ok := getString(valueJSON); ok {
result.JSXFragmentFactory = parseMemberExpressionForJSX(log, &source, &tracker, valueJSON.Loc, value)
}
}
// Parse "useDefineForClassFields"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "useDefineForClassFields"); ok {
if value, ok := getBool(valueJSON); ok {
if value {
result.UseDefineForClassFields = config.True
} else {
result.UseDefineForClassFields = config.False
}
}
}
// Parse "target"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "target"); ok {
if value, ok := getString(valueJSON); ok {
constraints := make(map[compat.Engine][]int)
r := source.RangeOfString(valueJSON.Loc)
ok := true
// See https://www.typescriptlang.org/tsconfig#target
switch strings.ToLower(value) {
case "es5":
constraints[compat.ES] = []int{5}
case "es6", "es2015":
constraints[compat.ES] = []int{2015}
case "es2016":
constraints[compat.ES] = []int{2016}
case "es2017":
constraints[compat.ES] = []int{2017}
case "es2018":
constraints[compat.ES] = []int{2018}
case "es2019":
constraints[compat.ES] = []int{2019}
case "es2020":
constraints[compat.ES] = []int{2020}
case "es2021":
constraints[compat.ES] = []int{2021}
case "esnext":
// Nothing to do in this case
default:
ok = false
if !helpers.IsInsideNodeModules(source.KeyPath.Text) {
log.Add(logger.Warning, &tracker, r,
fmt.Sprintf("Unrecognized target environment %q", value))
}
}
// These feature restrictions are merged with esbuild's own restrictions
if ok {
result.TSTarget = &config.TSTarget{
Source: source,
Range: r,
Target: value,
UnsupportedJSFeatures: compat.UnsupportedJSFeatures(constraints),
}
}
}
}
// Parse "importsNotUsedAsValues"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "importsNotUsedAsValues"); ok {
if value, ok := getString(valueJSON); ok {
switch value {
case "preserve", "error":
result.PreserveImportsNotUsedAsValues = true
case "remove":
default:
log.Add(logger.Warning, &tracker, source.RangeOfString(valueJSON.Loc),
fmt.Sprintf("Invalid value %q for \"importsNotUsedAsValues\"", value))
}
}
}
// Parse "preserveValueImports"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "preserveValueImports"); ok {
if value, ok := getBool(valueJSON); ok {
result.PreserveValueImports = value
}
}
// Parse "paths"
if valueJSON, _, ok := getProperty(compilerOptionsJSON, "paths"); ok {
if paths, ok := valueJSON.Data.(*js_ast.EObject); ok {
hasBaseURL := result.BaseURL != nil
if hasBaseURL {
result.BaseURLForPaths = *result.BaseURL
} else {
result.BaseURLForPaths = "."
}
result.Paths = make(map[string][]string)
for _, prop := range paths.Properties {
if key, ok := getString(prop.Key); ok {
if !isValidTSConfigPathPattern(key, log, &source, &tracker, prop.Key.Loc) {
continue
}
// The "paths" field is an object which maps a pattern to an
// array of remapping patterns to try, in priority order. See
// the documentation for examples of how this is used:
// https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping.
//
// One particular example:
//
// {
// "compilerOptions": {
// "baseUrl": "projectRoot",
// "paths": {
// "*": [
// "*",
// "generated/*"
// ]
// }
// }
// }
//
// Matching "folder1/file2" should first check "projectRoot/folder1/file2"
// and then, if that didn't work, also check "projectRoot/generated/folder1/file2".
if array, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
for _, item := range array.Items {
if str, ok := getString(item); ok {
if isValidTSConfigPathPattern(str, log, &source, &tracker, item.Loc) &&
(hasBaseURL || isValidTSConfigPathNoBaseURLPattern(str, log, &source, &tracker, item.Loc)) {
result.Paths[key] = append(result.Paths[key], str)
}
}
}
} else {
log.Add(logger.Warning, &tracker, source.RangeOfString(prop.ValueOrNil.Loc), fmt.Sprintf(
"Substitutions for pattern %q should be an array", key))
}
}
}
}
}
}
return &result
}
func parseMemberExpressionForJSX(log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc, text string) []string {
if text == "" {
return nil
}
parts := strings.Split(text, ".")
for _, part := range parts {
if !js_lexer.IsIdentifier(part) {
warnRange := source.RangeOfString(loc)
log.Add(logger.Warning, tracker, warnRange, fmt.Sprintf("Invalid JSX member expression: %q", text))
return nil
}
}
return parts
}
func isValidTSConfigPathPattern(text string, log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc) bool {
foundAsterisk := false
for i := 0; i < len(text); i++ {
if text[i] == '*' {
if foundAsterisk {
r := source.RangeOfString(loc)
log.Add(logger.Warning, tracker, r, fmt.Sprintf(
"Invalid pattern %q, must have at most one \"*\" character", text))
return false
}
foundAsterisk = true
}
}
return true
}
func isSlash(c byte) bool {
return c == '/' || c == '\\'
}
func isValidTSConfigPathNoBaseURLPattern(text string, log logger.Log, source *logger.Source, tracker *logger.LineColumnTracker, loc logger.Loc) bool {
var c0 byte
var c1 byte
var c2 byte
n := len(text)
if n > 0 {
c0 = text[0]
if n > 1 {
c1 = text[1]
if n > 2 {
c2 = text[2]
}
}
}
// Relative "." or ".."
if c0 == '.' && (n == 1 || (n == 2 && c1 == '.')) {
return true
}
// Relative "./" or "../" or ".\\" or "..\\"
if c0 == '.' && (isSlash(c1) || (c1 == '.' && isSlash(c2))) {
return true
}
// Absolute POSIX "/" or UNC "\\"
if isSlash(c0) {
return true
}
// Absolute DOS "c:/" or "c:\\"
if ((c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z')) && c1 == ':' && isSlash(c2) {
return true
}
r := source.RangeOfString(loc)
log.Add(logger.Warning, tracker, r, fmt.Sprintf(
"Non-relative path %q is not allowed when \"baseUrl\" is not set (did you forget a leading \"./\"?)", text))
return false
}

View File

@ -1,425 +0,0 @@
// This is esbuild's runtime code. It contains helper functions that are
// automatically injected into output files to implement certain features. For
// example, the "**" operator is replaced with a call to "__pow" when targeting
// ES2015. Tree shaking automatically removes unused code from the runtime.
package runtime
import (
"github.com/evanw/esbuild/internal/compat"
"github.com/evanw/esbuild/internal/logger"
)
// The runtime source is always at a special index. The index is always zero
// but this constant is always used instead to improve readability and ensure
// all code that references this index can be discovered easily.
const SourceIndex = uint32(0)
func CanUseES6(unsupportedFeatures compat.JSFeature) bool {
return !unsupportedFeatures.Has(compat.Let) && !unsupportedFeatures.Has(compat.ForOf)
}
func code(isES6 bool) string {
// Note: These helper functions used to be named similar things to the helper
// functions from the TypeScript compiler. However, people sometimes use these
// two projects in combination and TypeScript's implementation of these helpers
// causes name collisions. Some examples:
//
// * The "tslib" library will overwrite esbuild's helper functions if the bundled
// code is run in the global scope: https://github.com/evanw/esbuild/issues/1102
//
// * Running the TypeScript compiler on esbuild's output to convert ES6 to ES5
// will also overwrite esbuild's helper functions because TypeScript doesn't
// change the names of its helper functions to avoid name collisions:
// https://github.com/microsoft/TypeScript/issues/43296
//
// These can both be considered bugs in TypeScript. However, they are unlikely
// to be fixed and it's simplest to just avoid using the same names to avoid
// these bugs. Forbidden names (from "tslib"):
//
// __assign
// __asyncDelegator
// __asyncGenerator
// __asyncValues
// __await
// __awaiter
// __classPrivateFieldGet
// __classPrivateFieldSet
// __createBinding
// __decorate
// __exportStar
// __extends
// __generator
// __importDefault
// __importStar
// __makeTemplateObject
// __metadata
// __param
// __read
// __rest
// __spread
// __spreadArray
// __spreadArrays
// __values
//
// Note: The "__objRest" function has a for-of loop which requires ES6, but
// transforming destructuring to ES5 isn't even supported so it's ok.
text := `
var __create = Object.create
var __freeze = Object.freeze
var __defProp = Object.defineProperty
var __defProps = Object.defineProperties
var __getOwnPropDesc = Object.getOwnPropertyDescriptor // Note: can return "undefined" due to a Safari bug
var __getOwnPropDescs = Object.getOwnPropertyDescriptors
var __getOwnPropNames = Object.getOwnPropertyNames
var __getOwnPropSymbols = Object.getOwnPropertySymbols
var __getProtoOf = Object.getPrototypeOf
var __hasOwnProp = Object.prototype.hasOwnProperty
var __propIsEnum = Object.prototype.propertyIsEnumerable
var __reflectGet = Reflect.get
var __reflectSet = Reflect.set
export var __pow = Math.pow
var __defNormalProp = (obj, key, value) => key in obj
? __defProp(obj, key, {enumerable: true, configurable: true, writable: true, value})
: obj[key] = value
export var __spreadValues = (a, b) => {
for (var prop in b ||= {})
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop])
if (__getOwnPropSymbols)
`
// Avoid "of" when not using ES6
if isES6 {
text += `
for (var prop of __getOwnPropSymbols(b)) {
`
} else {
text += `
for (var props = __getOwnPropSymbols(b), i = 0, n = props.length, prop; i < n; i++) {
prop = props[i]
`
}
text += `
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop])
}
return a
}
export var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b))
// Tells importing modules that this can be considered an ES module
var __markAsModule = target => __defProp(target, '__esModule', { value: true })
// Update the "name" property on the function or class for "--keep-names"
export var __name = (target, value) => __defProp(target, 'name', { value, configurable: true })
// This fallback "require" function exists so that "typeof require" can
// naturally be "function" even in non-CommonJS environments since esbuild
// emulates a CommonJS environment (issue #1202). However, people want this
// shim to fall back to "globalThis.require" even if it's defined later
// (including property accesses such as "require.resolve") so we need to
// use a proxy (issue #1614).
export var __require =
/* @__PURE__ */ (x =>
typeof require !== 'undefined' ? require :
typeof Proxy !== 'undefined' ? new Proxy(x, {
get: (a, b) => (typeof require !== 'undefined' ? require : a)[b]
}) : x
)(function(x) {
if (typeof require !== 'undefined') return require.apply(this, arguments)
throw new Error('Dynamic require of "' + x + '" is not supported')
})
// For object rest patterns
export var __restKey = key => typeof key === 'symbol' ? key : key + ''
export var __objRest = (source, exclude) => {
var target = {}
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop]
if (source != null && __getOwnPropSymbols)
`
// Avoid "of" when not using ES6
if isES6 {
text += `
for (var prop of __getOwnPropSymbols(source)) {
`
} else {
text += `
for (var props = __getOwnPropSymbols(source), i = 0, n = props.length, prop; i < n; i++) {
prop = props[i]
`
}
text += `
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop]
}
return target
}
// This is for lazily-initialized ESM code. This has two implementations, a
// compact one for minified code and a verbose one that generates friendly
// names in V8's profiler and in stack traces.
export var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res
}
export var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res)
// Wraps a CommonJS closure and returns a require() function. This has two
// implementations, a compact one for minified code and a verbose one that
// generates friendly names in V8's profiler and in stack traces.
export var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = {exports: {}}).exports, mod), mod.exports
}
export var __commonJSMin = (cb, mod) => () => (mod || cb((mod = {exports: {}}).exports, mod), mod.exports)
// Used to implement ESM exports both for "require()" and "import * as"
export var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true })
}
export var __reExport = (target, module, copyDefault, desc) => {
if (module && typeof module === 'object' || typeof module === 'function')
`
// Avoid "let" when not using ES6
if isES6 {
text += `
for (let key of __getOwnPropNames(module))
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== 'default'))
__defProp(target, key, { get: () => module[key], enumerable: !(desc = __getOwnPropDesc(module, key)) || desc.enumerable })
`
} else {
text += `
for (var keys = __getOwnPropNames(module), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i]
if (!__hasOwnProp.call(target, key) && (copyDefault || key !== 'default'))
__defProp(target, key, { get: (k => module[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(module, key)) || desc.enumerable })
}
`
}
text += `
return target
}
// Converts the module from CommonJS to ESM
export var __toESM = (module, isNodeMode) => {
return __reExport(__markAsModule(
__defProp(
module != null ? __create(__getProtoOf(module)) : {},
'default',
// If the importer is not in node compatibility mode and this is an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has been set), then forward
// "default" to the export named "default". Otherwise set "default" to
// "module.exports" for node compatibility.
!isNodeMode && module && module.__esModule
? { get: () => module.default, enumerable: true }
: { value: module, enumerable: true })
), module)
}
// Converts the module from ESM to CommonJS
export var __toCommonJS = /* @__PURE__ */ (cache => {
return (module, temp) => {
return (cache && cache.get(module)) || (
temp = __reExport(__markAsModule({}), module, /* copyDefault */ 1),
cache && cache.set(module, temp),
temp)
}
})(typeof WeakMap !== 'undefined' ? new WeakMap : 0)
// For TypeScript decorators
// - kind === undefined: class
// - kind === 1: method, parameter
// - kind === 2: field
export var __decorateClass = (decorators, target, key, kind) => {
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target
for (var i = decorators.length - 1, decorator; i >= 0; i--)
if (decorator = decorators[i])
result = (kind ? decorator(target, key, result) : decorator(result)) || result
if (kind && result)
__defProp(target, key, result)
return result
}
export var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index)
// For class members
export var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value)
return value
}
var __accessCheck = (obj, member, msg) => {
if (!member.has(obj)) throw TypeError('Cannot ' + msg)
}
export var __privateIn = (member, obj) => {
if (Object(obj) !== obj) throw TypeError('Cannot use the "in" operator on this value')
return member.has(obj)
}
export var __privateGet = (obj, member, getter) => {
__accessCheck(obj, member, 'read from private field')
return getter ? getter.call(obj) : member.get(obj)
}
export var __privateAdd = (obj, member, value) => {
if (member.has(obj)) throw TypeError('Cannot add the same private member more than once')
member instanceof WeakSet ? member.add(obj) : member.set(obj, value)
}
export var __privateSet = (obj, member, value, setter) => {
__accessCheck(obj, member, 'write to private field')
setter ? setter.call(obj, value) : member.set(obj, value)
return value
}
export var __privateWrapper = (obj, member, setter, getter) => {
return {
set _(value) { __privateSet(obj, member, value, setter) },
get _() { return __privateGet(obj, member, getter) },
}
}
export var __privateMethod = (obj, member, method) => {
__accessCheck(obj, member, 'access private method')
return method
}
// For "super" property accesses
export var __superStaticGet = (obj, member) => __reflectGet(__getProtoOf(obj), member, obj)
export var __superStaticSet = (obj, member, value) => (__reflectSet(__getProtoOf(obj), member, value, obj), value)
export var __superWrapper = (getter, setter, member) => {
return {
set _(value) { setter(member, value) },
get _() { return getter(member) },
}
}
export var __superStaticWrapper = (obj, member) => {
return {
set _(value) { __superStaticSet(obj, member, value) },
get _() { return __superStaticGet(obj, member) },
}
}
// For lowering tagged template literals
export var __template = (cooked, raw) => __freeze(__defProp(cooked, 'raw', { value: __freeze(raw || cooked.slice()) }))
// This helps for lowering async functions
export var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = value => {
try {
step(generator.next(value))
} catch (e) {
reject(e)
}
}
var rejected = value => {
try {
step(generator.throw(value))
} catch (e) {
reject(e)
}
}
var step = x => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected)
step((generator = generator.apply(__this, __arguments)).next())
})
}
// This is for the "binary" loader (custom code is ~2x faster than "atob")
export var __toBinaryNode = base64 => new Uint8Array(Buffer.from(base64, 'base64'))
export var __toBinary = /* @__PURE__ */ (() => {
var table = new Uint8Array(128)
for (var i = 0; i < 64; i++) table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i
return base64 => {
var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == '=') - (base64[n - 2] == '=')) * 3 / 4 | 0)
for (var i = 0, j = 0; i < n;) {
var c0 = table[base64.charCodeAt(i++)], c1 = table[base64.charCodeAt(i++)]
var c2 = table[base64.charCodeAt(i++)], c3 = table[base64.charCodeAt(i++)]
bytes[j++] = (c0 << 2) | (c1 >> 4)
bytes[j++] = (c1 << 4) | (c2 >> 2)
bytes[j++] = (c2 << 6) | c3
}
return bytes
}
})()
`
return text
}
var ES6Source = logger.Source{
Index: SourceIndex,
KeyPath: logger.Path{Text: "<runtime>"},
PrettyPath: "<runtime>",
IdentifierName: "runtime",
Contents: code(true /* isES6 */),
}
var ES5Source = logger.Source{
Index: SourceIndex,
KeyPath: logger.Path{Text: "<runtime>"},
PrettyPath: "<runtime>",
IdentifierName: "runtime",
Contents: code(false /* isES6 */),
}
// The TypeScript decorator transform behaves similar to the official
// TypeScript compiler.
//
// One difference is that the "__decorateClass" function doesn't contain a reference
// to the non-existent "Reflect.decorate" function. This function was never
// standardized and checking for it is wasted code (as well as a potentially
// dangerous cause of unintentional behavior changes in the future).
//
// Another difference is that the "__decorateClass" function doesn't take in an
// optional property descriptor like it does in the official TypeScript
// compiler's support code. This appears to be a dead code path in the official
// support code that is only there for legacy reasons.
//
// Here are some examples of how esbuild's decorator transform works:
//
// ============================= Class decorator ==============================
//
// // TypeScript // JavaScript
// @dec let C = class {
// class C { };
// } C = __decorateClass([
// dec
// ], C);
//
// ============================ Method decorator ==============================
//
// // TypeScript // JavaScript
// class C { class C {
// @dec foo() {}
// foo() {} }
// } __decorateClass([
// dec
// ], C.prototype, 'foo', 1);
//
// =========================== Parameter decorator ============================
//
// // TypeScript // JavaScript
// class C { class C {
// foo(@dec bar) {} foo(bar) {}
// } }
// __decorateClass([
// __decorateParam(0, dec)
// ], C.prototype, 'foo', 1);
//
// ============================= Field decorator ==============================
//
// // TypeScript // JavaScript
// class C { class C {
// @dec constructor() {
// foo = 123 this.foo = 123
// } }
// }
// __decorateClass([
// dec
// ], C.prototype, 'foo', 2);

View File

@ -1,757 +0,0 @@
package sourcemap
import (
"bytes"
"unicode/utf8"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
)
type Mapping struct {
GeneratedLine int32 // 0-based
GeneratedColumn int32 // 0-based count of UTF-16 code units
SourceIndex int32 // 0-based
OriginalLine int32 // 0-based
OriginalColumn int32 // 0-based count of UTF-16 code units
}
type SourceMap struct {
Sources []string
SourcesContent []SourceContent
Mappings []Mapping
}
type SourceContent struct {
// This stores both the unquoted and the quoted values. We try to use the
// already-quoted value if possible so we don't need to re-quote it
// unnecessarily for maximum performance.
Quoted string
// But sometimes we need to re-quote the value, such as when it contains
// non-ASCII characters and we are in ASCII-only mode. In that case we quote
// this parsed UTF-16 value.
Value []uint16
}
func (sm *SourceMap) Find(line int32, column int32) *Mapping {
mappings := sm.Mappings
// Binary search
count := len(mappings)
index := 0
for count > 0 {
step := count / 2
i := index + step
mapping := mappings[i]
if mapping.GeneratedLine < line || (mapping.GeneratedLine == line && mapping.GeneratedColumn <= column) {
index = i + 1
count -= step + 1
} else {
count = step
}
}
// Handle search failure
if index > 0 {
mapping := &mappings[index-1]
// Match the behavior of the popular "source-map" library from Mozilla
if mapping.GeneratedLine == line {
return mapping
}
}
return nil
}
var base64 = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
// A single base 64 digit can contain 6 bits of data. For the base 64 variable
// length quantities we use in the source map spec, the first bit is the sign,
// the next four bits are the actual value, and the 6th bit is the continuation
// bit. The continuation bit tells us whether there are more digits in this
// value following this digit.
//
// Continuation
// | Sign
// | |
// V V
// 101011
//
func EncodeVLQ(value int) []byte {
var vlq int
if value < 0 {
vlq = ((-value) << 1) | 1
} else {
vlq = value << 1
}
// Handle the common case up front without allocations
if (vlq >> 5) == 0 {
digit := vlq & 31
return base64[digit : digit+1]
}
encoded := []byte{}
for {
digit := vlq & 31
vlq >>= 5
// If there are still more digits in this value, we must make sure the
// continuation bit is marked
if vlq != 0 {
digit |= 32
}
encoded = append(encoded, base64[digit])
if vlq == 0 {
break
}
}
return encoded
}
func DecodeVLQ(encoded []byte, start int) (int, int) {
shift := 0
vlq := 0
// Scan over the input
for {
index := bytes.IndexByte(base64, encoded[start])
if index < 0 {
break
}
// Decode a single byte
vlq |= (index & 31) << shift
start++
shift += 5
// Stop if there's no continuation bit
if (index & 32) == 0 {
break
}
}
// Recover the value
value := vlq >> 1
if (vlq & 1) != 0 {
value = -value
}
return value, start
}
func DecodeVLQUTF16(encoded []uint16) (int, int, bool) {
n := len(encoded)
if n == 0 {
return 0, 0, false
}
// Scan over the input
current := 0
shift := 0
vlq := 0
for {
if current >= n {
return 0, 0, false
}
index := bytes.IndexByte(base64, byte(encoded[current]))
if index < 0 {
return 0, 0, false
}
// Decode a single byte
vlq |= (index & 31) << shift
current++
shift += 5
// Stop if there's no continuation bit
if (index & 32) == 0 {
break
}
}
// Recover the value
var value = vlq >> 1
if (vlq & 1) != 0 {
value = -value
}
return value, current, true
}
type LineColumnOffset struct {
Lines int
Columns int
}
func (a LineColumnOffset) ComesBefore(b LineColumnOffset) bool {
return a.Lines < b.Lines || (a.Lines == b.Lines && a.Columns < b.Columns)
}
func (a *LineColumnOffset) Add(b LineColumnOffset) {
if b.Lines == 0 {
a.Columns += b.Columns
} else {
a.Lines += b.Lines
a.Columns = b.Columns
}
}
func (offset *LineColumnOffset) AdvanceBytes(bytes []byte) {
columns := offset.Columns
for len(bytes) > 0 {
c, width := utf8.DecodeRune(bytes)
bytes = bytes[width:]
switch c {
case '\r', '\n', '\u2028', '\u2029':
// Handle Windows-specific "\r\n" newlines
if c == '\r' && len(bytes) > 0 && bytes[0] == '\n' {
columns++
continue
}
offset.Lines++
columns = 0
default:
// Mozilla's "source-map" library counts columns using UTF-16 code units
if c <= 0xFFFF {
columns++
} else {
columns += 2
}
}
}
offset.Columns = columns
}
func (offset *LineColumnOffset) AdvanceString(text string) {
columns := offset.Columns
for i, c := range text {
switch c {
case '\r', '\n', '\u2028', '\u2029':
// Handle Windows-specific "\r\n" newlines
if c == '\r' && i+1 < len(text) && text[i+1] == '\n' {
columns++
continue
}
offset.Lines++
columns = 0
default:
// Mozilla's "source-map" library counts columns using UTF-16 code units
if c <= 0xFFFF {
columns++
} else {
columns += 2
}
}
}
offset.Columns = columns
}
type SourceMapPieces struct {
Prefix []byte
Mappings []byte
Suffix []byte
}
func (pieces SourceMapPieces) HasContent() bool {
return len(pieces.Prefix)+len(pieces.Mappings)+len(pieces.Suffix) > 0
}
type SourceMapShift struct {
Before LineColumnOffset
After LineColumnOffset
}
func (pieces SourceMapPieces) Finalize(shifts []SourceMapShift) []byte {
// An optimized path for when there are no shifts
if len(shifts) == 1 {
bytes := pieces.Prefix
minCap := len(bytes) + len(pieces.Mappings) + len(pieces.Suffix)
if cap(bytes) < minCap {
bytes = append(make([]byte, 0, minCap), bytes...)
}
bytes = append(bytes, pieces.Mappings...)
bytes = append(bytes, pieces.Suffix...)
return bytes
}
startOfRun := 0
current := 0
generated := LineColumnOffset{}
prevShiftColumnDelta := 0
j := helpers.Joiner{}
// Start the source map
j.AddBytes(pieces.Prefix)
// This assumes that a) all mappings are valid and b) all mappings are ordered
// by increasing generated position. This should be the case for all mappings
// generated by esbuild, which should be the only mappings we process here.
for current < len(pieces.Mappings) {
// Handle a line break
if pieces.Mappings[current] == ';' {
generated.Lines++
generated.Columns = 0
prevShiftColumnDelta = 0
current++
continue
}
potentialEndOfRun := current
// Read the generated column
generatedColumnDelta, next := DecodeVLQ(pieces.Mappings, current)
generated.Columns += generatedColumnDelta
current = next
potentialStartOfRun := current
// Skip over the original position information
_, current = DecodeVLQ(pieces.Mappings, current) // The original source
_, current = DecodeVLQ(pieces.Mappings, current) // The original line
_, current = DecodeVLQ(pieces.Mappings, current) // The original column
// Skip a trailing comma
if current < len(pieces.Mappings) && pieces.Mappings[current] == ',' {
current++
}
// Detect crossing shift boundaries
didCrossBoundary := false
for len(shifts) > 1 && shifts[1].Before.ComesBefore(generated) {
shifts = shifts[1:]
didCrossBoundary = true
}
if !didCrossBoundary {
continue
}
// This shift isn't relevant if the next mapping after this shift is on a
// following line. In that case, don't split and keep scanning instead.
shift := shifts[0]
if shift.After.Lines != generated.Lines {
continue
}
// Add all previous mappings in a single run for efficiency. Since source
// mappings are relative, no data needs to be modified inside this run.
j.AddBytes(pieces.Mappings[startOfRun:potentialEndOfRun])
// Then modify the first mapping across the shift boundary with the updated
// generated column value. It's simplest to only support column shifts. This
// is reasonable because import paths should not contain newlines.
if shift.Before.Lines != shift.After.Lines {
panic("Unexpected line change when shifting source maps")
}
shiftColumnDelta := shift.After.Columns - shift.Before.Columns
j.AddBytes(EncodeVLQ(generatedColumnDelta + shiftColumnDelta - prevShiftColumnDelta))
prevShiftColumnDelta = shiftColumnDelta
// Finally, start the next run after the end of this generated column offset
startOfRun = potentialStartOfRun
}
// Finish the source map
j.AddBytes(pieces.Mappings[startOfRun:])
j.AddBytes(pieces.Suffix)
return j.Done()
}
// Coordinates in source maps are stored using relative offsets for size
// reasons. When joining together chunks of a source map that were emitted
// in parallel for different parts of a file, we need to fix up the first
// segment of each chunk to be relative to the end of the previous chunk.
type SourceMapState struct {
// This isn't stored in the source map. It's only used by the bundler to join
// source map chunks together correctly.
GeneratedLine int
// These are stored in the source map in VLQ format.
GeneratedColumn int
SourceIndex int
OriginalLine int
OriginalColumn int
}
// Source map chunks are computed in parallel for speed. Each chunk is relative
// to the zero state instead of being relative to the end state of the previous
// chunk, since it's impossible to know the end state of the previous chunk in
// a parallel computation.
//
// After all chunks are computed, they are joined together in a second pass.
// This rewrites the first mapping in each chunk to be relative to the end
// state of the previous chunk.
func AppendSourceMapChunk(j *helpers.Joiner, prevEndState SourceMapState, startState SourceMapState, sourceMap []byte) {
// Handle line breaks in between this mapping and the previous one
if startState.GeneratedLine != 0 {
j.AddBytes(bytes.Repeat([]byte{';'}, startState.GeneratedLine))
prevEndState.GeneratedColumn = 0
}
// Skip past any leading semicolons, which indicate line breaks
semicolons := 0
for sourceMap[semicolons] == ';' {
semicolons++
}
if semicolons > 0 {
j.AddBytes(sourceMap[:semicolons])
sourceMap = sourceMap[semicolons:]
prevEndState.GeneratedColumn = 0
startState.GeneratedColumn = 0
}
// Strip off the first mapping from the buffer. The first mapping should be
// for the start of the original file (the printer always generates one for
// the start of the file).
generatedColumn, i := DecodeVLQ(sourceMap, 0)
sourceIndex, i := DecodeVLQ(sourceMap, i)
originalLine, i := DecodeVLQ(sourceMap, i)
originalColumn, i := DecodeVLQ(sourceMap, i)
sourceMap = sourceMap[i:]
// Rewrite the first mapping to be relative to the end state of the previous
// chunk. We now know what the end state is because we're in the second pass
// where all chunks have already been generated.
startState.SourceIndex += sourceIndex
startState.GeneratedColumn += generatedColumn
startState.OriginalLine += originalLine
startState.OriginalColumn += originalColumn
j.AddBytes(appendMappingToBuffer(nil, j.LastByte(), prevEndState, startState))
// Then append everything after that without modification.
j.AddBytes(sourceMap)
}
func appendMappingToBuffer(buffer []byte, lastByte byte, prevState SourceMapState, currentState SourceMapState) []byte {
// Put commas in between mappings
if lastByte != 0 && lastByte != ';' && lastByte != '"' {
buffer = append(buffer, ',')
}
// Record the generated column (the line is recorded using ';' elsewhere)
buffer = append(buffer, EncodeVLQ(currentState.GeneratedColumn-prevState.GeneratedColumn)...)
prevState.GeneratedColumn = currentState.GeneratedColumn
// Record the generated source
buffer = append(buffer, EncodeVLQ(currentState.SourceIndex-prevState.SourceIndex)...)
prevState.SourceIndex = currentState.SourceIndex
// Record the original line
buffer = append(buffer, EncodeVLQ(currentState.OriginalLine-prevState.OriginalLine)...)
prevState.OriginalLine = currentState.OriginalLine
// Record the original column
buffer = append(buffer, EncodeVLQ(currentState.OriginalColumn-prevState.OriginalColumn)...)
prevState.OriginalColumn = currentState.OriginalColumn
return buffer
}
type LineOffsetTable struct {
byteOffsetToStartOfLine int32
// The source map specification is very loose and does not specify what
// column numbers actually mean. The popular "source-map" library from Mozilla
// appears to interpret them as counts of UTF-16 code units, so we generate
// those too for compatibility.
//
// We keep mapping tables around to accelerate conversion from byte offsets
// to UTF-16 code unit counts. However, this mapping takes up a lot of memory
// and generates a lot of garbage. Since most JavaScript is ASCII and the
// mapping for ASCII is 1:1, we avoid creating a table for ASCII-only lines
// as an optimization.
byteOffsetToFirstNonASCII int32
columnsForNonASCII []int32
}
func GenerateLineOffsetTables(contents string, approximateLineCount int32) []LineOffsetTable {
var ColumnsForNonASCII []int32
ByteOffsetToFirstNonASCII := int32(0)
lineByteOffset := 0
columnByteOffset := 0
column := int32(0)
// Preallocate the top-level table using the approximate line count from the lexer
lineOffsetTables := make([]LineOffsetTable, 0, approximateLineCount)
for i, c := range contents {
// Mark the start of the next line
if column == 0 {
lineByteOffset = i
}
// Start the mapping if this character is non-ASCII
if c > 0x7F && ColumnsForNonASCII == nil {
columnByteOffset = i - lineByteOffset
ByteOffsetToFirstNonASCII = int32(columnByteOffset)
ColumnsForNonASCII = []int32{}
}
// Update the per-byte column offsets
if ColumnsForNonASCII != nil {
for lineBytesSoFar := i - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
ColumnsForNonASCII = append(ColumnsForNonASCII, column)
}
}
switch c {
case '\r', '\n', '\u2028', '\u2029':
// Handle Windows-specific "\r\n" newlines
if c == '\r' && i+1 < len(contents) && contents[i+1] == '\n' {
column++
continue
}
lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
byteOffsetToStartOfLine: int32(lineByteOffset),
byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
columnsForNonASCII: ColumnsForNonASCII,
})
columnByteOffset = 0
ByteOffsetToFirstNonASCII = 0
ColumnsForNonASCII = nil
column = 0
default:
// Mozilla's "source-map" library counts columns using UTF-16 code units
if c <= 0xFFFF {
column++
} else {
column += 2
}
}
}
// Mark the start of the next line
if column == 0 {
lineByteOffset = len(contents)
}
// Do one last update for the column at the end of the file
if ColumnsForNonASCII != nil {
for lineBytesSoFar := len(contents) - lineByteOffset; columnByteOffset <= lineBytesSoFar; columnByteOffset++ {
ColumnsForNonASCII = append(ColumnsForNonASCII, column)
}
}
lineOffsetTables = append(lineOffsetTables, LineOffsetTable{
byteOffsetToStartOfLine: int32(lineByteOffset),
byteOffsetToFirstNonASCII: ByteOffsetToFirstNonASCII,
columnsForNonASCII: ColumnsForNonASCII,
})
return lineOffsetTables
}
type Chunk struct {
Buffer []byte
// This end state will be used to rewrite the start of the following source
// map chunk so that the delta-encoded VLQ numbers are preserved.
EndState SourceMapState
// There probably isn't a source mapping at the end of the file (nor should
// there be) but if we're appending another source map chunk after this one,
// we'll need to know how many characters were in the last line we generated.
FinalGeneratedColumn int
ShouldIgnore bool
}
type ChunkBuilder struct {
inputSourceMap *SourceMap
sourceMap []byte
prevLoc logger.Loc
prevState SourceMapState
lastGeneratedUpdate int
generatedColumn int
hasPrevState bool
lineOffsetTables []LineOffsetTable
// This is a workaround for a bug in the popular "source-map" library:
// https://github.com/mozilla/source-map/issues/261. The library will
// sometimes return null when querying a source map unless every line
// starts with a mapping at column zero.
//
// The workaround is to replicate the previous mapping if a line ends
// up not starting with a mapping. This is done lazily because we want
// to avoid replicating the previous mapping if we don't need to.
lineStartsWithMapping bool
coverLinesWithoutMappings bool
}
func MakeChunkBuilder(inputSourceMap *SourceMap, lineOffsetTables []LineOffsetTable) ChunkBuilder {
return ChunkBuilder{
inputSourceMap: inputSourceMap,
prevLoc: logger.Loc{Start: -1},
lineOffsetTables: lineOffsetTables,
// We automatically repeat the previous source mapping if we ever generate
// a line that doesn't start with a mapping. This helps give files more
// complete mapping coverage without gaps.
//
// However, we probably shouldn't do this if the input file has a nested
// source map that we will be remapping through. We have no idea what state
// that source map is in and it could be pretty scrambled.
//
// I've seen cases where blindly repeating the last mapping for subsequent
// lines gives very strange and unhelpful results with source maps from
// other tools.
coverLinesWithoutMappings: inputSourceMap == nil,
}
}
func (b *ChunkBuilder) AddSourceMapping(loc logger.Loc, output []byte) {
if loc == b.prevLoc {
return
}
b.prevLoc = loc
// Binary search to find the line
lineOffsetTables := b.lineOffsetTables
count := len(lineOffsetTables)
originalLine := 0
for count > 0 {
step := count / 2
i := originalLine + step
if lineOffsetTables[i].byteOffsetToStartOfLine <= loc.Start {
originalLine = i + 1
count = count - step - 1
} else {
count = step
}
}
originalLine--
// Use the line to compute the column
line := &lineOffsetTables[originalLine]
originalColumn := int(loc.Start - line.byteOffsetToStartOfLine)
if line.columnsForNonASCII != nil && originalColumn >= int(line.byteOffsetToFirstNonASCII) {
originalColumn = int(line.columnsForNonASCII[originalColumn-int(line.byteOffsetToFirstNonASCII)])
}
b.updateGeneratedLineAndColumn(output)
// If this line doesn't start with a mapping and we're about to add a mapping
// that's not at the start, insert a mapping first so the line starts with one.
if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.generatedColumn > 0 && b.hasPrevState {
b.appendMappingWithoutRemapping(SourceMapState{
GeneratedLine: b.prevState.GeneratedLine,
GeneratedColumn: 0,
SourceIndex: b.prevState.SourceIndex,
OriginalLine: b.prevState.OriginalLine,
OriginalColumn: b.prevState.OriginalColumn,
})
}
b.appendMapping(SourceMapState{
GeneratedLine: b.prevState.GeneratedLine,
GeneratedColumn: b.generatedColumn,
OriginalLine: originalLine,
OriginalColumn: originalColumn,
})
// This line now has a mapping on it, so don't insert another one
b.lineStartsWithMapping = true
}
func (b *ChunkBuilder) GenerateChunk(output []byte) Chunk {
b.updateGeneratedLineAndColumn(output)
shouldIgnore := true
for _, c := range b.sourceMap {
if c != ';' {
shouldIgnore = false
break
}
}
return Chunk{
Buffer: b.sourceMap,
EndState: b.prevState,
FinalGeneratedColumn: b.generatedColumn,
ShouldIgnore: shouldIgnore,
}
}
// Scan over the printed text since the last source mapping and update the
// generated line and column numbers
func (b *ChunkBuilder) updateGeneratedLineAndColumn(output []byte) {
for i, c := range string(output[b.lastGeneratedUpdate:]) {
switch c {
case '\r', '\n', '\u2028', '\u2029':
// Handle Windows-specific "\r\n" newlines
if c == '\r' {
newlineCheck := b.lastGeneratedUpdate + i + 1
if newlineCheck < len(output) && output[newlineCheck] == '\n' {
continue
}
}
// If we're about to move to the next line and the previous line didn't have
// any mappings, add a mapping at the start of the previous line.
if b.coverLinesWithoutMappings && !b.lineStartsWithMapping && b.hasPrevState {
b.appendMappingWithoutRemapping(SourceMapState{
GeneratedLine: b.prevState.GeneratedLine,
GeneratedColumn: 0,
SourceIndex: b.prevState.SourceIndex,
OriginalLine: b.prevState.OriginalLine,
OriginalColumn: b.prevState.OriginalColumn,
})
}
b.prevState.GeneratedLine++
b.prevState.GeneratedColumn = 0
b.generatedColumn = 0
b.sourceMap = append(b.sourceMap, ';')
// This new line doesn't have a mapping yet
b.lineStartsWithMapping = false
default:
// Mozilla's "source-map" library counts columns using UTF-16 code units
if c <= 0xFFFF {
b.generatedColumn++
} else {
b.generatedColumn += 2
}
}
}
b.lastGeneratedUpdate = len(output)
}
func (b *ChunkBuilder) appendMapping(currentState SourceMapState) {
// If the input file had a source map, map all the way back to the original
if b.inputSourceMap != nil {
mapping := b.inputSourceMap.Find(
int32(currentState.OriginalLine),
int32(currentState.OriginalColumn))
// Some locations won't have a mapping
if mapping == nil {
return
}
currentState.SourceIndex = int(mapping.SourceIndex)
currentState.OriginalLine = int(mapping.OriginalLine)
currentState.OriginalColumn = int(mapping.OriginalColumn)
}
b.appendMappingWithoutRemapping(currentState)
}
func (b *ChunkBuilder) appendMappingWithoutRemapping(currentState SourceMapState) {
var lastByte byte
if len(b.sourceMap) != 0 {
lastByte = b.sourceMap[len(b.sourceMap)-1]
}
b.sourceMap = appendMappingToBuffer(b.sourceMap, lastByte, b.prevState, currentState)
b.prevState = currentState
b.hasPrevState = true
}

View File

@ -1,22 +0,0 @@
Copyright (c) 2016 Caleb Spare
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1 +0,0 @@
This Go implementation of xxHash is from https://github.com/cespare/xxhash.

View File

@ -1,235 +0,0 @@
// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described
// at http://cyan4973.github.io/xxHash/.
package xxhash
import (
"encoding/binary"
"errors"
"math/bits"
)
const (
prime1 uint64 = 11400714785074694791
prime2 uint64 = 14029467366897019727
prime3 uint64 = 1609587929392839161
prime4 uint64 = 9650029242287828579
prime5 uint64 = 2870177450012600261
)
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
// possible in the Go code is worth a small (but measurable) performance boost
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
// convenience in the Go code in a few places where we need to intentionally
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
// result overflows a uint64).
var (
prime1v = prime1
prime2v = prime2
prime3v = prime3
prime4v = prime4
prime5v = prime5
)
// Digest implements hash.Hash64.
type Digest struct {
v1 uint64
v2 uint64
v3 uint64
v4 uint64
total uint64
mem [32]byte
n int // how much of mem is used
}
// New creates a new Digest that computes the 64-bit xxHash algorithm.
func New() *Digest {
var d Digest
d.Reset()
return &d
}
// Reset clears the Digest's state so that it can be reused.
func (d *Digest) Reset() {
d.v1 = prime1v + prime2
d.v2 = prime2
d.v3 = 0
d.v4 = -prime1v
d.total = 0
d.n = 0
}
// Size always returns 8 bytes.
func (d *Digest) Size() int { return 8 }
// BlockSize always returns 32 bytes.
func (d *Digest) BlockSize() int { return 32 }
// Write adds more data to d. It always returns len(b), nil.
func (d *Digest) Write(b []byte) (n int, err error) {
n = len(b)
d.total += uint64(n)
if d.n+n < 32 {
// This new data doesn't even fill the current block.
copy(d.mem[d.n:], b)
d.n += n
return
}
if d.n > 0 {
// Finish off the partial block.
copy(d.mem[d.n:], b)
d.v1 = round(d.v1, u64(d.mem[0:8]))
d.v2 = round(d.v2, u64(d.mem[8:16]))
d.v3 = round(d.v3, u64(d.mem[16:24]))
d.v4 = round(d.v4, u64(d.mem[24:32]))
b = b[32-d.n:]
d.n = 0
}
if len(b) >= 32 {
// One or more full blocks left.
nw := writeBlocks(d, b)
b = b[nw:]
}
// Store any remaining partial block.
copy(d.mem[:], b)
d.n = len(b)
return
}
// Sum appends the current hash to b and returns the resulting slice.
func (d *Digest) Sum(b []byte) []byte {
s := d.Sum64()
return append(
b,
byte(s>>56),
byte(s>>48),
byte(s>>40),
byte(s>>32),
byte(s>>24),
byte(s>>16),
byte(s>>8),
byte(s),
)
}
// Sum64 returns the current hash.
func (d *Digest) Sum64() uint64 {
var h uint64
if d.total >= 32 {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = d.v3 + prime5
}
h += d.total
i, end := 0, d.n
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(d.mem[i:i+8]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(d.mem[i:i+4])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
}
for i < end {
h ^= uint64(d.mem[i]) * prime5
h = rol11(h) * prime1
i++
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
const (
magic = "xxh\x06"
marshaledSize = len(magic) + 8*5 + 32
)
// MarshalBinary implements the encoding.BinaryMarshaler interface.
func (d *Digest) MarshalBinary() ([]byte, error) {
b := make([]byte, 0, marshaledSize)
b = append(b, magic...)
b = appendUint64(b, d.v1)
b = appendUint64(b, d.v2)
b = appendUint64(b, d.v3)
b = appendUint64(b, d.v4)
b = appendUint64(b, d.total)
b = append(b, d.mem[:d.n]...)
b = b[:len(b)+len(d.mem)-d.n]
return b, nil
}
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
func (d *Digest) UnmarshalBinary(b []byte) error {
if len(b) < len(magic) || string(b[:len(magic)]) != magic {
return errors.New("xxhash: invalid hash state identifier")
}
if len(b) != marshaledSize {
return errors.New("xxhash: invalid hash state size")
}
b = b[len(magic):]
b, d.v1 = consumeUint64(b)
b, d.v2 = consumeUint64(b)
b, d.v3 = consumeUint64(b)
b, d.v4 = consumeUint64(b)
b, d.total = consumeUint64(b)
copy(d.mem[:], b)
d.n = int(d.total % uint64(len(d.mem)))
return nil
}
func appendUint64(b []byte, x uint64) []byte {
var a [8]byte
binary.LittleEndian.PutUint64(a[:], x)
return append(b, a[:]...)
}
func consumeUint64(b []byte) ([]byte, uint64) {
x := u64(b)
return b[8:], x
}
func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) }
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
func round(acc, input uint64) uint64 {
acc += input * prime2
acc = rol31(acc)
acc *= prime1
return acc
}
func mergeRound(acc, val uint64) uint64 {
val = round(0, val)
acc ^= val
acc = acc*prime1 + prime4
return acc
}
func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) }
func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) }
func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) }
func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) }
func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) }
func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) }
func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) }
func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) }

View File

@ -1,74 +0,0 @@
package xxhash
// Sum64 computes the 64-bit xxHash digest of b.
func Sum64(b []byte) uint64 {
// A simpler version would be
// d := New()
// d.Write(b)
// return d.Sum64()
// but this is faster, particularly for small inputs.
n := len(b)
var h uint64
if n >= 32 {
v1 := prime1v + prime2
v2 := prime2
v3 := uint64(0)
v4 := -prime1v
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
h = mergeRound(h, v1)
h = mergeRound(h, v2)
h = mergeRound(h, v3)
h = mergeRound(h, v4)
} else {
h = prime5
}
h += uint64(n)
i, end := 0, len(b)
for ; i+8 <= end; i += 8 {
k1 := round(0, u64(b[i:i+8:len(b)]))
h ^= k1
h = rol27(h)*prime1 + prime4
}
if i+4 <= end {
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1
h = rol23(h)*prime2 + prime3
i += 4
}
for ; i < end; i++ {
h ^= uint64(b[i]) * prime5
h = rol11(h) * prime1
}
h ^= h >> 33
h *= prime2
h ^= h >> 29
h *= prime3
h ^= h >> 32
return h
}
func writeBlocks(d *Digest, b []byte) int {
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
n := len(b)
for len(b) >= 32 {
v1 = round(v1, u64(b[0:8:len(b)]))
v2 = round(v2, u64(b[8:16:len(b)]))
v3 = round(v3, u64(b[16:24:len(b)]))
v4 = round(v4, u64(b[24:32:len(b)]))
b = b[32:len(b):len(b)]
}
d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4
return n - len(b)
}

View File

@ -1,566 +0,0 @@
// This API exposes esbuild's two main operations: building and transforming.
// It's intended for integrating esbuild into other tools as a library.
//
// If you are just trying to run esbuild from Go without the overhead of
// creating a child process, there is also an API for the command-line
// interface itself: https://godoc.org/github.com/evanw/esbuild/pkg/cli.
//
// Build API
//
// This function runs an end-to-end build operation. It takes an array of file
// paths as entry points, parses them and all of their dependencies, and
// returns the output files to write to the file system. The available options
// roughly correspond to esbuild's command-line flags.
//
// Example usage:
//
// package main
//
// import (
// "os"
//
// "github.com/evanw/esbuild/pkg/api"
// )
//
// func main() {
// result := api.Build(api.BuildOptions{
// EntryPoints: []string{"input.js"},
// Outfile: "output.js",
// Bundle: true,
// Write: true,
// LogLevel: api.LogLevelInfo,
// })
//
// if len(result.Errors) > 0 {
// os.Exit(1)
// }
// }
//
// Transform API
//
// This function transforms a string of source code into JavaScript. It can be
// used to minify JavaScript, convert TypeScript/JSX to JavaScript, or convert
// newer JavaScript to older JavaScript. The available options roughly
// correspond to esbuild's command-line flags.
//
// Example usage:
//
// package main
//
// import (
// "fmt"
// "os"
//
// "github.com/evanw/esbuild/pkg/api"
// )
//
// func main() {
// jsx := `
// import * as React from 'react'
// import * as ReactDOM from 'react-dom'
//
// ReactDOM.render(
// <h1>Hello, world!</h1>,
// document.getElementById('root')
// );
// `
//
// result := api.Transform(jsx, api.TransformOptions{
// Loader: api.LoaderJSX,
// })
//
// fmt.Printf("%d errors and %d warnings\n",
// len(result.Errors), len(result.Warnings))
//
// os.Stdout.Write(result.Code)
// }
//
package api
type SourceMap uint8
const (
SourceMapNone SourceMap = iota
SourceMapInline
SourceMapLinked
SourceMapExternal
SourceMapInlineAndExternal
)
type SourcesContent uint8
const (
SourcesContentInclude SourcesContent = iota
SourcesContentExclude
)
type LegalComments uint8
const (
LegalCommentsDefault LegalComments = iota
LegalCommentsNone
LegalCommentsInline
LegalCommentsEndOfFile
LegalCommentsLinked
LegalCommentsExternal
)
type JSXMode uint8
const (
JSXModeTransform JSXMode = iota
JSXModePreserve
)
type Target uint8
const (
DefaultTarget Target = iota
ESNext
ES5
ES2015
ES2016
ES2017
ES2018
ES2019
ES2020
ES2021
)
type Loader uint8
const (
LoaderNone Loader = iota
LoaderJS
LoaderJSX
LoaderTS
LoaderTSX
LoaderJSON
LoaderText
LoaderBase64
LoaderDataURL
LoaderFile
LoaderBinary
LoaderCSS
LoaderDefault
)
type Platform uint8
const (
PlatformBrowser Platform = iota
PlatformNode
PlatformNeutral
)
type Format uint8
const (
FormatDefault Format = iota
FormatIIFE
FormatCommonJS
FormatESModule
)
type EngineName uint8
const (
EngineChrome EngineName = iota
EngineEdge
EngineFirefox
EngineIOS
EngineNode
EngineSafari
)
type Engine struct {
Name EngineName
Version string
}
type Location struct {
File string
Namespace string
Line int // 1-based
Column int // 0-based, in bytes
Length int // in bytes
LineText string
Suggestion string
}
type Message struct {
PluginName string
Text string
Location *Location
Notes []Note
// Optional user-specified data that is passed through unmodified. You can
// use this to stash the original error, for example.
Detail interface{}
}
type Note struct {
Text string
Location *Location
}
type StderrColor uint8
const (
ColorIfTerminal StderrColor = iota
ColorNever
ColorAlways
)
type LogLevel uint8
const (
LogLevelSilent LogLevel = iota
LogLevelVerbose
LogLevelDebug
LogLevelInfo
LogLevelWarning
LogLevelError
)
type Charset uint8
const (
CharsetDefault Charset = iota
CharsetASCII
CharsetUTF8
)
type TreeShaking uint8
const (
TreeShakingDefault TreeShaking = iota
TreeShakingFalse
TreeShakingTrue
)
////////////////////////////////////////////////////////////////////////////////
// Build API
type BuildOptions struct {
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
Sourcemap SourceMap // Documentation: https://esbuild.github.io/api/#sourcemap
SourceRoot string // Documentation: https://esbuild.github.io/api/#source-root
SourcesContent SourcesContent // Documentation: https://esbuild.github.io/api/#sources-content
Target Target // Documentation: https://esbuild.github.io/api/#target
Engines []Engine // Documentation: https://esbuild.github.io/api/#target
MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify
MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify
MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify
Charset Charset // Documentation: https://esbuild.github.io/api/#charset
TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking
IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations
LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments
JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx-mode
JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
Define map[string]string // Documentation: https://esbuild.github.io/api/#define
Pure []string // Documentation: https://esbuild.github.io/api/#pure
KeepNames bool // Documentation: https://esbuild.github.io/api/#keep-names
GlobalName string // Documentation: https://esbuild.github.io/api/#global-name
Bundle bool // Documentation: https://esbuild.github.io/api/#bundle
PreserveSymlinks bool // Documentation: https://esbuild.github.io/api/#preserve-symlinks
Splitting bool // Documentation: https://esbuild.github.io/api/#splitting
Outfile string // Documentation: https://esbuild.github.io/api/#outfile
Metafile bool // Documentation: https://esbuild.github.io/api/#metafile
Outdir string // Documentation: https://esbuild.github.io/api/#outdir
Outbase string // Documentation: https://esbuild.github.io/api/#outbase
AbsWorkingDir string // Documentation: https://esbuild.github.io/api/#working-directory
Platform Platform // Documentation: https://esbuild.github.io/api/#platform
Format Format // Documentation: https://esbuild.github.io/api/#format
External []string // Documentation: https://esbuild.github.io/api/#external
MainFields []string // Documentation: https://esbuild.github.io/api/#main-fields
Conditions []string // Documentation: https://esbuild.github.io/api/#conditions
Loader map[string]Loader // Documentation: https://esbuild.github.io/api/#loader
ResolveExtensions []string // Documentation: https://esbuild.github.io/api/#resolve-extensions
Tsconfig string // Documentation: https://esbuild.github.io/api/#tsconfig
OutExtensions map[string]string // Documentation: https://esbuild.github.io/api/#out-extension
PublicPath string // Documentation: https://esbuild.github.io/api/#public-path
Inject []string // Documentation: https://esbuild.github.io/api/#inject
Banner map[string]string // Documentation: https://esbuild.github.io/api/#banner
Footer map[string]string // Documentation: https://esbuild.github.io/api/#footer
NodePaths []string // Documentation: https://esbuild.github.io/api/#node-paths
EntryNames string // Documentation: https://esbuild.github.io/api/#entry-names
ChunkNames string // Documentation: https://esbuild.github.io/api/#chunk-names
AssetNames string // Documentation: https://esbuild.github.io/api/#asset-names
EntryPoints []string // Documentation: https://esbuild.github.io/api/#entry-points
EntryPointsAdvanced []EntryPoint // Documentation: https://esbuild.github.io/api/#entry-points
Stdin *StdinOptions // Documentation: https://esbuild.github.io/api/#stdin
Write bool // Documentation: https://esbuild.github.io/api/#write
AllowOverwrite bool // Documentation: https://esbuild.github.io/api/#allow-overwrite
Incremental bool // Documentation: https://esbuild.github.io/api/#incremental
Plugins []Plugin // Documentation: https://esbuild.github.io/plugins/
Watch *WatchMode // Documentation: https://esbuild.github.io/api/#watch
}
type EntryPoint struct {
InputPath string
OutputPath string
}
type WatchMode struct {
OnRebuild func(BuildResult)
}
type StdinOptions struct {
Contents string
ResolveDir string
Sourcefile string
Loader Loader
}
type BuildResult struct {
Errors []Message
Warnings []Message
OutputFiles []OutputFile
Metafile string
Rebuild func() BuildResult // Only when "Incremental: true"
Stop func() // Only when "Watch: true"
}
type OutputFile struct {
Path string
Contents []byte
}
// Documentation: https://esbuild.github.io/api/#build-api
func Build(options BuildOptions) BuildResult {
return buildImpl(options).result
}
////////////////////////////////////////////////////////////////////////////////
// Transform API
type TransformOptions struct {
Color StderrColor // Documentation: https://esbuild.github.io/api/#color
LogLimit int // Documentation: https://esbuild.github.io/api/#log-limit
LogLevel LogLevel // Documentation: https://esbuild.github.io/api/#log-level
Sourcemap SourceMap // Documentation: https://esbuild.github.io/api/#sourcemap
SourceRoot string // Documentation: https://esbuild.github.io/api/#source-root
SourcesContent SourcesContent // Documentation: https://esbuild.github.io/api/#sources-content
Target Target // Documentation: https://esbuild.github.io/api/#target
Engines []Engine // Documentation: https://esbuild.github.io/api/#target
Format Format // Documentation: https://esbuild.github.io/api/#format
GlobalName string // Documentation: https://esbuild.github.io/api/#global-name
MinifyWhitespace bool // Documentation: https://esbuild.github.io/api/#minify
MinifyIdentifiers bool // Documentation: https://esbuild.github.io/api/#minify
MinifySyntax bool // Documentation: https://esbuild.github.io/api/#minify
Charset Charset // Documentation: https://esbuild.github.io/api/#charset
TreeShaking TreeShaking // Documentation: https://esbuild.github.io/api/#tree-shaking
IgnoreAnnotations bool // Documentation: https://esbuild.github.io/api/#ignore-annotations
LegalComments LegalComments // Documentation: https://esbuild.github.io/api/#legal-comments
JSXMode JSXMode // Documentation: https://esbuild.github.io/api/#jsx
JSXFactory string // Documentation: https://esbuild.github.io/api/#jsx-factory
JSXFragment string // Documentation: https://esbuild.github.io/api/#jsx-fragment
TsconfigRaw string // Documentation: https://esbuild.github.io/api/#tsconfig-raw
Banner string // Documentation: https://esbuild.github.io/api/#banner
Footer string // Documentation: https://esbuild.github.io/api/#footer
Define map[string]string // Documentation: https://esbuild.github.io/api/#define
Pure []string // Documentation: https://esbuild.github.io/api/#pure
KeepNames bool // Documentation: https://esbuild.github.io/api/#keep-names
Sourcefile string // Documentation: https://esbuild.github.io/api/#sourcefile
Loader Loader // Documentation: https://esbuild.github.io/api/#loader
}
type TransformResult struct {
Errors []Message
Warnings []Message
Code []byte
Map []byte
}
// Documentation: https://esbuild.github.io/api/#transform-api
func Transform(input string, options TransformOptions) TransformResult {
return transformImpl(input, options)
}
////////////////////////////////////////////////////////////////////////////////
// Serve API
// Documentation: https://esbuild.github.io/api/#serve-arguments
type ServeOptions struct {
Port uint16
Host string
Servedir string
OnRequest func(ServeOnRequestArgs)
}
type ServeOnRequestArgs struct {
RemoteAddress string
Method string
Path string
Status int
TimeInMS int // The time to generate the response, not to send it
}
// Documentation: https://esbuild.github.io/api/#serve-return-values
type ServeResult struct {
Port uint16
Host string
Wait func() error
Stop func()
}
// Documentation: https://esbuild.github.io/api/#serve
func Serve(serveOptions ServeOptions, buildOptions BuildOptions) (ServeResult, error) {
return serveImpl(serveOptions, buildOptions)
}
////////////////////////////////////////////////////////////////////////////////
// Plugin API
type SideEffects uint8
const (
SideEffectsTrue SideEffects = iota
SideEffectsFalse
)
type Plugin struct {
Name string
Setup func(PluginBuild)
}
type PluginBuild struct {
InitialOptions *BuildOptions
OnStart func(callback func() (OnStartResult, error))
OnEnd func(callback func(result *BuildResult))
OnResolve func(options OnResolveOptions, callback func(OnResolveArgs) (OnResolveResult, error))
OnLoad func(options OnLoadOptions, callback func(OnLoadArgs) (OnLoadResult, error))
}
type OnStartResult struct {
Errors []Message
Warnings []Message
}
type OnResolveOptions struct {
Filter string
Namespace string
}
type OnResolveArgs struct {
Path string
Importer string
Namespace string
ResolveDir string
Kind ResolveKind
PluginData interface{}
}
type OnResolveResult struct {
PluginName string
Errors []Message
Warnings []Message
Path string
External bool
SideEffects SideEffects
Namespace string
Suffix string
PluginData interface{}
WatchFiles []string
WatchDirs []string
}
type OnLoadOptions struct {
Filter string
Namespace string
}
type OnLoadArgs struct {
Path string
Namespace string
Suffix string
PluginData interface{}
}
type OnLoadResult struct {
PluginName string
Errors []Message
Warnings []Message
Contents *string
ResolveDir string
Loader Loader
PluginData interface{}
WatchFiles []string
WatchDirs []string
}
type ResolveKind uint8
const (
ResolveEntryPoint ResolveKind = iota
ResolveJSImportStatement
ResolveJSRequireCall
ResolveJSDynamicImport
ResolveJSRequireResolve
ResolveCSSImportRule
ResolveCSSURLToken
)
////////////////////////////////////////////////////////////////////////////////
// FormatMessages API
type MessageKind uint8
const (
ErrorMessage MessageKind = iota
WarningMessage
)
type FormatMessagesOptions struct {
TerminalWidth int
Kind MessageKind
Color bool
}
func FormatMessages(msgs []Message, opts FormatMessagesOptions) []string {
return formatMsgsImpl(msgs, opts)
}
////////////////////////////////////////////////////////////////////////////////
// AnalyzeMetafile API
type AnalyzeMetafileOptions struct {
Color bool
Verbose bool
}
// Documentation: https://esbuild.github.io/api/#analyze
func AnalyzeMetafile(metafile string, opts AnalyzeMetafileOptions) string {
return analyzeMetafileImpl(metafile, opts)
}

File diff suppressed because it is too large Load Diff

View File

@ -1,620 +0,0 @@
//go:build !js || !wasm
// +build !js !wasm
package api
import (
"fmt"
"net"
"net/http"
"path"
"sort"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/evanw/esbuild/internal/config"
"github.com/evanw/esbuild/internal/fs"
"github.com/evanw/esbuild/internal/helpers"
"github.com/evanw/esbuild/internal/logger"
)
////////////////////////////////////////////////////////////////////////////////
// Serve API
type apiHandler struct {
mutex sync.Mutex
outdirPathPrefix string
servedir string
options *config.Options
onRequest func(ServeOnRequestArgs)
rebuild func() BuildResult
currentBuild *runningBuild
fs fs.FS
serveWaitGroup sync.WaitGroup
serveError error
}
type runningBuild struct {
waitGroup sync.WaitGroup
result BuildResult
}
func (h *apiHandler) build() BuildResult {
build := func() *runningBuild {
h.mutex.Lock()
defer h.mutex.Unlock()
if h.currentBuild == nil {
build := &runningBuild{}
build.waitGroup.Add(1)
h.currentBuild = build
// Build on another thread
go func() {
result := h.rebuild()
h.rebuild = result.Rebuild
build.result = result
build.waitGroup.Done()
// Build results stay valid for a little bit afterward since a page
// load may involve multiple requests and don't want to rebuild
// separately for each of those requests.
time.Sleep(250 * time.Millisecond)
h.mutex.Lock()
defer h.mutex.Unlock()
h.currentBuild = nil
}()
}
return h.currentBuild
}()
build.waitGroup.Wait()
return build.result
}
func escapeForHTML(text string) string {
text = strings.ReplaceAll(text, "&", "&amp;")
text = strings.ReplaceAll(text, "<", "&lt;")
text = strings.ReplaceAll(text, ">", "&gt;")
return text
}
func escapeForAttribute(text string) string {
text = escapeForHTML(text)
text = strings.ReplaceAll(text, "\"", "&quot;")
text = strings.ReplaceAll(text, "'", "&apos;")
return text
}
func (h *apiHandler) notifyRequest(duration time.Duration, req *http.Request, status int) {
if h.onRequest != nil {
h.onRequest(ServeOnRequestArgs{
RemoteAddress: req.RemoteAddr,
Method: req.Method,
Path: req.URL.Path,
Status: status,
TimeInMS: int(duration.Milliseconds()),
})
}
}
func errorsToString(errors []Message) string {
stderrOptions := logger.OutputOptions{IncludeSource: true}
terminalOptions := logger.TerminalInfo{}
sb := strings.Builder{}
limit := 5
for i, msg := range convertMessagesToInternal(nil, logger.Error, errors) {
if i == limit {
sb.WriteString(fmt.Sprintf("%d out of %d errors shown\n", limit, len(errors)))
break
}
sb.WriteString(msg.String(stderrOptions, terminalOptions))
}
return sb.String()
}
func (h *apiHandler) ServeHTTP(res http.ResponseWriter, req *http.Request) {
start := time.Now()
// Handle get requests
if req.Method == "GET" && strings.HasPrefix(req.URL.Path, "/") {
res.Header().Set("Access-Control-Allow-Origin", "*")
queryPath := path.Clean(req.URL.Path)[1:]
result := h.build()
// Requests fail if the build had errors
if len(result.Errors) > 0 {
go h.notifyRequest(time.Since(start), req, http.StatusServiceUnavailable)
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
res.WriteHeader(http.StatusServiceUnavailable)
res.Write([]byte(errorsToString(result.Errors)))
return
}
var kind fs.EntryKind
var fileContents fs.OpenedFile
dirEntries := make(map[string]bool)
fileEntries := make(map[string]bool)
// Check for a match with the results if we're within the output directory
if strings.HasPrefix(queryPath, h.outdirPathPrefix) {
outdirQueryPath := queryPath[len(h.outdirPathPrefix):]
if strings.HasPrefix(outdirQueryPath, "/") {
outdirQueryPath = outdirQueryPath[1:]
}
resultKind, inMemoryBytes := h.matchQueryPathToResult(outdirQueryPath, &result, dirEntries, fileEntries)
kind = resultKind
fileContents = &fs.InMemoryOpenedFile{Contents: inMemoryBytes}
} else {
// Create a fake directory entry for the output path so that it appears to be a real directory
p := h.outdirPathPrefix
for p != "" {
var dir string
var base string
if slash := strings.IndexByte(p, '/'); slash == -1 {
base = p
} else {
dir = p[:slash]
base = p[slash+1:]
}
if dir == queryPath {
kind = fs.DirEntry
dirEntries[base] = true
break
}
p = dir
}
}
// Check for a file in the fallback directory
if h.servedir != "" && kind != fs.FileEntry {
absPath := h.fs.Join(h.servedir, queryPath)
if absDir := h.fs.Dir(absPath); absDir != absPath {
if entries, err, _ := h.fs.ReadDirectory(absDir); err == nil {
if entry, _ := entries.Get(h.fs.Base(absPath)); entry != nil && entry.Kind(h.fs) == fs.FileEntry {
if contents, err, _ := h.fs.OpenFile(absPath); err == nil {
defer contents.Close()
fileContents = contents
kind = fs.FileEntry
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
}
}
}
// Check for a directory in the fallback directory
var fallbackIndexName string
if h.servedir != "" && kind != fs.FileEntry {
if entries, err, _ := h.fs.ReadDirectory(h.fs.Join(h.servedir, queryPath)); err == nil {
kind = fs.DirEntry
for _, name := range entries.SortedKeys() {
entry, _ := entries.Get(name)
switch entry.Kind(h.fs) {
case fs.DirEntry:
dirEntries[name] = true
case fs.FileEntry:
fileEntries[name] = true
if name == "index.html" {
fallbackIndexName = name
}
}
}
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
// Redirect to a trailing slash for directories
if kind == fs.DirEntry && !strings.HasSuffix(req.URL.Path, "/") {
res.Header().Set("Location", req.URL.Path+"/")
go h.notifyRequest(time.Since(start), req, http.StatusFound)
res.WriteHeader(http.StatusFound)
res.Write(nil)
return
}
// Serve a "index.html" file if present
if kind == fs.DirEntry && fallbackIndexName != "" {
queryPath += "/" + fallbackIndexName
if contents, err, _ := h.fs.OpenFile(h.fs.Join(h.servedir, queryPath)); err == nil {
defer contents.Close()
fileContents = contents
kind = fs.FileEntry
} else if err != syscall.ENOENT {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
}
// Serve a file
if kind == fs.FileEntry {
// Default to serving the whole file
status := http.StatusOK
fileContentsLen := fileContents.Len()
begin := 0
end := fileContentsLen
isRange := false
// Handle range requests so that video playback works in Safari
if rangeBegin, rangeEnd, ok := parseRangeHeader(req.Header.Get("Range"), fileContentsLen); ok && rangeBegin < rangeEnd {
// Note: The content range is inclusive so subtract 1 from the end
isRange = true
begin = rangeBegin
end = rangeEnd
status = http.StatusPartialContent
}
// Try to read the range from the file, which may fail
fileBytes, err := fileContents.Read(begin, end)
if err != nil {
go h.notifyRequest(time.Since(start), req, http.StatusInternalServerError)
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(fmt.Sprintf("500 - Internal server error: %s", err.Error())))
return
}
// If we get here, the request was successful
if contentType := helpers.MimeTypeByExtension(path.Ext(queryPath)); contentType != "" {
res.Header().Set("Content-Type", contentType)
} else {
res.Header().Set("Content-Type", "application/octet-stream")
}
if isRange {
res.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", begin, end-1, fileContentsLen))
}
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(fileBytes)))
go h.notifyRequest(time.Since(start), req, status)
res.WriteHeader(status)
res.Write(fileBytes)
return
}
// Serve a directory listing
if kind == fs.DirEntry {
html := respondWithDirList(queryPath, dirEntries, fileEntries)
res.Header().Set("Content-Type", "text/html; charset=utf-8")
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(html)))
go h.notifyRequest(time.Since(start), req, http.StatusOK)
res.Write(html)
return
}
}
// Default to a 404
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
go h.notifyRequest(time.Since(start), req, http.StatusNotFound)
res.WriteHeader(http.StatusNotFound)
res.Write([]byte("404 - Not Found"))
}
// Handle enough of the range specification so that video playback works in Safari
func parseRangeHeader(r string, contentLength int) (int, int, bool) {
if strings.HasPrefix(r, "bytes=") {
r = r[len("bytes="):]
if dash := strings.IndexByte(r, '-'); dash != -1 {
// Note: The range is inclusive so the limit is deliberately "length - 1"
if begin, ok := parseRangeInt(r[:dash], contentLength-1); ok {
if end, ok := parseRangeInt(r[dash+1:], contentLength-1); ok {
// Note: The range is inclusive so a range of "0-1" is two bytes long
return begin, end + 1, true
}
}
}
}
return 0, 0, false
}
func parseRangeInt(text string, maxValue int) (int, bool) {
if text == "" {
return 0, false
}
value := 0
for _, c := range text {
if c < '0' || c > '9' {
return 0, false
}
value = value*10 + int(c-'0')
if value > maxValue {
return 0, false
}
}
return value, true
}
func (h *apiHandler) matchQueryPathToResult(
queryPath string,
result *BuildResult,
dirEntries map[string]bool,
fileEntries map[string]bool,
) (fs.EntryKind, []byte) {
queryIsDir := false
queryDir := queryPath
if queryDir != "" {
queryDir += "/"
}
// Check the output files for a match
for _, file := range result.OutputFiles {
if relPath, ok := h.fs.Rel(h.options.AbsOutputDir, file.Path); ok {
relPath = strings.ReplaceAll(relPath, "\\", "/")
// An exact match
if relPath == queryPath {
return fs.FileEntry, file.Contents
}
// A match inside this directory
if strings.HasPrefix(relPath, queryDir) {
entry := relPath[len(queryDir):]
queryIsDir = true
if slash := strings.IndexByte(entry, '/'); slash == -1 {
fileEntries[entry] = true
} else if dir := entry[:slash]; !dirEntries[dir] {
dirEntries[dir] = true
}
}
}
}
// Treat this as a directory if it's non-empty
if queryIsDir {
return fs.DirEntry, nil
}
return 0, nil
}
func respondWithDirList(queryPath string, dirEntries map[string]bool, fileEntries map[string]bool) []byte {
queryPath = "/" + queryPath
queryDir := queryPath
if queryDir != "/" {
queryDir += "/"
}
html := strings.Builder{}
html.WriteString(`<!doctype html>`)
html.WriteString(`<meta charset="utf8">`)
html.WriteString(`<title>Directory: `)
html.WriteString(escapeForHTML(queryDir))
html.WriteString(`</title>`)
html.WriteString(`<h1>Directory: `)
html.WriteString(escapeForHTML(queryDir))
html.WriteString(`</h1>`)
html.WriteString(`<ul>`)
// Link to the parent directory
if queryPath != "/" {
parentDir := path.Dir(queryPath)
if parentDir != "/" {
parentDir += "/"
}
html.WriteString(fmt.Sprintf(`<li><a href="%s">../</a></li>`, escapeForAttribute(parentDir)))
}
// Link to child directories
strings := make([]string, 0, len(dirEntries)+len(fileEntries))
for entry := range dirEntries {
strings = append(strings, entry)
}
sort.Strings(strings)
for _, entry := range strings {
html.WriteString(fmt.Sprintf(`<li><a href="%s/">%s/</a></li>`, escapeForAttribute(path.Join(queryPath, entry)), escapeForHTML(entry)))
}
// Link to files in the directory
strings = strings[:0]
for entry := range fileEntries {
strings = append(strings, entry)
}
sort.Strings(strings)
for _, entry := range strings {
html.WriteString(fmt.Sprintf(`<li><a href="%s">%s</a></li>`, escapeForAttribute(path.Join(queryPath, entry)), escapeForHTML(entry)))
}
html.WriteString(`</ul>`)
return []byte(html.String())
}
// This is used to make error messages platform-independent
func prettyPrintPath(fs fs.FS, path string) string {
if relPath, ok := fs.Rel(fs.Cwd(), path); ok {
return strings.ReplaceAll(relPath, "\\", "/")
}
return path
}
func serveImpl(serveOptions ServeOptions, buildOptions BuildOptions) (ServeResult, error) {
realFS, err := fs.RealFS(fs.RealFSOptions{
AbsWorkingDir: buildOptions.AbsWorkingDir,
// This is a long-lived file system object so do not cache calls to
// ReadDirectory() (they are normally cached for the duration of a build
// for performance).
DoNotCache: true,
})
if err != nil {
return ServeResult{}, err
}
buildOptions.Incremental = true
buildOptions.Write = false
// Watch and serve are both different ways of rebuilding, and cannot be combined
if buildOptions.Watch != nil {
return ServeResult{}, fmt.Errorf("Cannot use \"watch\" with \"serve\"")
}
// Validate the fallback path
if serveOptions.Servedir != "" {
if absPath, ok := realFS.Abs(serveOptions.Servedir); ok {
serveOptions.Servedir = absPath
} else {
return ServeResult{}, fmt.Errorf("Invalid serve path: %s", serveOptions.Servedir)
}
}
// If there is no output directory, set the output directory to something so
// the build doesn't try to write to stdout. Make sure not to set this to a
// path that may contain the user's files in it since we don't want to get
// errors about overwriting input files.
outdirPathPrefix := ""
if buildOptions.Outdir == "" && buildOptions.Outfile == "" {
buildOptions.Outdir = realFS.Join(realFS.Cwd(), "...")
} else if serveOptions.Servedir != "" {
// Compute the output directory
var outdir string
if buildOptions.Outdir != "" {
if absPath, ok := realFS.Abs(buildOptions.Outdir); ok {
outdir = absPath
} else {
return ServeResult{}, fmt.Errorf("Invalid outdir path: %s", buildOptions.Outdir)
}
} else {
if absPath, ok := realFS.Abs(buildOptions.Outfile); ok {
outdir = realFS.Dir(absPath)
} else {
return ServeResult{}, fmt.Errorf("Invalid outdir path: %s", buildOptions.Outfile)
}
}
// Make sure the output directory is contained in the fallback directory
relPath, ok := realFS.Rel(serveOptions.Servedir, outdir)
if !ok {
return ServeResult{}, fmt.Errorf(
"Cannot compute relative path from %q to %q\n", serveOptions.Servedir, outdir)
}
relPath = strings.ReplaceAll(relPath, "\\", "/") // Fix paths on Windows
if relPath == ".." || strings.HasPrefix(relPath, "../") {
return ServeResult{}, fmt.Errorf(
"Output directory %q must be contained in serve directory %q",
prettyPrintPath(realFS, outdir),
prettyPrintPath(realFS, serveOptions.Servedir),
)
}
if relPath != "." {
outdirPathPrefix = relPath
}
}
// Determine the host
var listener net.Listener
network := "tcp4"
host := "0.0.0.0"
if serveOptions.Host != "" {
host = serveOptions.Host
// Only use "tcp4" if this is an IPv4 address, otherwise use "tcp"
if ip := net.ParseIP(host); ip == nil || ip.To4() == nil {
network = "tcp"
}
}
// Pick the port
if serveOptions.Port == 0 {
// Default to picking a "800X" port
for port := 8000; port <= 8009; port++ {
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", port))); err == nil {
listener = result
break
}
}
}
if listener == nil {
// Otherwise pick the provided port
if result, err := net.Listen(network, net.JoinHostPort(host, fmt.Sprintf("%d", serveOptions.Port))); err != nil {
return ServeResult{}, err
} else {
listener = result
}
}
// Try listening on the provided port
addr := listener.Addr().String()
// Extract the real port in case we passed a port of "0"
var result ServeResult
if host, text, err := net.SplitHostPort(addr); err == nil {
if port, err := strconv.ParseInt(text, 10, 32); err == nil {
result.Port = uint16(port)
result.Host = host
}
}
var stoppingMutex sync.Mutex
isStopping := false
// The first build will just build normally
var handler *apiHandler
handler = &apiHandler{
onRequest: serveOptions.OnRequest,
outdirPathPrefix: outdirPathPrefix,
servedir: serveOptions.Servedir,
rebuild: func() BuildResult {
stoppingMutex.Lock()
defer stoppingMutex.Unlock()
// Don't start more rebuilds if we were told to stop
if isStopping {
return BuildResult{}
}
build := buildImpl(buildOptions)
if handler.options == nil {
handler.options = &build.options
}
return build.result
},
fs: realFS,
}
// When wait is called, block until the server's call to "Serve()" returns
result.Wait = func() error {
handler.serveWaitGroup.Wait()
return handler.serveError
}
// Create the server
server := &http.Server{Addr: addr, Handler: handler}
// When stop is called, block further rebuilds and then close the server
result.Stop = func() {
stoppingMutex.Lock()
defer stoppingMutex.Unlock()
// Only try to close the server once
if isStopping {
return
}
isStopping = true
// Close the server and wait for it to close
server.Close()
handler.serveWaitGroup.Wait()
}
// Start the server and signal on "serveWaitGroup" when it stops
handler.serveWaitGroup.Add(1)
go func() {
if err := server.Serve(listener); err != http.ErrServerClosed {
handler.serveError = err
}
handler.serveWaitGroup.Done()
}()
// Start the first build shortly after this function returns (but not
// immediately so that stuff we print right after this will come first)
go func() {
time.Sleep(10 * time.Millisecond)
handler.build()
}()
return result, nil
}

View File

@ -1,12 +0,0 @@
//go:build js && wasm
// +build js,wasm
package api
import "fmt"
// Remove the serve API in the WebAssembly build. This removes 2.7mb of stuff.
func serveImpl(serveOptions ServeOptions, buildOptions BuildOptions) (ServeResult, error) {
return ServeResult{}, fmt.Errorf("The \"serve\" API is not supported when using WebAssembly")
}

View File

@ -1,25 +0,0 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
.idea/
*.iml

View File

@ -1,9 +0,0 @@
# This is the official list of Gorilla WebSocket authors for copyright
# purposes.
#
# Please keep the list sorted.
Gary Burd <gary@beagledreams.com>
Google LLC (https://opensource.google.com/)
Joachim Bauch <mail@joachim-bauch.de>

View File

@ -1,22 +0,0 @@
Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,64 +0,0 @@
# Gorilla WebSocket
[![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
[![CircleCI](https://circleci.com/gh/gorilla/websocket.svg?style=svg)](https://circleci.com/gh/gorilla/websocket)
Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
[WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
### Documentation
* [API Reference](https://pkg.go.dev/github.com/gorilla/websocket?tab=doc)
* [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
* [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
* [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
* [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
### Status
The Gorilla WebSocket package provides a complete and tested implementation of
the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
package API is stable.
### Installation
go get github.com/gorilla/websocket
### Protocol Compliance
The Gorilla WebSocket package passes the server tests in the [Autobahn Test
Suite](https://github.com/crossbario/autobahn-testsuite) using the application in the [examples/autobahn
subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
### Gorilla WebSocket compared with other packages
<table>
<tr>
<th></th>
<th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
<th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
</tr>
<tr>
<tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
<tr><td>Passes <a href="https://github.com/crossbario/autobahn-testsuite">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
<tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
<tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
<tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
<tr><td colspan="3">Other Features</tr></td>
<tr><td><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</td></tr>
<tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
<tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
</table>
Notes:
1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
2. The application can get the type of a received data message by implementing
a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
function.
3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
Read returns when the input buffer is full or a frame boundary is
encountered. Each call to Write sends a single frame message. The Gorilla
io.Reader and io.WriteCloser operate on a single WebSocket message.

View File

@ -1,395 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bytes"
"context"
"crypto/tls"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
)
// ErrBadHandshake is returned when the server response to opening handshake is
// invalid.
var ErrBadHandshake = errors.New("websocket: bad handshake")
var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
// NewClient creates a new client connection using the given net connection.
// The URL u specifies the host and request URI. Use requestHeader to specify
// the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
// (Cookie). Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etc.
//
// Deprecated: Use Dialer instead.
func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
d := Dialer{
ReadBufferSize: readBufSize,
WriteBufferSize: writeBufSize,
NetDial: func(net, addr string) (net.Conn, error) {
return netConn, nil
},
}
return d.Dial(u.String(), requestHeader)
}
// A Dialer contains options for connecting to WebSocket server.
type Dialer struct {
// NetDial specifies the dial function for creating TCP connections. If
// NetDial is nil, net.Dial is used.
NetDial func(network, addr string) (net.Conn, error)
// NetDialContext specifies the dial function for creating TCP connections. If
// NetDialContext is nil, net.DialContext is used.
NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)
// Proxy specifies a function to return a proxy for a given
// Request. If the function returns a non-nil error, the
// request is aborted with the provided error.
// If Proxy is nil or returns a nil *URL, no proxy is used.
Proxy func(*http.Request) (*url.URL, error)
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
// If nil, the default configuration is used.
TLSClientConfig *tls.Config
// HandshakeTimeout specifies the duration for the handshake to complete.
HandshakeTimeout time.Duration
// ReadBufferSize and WriteBufferSize specify I/O buffer sizes in bytes. If a buffer
// size is zero, then a useful default size is used. The I/O buffer sizes
// do not limit the size of the messages that can be sent or received.
ReadBufferSize, WriteBufferSize int
// WriteBufferPool is a pool of buffers for write operations. If the value
// is not set, then write buffers are allocated to the connection for the
// lifetime of the connection.
//
// A pool is most useful when the application has a modest volume of writes
// across a large number of connections.
//
// Applications should use a single pool for each unique value of
// WriteBufferSize.
WriteBufferPool BufferPool
// Subprotocols specifies the client's requested subprotocols.
Subprotocols []string
// EnableCompression specifies if the client should attempt to negotiate
// per message compression (RFC 7692). Setting this value to true does not
// guarantee that compression will be supported. Currently only "no context
// takeover" modes are supported.
EnableCompression bool
// Jar specifies the cookie jar.
// If Jar is nil, cookies are not sent in requests and ignored
// in responses.
Jar http.CookieJar
}
// Dial creates a new client connection by calling DialContext with a background context.
func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
return d.DialContext(context.Background(), urlStr, requestHeader)
}
var errMalformedURL = errors.New("malformed ws or wss URL")
func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
hostPort = u.Host
hostNoPort = u.Host
if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
hostNoPort = hostNoPort[:i]
} else {
switch u.Scheme {
case "wss":
hostPort += ":443"
case "https":
hostPort += ":443"
default:
hostPort += ":80"
}
}
return hostPort, hostNoPort
}
// DefaultDialer is a dialer with all fields set to the default values.
var DefaultDialer = &Dialer{
Proxy: http.ProxyFromEnvironment,
HandshakeTimeout: 45 * time.Second,
}
// nilDialer is dialer to use when receiver is nil.
var nilDialer = *DefaultDialer
// DialContext creates a new client connection. Use requestHeader to specify the
// origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
// Use the response.Header to get the selected subprotocol
// (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
//
// The context will be used in the request and in the Dialer.
//
// If the WebSocket handshake fails, ErrBadHandshake is returned along with a
// non-nil *http.Response so that callers can handle redirects, authentication,
// etcetera. The response body may not contain the entire response and does not
// need to be closed by the application.
func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
if d == nil {
d = &nilDialer
}
challengeKey, err := generateChallengeKey()
if err != nil {
return nil, nil, err
}
u, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
switch u.Scheme {
case "ws":
u.Scheme = "http"
case "wss":
u.Scheme = "https"
default:
return nil, nil, errMalformedURL
}
if u.User != nil {
// User name and password are not allowed in websocket URIs.
return nil, nil, errMalformedURL
}
req := &http.Request{
Method: "GET",
URL: u,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header),
Host: u.Host,
}
req = req.WithContext(ctx)
// Set the cookies present in the cookie jar of the dialer
if d.Jar != nil {
for _, cookie := range d.Jar.Cookies(u) {
req.AddCookie(cookie)
}
}
// Set the request headers using the capitalization for names and values in
// RFC examples. Although the capitalization shouldn't matter, there are
// servers that depend on it. The Header.Set method is not used because the
// method canonicalizes the header names.
req.Header["Upgrade"] = []string{"websocket"}
req.Header["Connection"] = []string{"Upgrade"}
req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
req.Header["Sec-WebSocket-Version"] = []string{"13"}
if len(d.Subprotocols) > 0 {
req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
}
for k, vs := range requestHeader {
switch {
case k == "Host":
if len(vs) > 0 {
req.Host = vs[0]
}
case k == "Upgrade" ||
k == "Connection" ||
k == "Sec-Websocket-Key" ||
k == "Sec-Websocket-Version" ||
k == "Sec-Websocket-Extensions" ||
(k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
case k == "Sec-Websocket-Protocol":
req.Header["Sec-WebSocket-Protocol"] = vs
default:
req.Header[k] = vs
}
}
if d.EnableCompression {
req.Header["Sec-WebSocket-Extensions"] = []string{"permessage-deflate; server_no_context_takeover; client_no_context_takeover"}
}
if d.HandshakeTimeout != 0 {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, d.HandshakeTimeout)
defer cancel()
}
// Get network dial function.
var netDial func(network, add string) (net.Conn, error)
if d.NetDialContext != nil {
netDial = func(network, addr string) (net.Conn, error) {
return d.NetDialContext(ctx, network, addr)
}
} else if d.NetDial != nil {
netDial = d.NetDial
} else {
netDialer := &net.Dialer{}
netDial = func(network, addr string) (net.Conn, error) {
return netDialer.DialContext(ctx, network, addr)
}
}
// If needed, wrap the dial function to set the connection deadline.
if deadline, ok := ctx.Deadline(); ok {
forwardDial := netDial
netDial = func(network, addr string) (net.Conn, error) {
c, err := forwardDial(network, addr)
if err != nil {
return nil, err
}
err = c.SetDeadline(deadline)
if err != nil {
c.Close()
return nil, err
}
return c, nil
}
}
// If needed, wrap the dial function to connect through a proxy.
if d.Proxy != nil {
proxyURL, err := d.Proxy(req)
if err != nil {
return nil, nil, err
}
if proxyURL != nil {
dialer, err := proxy_FromURL(proxyURL, netDialerFunc(netDial))
if err != nil {
return nil, nil, err
}
netDial = dialer.Dial
}
}
hostPort, hostNoPort := hostPortNoPort(u)
trace := httptrace.ContextClientTrace(ctx)
if trace != nil && trace.GetConn != nil {
trace.GetConn(hostPort)
}
netConn, err := netDial("tcp", hostPort)
if trace != nil && trace.GotConn != nil {
trace.GotConn(httptrace.GotConnInfo{
Conn: netConn,
})
}
if err != nil {
return nil, nil, err
}
defer func() {
if netConn != nil {
netConn.Close()
}
}()
if u.Scheme == "https" {
cfg := cloneTLSConfig(d.TLSClientConfig)
if cfg.ServerName == "" {
cfg.ServerName = hostNoPort
}
tlsConn := tls.Client(netConn, cfg)
netConn = tlsConn
var err error
if trace != nil {
err = doHandshakeWithTrace(trace, tlsConn, cfg)
} else {
err = doHandshake(tlsConn, cfg)
}
if err != nil {
return nil, nil, err
}
}
conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize, d.WriteBufferPool, nil, nil)
if err := req.Write(netConn); err != nil {
return nil, nil, err
}
if trace != nil && trace.GotFirstResponseByte != nil {
if peek, err := conn.br.Peek(1); err == nil && len(peek) == 1 {
trace.GotFirstResponseByte()
}
}
resp, err := http.ReadResponse(conn.br, req)
if err != nil {
return nil, nil, err
}
if d.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
d.Jar.SetCookies(u, rc)
}
}
if resp.StatusCode != 101 ||
!strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
!strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
// Before closing the network connection on return from this
// function, slurp up some of the response to aid application
// debugging.
buf := make([]byte, 1024)
n, _ := io.ReadFull(resp.Body, buf)
resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
return nil, resp, ErrBadHandshake
}
for _, ext := range parseExtensions(resp.Header) {
if ext[""] != "permessage-deflate" {
continue
}
_, snct := ext["server_no_context_takeover"]
_, cnct := ext["client_no_context_takeover"]
if !snct || !cnct {
return nil, resp, errInvalidCompression
}
conn.newCompressionWriter = compressNoContextTakeover
conn.newDecompressionReader = decompressNoContextTakeover
break
}
resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
netConn.SetDeadline(time.Time{})
netConn = nil // to avoid close in defer.
return conn, resp, nil
}
func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error {
if err := tlsConn.Handshake(); err != nil {
return err
}
if !cfg.InsecureSkipVerify {
if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
return err
}
}
return nil
}

View File

@ -1,16 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "crypto/tls"
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return cfg.Clone()
}

View File

@ -1,38 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
import "crypto/tls"
// cloneTLSConfig clones all public fields except the fields
// SessionTicketsDisabled and SessionTicketKey. This avoids copying the
// sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
// config in active use.
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
if cfg == nil {
return &tls.Config{}
}
return &tls.Config{
Rand: cfg.Rand,
Time: cfg.Time,
Certificates: cfg.Certificates,
NameToCertificate: cfg.NameToCertificate,
GetCertificate: cfg.GetCertificate,
RootCAs: cfg.RootCAs,
NextProtos: cfg.NextProtos,
ServerName: cfg.ServerName,
ClientAuth: cfg.ClientAuth,
ClientCAs: cfg.ClientCAs,
InsecureSkipVerify: cfg.InsecureSkipVerify,
CipherSuites: cfg.CipherSuites,
PreferServerCipherSuites: cfg.PreferServerCipherSuites,
ClientSessionCache: cfg.ClientSessionCache,
MinVersion: cfg.MinVersion,
MaxVersion: cfg.MaxVersion,
CurvePreferences: cfg.CurvePreferences,
}
}

View File

@ -1,148 +0,0 @@
// Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"compress/flate"
"errors"
"io"
"strings"
"sync"
)
const (
minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
maxCompressionLevel = flate.BestCompression
defaultCompressionLevel = 1
)
var (
flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
flateReaderPool = sync.Pool{New: func() interface{} {
return flate.NewReader(nil)
}}
)
func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
const tail =
// Add four bytes as specified in RFC
"\x00\x00\xff\xff" +
// Add final block to squelch unexpected EOF error from flate reader.
"\x01\x00\x00\xff\xff"
fr, _ := flateReaderPool.Get().(io.ReadCloser)
fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
return &flateReadWrapper{fr}
}
func isValidCompressionLevel(level int) bool {
return minCompressionLevel <= level && level <= maxCompressionLevel
}
func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
p := &flateWriterPools[level-minCompressionLevel]
tw := &truncWriter{w: w}
fw, _ := p.Get().(*flate.Writer)
if fw == nil {
fw, _ = flate.NewWriter(tw, level)
} else {
fw.Reset(tw)
}
return &flateWriteWrapper{fw: fw, tw: tw, p: p}
}
// truncWriter is an io.Writer that writes all but the last four bytes of the
// stream to another io.Writer.
type truncWriter struct {
w io.WriteCloser
n int
p [4]byte
}
func (w *truncWriter) Write(p []byte) (int, error) {
n := 0
// fill buffer first for simplicity.
if w.n < len(w.p) {
n = copy(w.p[w.n:], p)
p = p[n:]
w.n += n
if len(p) == 0 {
return n, nil
}
}
m := len(p)
if m > len(w.p) {
m = len(w.p)
}
if nn, err := w.w.Write(w.p[:m]); err != nil {
return n + nn, err
}
copy(w.p[:], w.p[m:])
copy(w.p[len(w.p)-m:], p[len(p)-m:])
nn, err := w.w.Write(p[:len(p)-m])
return n + nn, err
}
type flateWriteWrapper struct {
fw *flate.Writer
tw *truncWriter
p *sync.Pool
}
func (w *flateWriteWrapper) Write(p []byte) (int, error) {
if w.fw == nil {
return 0, errWriteClosed
}
return w.fw.Write(p)
}
func (w *flateWriteWrapper) Close() error {
if w.fw == nil {
return errWriteClosed
}
err1 := w.fw.Flush()
w.p.Put(w.fw)
w.fw = nil
if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
}
err2 := w.tw.w.Close()
if err1 != nil {
return err1
}
return err2
}
type flateReadWrapper struct {
fr io.ReadCloser
}
func (r *flateReadWrapper) Read(p []byte) (int, error) {
if r.fr == nil {
return 0, io.ErrClosedPipe
}
n, err := r.fr.Read(p)
if err == io.EOF {
// Preemptively place the reader back in the pool. This helps with
// scenarios where the application does not call NextReader() soon after
// this final read.
r.Close()
}
return n, err
}
func (r *flateReadWrapper) Close() error {
if r.fr == nil {
return io.ErrClosedPipe
}
err := r.fr.Close()
flateReaderPool.Put(r.fr)
r.fr = nil
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build go1.8
package websocket
import "net"
func (c *Conn) writeBufs(bufs ...[]byte) error {
b := net.Buffers(bufs)
_, err := b.WriteTo(c.conn)
return err
}

View File

@ -1,18 +0,0 @@
// Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !go1.8
package websocket
func (c *Conn) writeBufs(bufs ...[]byte) error {
for _, buf := range bufs {
if len(buf) > 0 {
if _, err := c.conn.Write(buf); err != nil {
return err
}
}
}
return nil
}

View File

@ -1,227 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websocket implements the WebSocket protocol defined in RFC 6455.
//
// Overview
//
// The Conn type represents a WebSocket connection. A server application calls
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
//
// var upgrader = websocket.Upgrader{
// ReadBufferSize: 1024,
// WriteBufferSize: 1024,
// }
//
// func handler(w http.ResponseWriter, r *http.Request) {
// conn, err := upgrader.Upgrade(w, r, nil)
// if err != nil {
// log.Println(err)
// return
// }
// ... Use conn to send and receive messages.
// }
//
// Call the connection's WriteMessage and ReadMessage methods to send and
// receive messages as a slice of bytes. This snippet of code shows how to echo
// messages using these methods:
//
// for {
// messageType, p, err := conn.ReadMessage()
// if err != nil {
// log.Println(err)
// return
// }
// if err := conn.WriteMessage(messageType, p); err != nil {
// log.Println(err)
// return
// }
// }
//
// In above snippet of code, p is a []byte and messageType is an int with value
// websocket.BinaryMessage or websocket.TextMessage.
//
// An application can also send and receive messages using the io.WriteCloser
// and io.Reader interfaces. To send a message, call the connection NextWriter
// method to get an io.WriteCloser, write the message to the writer and close
// the writer when done. To receive a message, call the connection NextReader
// method to get an io.Reader and read until io.EOF is returned. This snippet
// shows how to echo messages using the NextWriter and NextReader methods:
//
// for {
// messageType, r, err := conn.NextReader()
// if err != nil {
// return
// }
// w, err := conn.NextWriter(messageType)
// if err != nil {
// return err
// }
// if _, err := io.Copy(w, r); err != nil {
// return err
// }
// if err := w.Close(); err != nil {
// return err
// }
// }
//
// Data Messages
//
// The WebSocket protocol distinguishes between text and binary data messages.
// Text messages are interpreted as UTF-8 encoded text. The interpretation of
// binary messages is left to the application.
//
// This package uses the TextMessage and BinaryMessage integer constants to
// identify the two data message types. The ReadMessage and NextReader methods
// return the type of the received message. The messageType argument to the
// WriteMessage and NextWriter methods specifies the type of a sent message.
//
// It is the application's responsibility to ensure that text messages are
// valid UTF-8 encoded text.
//
// Control Messages
//
// The WebSocket protocol defines three types of control messages: close, ping
// and pong. Call the connection WriteControl, WriteMessage or NextWriter
// methods to send a control message to the peer.
//
// Connections handle received close messages by calling the handler function
// set with the SetCloseHandler method and by returning a *CloseError from the
// NextReader, ReadMessage or the message Read method. The default close
// handler sends a close message to the peer.
//
// Connections handle received ping messages by calling the handler function
// set with the SetPingHandler method. The default ping handler sends a pong
// message to the peer.
//
// Connections handle received pong messages by calling the handler function
// set with the SetPongHandler method. The default pong handler does nothing.
// If an application sends ping messages, then the application should set a
// pong handler to receive the corresponding pong.
//
// The control message handler functions are called from the NextReader,
// ReadMessage and message reader Read methods. The default close and ping
// handlers can block these methods for a short time when the handler writes to
// the connection.
//
// The application must read the connection to process close, ping and pong
// messages sent from the peer. If the application is not otherwise interested
// in messages from the peer, then the application should start a goroutine to
// read and discard messages from the peer. A simple example is:
//
// func readLoop(c *websocket.Conn) {
// for {
// if _, _, err := c.NextReader(); err != nil {
// c.Close()
// break
// }
// }
// }
//
// Concurrency
//
// Connections support one concurrent reader and one concurrent writer.
//
// Applications are responsible for ensuring that no more than one goroutine
// calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
// WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
// that no more than one goroutine calls the read methods (NextReader,
// SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
// concurrently.
//
// The Close and WriteControl methods can be called concurrently with all other
// methods.
//
// Origin Considerations
//
// Web browsers allow Javascript applications to open a WebSocket connection to
// any host. It's up to the server to enforce an origin policy using the Origin
// request header sent by the browser.
//
// The Upgrader calls the function specified in the CheckOrigin field to check
// the origin. If the CheckOrigin function returns false, then the Upgrade
// method fails the WebSocket handshake with HTTP status 403.
//
// If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
// the handshake if the Origin request header is present and the Origin host is
// not equal to the Host request header.
//
// The deprecated package-level Upgrade function does not perform origin
// checking. The application is responsible for checking the Origin header
// before calling the Upgrade function.
//
// Buffers
//
// Connections buffer network input and output to reduce the number
// of system calls when reading or writing messages.
//
// Write buffers are also used for constructing WebSocket frames. See RFC 6455,
// Section 5 for a discussion of message framing. A WebSocket frame header is
// written to the network each time a write buffer is flushed to the network.
// Decreasing the size of the write buffer can increase the amount of framing
// overhead on the connection.
//
// The buffer sizes in bytes are specified by the ReadBufferSize and
// WriteBufferSize fields in the Dialer and Upgrader. The Dialer uses a default
// size of 4096 when a buffer size field is set to zero. The Upgrader reuses
// buffers created by the HTTP server when a buffer size field is set to zero.
// The HTTP server buffers have a size of 4096 at the time of this writing.
//
// The buffer sizes do not limit the size of a message that can be read or
// written by a connection.
//
// Buffers are held for the lifetime of the connection by default. If the
// Dialer or Upgrader WriteBufferPool field is set, then a connection holds the
// write buffer only when writing a message.
//
// Applications should tune the buffer sizes to balance memory use and
// performance. Increasing the buffer size uses more memory, but can reduce the
// number of system calls to read or write the network. In the case of writing,
// increasing the buffer size can reduce the number of frame headers written to
// the network.
//
// Some guidelines for setting buffer parameters are:
//
// Limit the buffer sizes to the maximum expected message size. Buffers larger
// than the largest message do not provide any benefit.
//
// Depending on the distribution of message sizes, setting the buffer size to
// a value less than the maximum expected message size can greatly reduce memory
// use with a small impact on performance. Here's an example: If 99% of the
// messages are smaller than 256 bytes and the maximum message size is 512
// bytes, then a buffer size of 256 bytes will result in 1.01 more system calls
// than a buffer size of 512 bytes. The memory savings is 50%.
//
// A write buffer pool is useful when the application has a modest number
// writes over a large number of connections. when buffers are pooled, a larger
// buffer size has a reduced impact on total memory use and has the benefit of
// reducing system calls and frame overhead.
//
// Compression EXPERIMENTAL
//
// Per message compression extensions (RFC 7692) are experimentally supported
// by this package in a limited capacity. Setting the EnableCompression option
// to true in Dialer or Upgrader will attempt to negotiate per message deflate
// support.
//
// var upgrader = websocket.Upgrader{
// EnableCompression: true,
// }
//
// If compression was successfully negotiated with the connection's peer, any
// message received in compressed form will be automatically decompressed.
// All Read methods will return uncompressed bytes.
//
// Per message compression of messages written to a connection can be enabled
// or disabled by calling the corresponding Conn method:
//
// conn.EnableWriteCompression(false)
//
// Currently this package does not support compression with "context takeover".
// This means that messages must be compressed and decompressed in isolation,
// without retaining sliding window or dictionary state across messages. For
// more details refer to RFC 7692.
//
// Use of compression is experimental and may result in decreased performance.
package websocket

View File

@ -1,42 +0,0 @@
// Copyright 2019 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"io"
"strings"
)
// JoinMessages concatenates received messages to create a single io.Reader.
// The string term is appended to each message. The returned reader does not
// support concurrent calls to the Read method.
func JoinMessages(c *Conn, term string) io.Reader {
return &joinReader{c: c, term: term}
}
type joinReader struct {
c *Conn
term string
r io.Reader
}
func (r *joinReader) Read(p []byte) (int, error) {
if r.r == nil {
var err error
_, r.r, err = r.c.NextReader()
if err != nil {
return 0, err
}
if r.term != "" {
r.r = io.MultiReader(r.r, strings.NewReader(r.term))
}
}
n, err := r.r.Read(p)
if err == io.EOF {
err = nil
r.r = nil
}
return n, err
}

View File

@ -1,60 +0,0 @@
// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"encoding/json"
"io"
)
// WriteJSON writes the JSON encoding of v as a message.
//
// Deprecated: Use c.WriteJSON instead.
func WriteJSON(c *Conn, v interface{}) error {
return c.WriteJSON(v)
}
// WriteJSON writes the JSON encoding of v as a message.
//
// See the documentation for encoding/json Marshal for details about the
// conversion of Go values to JSON.
func (c *Conn) WriteJSON(v interface{}) error {
w, err := c.NextWriter(TextMessage)
if err != nil {
return err
}
err1 := json.NewEncoder(w).Encode(v)
err2 := w.Close()
if err1 != nil {
return err1
}
return err2
}
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// Deprecated: Use c.ReadJSON instead.
func ReadJSON(c *Conn, v interface{}) error {
return c.ReadJSON(v)
}
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// See the documentation for the encoding/json Unmarshal function for details
// about the conversion of JSON to a Go value.
func (c *Conn) ReadJSON(v interface{}) error {
_, r, err := c.NextReader()
if err != nil {
return err
}
err = json.NewDecoder(r).Decode(v)
if err == io.EOF {
// One value is expected in the message.
err = io.ErrUnexpectedEOF
}
return err
}

Some files were not shown because too many files have changed in this diff Show More