Files
kjol/cmd/typecheck/main.go

151 lines
3.5 KiB
Go

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).
//
// go run ./cmd/typecheck
// go run ./cmd/typecheck -p tsconfig.json
import (
"archive/tar"
"compress/gzip"
"flag"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
)
const typescriptVersion = "5.8.3"
func main() {
tsconfig := flag.String("p", "tsconfig.json", "path to tsconfig.json")
flag.Parse()
if err := runTypecheck(*tsconfig); 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
}
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)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
fmt.Println("No type errors.")
return nil
}
func ensureTypeScript() (string, error) {
root, 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
}
fmt.Printf("Downloading TypeScript %s...\n", typescriptVersion)
if err := downloadTypeScript(cacheDir); err != nil {
return "", err
}
if _, err := os.Stat(tscPath); err != nil {
return "", fmt.Errorf("tsc not found after download: %s", tscPath)
}
return tscPath, 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
}
}
}
}