web / no js runtime
My laptop, my CI runners, and the VMs I run in production have no node, npm, or bun. My web apps still ship a TypeScript bundle. I build, check, and format it with native tools.
Why
My apps use a Go backend, hml templates, and a small TypeScript bundle with no npm dependencies. Standalone native binaries handle bundling, typechecking, formatting, and language servers.
Avoiding npm removes package scripts, lockfiles, dependency trees, and associated supply-chain attack vectors.
Tools
I install each tool as a standalone binary (see cmd / laptop). Production VMs need none of them. The server runs a single Go binary that embeds pre-built assets, saving disk space and build time.
Bundling
I bundle TypeScript with esbuild through its Go API.
A Go program imports github.com/evanw/esbuild/pkg/api and calls api.Build.
The program sets entry points, minification, and output names with content hashes.
See web / cdn.
result := api.Build(api.BuildOptions{
EntryPoints: []string{"js/app.ts"},
Bundle: true,
MinifySyntax: true,
Outdir: "public",
EntryNames: "[dir]/[name]-[hash]",
Write: true,
})
if len(result.Errors) > 0 {
os.Exit(1)
}
Because Go compiles esbuild into a native binary, bundling needs no node process.
Typechecking
I check types with tsgo, the native TypeScript compiler.
I run tsgo --noEmit against a standalone tsconfig.json.
The configuration extends nothing:
{
"compilerOptions": {
"strict": true,
"target": "es2022"
},
"include": ["js/**/*"]
}
The compiler reads types from its internal library, so it needs no packages from npm.
Formatting
I format source files with dprint.
dprint runs as a single binary.
It downloads WebAssembly plugins and formats TypeScript, CSS, HTML, JSON, and Markdown.
It replaces prettier and needs no node runtime.
Language servers
For Neovim, I use language servers that do not need node:
- Bash: bashd, a Go server that calls
shellcheck. - HTML: superhtml, a single Zig binary.
- TypeScript:
tsgo --lsp --stdio, the same binary that typechecks.
Parsers
Tree-sitter generates parser code from grammar.js files.
The tree-sitter binary embeds QuickJS, so it needs no external runtime.
See vim / tree-sitter grammar.
Client dependencies
If I want a JavaScript library in the bundle, I vendor its source or I write the part I need. So far the part I needed was a combobox and a typeahead. The esbuild Go API bundles vendored source files directly.
A chart is the usual reason to add a large dependency such as d3. I compute the geometry in Go and draw with HTML and CSS instead. See web / charts.
Tradeoffs
- Prettier plugins do not work with dprint.
- Tools that publish only to npm are unavailable.
Native tools compile the code, and only the browser executes JavaScript.