1 // Package tsserver implements a simple wrapper around fs.FS to intercept calls 2 // to open Javascript files, and instead open their Typescript equivelants and 3 // generate the Javascript. 4 package tsserver // import "vimagination.zapto.org/tsserver" 5 6 import ( 7 "bytes" 8 "fmt" 9 "io/fs" 10 "strings" 11 12 "vimagination.zapto.org/javascript" 13 "vimagination.zapto.org/parser" 14 ) 15 16 const ( 17 jsExt = ".js" 18 tsExt = ".ts" 19 ) 20 21 type wrapped struct { 22 fs.FS 23 } 24 25 // WrapFS takes a fs.FS and intercepts any calls to open .js files, and instead 26 // generates a file from a similarly named .ts file, if one exists. 27 // 28 // If a .ts file does not exists, fails to be converted to javascript, or if the 29 // file being opened is not a .js file then the file open will not be 30 // intercepted. 31 func WrapFS(f fs.FS) fs.FS { 32 return &wrapped{FS: f} 33 } 34 35 func (w *wrapped) Open(name string) (fs.File, error) { 36 if strings.HasSuffix(name, jsExt) { 37 if tsf, err := w.FS.Open(strings.TrimSuffix(name, jsExt) + tsExt); err == nil { 38 if stat, err := tsf.Stat(); err == nil { 39 tk := parser.NewReaderTokeniser(tsf) 40 m, err := javascript.ParseModule(javascript.AsTypescript(&tk)) 41 if err == nil { 42 var buf bytes.Buffer 43 fmt.Fprintf(&buf, "%s", m) 44 return &file{ 45 Reader: bytes.NewReader(buf.Bytes()), 46 name: name, 47 FileInfo: stat, 48 }, nil 49 } 50 } 51 } 52 } 53 return w.FS.Open(name) 54 } 55 56 type file struct { 57 *bytes.Reader 58 name string 59 fs.FileInfo 60 } 61 62 func (file) Readdir(count int) ([]fs.FileInfo, error) { 63 return nil, fs.ErrInvalid 64 } 65 66 func (f *file) Stat() (fs.FileInfo, error) { 67 return f, nil 68 } 69 70 func (file) Close() error { 71 return nil 72 } 73 74 func (f *file) Name() string { 75 return f.name 76 } 77 78 func (f *file) Size() int64 { 79 return f.Reader.Size() 80 } 81