build: add the make runner
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 <surface>:<action>. 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).
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
SHELL := /bin/bash
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
# Target naming convention: <surface>:<action>
|
||||
#
|
||||
# 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)<surface>:<action>$(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
|
||||
Reference in New Issue
Block a user