website: create typescript file handler

To allow for the use of TypeScript in go.dev pages
added a handler for TypeScript files served from
_content/ts. Files requested from this directory
are first transformed from TypeScript to JavaScript
using github.com/evanw/esbuild. JavaScript output is
written to a simple cache so subsequent requests skip
the tranformation step.

Change-Id: I0a161ce3dd20eaddddd5d369d359c65c90d9f607
Reviewed-on: https://go-review.googlesource.com/c/website/+/373718
Run-TryBot: Jamal Carvalho <jamal@golang.org>
Trust: Jamal Carvalho <jamalcarvalho@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alex Rakoczy <alex@golang.org>
This commit is contained in:
Jamal Carvalho 2021-12-23 17:00:03 +00:00 коммит произвёл Jamal Carvalho
Родитель ee355832f5
Коммит 56b50be279
9 изменённых файлов: 224 добавлений и 0 удалений

Просмотреть файл

@ -36,6 +36,7 @@ import (
"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"
@ -264,6 +265,7 @@ func newSite(mux *http.ServeMux, host string, content, goroot fs.FS) (*web.Site,
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
}

1
go.mod
Просмотреть файл

@ -7,6 +7,7 @@ require (
cloud.google.com/go/datastore v1.2.0
github.com/chromedp/cdproto v0.0.0-20211205231339-d2673e93eee4
github.com/chromedp/chromedp v0.7.6
github.com/evanw/esbuild v0.14.7
github.com/gomodule/redigo v2.0.0+incompatible
github.com/google/go-cmp v0.5.6
github.com/microcosm-cc/bluemonday v1.0.2

3
go.sum
Просмотреть файл

@ -191,6 +191,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/esimov/stackblur-go v1.0.1/go.mod h1:a3zzeKuJKUpCcReHmEsuPaEnq42D2b/bHoCI8UjIuMY=
github.com/evanw/esbuild v0.14.7 h1:At4sSDNq+beZA+z6GUA/sRoqHys9qxKH1RT05eN6Kpo=
github.com/evanw/esbuild v0.14.7/go.mod h1:GG+zjdi59yh3ehDn4ZWfPcATxjPDUH53iU4ZJbp7dkY=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
@ -936,6 +938,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

Просмотреть файл

@ -0,0 +1,88 @@
// 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,
})
}

Просмотреть файл

@ -0,0 +1,85 @@
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 &#34;function&#34;\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)
}
})
}
}

7
internal/esbuild/testdata/error.tmpl поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
<!--
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.
-->
{{define "layout"}}{{.error}}{{end}}

8
internal/esbuild/testdata/error.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,8 @@
/**
* @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.
*/
const function = () => {};

23
internal/esbuild/testdata/example.ts поставляемый Normal file
Просмотреть файл

@ -0,0 +1,23 @@
/**
* @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.
*/
interface Target {
toString(): string;
}
function sayHello(to: Target): void {
console.log('Hello, ' + to + '!');
}
const world = {
name: 'World',
toString(): string {
return this.name;
},
};
sayHello(world);

7
internal/esbuild/testdata/site.tmpl поставляемый Normal file
Просмотреть файл

@ -0,0 +1,7 @@
<!--
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.
-->
{{block "entirepage" .}}{{block "layout" .}}{{.Content}}{{end}}{{end}}