Node.js
Install, build and test a TypeScript project in containers, cached by its lockfile and sources.
This tutorial builds a small TypeScript library with npm ci, tsc and
node --test, each in a node:24-alpine container and each skipped when its
inputs haven't changed. You need hammerkit and Docker; Node
doesn't have to be installed on your machine.
The finished project is
examples/tutorial-node.
The project
tutorial-node/
├── package.json
├── package-lock.json
├── tsconfig.json
├── src/greet.ts
└── test/greet.test.js{
"name": "greet",
"private": true,
"type": "module",
"scripts": {
"build": "tsc",
"test": "node --test"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
export function hello(name: string): string {
return `Hello, ${name}!`
}
The tests import the compiled code from dist:
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { hello } from '../dist/greet.js'
test('greets by name', () => {
assert.equal(hello('hammerkit'), 'Hello, hammerkit!')
})
1. Install dependencies
A task names the image it runs in, the files it reads (src) and the files it
produces (generates). Create .hammerkit.yaml with an install task:
tasks:
install:
description: install the locked dependencies
image: node:24-alpine
src: [package.json, package-lock.json]
generates: [node_modules]
cmds:
- npm cihammerkit installnpm ci runs in the container and node_modules is kept as the task's output.
Run it again and hammerkit skips it: package.json and package-lock.json are
unchanged, so there's nothing to install.
2. Build
The build task compiles src. deps: [install] runs install first (or reuses
its cached result) and mounts its node_modules into the build container:
tasks:
build:
description: compile src to dist
image: node:24-alpine
deps: [install]
src: [src, tsconfig.json]
generates:
- path: dist
export: true
cmds:
- npm run buildA container task's outputs stay in a volume hammerkit manages, ready for the tasks
that depend on it. export: true also copies dist into your project, so you
can use the compiled code outside hammerkit.
Edit src/greet.ts and run hammerkit build: install is cached and only build
runs.
3. Test
The tests read test and the dist that build generates:
tasks:
test:
description: test the compiled code
image: node:24-alpine
deps: [build]
src: [test]
cmds:
- npm testtest only declares its own folder in src. Its dependency's sources are part of
its cache key anyway, so a change in src/ reruns build and then test, while a
change in test/ reruns only test.
4. One task for CI
A task without cmds groups other tasks. ci names everything a change has to
pass. The complete build file:
tasks:
install:
description: install the locked dependencies
image: node:24-alpine
src: [package.json, package-lock.json]
generates: [node_modules]
cmds:
- npm ci
build:
description: compile src to dist
image: node:24-alpine
deps: [install]
src: [src, tsconfig.json]
generates:
- path: dist
export: true
cmds:
- npm run build
test:
description: test the compiled code
image: node:24-alpine
deps: [build]
src: [test]
cmds:
- npm test
ci:
description: everything CI checks
deps: [build, test]
hammerkit run ciSummary:
build executed 2.4s
ci executed 3.1s
install executed 1.2s
test executed 3.1s
4 executed, 0 cached (0% cache hit), 3.1s totalRun it again without changing anything:
Summary:
build cached 1ms
ci cached 14ms
install skipped 0ms
test cached 0ms
0 executed, 3 cached, 1 skipped (100% cache hit), 30ms totalinstall is skipped: every task that needs it was a cache hit, so its output
isn't needed either.
Ignore what hammerkit and the tools write into the project:
.hammerkit
node_modules
dist
Run it in CI
CI runs the same command: install hammerkit and run
hammerkit run ci. A fresh runner starts with an empty cache; to reuse what your
laptop or the previous run already built, share the cache as described in
caching in CI.
Next steps
- Tests that need a database: start it as a service and let the
task
needit, see services & networking. - Several packages in one repository: monorepos.
- A task reruns and you don't know why:
hammerkit explain.