blob: 36c5bdf8565e9b964ad9d69739bab22c7fe9b504 [file]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// github.com/evanw/esbuild doesn't compile on plan9
//go:build !plan9
// Package static builds static assets for the frontend and the worker.
package static
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/evanw/esbuild/pkg/api"
"golang.org/x/pkgsite/internal/derrors"
)
// Build compiles TypeScript files into minified JavaScript
// files using github.com/evanw/esbuild.
//
// This function is used in Server.staticHandler with Watch=true
// when cmd/frontend is run in dev mode and in
// devtools/cmd/static/main.go with Watch=false for building
// productionized assets.
func Build(config Config) error {
files, err := getFiles(config.EntryPoint, config.Bundle)
if err != nil {
return err
}
for _, file := range files {
if err := processFile(config, file); err != nil {
return err
}
}
return nil
}
func processFile(config Config, filename string) (err error) {
defer derrors.Wrap(&err, "%s", filename)
contents, err := os.ReadFile(filename)
if err != nil {
return err
}
license, err := extractLicense(contents)
if err != nil {
return err
}
license = normalizeLicense(license)
var ext string
if strings.HasSuffix(filename, ".ts") {
ext = "js"
} else if strings.HasSuffix(filename, ".css") {
ext = "css"
} else {
return errors.New("unsupported filename type")
}
options := api.BuildOptions{
EntryPoints: []string{filename},
Bundle: config.Bundle,
Outdir: config.EntryPoint,
Outbase: config.EntryPoint,
Write: true,
Platform: api.PlatformBrowser,
Format: api.FormatESModule,
OutExtension: map[string]string{".css": ".min.css"},
External: []string{"*.svg"},
Banner: map[string]string{ext: license},
}
options.MinifyIdentifiers = true
options.MinifySyntax = true
options.MinifyWhitespace = true
options.Sourcemap = api.SourceMapLinked
if config.Watch {
ctx, err := api.Context(options)
if err != nil {
return err
}
defer ctx.Dispose()
if err := ctx.Watch(api.WatchOptions{}); err != nil {
return err
}
} else {
result := api.Build(options)
if len(result.Errors) > 0 {
return fmt.Errorf("error building: %v", result.Errors)
}
if len(result.Warnings) > 0 {
return fmt.Errorf("warning building: %v", result.Warnings)
}
}
return nil
}
// getFiles walks the given directory and collects entry file paths
// for esbuild. It ignores test files and files prefixed with an underscore.
// Underscore prefixed files are assumed to be imported by and bundled together
// with the output of an entry file.
func getFiles(dir string, bundle bool) ([]string, error) {
var matches []string
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
basePath := filepath.Base(path)
notPartial := !strings.HasPrefix(basePath, "_")
notTest := !strings.HasSuffix(basePath, ".test.ts")
isTS := strings.HasSuffix(basePath, ".ts")
isCSS := strings.HasSuffix(basePath, ".css") && !strings.HasSuffix(basePath, ".min.css")
if notPartial && notTest && (isTS || (bundle && isCSS)) {
matches = append(matches, path)
}
return nil
})
if err != nil {
return nil, err
}
return matches, nil
}
// Match a /* ... */ comment starting at the beginning of the input.
// The (?s:...) syntax matches over multiple lines.
// match[1] contains the comment text with its close delimeter.
var reBlock = regexp.MustCompile(`(?s:^\s*/\*[*!]?(.*?\*/))`)
// extractLicense takes a []byte and returns the contents of the license comment in
// it. The comment must be of the form /*...*/ at the start of the file.
// The returned string includes the close comment delimiter but not the open one.
func extractLicense(contents []byte) (string, error) {
match := reBlock.FindSubmatch(contents)
if match == nil {
return "", errors.New("no initial /*...*/ comment")
}
comment := string(match[1])
if !strings.Contains(strings.ToLower(comment), "copyright") {
return "", errors.New("initial comment missing 'copyright'")
}
return comment, nil
}
func normalizeLicense(lic string) string {
// TS files have a "@license" line which we want to remove.
return "/*!" + strings.Replace(lic, " * @license\n", "", 1)
}