| name | node-makefile |
| description | The Makefile for a Node Web Only project - vendoring pinned frontend assets into public/, compiling a native addon from source, verifying it loads, and the semver version calculation. Use when creating or changing the Makefile, adding a vendored asset, or wiring the native addon build. Triggers on make vendor, make verify, make setup, npm ci, npm_config_build_from_source, node-gyp, fnm, uv tool run, and fonttools woff2 compression. |
| user-invocable | false |
Node Makefile
Vendor the frontend, compile any native addon from source, prove it loads, then hand off to the release step.
The native-addon targets exist only when the project has one. A pure-JS app drops verify and the npm_config_build_from_source prefix and uses a plain npm ci.
Assets Are Never Committed
make vendor produces everything under public/fonts/, public/css/, and public/vendor/, and the release workflow calls that same target rather than reimplementing the copies. Those directories are listed in .gitignore, so the tree in git holds only what a person wrote.
Every version is pinned to an exact release. A floating range makes two builds of one commit differ, and moving a pin is an edit to this file, which is the intended maintenance cost.
A stamp file guards the whole target and depends on the Makefile and the lockfile. make bundle on an unchanged checkout then vendors nothing, and a moved pin re-vendors everything.
.gitignore needs its own entry for public/.vendor-stamp, which sits beside the vendored directories rather than inside one.
Fonts
Three families are downloaded by default and all three come from Google Fonts as woff2. Nothing is converted, since the css2 endpoint already serves woff2 to a browser-shaped User-Agent.
| Family | Role |
|---|
| Inter | body and UI text |
| Google Sans | display headings and branding |
| JetBrains Mono | code and monospace |
Only the latin and latin-ext blocks of each stylesheet are kept. Google Fonts declares every subset it has, which for Google Sans is twenty-five files covering scripts the page never renders, and the release bundle carries all of them. Filtering leaves six woff2 files across the three families.
The Nerd Font variant is off by default. It exists only for a page that renders Nerd Font glyphs, and it costs two megabytes and roughly ten seconds of woff2 compression against sixty kilobytes and half a second for the plain family. Set NERDFONT := 1 in the Makefile when a page needs the glyphs; the target then fills the same css/jetbrains-mono.css under the same JetBrains Mono family name, so no page changes either way.
Toolchain
fnm pins Node to .node-version, so make is run from a shell where it has activated.
uv supplies both the Python node-gyp needs and the fonttools used for woff2 compression, neither of which is installed globally or into the system interpreter.
Template
.PHONY: help setup vendor font nerdfont verify clean bundle binary version
APP_NAME := [APP_NAME]
VERSION ?= dev-build
NODE_VERSION := 24.20.0
PUBLIC_DIR := public
VENDOR_DIR := $(PUBLIC_DIR)/vendor
CSS_DIR := $(PUBLIC_DIR)/css
FONTS_DIR := $(PUBLIC_DIR)/fonts
STAMP := $(PUBLIC_DIR)/.vendor-stamp
NERDFONT_VERSION := 3.5.1
NERDFONT := 0
MONO := $(if $(filter 1,$(NERDFONT)),nerdfont,font FAMILY="JetBrains+Mono" SLUG=jetbrains-mono WEIGHTS="400;700")
UA := Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36
UVX := uv tool run
CYAN := \033[0;36m
GREEN := \033[0;32m
NC := \033[0m
help: ## Show this help
@echo "$(CYAN)Available targets:$(NC)"
@grep -E '^[a-zA-Z_-]+:.*?
.DEFAULT_GOAL := help
setup: node_modules vendor verify ## Install deps, vendor assets, verify the addon
@uv python install
npm_config_build_from_source=true PYTHON= npm ci
@touch node_modules
@:
: Makefile package-lock.json | node_modules
@mkdir -p
@cp node_modules/@tailwindcss/browser/dist/index.global.js /tailwind.js
@cp node_modules/lucide/dist/umd/lucide.min.js /lucide.min.js
@cp node_modules/@xterm/xterm/lib/xterm.js /xterm.js
@cp node_modules/@xterm/xterm/css/xterm.css /xterm.css
@ --no-print-directory font FAMILY= SLUG=inter WEIGHTS=
@ --no-print-directory font FAMILY= SLUG=google-sans WEIGHTS=
@ --no-print-directory
@touch
@echo
@curl -sfL -H \
\
-o
@awk '/^\/\* /{keep = ($$0 ~ /^\/\* latin(-ext)? \*\/$$/)} keep' \
>
@rm -f
@grep -o 'https://fonts.gstatic.com/[^)]*' | sort -u \
| xargs -P 8 -I{} sh -c 'curl -sfL -o $$1' _ {}
@sed -i.bak -E 's|https://fonts\.gstatic\.com/[^)]*/([^/)]+)|/fonts/\1|g'
@rm -f
@set -e; tmp=; trap 'rm -rf ' EXIT; \
curl -sfL -o \
; \
tar -xJf -C \
JetBrainsMonoNerdFontMono-Regular.ttf JetBrainsMonoNerdFontMono-Bold.ttf; \
for w in Regular Bold; do \
-q --from fonttools ttLib.woff2 compress \
-o \
>/dev/null 2>&1; \
done
@{ \
for pair in 400:Regular 700:Bold; do \
printf '@font-face{font-family:;font-style:normal;font-weight:%s;font-display:swap;src:url() format();}\n' \
; \
done; \
} >
@node --input-type=module -e
@echo
@bash scripts/bundle.sh
@echo
@bun build ./bin/.js --compile --minify --outfile dist/
@echo
@rm -rf node_modules /inter.css /google-sans.css /jetbrains-mono.css dist
@echo
@LATEST_TAG=$; \
LATEST_TAG=$${LATEST_TAG
MAJOR=$; \
MINOR=$; \
PATCH=$; \
MAJOR=$${MAJOR:-0}; MINOR=$${MINOR:-0}; PATCH=$${PATCH:-0}; \
COMMIT_MSG=; \
if echo | grep -q ; then \
MAJOR=$$((MAJOR + 1)); MINOR=0; PATCH=0; \
elif echo | grep -q ; then \
MINOR=$$((MINOR + 1)); PATCH=0; \
\
PATCH=$$((PATCH + 1)); \
fi; \
echo
Notes
node_modules is a file target gated on package-lock.json and touched afterwards, so make vendor twice in a row reinstalls nothing. Without the touch, make compares against a directory timestamp that npm updates unpredictably.
npm_config_build_from_source=true forces node-gyp to compile rather than download a prebuilt binary. A prebuilt addon is compiled against a different Node ABI and a different libc than the release targets, which surfaces as a load failure at run time rather than at build time.
verify spawns the addon and asserts on its actual output rather than checking that a file exists. A .node file that is present but built for the wrong architecture passes a file check and fails on the first request.
vendor copies JS from node_modules rather than downloading it, because the version is already pinned in package-lock.json and a second source of truth would drift from it. Fonts are downloaded, since they are not npm packages.
Tailwind and Lucide are vendored the same way, from @tailwindcss/browser and lucide in package.json, so the Node frontend styles itself with the same utilities and icons as the Go one.
Every target is silent on success apart from the one line that says what it produced. A tool that narrates its own progress buries the one line a failed build needs, which is why fonttools has both its streams redirected and uv runs under -q.
The version target uses the same commit-marker convention and the same calculation as the Go Makefile. The release workflow calls make -s version rather than reimplementing it.
| Placeholder | Replace with |
|---|
[APP_NAME] | the application name |
@xterm/xterm copies | whatever JS the frontend vendors beyond Tailwind and Lucide |
node-pty in verify | the project's real native addon, or delete the target |
NODE_VERSION | the pinned version, matching .node-version |