cmd,internal: move typescript transform to web package
To transform .ts files from any path on a site and to ensure
it works for all domains the transform logic is moved to the
web package.
Change-Id: I1ce62eb35dbdfb987ccc2f48e79c558bcdadfbbe
Reviewed-on: https://go-review.googlesource.com/c/website/+/374994
Trust: Jamal Carvalho <jamalcarvalho@google.com>
Run-TryBot: Jamal Carvalho <jamal@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go
index 6e1b371..41d2c6b 100644
--- a/cmd/golangorg/server.go
+++ b/cmd/golangorg/server.go
@@ -36,7 +36,6 @@
"golang.org/x/website/internal/blog"
"golang.org/x/website/internal/codewalk"
"golang.org/x/website/internal/dl"
- "golang.org/x/website/internal/esbuild"
"golang.org/x/website/internal/gitfs"
"golang.org/x/website/internal/history"
"golang.org/x/website/internal/memcache"
@@ -266,7 +265,6 @@
mux.Handle(host+"/cmd/", docs)
mux.Handle(host+"/pkg/", docs)
mux.Handle(host+"/doc/codewalk/", codewalk.NewServer(fsys, site))
- mux.Handle(host+"/ts/", esbuild.NewServer(fsys, site))
return site, nil
}
diff --git a/internal/esbuild/esbuild.go b/internal/esbuild/esbuild.go
deleted file mode 100644
index a356e1e..0000000
--- a/internal/esbuild/esbuild.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// 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.
-
-// package esbuild transforms TypeScript code into
-// JavaScript code.
-package esbuild
-
-import (
- "bytes"
- "errors"
- "fmt"
- "io"
- "io/fs"
- "net/http"
- "path"
- "sync"
-
- "github.com/evanw/esbuild/pkg/api"
- "golang.org/x/website/internal/web"
-)
-
-const cacheHeader = "X-Go-Dev-Cache-Hit"
-
-type server struct {
- fsys fs.FS
- site *web.Site
- cache sync.Map // TypeScript filepath -> JavaScript output
-}
-
-// NewServer returns a new server for handling TypeScript files.
-func NewServer(fsys fs.FS, site *web.Site) http.Handler {
- return &server{fsys, site, sync.Map{}}
-}
-
-type JSOut struct {
- output []byte
- stat fs.FileInfo // stat for file when page was loaded
-}
-
-// Handler for TypeScript files. Transforms TypeScript code into
-// JavaScript code before serving them.
-func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- filename := path.Clean(r.URL.Path)[1:]
- if cjs, ok := s.cache.Load(filename); ok {
- js := cjs.(*JSOut)
- info, err := fs.Stat(s.fsys, filename)
- if err == nil && info.ModTime().Equal(js.stat.ModTime()) {
- w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
- w.Header().Set(cacheHeader, "true")
- http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(js.output))
- return
- }
- }
- file, err := s.fsys.Open(filename)
- if err != nil {
- s.site.ServeError(w, r, err)
- return
- }
- var contents bytes.Buffer
- _, err = io.Copy(&contents, file)
- if err != nil {
- s.site.ServeError(w, r, err)
- return
- }
- result := api.Transform(contents.String(), api.TransformOptions{
- Loader: api.LoaderTS,
- })
- var buf bytes.Buffer
- for _, v := range result.Errors {
- fmt.Fprintln(&buf, v.Text)
- }
- if buf.Len() > 0 {
- s.site.ServeError(w, r, errors.New(buf.String()))
- return
- }
- info, err := file.Stat()
- if err != nil {
- s.site.ServeError(w, r, err)
- return
- }
- w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
- http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(result.Code))
- s.cache.Store(filename, &JSOut{
- output: result.Code,
- stat: info,
- })
-}
diff --git a/internal/esbuild/esbuild_test.go b/internal/esbuild/esbuild_test.go
deleted file mode 100644
index 565c6e7..0000000
--- a/internal/esbuild/esbuild_test.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package esbuild
-
-import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "os"
- "syscall"
- "testing"
-
- "github.com/google/go-cmp/cmp"
- "golang.org/x/website/internal/web"
-)
-
-func TestServeHTTP(t *testing.T) {
- exampleOut := `/**
- * @license
- * 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.
- */
-function sayHello(to) {
- console.log("Hello, " + to + "!");
-}
-const world = {
- name: "World",
- toString() {
- return this.name;
- }
-};
-sayHello(world);
-`
- tests := []struct {
- name string
- path string
- wantCode int
- wantBody string
- wantCacheHeader bool
- }{
- {
- name: "example code",
- path: "/example.ts",
- wantCode: 200,
- wantBody: exampleOut,
- },
- {
- name: "example code cached",
- path: "/example.ts",
- wantCode: 200,
- wantBody: exampleOut,
- wantCacheHeader: true,
- },
- {
- name: "file not found",
- path: "/notfound.ts",
- wantCode: 500,
- wantBody: fmt.Sprintf("\n\nopen testdata/notfound.ts: %s\n", syscall.ENOENT),
- },
- {
- name: "syntax error",
- path: "/error.ts",
- wantCode: 500,
- wantBody: "\n\nExpected identifier but found "function"\n\n",
- },
- }
- fsys := os.DirFS("testdata")
- server := NewServer(fsys, web.NewSite(fsys))
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- req := httptest.NewRequest(http.MethodGet, tt.path, nil)
- got := httptest.NewRecorder()
- server.ServeHTTP(got, req)
- gotHeader := got.Header().Get(cacheHeader) == "true"
- if got.Code != tt.wantCode {
- t.Errorf("got status %d but wanted %d", got.Code, http.StatusOK)
- }
- if (tt.wantCacheHeader && !gotHeader) || (!tt.wantCacheHeader && gotHeader) {
- t.Errorf("got cache hit %v but wanted %v", gotHeader, tt.wantCacheHeader)
- }
- if diff := cmp.Diff(tt.wantBody, got.Body.String()); diff != "" {
- t.Errorf("ServeHTTP() mismatch (-want +got):\n%s", diff)
- }
- })
- }
-}
diff --git a/internal/web/istext.go b/internal/web/istext.go
index aac2abb..a512bbf 100644
--- a/internal/web/istext.go
+++ b/internal/web/istext.go
@@ -41,7 +41,7 @@
return false
}
switch path.Ext(filename) {
- case ".css", ".js", ".svg":
+ case ".css", ".js", ".svg", ".ts":
return false
}
diff --git a/internal/web/site.go b/internal/web/site.go
index 73a0f8a..e0206d5 100644
--- a/internal/web/site.go
+++ b/internal/web/site.go
@@ -299,6 +299,7 @@
"errors"
"fmt"
"html"
+ "io"
"io/fs"
"log"
"net/http"
@@ -308,6 +309,7 @@
"strings"
"sync"
+ "github.com/evanw/esbuild/pkg/api"
"golang.org/x/website/internal/backport/html/template"
"golang.org/x/website/internal/spec"
"golang.org/x/website/internal/texthtml"
@@ -423,6 +425,12 @@
abspath := r.URL.Path
relpath := path.Clean(strings.TrimPrefix(abspath, "/"))
+ // Is it a TypeScript file?
+ if strings.HasSuffix(relpath, ".ts") {
+ s.serveTypeScript(w, r)
+ return
+ }
+
// Is it a page we can generate?
if p, err := s.openPage(relpath); err == nil {
if p.url != abspath {
@@ -611,3 +619,57 @@
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(text)
}
+
+const cacheHeader = "X-Go-Dev-Cache-Hit"
+
+type jsout struct {
+ output []byte
+ stat fs.FileInfo // stat for file when page was loaded
+}
+
+func (s *Site) serveTypeScript(w http.ResponseWriter, r *http.Request) {
+ filename := path.Clean(strings.TrimPrefix(r.URL.Path, "/"))
+ if cjs, ok := s.cache.Load(filename); ok {
+ js := cjs.(*jsout)
+ info, err := fs.Stat(s.fs, filename)
+ if err == nil && info.ModTime().Equal(js.stat.ModTime()) {
+ w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ w.Header().Set(cacheHeader, "true")
+ http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(js.output))
+ return
+ }
+ }
+ file, err := s.fs.Open(filename)
+ if err != nil {
+ s.ServeError(w, r, err)
+ return
+ }
+ var contents bytes.Buffer
+ _, err = io.Copy(&contents, file)
+ if err != nil {
+ s.ServeError(w, r, err)
+ return
+ }
+ result := api.Transform(contents.String(), api.TransformOptions{
+ Loader: api.LoaderTS,
+ })
+ var buf bytes.Buffer
+ for _, v := range result.Errors {
+ fmt.Fprintln(&buf, v.Text)
+ }
+ if buf.Len() > 0 {
+ s.ServeError(w, r, errors.New(buf.String()))
+ return
+ }
+ info, err := file.Stat()
+ if err != nil {
+ s.ServeError(w, r, err)
+ return
+ }
+ w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
+ http.ServeContent(w, r, filename, info.ModTime(), bytes.NewReader(result.Code))
+ s.cache.Store(filename, &jsout{
+ output: result.Code,
+ stat: info,
+ })
+}
diff --git a/internal/web/site_test.go b/internal/web/site_test.go
index f1397a1..488673f 100644
--- a/internal/web/site_test.go
+++ b/internal/web/site_test.go
@@ -5,12 +5,17 @@
package web
import (
+ "fmt"
"net/http"
"net/http/httptest"
"net/url"
+ "os"
"strings"
+ "syscall"
"testing"
"testing/fstest"
+
+ "github.com/google/go-cmp/cmp"
)
func testServeBody(t *testing.T, p *Site, path, body string) {
@@ -58,3 +63,62 @@
testServeBody(t, site, "/doc/test", "<strong>bold</strong>")
testServeBody(t, site, "/doc/test2", "<em>template</em>")
}
+
+func TestTypeScript(t *testing.T) {
+ exampleOut, err := os.ReadFile("testdata/example.js")
+ if err != nil {
+ t.Fatal(err)
+ }
+ tests := []struct {
+ name string
+ path string
+ wantCode int
+ wantBody string
+ wantCacheHeader bool
+ }{
+ {
+ name: "example code",
+ path: "/example.ts",
+ wantCode: 200,
+ wantBody: string(exampleOut),
+ },
+ {
+ name: "example code cached",
+ path: "/example.ts",
+ wantCode: 200,
+ wantBody: string(exampleOut),
+ wantCacheHeader: true,
+ },
+ {
+ name: "file not found",
+ path: "/notfound.ts",
+ wantCode: 500,
+ wantBody: fmt.Sprintf("\n\nopen testdata/notfound.ts: %s\n", syscall.ENOENT),
+ },
+ {
+ name: "syntax error",
+ path: "/error.ts",
+ wantCode: 500,
+ wantBody: "\n\nExpected identifier but found "function"\n\n",
+ },
+ }
+ fsys := os.DirFS("testdata")
+ site := NewSite(fsys)
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, tt.path, nil)
+ got := httptest.NewRecorder()
+ site.serveTypeScript(got, req)
+ gotHeader := got.Header().Get(cacheHeader) == "true"
+ if got.Code != tt.wantCode {
+ t.Errorf("got status %d but wanted %d", got.Code, http.StatusOK)
+ }
+ if (tt.wantCacheHeader && !gotHeader) || (!tt.wantCacheHeader && gotHeader) {
+ t.Errorf("got cache hit %v but wanted %v", gotHeader, tt.wantCacheHeader)
+ }
+ if diff := cmp.Diff(tt.wantBody, got.Body.String()); diff != "" {
+ t.Errorf("ServeHTTP() mismatch (-want +got):\n%s", diff)
+ }
+ })
+ }
+}
diff --git a/internal/esbuild/testdata/error.tmpl b/internal/web/testdata/error.tmpl
similarity index 100%
rename from internal/esbuild/testdata/error.tmpl
rename to internal/web/testdata/error.tmpl
diff --git a/internal/esbuild/testdata/error.ts b/internal/web/testdata/error.ts
similarity index 100%
rename from internal/esbuild/testdata/error.ts
rename to internal/web/testdata/error.ts
diff --git a/internal/web/testdata/example.js b/internal/web/testdata/example.js
new file mode 100644
index 0000000..0ab8d7f
--- /dev/null
+++ b/internal/web/testdata/example.js
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * 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.
+ */
+function sayHello(to) {
+ console.log("Hello, " + to + "!");
+}
+const world = {
+ name: "World",
+ toString() {
+ return this.name;
+ }
+};
+sayHello(world);
diff --git a/internal/esbuild/testdata/example.ts b/internal/web/testdata/example.ts
similarity index 100%
rename from internal/esbuild/testdata/example.ts
rename to internal/web/testdata/example.ts
diff --git a/internal/esbuild/testdata/site.tmpl b/internal/web/testdata/site.tmpl
similarity index 100%
rename from internal/esbuild/testdata/site.tmpl
rename to internal/web/testdata/site.tmpl