
How to publish a CLI created in Golang on NPM
Recently I started building tools that will help me automate my app deployment process easier and cheaper, to do that I knew CLIs are definitely going to be involved one way or another. So I started building two CLIs:
- Simp CLI: It's like Makefile just easy
- Thrusta CLI: It's a remote command execution tool
Both CLIs were built with Golang but only one is hosted on npm here, this tutorial will guide you on how you can do the same.
Note: Am still new to Go
Let's jump into it
Prerequisites
- Basic Golang syntax and concepts (Beginner friendly)
- NPM installed
- Golang installed (its a process, so here is a link)
What are we building?
We are going to create a CLI that serves a directory over a HTTP server.
Lets get started
Create a new folder inside
$GOPATH/src/<github.com|gitea.com|gitlab.com|bitbucket.com>/<username>/html-server-cli
Next, we will create our go.mod
file to track packages we are going to use
go mod init
I have my directory setup as below:
.
+-- cli
+-- cli.go
+-- helpers
+-- helpers.go
+-- server
+-- server.go
+-- go.mod
+-- go.sum
+-- html-server.go
Let add some code to our helpers/helpers.go
package helpers
import (
"math/rand"
"time"
)
// GeneratePortNumber handle generating port number
func GeneratePortNumber() int {
rand.Seed(time.Now().UnixNano())
min := 1000
max := 99999
port := rand.Intn(max-min+1) + min
return port
}
The block of code above enables us to generate a random port number, you can add more logic to see if a port is used.
Next, let's add some code to our server/server.go
package server
import (
"log"
"net/http"
"strconv"
"github.com/bywachira/html-server/helpers"
)
// Serve handles serving the directory
func Serve() {
// Generate our port number
port := helpers.GeneratePortNumber()
// Inform the user which port we are running
log.Println("We are running on port: " + strconv.Itoa(port))
// Exit to a live server
log.Fatal(http.ListenAndServe(":"+strconv.Itoa(port), http.FileServer(http.Dir("."))))
}
Let's create a method to initialize our CLI
package cli
import (
"fmt"
"sort"
"github.com/bywachira/html-server/server"
"github.com/urfave/cli/v2"
)
// SetupCLI initialize cli
func SetupCLI() *cli.App {
app := &cli.App{
// List your commands here
Commands: []*cli.Command{
{
// Provide details about our cli flags
Name: "run",
Aliases: []string{"r"},
Usage: "Serve your HTML file",
Action: func(c *cli.Context) error {
// The logic to be called when this commad is ran
fmt.Println("We are serving this directory")
server.Serve()
return nil
},
},
},
}
sort.Sort(cli.FlagsByName(app.Flags))
sort.Sort(cli.CommandsByName(app.Commands))
// Return our cli
return app
}
Let's update our package main
, which is html-server.go
package main
import (
"log"
"os"
"github.com/bywachira/html-server/cli"
)
func main() {
// Assign our cli to the app variable
app := cli.SetupCLI()
err := app.Run(os.Args)
// Exit program when we get an error and show error
if err != nil {
log.Fatal("Error: ", err)
}
}
Note: You don't have to use
github.com/urfave/cli/v2
package by default, you can use whatever package you want because all we need to publish is the binary file.
The Publishing Part
We are publishing out CLI to NPM so a package.json
file is a must
npm init
Follow the prompts and fill them with your own preferred fields
This is how mine looks like
{
"name": "@wachira/html-serve",
"version": "0.0.0",
"description": "Serve your current directory",
"main": "index.js",
"scripts": {
"postinstall": "go-npm install",
"preuninstall": "go-npm uninstall"
},
"repository": {
"type": "git",
"url": "git+https://github.com/bywachira/html-serve.git"
},
"keywords": ["cli", "automation"],
"author": "tesh254",
"license": "MIT",
"bugs": {
"url": "https://github.com/bywachira/html-serve/issues"
},
"homepage": "https://github.com/bywachira/html-serve#readme",
"dependencies": {
"@wachira/html-serve": "^1.0.3",
"global": "^4.4.0",
"go-npm": "^0.1.9"
},
// Specify details about your binary
"goBinary": {
// Name of the binary file and what npm will alias as
"name": "html-serve",
// Where to add the binary
"path": "./bin",
// Dynamic URL pointing to where the compressed binary exists based on version, platform, and the processor type (amd64, arm, and more)
"url": "https://github.com/bywachira/html-serve/releases/download/v{{version}}/html-serve_{{version}}_{{platform}}_{{arch}}.tar.gz"
}
}
We added two commands in scripts
which are
{
"postinstall": "go-npm install",
"preuninstall": "go-npm uninstall"
},
What postinstall
does is that after installing the package it will pull the binary from where you saved it Github or Amazon S3,
Note: We will host our binaries on Github, most probably you have a free account there. All you need is just a public URL pointing to your binary.
preuninstall
basically removes the existence of your binary from the bin
directory before NPM uninstalls the package.
For go-npm
to work, we need to add it as a dependency
npm install go-npm --save
This will create a node_modules
folder, add it to your .gitignore
file, to avoid pushing it to Github.
For our CLI tool to work on all operating systems, we need to build a binary that works for each, well not to worry we will use the goreleaser
package that bundles the app to multiple binaries for each OS specified and processor arch.
To install GoReleaser visit this link.
Let there be binaries
Before we can build our OS-specific binaries we need the following:
- Github/Gitlab token(based on where you want your binary to reside)
- Initialize version control (
git
) - Git basic commands
Creating our token
- Create your token here
- Create a
.env
file which should be also added to the.gitignore
file, and add the following
export GITHUB_TOKEN=<YOUR GITHUB TOKEN>
Initialize Git
git init
We need to create a tag and push it as GoReleaser will use the latest Git tag of your repo.
git tag -a <version> <commit> -m <release label>
Export our variable
source .env
Define goreleaser
config and define the arch
and operating systems you want to build for.
builds:
- binary: html-server
goos:
- windows
- darwin
- linux
goarch:
- amd64
Run goreleaser
goreleaser release
The above command will publish your CLI to Github or Gitlab based on where your repo is hosted.
If you visit your repo and click on releases, you should see something like this
Next, this CLI needs to be published to npm.
Before you can do that ensure you have the following done:
- An account on
npmjs.com
- Login to account using
npm cli
bash npm login
And now let's publish
npm publish --access=public
The --access=public
flag means the package will be publicly available for download
You just got your package published
Things you should note
- For package documentation, update your repo readme and update the version on your package.json for npm to pick-up.
- If you need to make any changes at all even a typo fix, you will have to update the npm version on package.json to update the package.
Summary
- We made a CLI in Golang
- We generated binaries for all major OSs
- We published our package
- Add dist folder created by
goreleaser
to .gitignore file
Questions
- Join my discord, I answer all questions, here
I have web monetization enabled so you can support my work or give me feedback on this article on what I should improve on.