kiruna

package module
v0.0.67 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2025 License: BSD-3-Clause Imports: 5 Imported by: 7

README

🏔️ Kiruna

Go Reference Go Report Card

Kiruna logo banner

Kiruna is a simple, powerful library for building and optimizing fullstack Go applications, with live browser refresh and excellent dev-prod parity. It's sort of like Vite, but for Go apps.

Dev server features
  • Automatic smart rebuilds and browser refreshes
  • Instant hot reloading for CSS files (without a full page refresh)
  • Highly configurable to support any use case
  • Glob pattern file watching
  • Granular build hooks with customizable timing strategies
Production optimizations
  • Static asset hashing and embedding
  • Basic CSS bundling and minification
  • Critical CSS inlining
  • Safely serve public static assets with immutable cache headers

Dev-time reloads are smart and fast. Based on the type of file you edit and your associated configuration options, Kiruna will do the minimum amount of work necessary to get your changes to your browser as quickly as possible.

Kiruna has a few lightweight runtime helpers for referencing hashed static assets from Go code and templates (e.g., Kiruna.GetPublicURL("favicon.ico")) and for including your CSS in your HTML templates (e.g., Kiruna.GetCriticalCSSStyleElement(), Kiruna.GetStyleSheetLinkElement()). They have zero third-party dependencies and are aggressively cached whenever possible, so you can feel free to call them even in the hot path of your application.

Kiruna is completely decoupled from any specific frameworks or libraries, so you can use it with any Go server framework or router you choose, or just use the standard library. Moreover, unlike some alternatives, Kiruna doesn't require you to install any tooling on your machine. It is orchestrated solely from inside your repo and its dependencies.

Starter Tutorial From Scratch (~5 minutes)

Let's get a Kiruna project set up from scratch. This should only take a few minutes to complete. The only prerequisite is that you have Go installed on your machine.

Scaffolding

Start by initializing a new Go module in an empty directory, replacing your-module-name with your own module name:

go mod init your-module-name

Then run the following commands to create the necessary directories and files for your project:

# Scaffold directories
mkdir -p cmd/app cmd/build cmd/dev
mkdir -p private-static public-static/prehashed
mkdir -p dist/kiruna internal/platform

# Create placeholder files
touch cmd/app/main.go cmd/build/main.go cmd/dev/main.go
touch private-static/index.go.html
touch critical.css main.css
touch dist/kiruna/x dist/dist.go internal/platform/kiruna.go

Setup dist/dist.go

Now copy this into your dist/dist.go file:

package dist

import "embed"

//go:embed kiruna
var FS embed.FS

Setup internal/platform/kiruna.go

Now copy this into your internal/platform/kiruna.go file, replacing your-module-name with your own module name:

package platform

import (
	"your-module-name/dist"

	"github.com/sjc5/kiruna"
)

var Kiruna = kiruna.New(&kiruna.Config{
	DistFS:           dist.FS,
	MainAppEntry:     "./cmd/app/main.go",
	DistDir:          "./dist",
	PrivateStaticDir: "./private-static",
	PublicStaticDir:  "./public-static",
	CriticalCSSEntry:  "./critical.css",
	NormalCSSEntry:    "./main.css",
})

Add Kiruna as a dependency

Now go get Kiruna and tidy up:

go get github.com/sjc5/kiruna
go mod tidy

Setup ./private-static/index.go.html

Now copy this into your ./private-static/index.go.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    {{.Kiruna.GetCriticalCSSStyleElement}} {{.Kiruna.GetStyleSheetLinkElement}}
  </head>
  <body>
    <div>
      <h1>Hello, world!</h1>
      <p>Hello from "./private-static/index.go.html"</p>
    </div>
    {{.Kiruna.GetRefreshScript}}
  </body>
</html>

Setup cmd/app/main.go

And now copy this into your cmd/app/main.go file, replacing your-module-name with your own module name:

package main

import (
	"fmt"
	"html/template"
	"net/http"
	"your-module-name/internal/platform"

	"github.com/sjc5/kiruna"
)

func main() {
	// Health check endpoint
	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))
	})

	// Serve static files from "dist/kiruna/static/public" directory, accessible at "/public/"
	http.Handle("/public/", platform.Kiruna.MustGetServeStaticHandler("/public/", true))

	// Serve an HTML file using html/template
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		privateFS, err := platform.Kiruna.GetPrivateFS()
		if err != nil {
			fmt.Println(err)
			http.Error(w, "Error loading template", http.StatusInternalServerError)
			return
		}

		tmpl, err := template.ParseFS(privateFS, "index.go.html")
		if err != nil {
			fmt.Println(err)
			http.Error(w, "Error loading template", http.StatusInternalServerError)
			return
		}

		err = tmpl.Execute(w, struct{ Kiruna *kiruna.Kiruna }{
			Kiruna: platform.Kiruna,
		})
		if err != nil {
			http.Error(w, "Error executing template", http.StatusInternalServerError)
		}
	})

	port := kiruna.MustGetPort()

	fmt.Printf("Starting server on: http://localhost:%d\n", port)
	http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
}

