Writing · 14 Aug 2026
Adding a language to Packkit should change nothing in the core
The test of a protocol is a second implementation that forces no change. I added Python, then Go, to Packkit and used the exceptions to find where my abstraction actually leaked.

I claimed in the last post that @packkit/core, the protocol under my project generator, does not model any language. That is easy to say when the only generator is the one you wrote the protocol around. The claim is worth nothing until a second language plugs in and needs core to bend, and it does not.
So I wrote a Python generator, create-packkit-py, and then a Go one, create-packkit-go. This post is about what those two proved, and about the one place they caught me modeling JavaScript by accident.
The rule I was testing
The rule: adding a language changes nothing in core. If a new generator forces me to edit @packkit/core, the abstraction leaked, and I would rather find that out with two languages than with ten.
The Go generator was the honest test, because Go is nothing like JavaScript. No package.json, a compiler, gofmt instead of a formatter you configure, a module system that is a file with a module path. If my "language-neutral" protocol was secretly npm-shaped, Go would expose it. So the Go generator became the proof: a JavaScript program whose output is an idiomatic Go project, go.mod and all, that passes gofmt, go vet, go build, and go test, with zero changes to core.
It emits go-lib, go-cli, go-worker, and go-service, and each one returns the same deployment contract that the Node and Python versions of that target return. A Go worker and a Node worker hand back the identical worker contract. That is the whole point: the thing downstream reads the contract, not the language.
The one change Go was allowed to force
I did not get through this clean. Go surfaced exactly one core change, and it was the right one.
Core had a contract called NodeServiceDeploymentContract, with type: 'node-service' and runtime: 'node'. I wrote that back when JavaScript was the only language, and I did not notice it was a leak until a Go HTTP service needed to describe itself and the only word available had "node" baked into it. That is core modeling a language, exactly the thing the rule forbids.
So I renamed it: ServiceDeploymentContract, type: 'service', runtime as a free string. Now a Node service, a Python service on FastAPI, and a Go service on net/http all emit the same service contract with runtime set to 'node', 'python-3.12', or 'go-1.x'. That rename was the last npm concept in core, and removing it was the Go spike paying for itself. One permitted change, made once, that makes every future language cheaper.
A checklist so a new language is not an afterthought
Early on, the Python and Go scaffolds were thinner than the JavaScript one. They wrote source and tests and a README and stopped. The JavaScript generator, meanwhile, emitted editorconfig, a CI workflow, a dependabot config, community files, an agents file, and four license choices. Python and Go got none of that, because I had built them feature by feature and forgotten half the list.
The fix was to stop keeping the list in my head. Core now carries a GENERATOR_CHECKLIST: 18 capabilities, each with a per-language note on how it is realized. A CI workflow in JavaScript is actions/setup-node, in Python it is setup-python plus uv, in Go it is setup-go plus gofmt and vet. Release automation in JavaScript is Changesets, in Python it is PyPI Trusted Publishing, in Go it is GoReleaser. Same capability, language-idiomatic realization, and the point is that when I add a language, the checklist tells me all 18 things it owes a user on day one, so Rust or whatever comes next does not arrive as a second-class citizen.
What I would tell someone doing this
Two things. First, do not add the second language to feel productive. Add it to test the seam. The Python generator was useful; the Go generator was the falsifier, and it earned its keep by breaking exactly one thing.
Second, the tab problem is real and stupid and cost me an afternoon. Go source is tab-indented, my templates were pasted with spaces, and my editing tool kept refusing to match tabs against spaces. I ended up scripting the template edits. Not a deep lesson, just a warning: when you generate a whitespace-significant language from another language's string templates, the whitespace will bite you before the semantics do.
Go is still marked experimental maturity, on purpose, because "it passes the suite" and "I trust it for your production service" are different claims. But the protocol held. Three languages, one core, and the only edit any of them forced was a rename I should have made on day one.
The generators are all on npm as create-packkit, create-packkit-py, and create-packkit-go, and the browser configurator at packkit-web.pages.dev offers all three.