vendor tsgo
This commit is contained in:
@@ -1,54 +1,54 @@
|
||||
package main
|
||||
|
||||
// Frontend TypeScript checker. Runs tsc in noEmit mode using
|
||||
// tsconfig.json. Requires node on PATH; downloads the pinned TypeScript
|
||||
// release on first run (no npm).
|
||||
// Frontend TypeScript checker using TypeScript 7 (tsgo) - Microsoft's native Go
|
||||
// port of tsc. No npm, no Node: tsgo is built from the vendored source under
|
||||
// kjol/tools/tsgo and executed directly.
|
||||
//
|
||||
// go run ./cmd/typecheck
|
||||
// go run ./cmd/typecheck -p tsconfig.json
|
||||
// go run kjol/cmd/typecheck
|
||||
// go run kjol/cmd/typecheck -p tsconfig.json
|
||||
// go run kjol/cmd/typecheck -tsgo path/to/kjol/tools/tsgo
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const typescriptVersion = "5.8.3"
|
||||
// tsgoVersion pins the vendored typescript-go source (kjol/tools/tsgo). Bump when
|
||||
// the vendored copy is updated; it also keys the built-binary cache.
|
||||
const tsgoVersion = "v0.0.0-20260709155237-487baf0cc74a" // TypeScript 7.1.0-dev
|
||||
|
||||
// goToolchain is the Go toolchain used to build tsgo. Its go.mod needs >= go 1.26;
|
||||
// this exact patch is known-good. Go downloads it once if absent.
|
||||
const goToolchain = "go1.26.5"
|
||||
|
||||
func main() {
|
||||
tsconfig := flag.String("p", "tsconfig.json", "path to tsconfig.json")
|
||||
tsgoDir := flag.String("tsgo", "kjol/tools/tsgo", "path to the vendored typescript-go source")
|
||||
flag.Parse()
|
||||
|
||||
if err := runTypecheck(*tsconfig); err != nil {
|
||||
if err := runTypecheck(*tsconfig, *tsgoDir); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Typecheck failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func runTypecheck(tsconfig string) error {
|
||||
node, err := exec.LookPath("node")
|
||||
if err != nil {
|
||||
return fmt.Errorf("node not found on PATH (required to run tsc): %w", err)
|
||||
}
|
||||
|
||||
tsc, err := ensureTypeScript()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func runTypecheck(tsconfig, tsgoDir string) error {
|
||||
if _, err := os.Stat(tsconfig); err != nil {
|
||||
return fmt.Errorf("tsconfig not found: %s", tsconfig)
|
||||
}
|
||||
|
||||
fmt.Printf("Typechecking with TypeScript %s...\n", typescriptVersion)
|
||||
cmd := exec.Command(node, tsc, "--noEmit", "-p", tsconfig)
|
||||
tsgo, err := ensureTsgo(tsgoDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Typechecking with TypeScript 7 (tsgo %s)...\n", tsgoVersion)
|
||||
cmd := exec.Command(tsgo, "--noEmit", "-p", tsconfig)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
@@ -59,92 +59,56 @@ func runTypecheck(tsconfig string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTypeScript() (string, error) {
|
||||
root, err := os.Getwd()
|
||||
// ensureTsgo builds tsgo from the vendored source once (cached under tmp/tsgo) and
|
||||
// returns the binary path.
|
||||
func ensureTsgo(tsgoDir string) (string, error) {
|
||||
exeName := "tsgo"
|
||||
if runtime.GOOS == "windows" {
|
||||
exeName += ".exe"
|
||||
}
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cacheDir := filepath.Join(root, "tools", ".cache", "typescript", typescriptVersion)
|
||||
tscPath := filepath.Join(cacheDir, "package", "lib", "tsc.js")
|
||||
if _, err := os.Stat(tscPath); err == nil {
|
||||
return tscPath, nil
|
||||
binPath := filepath.Join(cwd, "tmp", "tsgo", tsgoVersion, exeName)
|
||||
if _, err := os.Stat(binPath); err == nil {
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
fmt.Printf("Downloading TypeScript %s...\n", typescriptVersion)
|
||||
if err := downloadTypeScript(cacheDir); err != nil {
|
||||
absTsgo, err := filepath.Abs(tsgoDir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(tscPath); err != nil {
|
||||
return "", fmt.Errorf("tsc not found after download: %s", tscPath)
|
||||
if _, err := os.Stat(filepath.Join(absTsgo, "go.mod")); err != nil {
|
||||
return "", fmt.Errorf("vendored tsgo source not found at %s (pass -tsgo or check kjol/tools/tsgo)", absTsgo)
|
||||
}
|
||||
return tscPath, nil
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(binPath), 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println("Building tsgo from vendored source (first run; cached afterwards)...")
|
||||
build := exec.Command("go", "build", "-C", absTsgo, "-mod=vendor", "-o", binPath, "./cmd/tsgo")
|
||||
build.Env = buildEnv()
|
||||
build.Stdout = os.Stdout
|
||||
build.Stderr = os.Stderr
|
||||
if err := build.Run(); err != nil {
|
||||
return "", fmt.Errorf("building tsgo: %w", err)
|
||||
}
|
||||
if _, err := os.Stat(binPath); err != nil {
|
||||
return "", fmt.Errorf("tsgo not found after build: %s", binPath)
|
||||
}
|
||||
return binPath, nil
|
||||
}
|
||||
|
||||
func downloadTypeScript(destDir string) error {
|
||||
url := fmt.Sprintf("https://registry.npmjs.org/typescript/-/typescript-%s.tgz", typescriptVersion)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download typescript: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("download typescript: HTTP %s", resp.Status)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return extractTGZ(resp.Body, destDir)
|
||||
}
|
||||
|
||||
func extractTGZ(r io.Reader, destDir string) error {
|
||||
gz, err := gzip.NewReader(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read typescript archive: %w", err)
|
||||
}
|
||||
defer gz.Close()
|
||||
|
||||
tr := tar.NewReader(gz)
|
||||
cleanDest := filepath.Clean(destDir)
|
||||
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read typescript archive: %w", err)
|
||||
}
|
||||
|
||||
target := filepath.Join(destDir, filepath.FromSlash(hdr.Name))
|
||||
cleanTarget := filepath.Clean(target)
|
||||
if cleanTarget != cleanDest && !strings.HasPrefix(cleanTarget, cleanDest+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("invalid archive path: %s", hdr.Name)
|
||||
}
|
||||
|
||||
switch hdr.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(hdr.Mode)&0o777|0o600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, tr); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
// buildEnv builds tsgo standalone: outside any go.work, with a pinned toolchain.
|
||||
func buildEnv() []string {
|
||||
var env []string
|
||||
for _, e := range os.Environ() {
|
||||
if strings.HasPrefix(e, "GOWORK=") || strings.HasPrefix(e, "GOTOOLCHAIN=") {
|
||||
continue
|
||||
}
|
||||
env = append(env, e)
|
||||
}
|
||||
}
|
||||
return append(env, "GOWORK=off", "GOTOOLCHAIN="+goToolchain)
|
||||
}
|
||||
Reference in New Issue
Block a user