From d726e8b251a4478559f3f6a2a48ee3348aee765f Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 10 Aug 2026 17:24:00 +0530 Subject: [PATCH] build: add the make runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dumb runner: every recipe is one pnpm, turbo or script call, and nothing here knows anything the underlying tool does not. Targets are named :. airship has no deploy environments — one workspace, one site, one binary — so there is no env axis to lead with. What varies is the surface, so repo-wide operations stay bare (build, test, preflight) and everything else is prefixed with what it acts on (web:dev, run:codex). --- Makefile | 310 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a73c5b3 --- /dev/null +++ b/Makefile @@ -0,0 +1,310 @@ +SHELL := /bin/bash +.DEFAULT_GOAL := help + +# Target naming convention: : +# +# airship has no deploy environments — one workspace, one site, one binary — so +# unlike a multi-env repo there is no env axis to lead with. What varies here is +# the SURFACE: the repo, the site, the storybook, the CLI, the release. So +# repo-wide operations stay bare (install, build, test, check, preflight) and +# everything else is prefixed with the surface it acts on (web:dev, run:codex). +# +# The Makefile is a dumb runner. Every recipe is one pnpm/turbo/script call. + +# Colors +CYAN := \033[0;36m +BLUE := \033[0;34m +GREEN := \033[0;32m +YELLOW := \033[1;33m +RED := \033[0;31m +MAGENTA := \033[0;35m +RESET := \033[0m + +# The port apps/web's dev server listens on. airship's overlay always takes +# TARGET + 1, so the editor lands on 5174. +# +# Override to drive airship against something else entirely: +# make run TARGET=3000 APP=../my-app +TARGET ?= 5173 + +# The dev server's ROOT — not the repo root. airship resolves the root-relative +# URLs the dev server reports (/src/app.tsx) against this. +APP := apps/web + +CLI := apps/cli/dist/index.js + +# Generated, and committed on purpose — the route tree is checked in so a clean +# checkout typechecks without a build first (see .gitignore). Committed +# generated files drift, so preflight regenerates and diffs them. +# +# Regenerated by BUILDING apps/web, not by `make web:routes`. Two things write +# this file and they disagree: `tsr generate` emits the route tree alone, while +# the TanStack Start Vite plugin appends the `declare module` block that +# registers the router type for SSR. The build's output is the committed one, so +# the build is what the drift check has to run. +GENERATED := apps/web/src/routeTree.gen.ts + +# Reusable Y/n gate for targets that touch something published or shared — npm, +# Cloudflare. Anything other than y/Y aborts. +define confirm_shared +@printf "$(YELLOW)⚠ This affects a shared/published target — continue? [y/N] $(RESET)"; \ +read REPLY; \ +case "$$REPLY" in [yY]) ;; *) printf "$(RED)Aborted.$(RESET)\n"; exit 1 ;; esac +endef + +.PHONY: help +help: + @printf "$(CYAN)" + @printf " ▗▄▖ ▗▄▄▄▖▗▄▄▖ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▄▄▖ \n" + @printf " ▐▌ ▐▌ █ ▐▌ ▐▌▐▌ ▐▌ ▐▌ █ ▐▌ ▐▌\n" + @printf " ▐▛▀▜▌ █ ▐▛▀▚▖ ▝▀▚▖▐▛▀▜▌ █ ▐▛▀▘ \n" + @printf " ▐▌ ▐▌▗▄█▄▖▐▌ ▐▌▗▄▄▞▘▐▌ ▐▌▗▄█▄▖▐▌ \n" + @printf "$(RESET)\n" + @awk ' \ + /^##@/ { printf "\n\033[0;36m%s\033[0m\n", substr($$0, 5); next } \ + /^[a-zA-Z_\\][a-zA-Z0-9_\\:.-]*:.*## / { \ + target = $$0; sub(/: .*/, "", target); gsub(/\\/, "", target); \ + n = index($$0, "## "); desc = substr($$0, n + 3); \ + printf " \033[0;32m%-24s\033[0m %s\n", target, desc \ + }' $(MAKEFILE_LIST) + @printf "\n$(YELLOW)Convention:$(RESET) $(CYAN):$(RESET) — repo-wide ops stay bare.\n" + @printf "$(YELLOW)Shortcuts:$(RESET) $(CYAN)i$(RESET)=install $(CYAN)d$(RESET)=dev $(CYAN)b$(RESET)=build $(CYAN)l$(RESET)=lint $(CYAN)f$(RESET)=fix $(CYAN)t$(RESET)=typecheck $(CYAN)c$(RESET)=check\n" + @printf "$(YELLOW)Context:$(RESET) TARGET=$(TARGET) APP=$(APP) (the overlay takes TARGET+1)\n\n" + +##@ Repo + +.PHONY: install update build dev dev\:pkgs clean distclean + +install: ## Install workspace dependencies + @printf "$(BLUE)Installing dependencies...$(RESET)\n" + @pnpm install + @printf "$(GREEN)Dependencies installed!$(RESET)\n" + +update: ## Update dependencies interactively + @pnpm update --interactive --latest + +build: ## Build every package and the site (topological) + @pnpm build + +dev: ## Watch-build packages and serve the site + @pnpm dev + +dev\:pkgs: ## Watch-build the packages only, without the site + @pnpm dev:pkgs + +clean: ## Remove build output and turbo caches + @pnpm clean + +distclean: clean ## clean, plus every node_modules in the tree + @rm -rf node_modules apps/*/node_modules packages/*/node_modules + +##@ Quality + +.PHONY: lint fix fmt typecheck test test\:browser browsers check commitlint +.PHONY: preflight preflight\:fix + +lint: ## Lint everything (Biome via Ultracite) + @pnpm lint + +fix: ## Auto-fix formatting and lint across the repo + @pnpm fix + +fmt: fix ## Alias for fix + +typecheck: ## tsc --noEmit across the workspace + @pnpm typecheck + +test: ## Run the test suite + @pnpm test + +test\:browser: ## Run every story as a test in real Chromium (needs `make browsers`) + @pnpm turbo run test:browser --filter=@airship/overlay + +browsers: ## Download the Chromium build the browser tier runs in + @pnpm --filter @airship/overlay exec playwright install chromium + +check: lint typecheck test ## Lint, typecheck and test + +commitlint: ## Check this branch's commits against the Conventional Commits rules + @pnpm commitlint --from origin/main --to HEAD + +# Everything checks.yml gates a PR on, in one command, plus the drift check it +# layers on top. Worth running before you open a PR: a stale route tree fails +# there, and it is far cheaper to find here. +# +# Two flavours, same gate: +# preflight verifies only — safe on a clean tree, fails on anything CI would +# preflight:fix autofixes what is autofixable first, then verifies the rest +# +# One deliberate difference from CI: typecheck and test run repo-wide. CI scopes +# them with `turbo --affected` against the PR base; locally there is no base. +# +# Sub-makes rather than prerequisites: GNU make 3.81 (what macOS ships) never +# matches an escaped-colon name in a PREREQUISITE list against the target of the +# same name — it quietly "remakes" a nameless target instead, so the recipe +# passes having run nothing. Passing the goal to a sub-make quotes it past the +# escaping, and a failure still stops the recipe. +preflight: ## Run the full CI gate locally (lint + typecheck + test + route drift) + @printf "$(CYAN)» Lint$(RESET)\n" + @$(MAKE) lint + @printf "$(CYAN)» Typecheck$(RESET)\n" + @$(MAKE) typecheck + @printf "$(CYAN)» Test$(RESET)\n" + @$(MAKE) test + @printf "$(CYAN)» Route tree$(RESET)\n" + @$(MAKE) "web:build" + @if ! git diff --quiet -- $(GENERATED); then \ + printf "$(RED)The route tree is stale — run 'make web:build' and commit the result:$(RESET)\n"; \ + git diff --stat -- $(GENERATED); \ + exit 1; \ + fi + @printf "$(GREEN)Preflight passed — ready for a PR.$(RESET)\n" + +# Best-effort: applies every fix this repo can apply on its own, then runs the +# same verification tail. The regenerated route tree is REPORTED rather than +# failed on — this target just produced it, so failing on its own output would +# be pointless. Typecheck and test still fail; nothing autofixes those. +preflight\:fix: ## preflight, but autofix first (ultracite fix + regenerate routes) + @printf "$(CYAN)» Fix (ultracite)$(RESET)\n" + @$(MAKE) fix + @printf "$(CYAN)» Route tree$(RESET)\n" + @$(MAKE) "web:build" + @printf "$(CYAN)» Typecheck$(RESET)\n" + @$(MAKE) typecheck + @printf "$(CYAN)» Test$(RESET)\n" + @$(MAKE) test + @if git diff --quiet -- $(GENERATED); then \ + printf "$(GREEN)Preflight passed — ready for a PR.$(RESET)\n"; \ + else \ + printf "$(YELLOW)Preflight passed. The regenerated route tree still needs committing:$(RESET)\n"; \ + git diff --stat -- $(GENERATED); \ + fi + +##@ Site (apps/web) + +.PHONY: web\:dev web\:build web\:preview web\:tokens web\:og web\:routes +.PHONY: web\:deploy web\:deploy\:preview + +web\:dev: ## Start apps/web's dev server (see TARGET below) + # Through turbo, not `pnpm --filter … dev`: @airship/web#dev depends on + # @airship/site-tokens#build, which is what emits dist/tokens.css. Vite has + # nothing to import without it. + @pnpm turbo run dev --filter=@airship/web + +web\:build: ## Production-build apps/web + @pnpm turbo run build --filter=@airship/web + +web\:preview: ## Serve the built worker locally, exactly as Cloudflare will run it + @pnpm --filter @airship/web exec wrangler dev + +web\:tokens: ## Regenerate the site's design tokens from DESIGN.md + @pnpm --filter @airship/site-tokens build + +web\:og: ## Regenerate the social card (public/og.png) after changing the copy + # Needs site-tokens built: the card embeds Inter and JetBrains Mono from + # that package's dist/fonts, and falls back to system faces without them. + @pnpm turbo run build --filter=@airship/site-tokens + @pnpm --filter @airship/web og + +web\:routes: ## Scaffold apps/web's route tree after adding a route (build is authoritative) + # `tsr generate` writes the route tree but NOT the `declare module` block + # that registers the router type for SSR — the Start Vite plugin appends + # that during a build. Running this alone therefore leaves the committed + # file 9 lines short; `make web:build` puts them back. Fine for scaffolding + # a new route mid-edit, but build before you commit. + @pnpm --filter @airship/web routes + +web\:deploy: ## Deploy apps/web to Cloudflare Workers (production) + $(confirm_shared) + @printf "$(BLUE)Deploying apps/web to Cloudflare...$(RESET)\n" + @$(MAKE) "web:build" + @pnpm --filter @airship/web exec wrangler deploy --env production + @printf "$(GREEN)Deployed!$(RESET)\n" + +web\:deploy\:preview: ## Deploy apps/web to the Cloudflare preview environment + @$(MAKE) "web:build" + @pnpm --filter @airship/web exec wrangler deploy --env preview + +##@ Overlay (storybook) + +.PHONY: storybook storybook\:build + +storybook: ## Browse the overlay's controls, sections and panel (:6006) + # Through turbo, and for the same reason web:dev is: the story config maps + # @airship/editor-tokens' dist/fonts onto /__airship/fonts, and Storybook + # refuses to start when a staticDirs source is missing. A bare + # `pnpm --filter @airship/overlay storybook` therefore fails on a clean + # checkout, where that dist does not exist yet. + @pnpm turbo run storybook --filter=@airship/overlay + +storybook\:build: ## Build the static story catalogue into storybook-static/ + @pnpm turbo run build-storybook --filter=@airship/overlay + +##@ CLI (drive airship against APP) + +.PHONY: run run\:safe run\:codex run\:opencode run\:inline run\:solo doctor demo + +run: $(CLI) ## Point airship at apps/web (its dev server must be up) + @node $(CLI) --target $(TARGET) --cwd $(APP) + +run\:safe: $(CLI) ## Same as run, with the agent confined to the project + @node $(CLI) --target $(TARGET) --cwd $(APP) --safe + +run\:codex: $(CLI) ## Same as run, on Codex instead of Claude + @node $(CLI) --target $(TARGET) --cwd $(APP) --agent codex + +run\:opencode: $(CLI) ## Same as run, on OpenCode (needs `opencode` on PATH) + @node $(CLI) --target $(TARGET) --cwd $(APP) --agent opencode + +run\:inline: $(CLI) ## Same as run, on the inline surface instead of the canvas + @node $(CLI) --target $(TARGET) --cwd $(APP) --mode inline + +run\:solo: $(CLI) ## Run apps/web's dev server and airship together, one terminal + @node $(CLI) --cwd $(APP) --exec "pnpm turbo run dev --filter=@airship/web" + +doctor: $(CLI) ## Check the environment and report what is wrong + @node $(CLI) doctor --cwd $(APP) + +$(CLI): + @pnpm build + +demo: ## One-shot: install + build, then print the two-terminal recipe + @$(MAKE) install build + @echo + @echo " Ready. In one terminal:" + @echo " make web:dev # http://localhost:$(TARGET)" + @echo " then in another:" + @echo " make run # airship, editing that same app" + @echo " and open http://localhost:$$(( $(TARGET) + 1 ))" + @echo + @echo " That is airship editing its own home page. Pick the hero's button," + @echo " ask for a change, and watch the diff land in apps/web/src/." + +##@ Release (@airshiplabs/cli) + +.PHONY: release release\:ci release\:version + +release: ## Cut a release locally (bump + commit + tag; stops before push) + $(confirm_shared) + @bash scripts/release.sh + +release\:ci: ## Trigger the CI publish workflow. BUMP=patch|minor|major, VERSION=x.y.z, DRY=1 + @gh workflow run publish.yml --ref "$$(git rev-parse --abbrev-ref HEAD)" \ + -f bump="$(or $(BUMP),patch)" -f version="$(VERSION)" -f dry_run="$(if $(DRY),true,false)" + @echo "Dispatched publish on $$(git rev-parse --abbrev-ref HEAD). Watch: gh run watch" + +release\:version: ## Print the version the next release would get (BUMP=, VERSION=) + @node scripts/next-version.mjs $(if $(VERSION),--version "$(VERSION)",--bump "$(or $(BUMP),patch)") + +# Single-letter shortcuts. Deliberately undocumented — a `##` here would render +# as an empty section in help, and the footer already advertises them. + +.PHONY: i d b l f t c + +i: install +d: dev +b: build +l: lint +f: fix +t: typecheck +c: check