Parsing Typescript in Go using QuickJS

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.

Parsing Typescript in Go using V8Go

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.

Parsing Typescript in Java

This post talks on how you can parse Typescript code in Java using Eclipse V8 wrapper. Why would one do that? I used it to prepare Javascript documentation for Bedrock, my React component library. I tried using both React Styleguidist and Storybook but haven’t liked both (will discuss those reasons in a later post).

Parsing means generating an Abstract Syntax Tree from the TS source code. Once you have the AST you can crawl it, to run any transform or aggregation as you like.

Let’s start by creating a new V8 instance that also understands the NodeJS runtime

NodeJS nodeJS = NodeJS.createNodeJS();

Next step is to load the Typescript parser in V8 using require. I am using the entire folder from a previously existing node_modules folder.

V8Object typescript = nodeJS.require(new File("path_to/node_modules/typescript"));

Now we need to obtain the Typescript.ScriptTarget.Latest value. This value allows us to instruct TS compiler to use latest TS syntax guidelines during the parsing phase.

V8Object moduleKind = typescript.getObject("ScriptTarget");
Integer system = moduleKind.getInteger("Latest");

Next, we need to setup compiler options. These shall be needed in the next step.

V8Object compilerOptions = new V8Object(nodeJS.getRuntime());
compilerOptions.add("module", system);

Before we invoke the parser, let’s load the TS source code in memory from the file on disk.

String code = org.apache.io.FileUtils.readFiletoString("my-ts-file.ts");

Finally time to invoke the TS compiler:

V8Object result = (V8Object) typescript.executeJSFunction("createSourceFile", fileName, code, compilerOptions, true);

Iterating over the V8Object is difficult, so we will use a utility method to convert it to normal java.util.Map

Map<String, ? super Object> astAsMap = V8ObjectUtils.toMap(result);

And that is all what we need. You may now use astAsMap to crawl/iterate over the AST as your use-case desire. But before, we wrap it up here, we need to clean up the resources that V8 allocated:

while (nodeJS.isRunning()) {
    nodeJS.handleMessage();
}

// release all resources
result.release();
compilerOptions.release();
moduleKind.release();
typescript.release();
nodeJS.release();

You can find all the code together in this Github repository including conversion to strongly typed Java objects.

Experiments with Hugo!

In continuation of my last post, revisiting this blog, I spent time over weekend experiment and convert my site to Hugo. And I was amazed at the very first run. Hugo is not just fast, it is super fast. While Jekyll takes up 3-4 seconds to start, Hugo was done in less than a second. To top it all, it automatically detects changes to the configuration file as well and rebuilds. And to add cherries on top of the cake, Hugo was emitting content in both HTML and JSON formats to be consumed.

I also love the way Hugo allows you to configure your site either using YAML, or TOML or JSON. This allows a lot of flexibility for people who may be more familiar and comfortable using one format than other. I loved the way that at the flip of a configuration flag, all content (HTML/JS/CSS) was mininfied instantly. I use MermaidJS a lot for my sequence diagrams, and loved that Hugo has support for the same too.

Hugo has some level of customization in the form of shortcodes. It has a decent amount of built-in functions that you may leverage. However, I was disappointed to see no support for date format customization. I was born to read dates in dd/MM/yyyy format and now reading mm/dd/yyyy or yyyy-mm-dd format just doesn’t feel natural. Adding functionality requires writing a Go plugin which again may not be for everyeone.

But what makes Hugo stand apart is this one word: SPEED. Speed in setting it up - just download the binary and you are ready to go. Whether you are serving locally to test, or publishing your site for production, completing everything in less than a second is what I will call GOD SPEED.

However, if I were to move all my work, then I will need to change the date format again to something Hugo can undertand. Wish there was a simple way to define that in the configuration.

Revisiting the blog

It has been many years I wrote something worthy. A lot has changed since then. In these years front-end development has become more advanced with technologies like ReactJS, Vue, Svelte etc. Static sites need no longer confined to generate-time data and basic text processing. They should be able to make use of web components to better the user experience.

Today, as I look at this space it feels out-dated, pale and buggy. The headers and left-hand pane are not truly responsive. The typography does not properly align. Colors need more work. But these are least of my concerns. As a developer, I still have no true control over the way I write (seldom) and it gets published. For example, why should all posts be in a single folder, or in sub-folders based on date/time, or based on tag names?

With Jekyll setting up the site/blog or adding a new section within the existing one is difficult. Posts can only be recognized and paginated from under the _posts folder. There is no way to filter the posts. The software required to generate locally (ruby, bundler) etc itself takes time to install. And then you run into version issues. It took me 30+ minutes today get Jekyll running again. I believe we developers should do better here.

As I go over the latest documentation (version 4.2.2), I can’t find an easy way:

  • to customize functionality using Javascript
  • no way to optimize/thumbnail images directly
  • pull HTTP data to merge into posts
  • creating automatic post excerpt
  • inline/donwload external images
  • support for Velocity templates (yes, I come from Java world)
  • customizing date format to be used
  • and more…

I am sure there would be 3rd party plugins to support some of the above. However, I think this is something that should be supported out of the box. Over the next couple of days, I will evaluate the latest in static site generators. Have heard a lot about Hugo, Gatsby and Next. Time to find a worthy replacement for Jekyll.