Parsing Typescript in Go using V8Go
Posted on 24 April 2022
In my earlier post we saw how we can parse Typescript into an Abstract Syntax Tree using Eclipse J2V8. In this post, we will explore the same in Go using v8go.
Using Go
has advantages if you are building a tool as you can ship a single
native binary without any depdendency. Though Java
has the convenience of write once, run many
but it still requires the presence of JVM
on the user’s machine.
Back to the task at hand, the code requires the following steps:
- Initialize and load V8
- Load Typescript JS library
- Load the Typescript code that you would like to parse
- And, finally call the TS parser code to obtain the AST
Let’s get started. The first step is to initialize a new V8 context.
ctx := v8.NewContext()
Once V8 is up, we need to load the Typescript JS code. We make use of a locally
saved typescript.js
file for the same. Read it in memory and then use the RunScript
method to load the script.
tsSource, err := ioutil.ReadFile("/path/to/ts/on/disk/typescript.js")
if err != nil {
panic(err) // panic if load failed
}
// load typescript by converting []byte to string
ctx.RunScript(string(tsSource), "typescript.js")
As Typescript is now loaded, we need to build the compiler options to let TS know that we would like to use the latest syntax version for parsing
// read global object
obj := ctx.Global()
typescript, _ := obj.Get("ts")
ts, _ := typescript.AsObject()
// fmt.Println(typescript.IsObject())
moduleKindJs, _ := ts.Get("ScriptTarget")
moduleKind, _ := moduleKindJs.AsObject()
systemJs, _ := moduleKind.Get("Latest")
system := systemJs.String()
Next, obtain the function createSourceFile
from the loaded ts
object. This allows
us to invoke the function directly from Go.
fnJs, _ := ts.Get("createSourceFile")
fn, _ := fnJs.AsFunction()
Load the typescript code that you would like to parse:
// read the source code file
jsFile, err := ioutil.ReadFile("/ts/source/code/on/disk/index.tsx")
if err != nil {
panic(err)
}
We are now all set to invoke the parser. Though this requires setting up a new isolate
and creating a few wrapper objects to be passed into the function obtained above.
isolate := ctx.Isolate()
ctx.RunScript("const compilerOptions = { module: "+system+"};", "source-tree.js")
sourceFileName, err := v8.NewValue(isolate, "index.ts")
sourceCode, err := v8.NewValue(isolate, string(jsFile))
compilerOptions, _ := ctx.RunScript("compilerOptions", "source-tree.js")
booleanTrue, err := v8.NewValue(isolate, true)
// invoke the parser function
fnValue, err := fn.Call(ctx.Global(), sourceFileName, sourceCode, compilerOptions, booleanTrue)
Check if there was an error while parsing, and if yes, you may want to obtain relevant error message as well as stack trace on the Javascript side.
if err != nil {
e := err.(*v8.JSError) // JavaScript errors will be returned as the JSError struct
fmt.Println(e.Message) // the message of the exception thrown
fmt.Println(e.Location) // the filename, line number and the column where the error occured
fmt.Println(e.StackTrace) // the full stack trace of the error, if available
fmt.Printf("javascript error: %v", e) // will format the standard error message
fmt.Printf("javascript stack trace: %+v", e) // will format the full error stack trace
return
}
If there was no error, fnValue
contains the AST as an object. However, you will need
to iterate over it to convert to a pure Go object or a strongly-typed object. It is left as
an exercise for the reader.
// following shall return true to indicate it is an object
fmt.Println(fnValue.IsObject()) // returns true
Complete code is available in this gist.
In my next post we will see how we can achieve the same using QuickJS and its wrapper for Go, QuickJS-Go.
Happy Hacking.