internal/static: preserve license header The static.go tool, which generates files from .ts and .css files, now preserves license headers from the original source. Change-Id: Ife5e7e9cdd744fb5c8c0287a581d1a766a6a6964 Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/802160 Auto-Submit: Jonathan Amsterdam <jba@google.com> Reviewed-by: Ethan Lee <ethanalee@google.com> kokoro-CI: kokoro <noreply+kokoro@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/internal/static/static.go b/internal/static/static.go index caa1b84..058e2cb 100644 --- a/internal/static/static.go +++ b/internal/static/static.go
@@ -9,12 +9,15 @@ 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 @@ -24,59 +27,82 @@ // 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 := getEntry(config.EntryPoint, config.Bundle) +func Build(config Config) (err 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: files, + 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{ - "css": "/*!\n" + - " * Copyright 2021 The Go Authors. All rights reserved.\n" + - " * Use of this source code is governed by a BSD-style\n" + - " * license that can be found in the LICENSE file.\n" + - " */", - "js": "/*!\n" + - " * Copyright 2026 The Go Authors. All rights reserved.\n" + - " * Use of this source code is governed by a BSD-style\n" + - " * license that can be found in the LICENSE file.\n" + - " */", - }, + 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 } - return ctx.Watch(api.WatchOptions{}) - } - result := api.Build(options) - if len(result.Errors) > 0 { - return fmt.Errorf("error building static files: %v", result.Errors) - } - if len(result.Warnings) > 0 { - return fmt.Errorf("error building static files: %v", result.Warnings) + 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 } -// getEntry walks the given directory and collects entry file paths +// 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 getEntry(dir string, bundle bool) ([]string, error) { +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 { @@ -100,3 +126,28 @@ } 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) +}
diff --git a/internal/static/static_test.go b/internal/static/static_test.go new file mode 100644 index 0000000..fc71ff7 --- /dev/null +++ b/internal/static/static_test.go
@@ -0,0 +1,87 @@ +// Copyright 2026 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. + +//go:build !plan9 + +package static + +import ( + "testing" +) + +func TestExtractLicense(t *testing.T) { + testCases := []struct { + name string + input string + want string + wantErr bool + }{ + { + name: "standard", + input: "/*\nCopyright 2000 */", + want: "\nCopyright 2000 */", + wantErr: false, + }, + { + name: "exclamation", + input: "/*!\nCopyright 2021 The Go Authors. All rights reserved.\n */", + want: "\nCopyright 2021 The Go Authors. All rights reserved.\n */", + wantErr: false, + }, + { + name: "double asterisk", + input: "/**\n * Copyright 2026\n */", + want: "\n * Copyright 2026\n */", + wantErr: false, + }, + { + name: "leading whitespace", + input: " \n/*\nCopyright 2000 */", + want: "\nCopyright 2000 */", + wantErr: false, + }, + { + name: "error: no comment", + input: "var x = 1", + wantErr: true, + }, + { + name: "error: empty input", + input: "", + wantErr: true, + }, + { + name: "error: comment not at start", + input: "var x = 1 /*\nCopyright 2000 */", + wantErr: true, + }, + { + name: "error: line comment instead of block comment", + input: "// Copyright 2000", + wantErr: true, + }, + { + name: "error: unclosed block comment", + input: "/*\nCopyright 2000", + wantErr: true, + }, + { + name: "error: initial comment missing copyright", + input: "/* Hello World */", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := extractLicense([]byte(tc.input)) + if (err != nil) != tc.wantErr { + t.Fatalf("extractLicense(%q) error = %v, wantErr %v", tc.input, err, tc.wantErr) + } + if got != tc.want { + t.Errorf("extractLicense(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +}
diff --git a/static/frontend/about/about.min.css b/static/frontend/about/about.min.css index 62e0117..061bfe2 100644 --- a/static/frontend/about/about.min.css +++ b/static/frontend/about/about.min.css
@@ -1,5 +1,5 @@ /*! - * Copyright 2021 The Go Authors. All rights reserved. + * Copyright 2022 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. */
diff --git a/static/frontend/about/index.js b/static/frontend/about/index.js index 5234fb5..5fe8ce6 100644 --- a/static/frontend/about/index.js +++ b/static/frontend/about/index.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * Copyright 2022 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. */
diff --git a/static/frontend/fetch/fetch.js b/static/frontend/fetch/fetch.js index aba5a5a..deeb3ff 100644 --- a/static/frontend/fetch/fetch.js +++ b/static/frontend/fetch/fetch.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * Copyright 2020 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. */
diff --git a/static/frontend/frontend.js b/static/frontend/frontend.js index dcb3468..3ff859f 100644 --- a/static/frontend/frontend.js +++ b/static/frontend/frontend.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * Copyright 2020 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. */
diff --git a/static/frontend/homepage/homepage.min.css b/static/frontend/homepage/homepage.min.css index 553ada3..16665cd 100644 --- a/static/frontend/homepage/homepage.min.css +++ b/static/frontend/homepage/homepage.min.css
@@ -1,5 +1,5 @@ /*! - * Copyright 2021 The Go Authors. All rights reserved. + * Copyright 2020 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. */
diff --git a/static/frontend/search/search.js b/static/frontend/search/search.js index a698cec..fe4ef8c 100644 --- a/static/frontend/search/search.js +++ b/static/frontend/search/search.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * Copyright 2020 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. */
diff --git a/static/frontend/unit/main/main.min.css b/static/frontend/unit/main/main.min.css index 16d226b..b1ebdad 100644 --- a/static/frontend/unit/main/main.min.css +++ b/static/frontend/unit/main/main.min.css
@@ -1,5 +1,5 @@ /*! - * Copyright 2021 The Go Authors. All rights reserved. + * Copyright 2020 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. */
diff --git a/static/frontend/unit/unit.js b/static/frontend/unit/unit.js index ddc0143..cdf429b 100644 --- a/static/frontend/unit/unit.js +++ b/static/frontend/unit/unit.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * 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. */
diff --git a/static/frontend/unit/versions/versions.js b/static/frontend/unit/versions/versions.js index fcebeb5..8d3d848 100644 --- a/static/frontend/unit/versions/versions.js +++ b/static/frontend/unit/versions/versions.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * 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. */
diff --git a/static/worker/worker.js b/static/worker/worker.js index e8a1c48..50b0e92 100644 --- a/static/worker/worker.js +++ b/static/worker/worker.js
@@ -1,5 +1,5 @@ /*! - * Copyright 2026 The Go Authors. All rights reserved. + * 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. */
diff --git a/static/worker/worker.min.css b/static/worker/worker.min.css index d3f2c74..9b64689 100644 --- a/static/worker/worker.min.css +++ b/static/worker/worker.min.css
@@ -1,5 +1,5 @@ /*! - * Copyright 2021 The Go Authors. All rights reserved. + * Copyright 2019-2020 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. */