Setup cmd/build/main.go

And copy this into your cmd/build/main.go file, replacing your-module-name with your own module name:

package main

import "your-module-name/internal/platform"

func main() {
	err := platform.Kiruna.Build()
	if err != nil {
		panic(err)
	}
}

This file is what you'll want to run when you're ready to build for production. Running go run ./cmd/build will build your project and save your binary to dist/bin/main. Assuming you used DistFS to embed your static assets, you can now run your binary from anywhere on the build machine, and it will serve your static assets from the embedded filesystem. If you chose not to embed your static assets, you'll just need to make sure that the binary is a sibling of the dist/kiruna directory in order to serve your static assets from disk.

[!NOTE] Oftentimes you'll want to handle compilation of your Go binary yourself. In such cases, you can use platform.Kiruna.BuildWithoutCompilingGo() instead of platform.Kiruna.Build(). This will run all the same Kiruna-specific processing (static asset hashing, etc.) but will stop short of producing an executable.


Setup cmd/dev/main.go

Now copy this into your cmd/dev/main.go file, replacing your-module-name with your own module name:

package main

import (
	"your-module-name/internal/platform"

	"github.com/sjc5/kiruna"
)

func main() {
	platform.Kiruna.MustStartDev(&kiruna.DevConfig{
		HealthcheckEndpoint: "/healthz",
		WatchedFiles:        kiruna.WatchedFiles{{Pattern: "**/*.go.html"}},
	})
}

Run the dev server

Now try running the dev server:

go run ./cmd/dev

If you copied everything correctly, you should see some logging, with a link to your site on localhost, either at port 8080 or some fallback port. If you see an error, double check that you copied everything correctly.


Edit critical CSS

Now paste the following into your ./critical.css file, and hit save:

body {
  background-color: darkblue;
  color: white;
}

If you leave your browser open and your dev server running, you should see the changes reflected in your browser nearly instantly via hot CSS reloading. Notice that the CSS above is being inlined into your document head. This is because your CriticalCSSEntry config option is set to ./critical.css.


Edit normal CSS

Now let's make sure your normal stylesheet is also working. Copy this into your ./main.css file:

h1 {
  color: red;
}

When you hit save, this should also hot reload.

[!NOTE] If you want to separate your CSS into multiple files, you can do so using CSS @import syntax. This also works for your critical CSS file.


Edit your HTML template

Now let's try editing your html template at ./private-static/index.go.html.

Find the line that says <h1>Hello, world!</h1> (line 10) and change it to: <h1 style="color: green;">Hello, world!</h1>.

