6.8 KiB
kjol
kjol ("keel" in Norwegian) is a shared, multilingual base layer factored out of the
user's applications so they stay in sync. It is consumed as a git submodule inside each
app (at <app>/kjol). Current consumers: cdrateline.com_2.0 and Hotlap — near-identical
forks it was extracted from. Scope will grow to more projects and languages.
Golden rules
- The framework NEVER imports application code. Wherever kjol needs app-specific
behavior, the app injects it (interfaces, registration functions, config structs,
callbacks). See Coupling inversions below. If you find yourself wanting to
import "<app>/internal/..."from kjol, invert it instead. - Never run git operations here on the user's behalf. The user creates the submodule and makes all commits. You may edit files, build, and test.
- kjol is edited in place via the submodule +
go.work— there is no publish /go getstep. Editing a kjol file takes effect in the consuming app immediately. - When a file exists in both apps and has drifted, reconcile by merging best-of-both.
Organization — by language
Each top-level directory is one language / build root:
kjol/
go/ all Go. Module `kjol` (go.mod lives in go/). Imports are `kjol/<pkg>`.
web/ all JS/TS (browser + SSR). No build system of its own; built by go/webbundler.
# future: cpp/ kotlin/ swift/
Language-first, not feature-first. Consequence: the web bundler is Go and lives in
go/webbundler even though it builds web/. Don't "fix" this by splitting it.
go/ — module kjol
Packages, imported as kjol/<name>: appenv basic chrono config csv dbutil finance httputil l4g security snailmail validation webbundler tw, plus the gowasm web-UI engine (vdom
wasmruntime rsc wasmdevserver, and webui — a Tailwind-styled component kit ported
from web/kit; author components in pure Go compiled to WebAssembly; all stdlib-only), and
cmd/{bundle,twcss,migrate,loc,passgen,typecheck,wasmgen}. A runnable
example lives in cmd/examples/go-wasm-web (its own nested module so its go-chart dep stays
out of kjol).
webbundler is the JS build (TSX → Solid → esbuild). tw is the Tailwind v4
compiler, and it is deliberately NOT inside it: Tailwind only reads text and writes CSS,
and the text is just as likely to be Go — the gowasm kit writes its markup in Go and has no
JS build at all. Keeping it in the bundler made every Go-only consumer drag a JavaScript
bundler along for a CSS file.
Build / test (run from repo root):
go -C go build ./...
go -C go vet ./...
go -C go test ./...
The wasm host API (wasmruntime/host.go + host_wasm.go / host_native.go) is how neutral
component code reaches the browser: element measurement (Measure, Viewport), imperative style
writes (SetStyle — positioning must NOT go through signals, which re-render the whole tree),
the post-commit hook (AfterRender, the only point at which a just-rendered element can be
measured), document/window listeners, timers, localStorage, and file download. It is dual-build:
real under js && wasm, no-op stubs natively — which is what lets webui call it unconditionally
and still SSR. Refs come from vdom.Ref + vdom.WithRef; vdom.Portal mounts children at
document.body (needed to escape overflow:hidden / transform ancestors).
The reconciler is wasm-only and needs a DOM, so go test ./... cannot reach it. It has its own
harness — a minimal DOM under node:
GOOS=js GOARCH=wasm go -C go test -exec="node testdata/domexec.js" ./wasmruntime
webui components that measure the page (Tooltip, Popover, Menu, Modal, DatePicker, Tutorial,
AutoTable) are controllers: create them once alongside your signals, never inside a render
closure. Floating panels share one positioning engine (webui/position.go, pure math, unit-tested
natively) driven by the Floating controller (webui/floating.go).
web/
kit/— Solid.js.tsxcomponent kit. Apps import components as@ui/*.runtime/— vendored Solid runtime +vendor.json(base entrypoints). The app merges its ownvendor.json(chart.js, pdf-lib, ...) on top; kjol's solid-js must resolve first so there is a single reactive instance.icons/— FontAwesome SVG source kit (the bundler scans usage and generates a per-app registry; the generated file is app-owned, not committed here).styles/—theme.css(@themescaffold +:rootfa vars). Brand color/font tokens stay app-side; the app'sstyle.css@imports this.auth/ utils/ hooks/ ssr/ env.ts basic.ts finance.ts superfun.ts types.d.ts— generic TS scaffolding. Apps import as@kjol/*. (Concrete permission constants stay app-side.)
Frontend import aliases (resolved by the bundler and mirrored in each app's tsconfig
paths): @ui/* → web/uikit, @kjol/* → web/, @appgen/* → the app's generated dir
(e.g. the FA faIcons registry — app-owned, gitignored, regenerated each build). The kit's
own imports of sibling components stay relative (./Buttons.tsx).
Consumption (per app)
go.workat the app root:then vendor withuse ( . ./kjol/go )go work vendor(notgo mod vendor).- Startup wiring the app performs (this is how the inversions get their app-side halves):
dbutil.RegisterAll(models.Tables),l4g.SetDatabaseWriter(...),dbutil.Init(dbutil.ConnConfig{...}),snailmail.Configure(snailmail.Settings{...}), a thinconfig.Loadwrapper overconfig.Load[T], and an app-sideinternal/httpauthfor the session/authn middleware.
Coupling inversions (how the framework stays app-agnostic)
| Package | Inversion |
|---|---|
dbutil |
table names via Register/RegisterAll (not the app's models.Tables) |
l4g |
owns the Entry type; DB persistence via SetDatabaseWriter(func(Entry) error) |
config |
generic Load[T](file, *T) error; each app defines its own config struct |
dbutil.ConnConfig, snailmail.Settings |
DB / mail credentials injected, never read from app config |
appenv |
compile-time environment via build tags (-tags staging / -tags production); the bundler reads appenv.Environment for the JS __ENV_TYPE__ define |
httputil.CorsMiddleware(CorsConfig{...}) |
allowed domains + bundle-version source injected |
Stays app-side (never moves into kjol)
Domain models/repository/handlers-api, migrations, pages/routes/layouts, brand UI
(TopBar/AppSidebar/TransitionOverlay), concrete permission constants, the app config
struct, embed.go + wwwroot/, and generated artifacts (faIcons registry, routes.gen.ts,
public_pages.gen.go).
Provenance
Big-bang extraction from cdrateline + Hotlap. The detailed migration plan lives on the
author's machine at ~/.claude/plans/foamy-humming-tulip.md.