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).
311 lines
12 KiB
Makefile
311 lines
12 KiB
Makefile
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
|