When you hit save, your browser page should automatically refresh itself. This happens because of the {Pattern: "**/*.go.html"} item in the kiruna.WatchedFiles slice in cmd/dev/main.go. If you were to remove that item and restart your dev server, the page would not reload when you save your html file (if you don't believe me, go give it a try).

When you want to watch different file types, you can add them to the kiruna.WatchedFiles slice using glob patterns, and there are a whole bunch of ways to tweak this to get your desired reload behavior and sequencing, including callbacks and more. Feel free to explore your auto-complete options here or dive into the Kiruna source code to learn more.


Setup .gitignore

If desired, you can bootstrap a new .gitignore file by running the following:

echo "dist/*\n\!dist/dist.go" > .gitignore

Alternatives

If you're just looking for automatic Go application rebuilds only, without automatic browser refreshes or static asset build tooling, then Kiruna may be overkill for you, and you could just use Air instead.

That said, you can put Kiruna into a simpler ServerOnly mode if you want, which skips the frontend-targeted build steps and just does automatic Go application rebuilds.

In either case, one benefit of Kiruna over Air is that it doesn't require you to install any tooling on your machine. It is orchestrated solely from inside your repo and its dependencies. So when a new developer joins your team, they can just clone your repo and be ready to rock as soon as they run go mod tidy (instead of needing to install and configure Air on their machine first).

Copyright 2024 Samuel J. Cook. Licensed under the BSD 3-Clause License.

Documentation

Index

Constants

View Source
const (
	OnChangeStrategyPre              = ik.OnChangeStrategyPre
	OnChangeStrategyConcurrent       = ik.OnChangeStrategyConcurrent
	OnChangeStrategyConcurrentNoWait = ik.OnChangeStrategyConcurrentNoWait
	OnChangeStrategyPost             = ik.OnChangeStrategyPost
)

Variables

View Source
var (
	MustGetPort  = ik.MustGetPort
	GetIsDev     = ik.GetIsDev
	SetModeToDev = ik.SetModeToDev
)

Functions

This section is empty.

Types

type Config

type Config = ik.Config

type DevConfig

type DevConfig = ik.DevConfig

type FileMap added in v0.0.66

type FileMap = ik.FileMap

type IgnorePatterns added in v0.0.21

type IgnorePatterns = ik.IgnorePatterns

type Kiruna

type Kiruna struct {
	// contains filtered or unexported fields
}

func New

func New(c *ik.Config) *Kiruna

func (Kiruna) Build

func (k Kiruna) Build() error

func (Kiruna) BuildWithoutCompilingGo

func (k Kiruna) BuildWithoutCompilingGo() error

func (Kiruna) GetBaseFS added in v0.0.63

func (k Kiruna) GetBaseFS() (fs.FS, error)

func (Kiruna) GetCriticalCSS

func (k Kiruna) GetCriticalCSS() template.CSS

func (Kiruna) GetCriticalCSSElementID

func (k Kiruna) GetCriticalCSSElementID() string

func (Kiruna) GetCriticalCSSStyleElement

func (k Kiruna) GetCriticalCSSStyleElement() template.HTML

func (Kiruna) GetCriticalCSSStyleElementSha256Hash added in v0.0.53

func (k Kiruna) GetCriticalCSSStyleElementSha256Hash() string

func (Kiruna) GetPrivateFS

func (k Kiruna) GetPrivateFS() (fs.FS, error)

func (Kiruna) GetPublicFS

func (k Kiruna) GetPublicFS() (fs.FS, error)

func (Kiruna) GetPublicFileMap added in v0.0.53

func (k Kiruna) GetPublicFileMap() (FileMap, error)

func (Kiruna) GetPublicFileMapElements added in v0.0.38

func (k Kiruna) GetPublicFileMapElements() template.HTML

func (Kiruna) GetPublicFileMapKeysBuildtime added in v0.0.57

func (k Kiruna) GetPublicFileMapKeysBuildtime() ([]string, error)

func (Kiruna) GetPublicFileMapScriptSha256Hash added in v0.0.53

func (k Kiruna) GetPublicFileMapScriptSha256Hash() string

func (Kiruna) GetPublicFileMapURL added in v0.0.53

func (k Kiruna) GetPublicFileMapURL() string

func (Kiruna) GetPublicURL

func (k Kiruna) GetPublicURL(originalPublicURL string) string

func (Kiruna) GetRefreshScript

func (k Kiruna) GetRefreshScript() template.HTML

func (Kiruna) GetRefreshScriptSha256Hash added in v0.0.53

func (k Kiruna) GetRefreshScriptSha256Hash() string

func (Kiruna) GetServeStaticHandler

func (k Kiruna) GetServeStaticHandler(pathPrefix string, addImmutableCacheHeaders bool) (http.Handler, error)

func (Kiruna) GetStyleSheetElementID

func (k Kiruna) GetStyleSheetElementID() string

func (Kiruna) GetStyleSheetLinkElement

func (k Kiruna) GetStyleSheetLinkElement() template.HTML

func (Kiruna) GetStyleSheetURL

func (k Kiruna) GetStyleSheetURL() string

func (Kiruna) MustGetPrivateFS added in v0.0.63

func (k Kiruna) MustGetPrivateFS() fs.FS

func (Kiruna) MustGetPublicFS added in v0.0.63

func (k Kiruna) MustGetPublicFS() fs.FS

func (Kiruna) MustGetPublicURLBuildtime added in v0.0.65

func (k Kiruna) MustGetPublicURLBuildtime(originalPublicURL string) string

func (Kiruna) MustGetServeStaticHandler added in v0.0.55

func (k Kiruna) MustGetServeStaticHandler(pathPrefix string, addImmutableCacheHeaders bool) http.Handler

func (Kiruna) MustStartDev added in v0.0.18

func (k Kiruna) MustStartDev(devConfig *DevConfig)

func (Kiruna) SetupDistDir added in v0.0.65

func (k Kiruna) SetupDistDir()

type OnChange added in v0.0.14

type OnChange = ik.OnChange

type OnChangeFunc added in v0.0.9

type OnChangeFunc = ik.OnChangeFunc

type WatchedFile added in v0.0.17

type WatchedFile = ik.WatchedFile

type WatchedFiles

type WatchedFiles = ik.WatchedFiles

Directories

Path Synopsis
internal
scripts

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL