Using GopherJS with Go Modules

February 15, 2020

Go version 1.11 introduced modules, and as of Go 1.14, all other dependency management systems will be considered deprecated.

This puts GopherJS users in a bit of a bind, because, although GopherJS is (mostly) compatible with Go 1.12, module support has never been completed. Here’s hoping that will change, but until then, what can a GopherJS developer do?

If you’re eager for my final solution, and don’t need a lot of explanation, feel free to jump to teh codez.

Possible solutions

  1. One option is to just stick with an older version of Go. I took this approach for a while after 1.13 was released, and kept using 1.12 even for my non-GopherJS projects. This was not tenible for very long.

  2. My next strategy was to use gvm to maintain two versions of Go. I’d use 1.13 for all non-GopherJS projects, and 1.12 for GopherJS. This is also quite cumbersome, and still doesn’t allow a unified workflow. More important, it doesn’t work well for isomorophic packages like my Kivik library, which needs to support both modern Go, and GopherJS installations.

  3. My third approach is to use Go modules as a vendoring tool for GopherJS. This requires a few extra steps, which I’ll detail in the rest of this post, but it solves all of the problems I was facing. In particular, it solves the problem of the “magical” vX paths that are used for go modules. More on that in a moment. This solution made much easier with a very recently-added feature allowing GopherJS to work with a newer version of Go installed.

Install Go & GopherJS

I suggest using the new GOPHERJS_GOROOT capabilities, so that you can install GopherJS 1.12 along with Go 1.13 (or newer), but this is not a strict requirement, if you are content installing Go 1.12 as well.

Make sure you have the latest/desired version of Go installed, as described on the Go web site.

Then install the latest version of GopherJS. From the GopherJS documentation:

go get golang.org/dl/go1.12.16
go1.12.16 download
export GOPHERJS_GOROOT="$(go1.12.16 env GOROOT)"  # Also add this line to your .profile or equivalent.

Verify your go and gopherjs installations:

$ go version
go version go1.13 linux/amd64
$ go version
go version go1.13 linux/amd64

Prepare your package

Prepare your project as you would any other with module support. Please consult with the Go wiki for detailed instructions. For the sake of demonstration, I’ll create a dummy program, with a simple dependency on a module-enabled repo (my own kivik project)). This is my main.go:

package main

import (
	kivik "github.com/go-kivik/kivik/v4"
)

func main() {
	print(kivik.KivikVersion)
}

So I’ll now initialize the module:

go mod init github.com/flimzy/test

Now to verify that it works with standard Go (not GopherJS):

$ go run main.go
go: finding github.com/go-kivik/kivik/v4 latest
4.0.0-prerelease

Exactly what I expected. It downloaded the version 4 of the kivik library, compiled, then ran the program, with the expected output.

But if we do the same with gopherjs, we’ll notice a failure:

$ gopherjs run main.go
cannot find package "github.com/go-kivik/kivik/v4" in any of:
        /home/jonhall/sdk/go1.12.16/src/github.com/go-kivik/kivik/v4 (from $GOROOT)
        /home/jonhall/.gvm/pkgsets/go1.13/global/src/github.com/go-kivik/kivik/v4 (from $GOPATH)

Vendoring the dependencies

Under normal circumstances, there’s no longer a reason to use the vendor directory in most projects, but in this case we need to. You might think you could continue using dep or some other legacy vendory tool of your choice, but this will fail on a project like kivik, which doesn’t have a literal /v4 path component. This path component is managed entirely by the Go tool chain. If I were to use dep to vendor kivik, it would place everything in vendor/github.com/go-kivik/kivik (note the lack /v4 at the end).

By using Go modules as our vendoring tool, it solves this problem for us, effectively providing a backward compatibility layer for us.

So to do this, we simply use the vendoring support present in Go:

go mod vendor

This downloads all dependencies, and places them in vendor, in the paths expected by the import statements (in contrast to the paths from where they were downloaded).

Note that standard Go tools will continue to ignore the vendor directory, unless you specific -mod=vendor on the command line (or in the GOFLAGS environment variable). But GopherJS doesn’t have this restriction. GopherJS still looks at the vendor directory by default.

(Optional) Move the project under your GOPATH

One advantage of Go modules is that you can work outside of our GOPATH. GopherJS doesn’t support this, either, so you may find that you have to move your code back into your GOPATH (or temporarily set your GOPATH to match your working directory). If you’re already working in your GOPATH, you can skip this step.

You may be tempted to use a symlink for this, but symlinks confuse a number of Go tools, so do so at your own risk!

Compile with GopherJS

Now with the dependencies proper vendored, GopherJS should work as expected:

$ gopherjs run main.go
4.0.0-prerelease

Success!

Automating the process

You’ll probably want to automate this, possibly for use in CI, or perhaps for a local Makefile or build script. It’s pretty straight forward. The below script I have adapted from the GitLab CI script I use on my Kivik project.Adapt to your needs.

# Install Go1.12 standard library for GopherJS
go get golang.org/dl/go1.12.16
go1.12.16 download

# Install GopherJS
curl -sL https://deb.nodesource.com/setup_${NODE_VER}.x | bash -
apt-get update -qq && apt-get install -y nodejs
# Note that I've turned off Go Modules for the following operation, so that
# gopherjs doesn't end up being vendored along with all the other dependencies.
GO111MODULE=off go get -u github.com/gopherjs/gopherjs

# Move the project to GOPATH
export SRCDIR=${GOPATH}/src/github.com/flimzy/test
mkdir -p ${SRCDIR}
mv ${CI_PROJECT_DIR}/* ${SRCDIR}
cd ${SRCDIR}

# npm install   # Optional: Install any JS dependencies your project needs

go mod vendor # Download dependencies into vendor/

GOPHERJS_GOROOT="$(go1.12.16 env GOROOT)" gopherjs test ./...

That’s it! Happy Gophering!


comments powered by Disqus