This post is in series where I talk on how to parse Typescript code in Java and Go using v8go. Instead of using v8go, this time we will use QuickJS-Go to parse and obtain the abstract syntax tree.

As before, there are many use-cases on why this may be required:

  • you want to build a new documentation tool for Javascript/Typescript
  • you are building a new version of Hacker Rank
  • you want to enable scripting in your own code

Back to getting the job done, here are the broader steps:

  • Initialize and load QuickJS
  • 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 dig into the details. The first step is to initialize a new QuickJS instance. You would notice the use of runtime.LockOSThread() - this is to ensure that QuickJS always operates in the exact same thread.

// this is a must-step as per QuickJS documentation
stdruntime.LockOSThread()

// create new runtime
runtime := quickjs.NewRuntime()
defer runtime.Free()

// obtain a new context that we will work with
context := runtime.NewContext()
defer context.Free()

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 EvalFile 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 TS source code
result, err := context.EvalFile(string(typeScript), 0, "typescript.js")
check(err)
defer result.Free()

Notice the use of check(err) function call above. It is a convenience function borrowed from the documentation that allows us to visit the stack/cause in case something fails inside the QuickJS runtime. The function is as under.

func check(err error) {
	if err != nil {
		var evalErr *quickjs.Error
		if errors.As(err, &evalErr) {
			fmt.Println(evalErr.Cause)
			fmt.Println(evalErr.Stack)
		}
		panic(err)
	}
}

Once Typescript is loaded, we need to build the compiler options to let TS know that we would like to use the latest syntax version for parsing.

// never free this - throws cgo error at app termination
globals := context.Globals()

ts := globals.Get("ts")
defer ts.Free()

scriptTarget := ts.Get("ScriptTarget")
defer scriptTarget.Free()

system := scriptTarget.Get("Latest")
defer system.Free()

args := make([]quickjs.Value, 4)
args[0] = context.String("index.ts")
args[1] = context.String(string(sourceCode))
args[2] = context.String("")
args[3] = context.Bool(true)

Next, obtain the function createSourceFile from the loaded ts object. This allows us to invoke the function directly from Go.

parseCode := ts.Get("createSourceFile")
defer parseCode.Free()

Load the typescript code that you would like to parse, and then simply use context.Call to execute the parser.

sourceCode, err := ioutil.ReadFile("/path/on/disk/typescript/code/index.ts")
if err != nil {
    panic(err)
}

result, err = context.Call(globals, parseCode, args)
check(err)
defer result.Free()

If there was no error, result 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.

if result.IsObject() {
    // print the property names available
    names, err := result.PropertyNames()
    check(err)

    fmt.Println("Object:")
    for _, name := range names {
        val := result.GetByAtom(name.Atom)
        defer val.Free()

        fmt.Printf("'%s': %s\n", name, val)
    }
} else {
    fmt.Println(result.String())
}

Complete code is available in this gist

This concludes the series on different ways to parse Typescript code in Java using J2V8, Go with v8go and [Go with QuickJS][post3].

Happy Hacking.