diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5288c77..7df36ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,11 +113,12 @@ jobs: changelog-fragment: name: Changelog fragment present - # PRs must add a changelog fragment (see changelog.d/). Skippable via the - # `skip-changelog` label for changes that genuinely need no entry (typo - # fixes, CI tweaks, the release PR itself). + # PRs must add a changelog fragment (see changelog.d/). Exempt: `release/*` + # branches (a release PR assembles + consumes fragments, it doesn't add one) + # and any PR carrying the `skip-changelog` label (typo fixes, CI tweaks). if: >- github.event_name == 'pull_request' && + !startsWith(github.head_ref, 'release/') && !contains(github.event.pull_request.labels.*.name, 'skip-changelog') runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index a96c73a..ae40d62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,41 @@ into a versioned section here. Do not hand-edit this block. --- +## [0.3.0] - 2026-06-25 + +### Added + +- `[propagate]` Wired up TanStack Query (SSR-aware): `app/router.tsx` creates a `QueryClient` and wraps the router with `routerWithQueryClient`; `app/routes/__root.tsx` uses `createRootRouteWithContext` so loaders get a typed `context.queryClient`; `app/routes/index.tsx` shows a prefetched `useSuspenseQuery` example. Added `@tanstack/react-router-with-query`. +- `[template-only]` Added a production-build smoke CI job (`pnpm build && pnpm start`) that asserts the homepage returns 200 and its bundled stylesheet resolves — catching prod-only breakage the dev-server E2E misses. +- `[propagate]` Added a `GET /api/health` liveness route (kept in scaffolded projects) and made the request-validation example route optional: scaffold no longer strips it, and `node scripts/remove-example.mjs` deletes it plus drops `@hono/zod-validator` for projects that don't want it. +- `[propagate]` Added an interactive `Counter` example component with a `@testing-library/user-event` test, demonstrating the full Testing Library + Vitest flow (render → real click → assert). Switched the jest-dom setup import to the Vitest-specific entry (`@testing-library/jest-dom/vitest`). +- `[propagate]` Added an unused-dependency CI check (`pnpm knip`, configured in `knip.json`) that fails when a declared dependency is imported nowhere — the guardrail that would have caught the previously-unused `@tanstack/react-query`. Documented in `docs/agents/dependencies.md`. +- `[propagate]` Pinned the Node version: `engines.node` (`>=22`) in `package.json` and a root `.nvmrc`. CI now reads the pin via `actions/setup-node` `node-version-file` (in `ci.yml` and `validate-template.yml`) so the declared version and CI can't drift. +- `[propagate]` Changelog entries are now fragment files under `changelog.d/` (one per PR) instead of a hand-edited `## [Unreleased]` block, eliminating changelog merge conflicts. Adds `scripts/changelog.mjs` (`preview` / `check` / `release `) and a CI check that requires every PR to add a fragment (skippable with the `skip-changelog` label). Scaffolded instances inherit the system. + +### Changed + +- `[template-only]` Hardened the CI workflows: least-privilege `permissions`, concurrency cancellation of superseded runs, and SHA-pinned actions. +- `[template-only]` Hardened the E2E assertion and added Vitest coverage configuration. +- `[propagate]` Completed the `pnpm.overrides` set and switched it to exact pins: all **25** `@tanstack/*` router/start internal packages are now pinned to their exact resolved versions (`1.114.29`–`1.114.35`) instead of the original `~1.114.3` range, matching the policy in `docs/agents/dependencies.md`. (The `[0.2.0]` entry's "17 packages / `~1.114.3`" records what that release shipped; this entry records the later completion so the CHANGELOG no longer contradicts `package.json`.) + +### Fixed + +- `[propagate]` Bundle the stylesheet via a `?url` import in `app/routes/__root.tsx` so the production build is styled — referencing the source path 404s after `vinxi build`. +- `[propagate]` Made `env`/`db` lazy and pure, so importing them (e.g. transitively in unit tests) no longer requires a live `DATABASE_URL`. +- `[propagate]` The `/api` catch-all now forwards `OPTIONS`/`HEAD` to Hono, and the Hono app centralizes JSON 404 and 500 error handling without leaking internals. +- `[propagate]` Type-check the `db/` directory (added to `tsconfig` `include`) and made the documented `~/db/*` import alias resolve in tsc, Vite, and Vitest. +- `[propagate]` Fixed a hydration mismatch, dropdown defaults, and CRLF handling in the starter UI. +- `[propagate]` Reconciled the setup instructions across the README and docs with the actual scaffold and commands. +- `[template-only]` Hardened the scaffold script: argv-safe `execFileSync`, a re-run guard, and project-name slug validation. +- `[template-only]` Restored a green CI baseline (workflow gating, frozen lockfile, the pinned `@tanstack` build, and scaffold stdin handling). +- `[template-only]` Scaffold's label-setup recovery path now works. `scripts/labels.mjs` is runnable directly (`node scripts/labels.mjs`) via an `import.meta.url` main guard and is no longer deleted during scaffold, so the printed "set up labels later" instruction actually creates the labels. Scaffold reuses the same `createLabels()` helper. +- `[propagate]` Defined the `--color-muted-foreground` theme token in `app/styles/app.css` so the `text-muted-foreground` utility used by the starter routes actually renders (Tailwind v4 emits nothing for an undefined color utility). +- `[propagate]` Aligned the scaffold-generated README with the scaffold's "Next steps" output — one command order, and the `DATABASE_URL` step is now in both. +- `[propagate]` Clarified Neon connection-string guidance in `.env.example` and `docs/agents/environment.md`: with the `neon-http` driver the pooled vs. direct endpoint doesn't matter (the `-pooler` endpoint is for the WebSocket `Pool` driver). + +--- + ## [0.2.0] - 2026-04-21 ### Added diff --git a/changelog.d/42-example-routes.added.md b/changelog.d/42-example-routes.added.md deleted file mode 100644 index 9825a6c..0000000 --- a/changelog.d/42-example-routes.added.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Added a `GET /api/health` liveness route (kept in scaffolded projects) and made the request-validation example route optional: scaffold no longer strips it, and `node scripts/remove-example.mjs` deletes it plus drops `@hono/zod-validator` for projects that don't want it. diff --git a/changelog.d/42-testing-example.added.md b/changelog.d/42-testing-example.added.md deleted file mode 100644 index 82820cd..0000000 --- a/changelog.d/42-testing-example.added.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Added an interactive `Counter` example component with a `@testing-library/user-event` test, demonstrating the full Testing Library + Vitest flow (render → real click → assert). Switched the jest-dom setup import to the Vitest-specific entry (`@testing-library/jest-dom/vitest`). diff --git a/changelog.d/42-unused-deps.added.md b/changelog.d/42-unused-deps.added.md deleted file mode 100644 index a66bc1a..0000000 --- a/changelog.d/42-unused-deps.added.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Added an unused-dependency CI check (`pnpm knip`, configured in `knip.json`) that fails when a declared dependency is imported nowhere — the guardrail that would have caught the previously-unused `@tanstack/react-query`. Documented in `docs/agents/dependencies.md`. diff --git a/changelog.d/43-label-recovery.fixed.md b/changelog.d/43-label-recovery.fixed.md deleted file mode 100644 index 1348097..0000000 --- a/changelog.d/43-label-recovery.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- `[template-only]` Scaffold's label-setup recovery path now works. `scripts/labels.mjs` is runnable directly (`node scripts/labels.mjs`) via an `import.meta.url` main guard and is no longer deleted during scaffold, so the printed "set up labels later" instruction actually creates the labels. Scaffold reuses the same `createLabels()` helper. diff --git a/changelog.d/44-overrides-exact.changed.md b/changelog.d/44-overrides-exact.changed.md deleted file mode 100644 index fe0ac83..0000000 --- a/changelog.d/44-overrides-exact.changed.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Completed the `pnpm.overrides` set and switched it to exact pins: all **25** `@tanstack/*` router/start internal packages are now pinned to their exact resolved versions (`1.114.29`–`1.114.35`) instead of the original `~1.114.3` range, matching the policy in `docs/agents/dependencies.md`. (The `[0.2.0]` entry's "17 packages / `~1.114.3`" records what that release shipped; this entry records the later completion so the CHANGELOG no longer contradicts `package.json`.) diff --git a/changelog.d/46-node-pin.added.md b/changelog.d/46-node-pin.added.md deleted file mode 100644 index 9c95dbd..0000000 --- a/changelog.d/46-node-pin.added.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Pinned the Node version: `engines.node` (`>=22`) in `package.json` and a root `.nvmrc`. CI now reads the pin via `actions/setup-node` `node-version-file` (in `ci.yml` and `validate-template.yml`) so the declared version and CI can't drift. diff --git a/changelog.d/47-polish-bundle.fixed.md b/changelog.d/47-polish-bundle.fixed.md deleted file mode 100644 index 39dbb0f..0000000 --- a/changelog.d/47-polish-bundle.fixed.md +++ /dev/null @@ -1,3 +0,0 @@ -- `[propagate]` Defined the `--color-muted-foreground` theme token in `app/styles/app.css` so the `text-muted-foreground` utility used by the starter routes actually renders (Tailwind v4 emits nothing for an undefined color utility). -- `[propagate]` Aligned the scaffold-generated README with the scaffold's "Next steps" output — one command order, and the `DATABASE_URL` step is now in both. -- `[propagate]` Clarified Neon connection-string guidance in `.env.example` and `docs/agents/environment.md`: with the `neon-http` driver the pooled vs. direct endpoint doesn't matter (the `-pooler` endpoint is for the WebSocket `Pool` driver). diff --git a/changelog.d/53-changelog-fragments.added.md b/changelog.d/53-changelog-fragments.added.md deleted file mode 100644 index f2ecc89..0000000 --- a/changelog.d/53-changelog-fragments.added.md +++ /dev/null @@ -1 +0,0 @@ -- `[propagate]` Changelog entries are now fragment files under `changelog.d/` (one per PR) instead of a hand-edited `## [Unreleased]` block, eliminating changelog merge conflicts. Adds `scripts/changelog.mjs` (`preview` / `check` / `release `) and a CI check that requires every PR to add a fragment (skippable with the `skip-changelog` label). Scaffolded instances inherit the system. diff --git a/changelog.d/README.md b/changelog.d/README.md index bf73b9b..9a02c5e 100644 --- a/changelog.d/README.md +++ b/changelog.d/README.md @@ -4,9 +4,10 @@ Pending changelog entries live here as individual files instead of a hand-edited `## [Unreleased]` block in `CHANGELOG.md`. Because each PR adds its own file, PRs never collide on the changelog. -CI **requires every PR to add a fragment here.** If a change genuinely needs no -changelog entry (a typo fix, a CI tweak, the release PR itself), apply the -`skip-changelog` label to the PR. +CI **requires every PR to add a fragment here.** Exceptions: PRs from a +`release/*` branch (a release assembles the fragments — see +`docs/agents/releases.md`) and any PR carrying the `skip-changelog` label (typo +fixes, CI tweaks). ## Adding an entry diff --git a/docs/migrations/v0.3.md b/docs/migrations/v0.3.md new file mode 100644 index 0000000..1197283 --- /dev/null +++ b/docs/migrations/v0.3.md @@ -0,0 +1,122 @@ +# Migration: v0.2.0 -> v0.3.0 + +## Version + +- **From:** 0.2.0 +- **To:** 0.3.0 + +## Breaking Changes + +- **Node >= 22 is now required.** `package.json` declares `engines.node` `">=22"` and CI reads a root `.nvmrc`. Instances on an older Node major should upgrade. +- Otherwise additive. The changelog workflow changes (see step 13) — if your instance adopted the hand-edited `## [Unreleased]` convention, switch to changelog fragments. + +## General Rule + +Whenever this guide says "copy X from construct," copy the file verbatim rather than hand-writing its contents. Hand-writing drifts; copying does not. After copying, the only permitted edit is merging with existing instance-owned content (e.g. keeping an instance's customizations intact). Only `[propagate]` changelog entries are covered here; `[template-only]` entries (CI hardening, scaffold internals, the production-build smoke job) are not propagated. + +## Migration Steps + +Follow these steps in order. Run `pnpm preflight` and `pnpm build` after completing all steps. + +### 1. Pin the Node version + +- Add `"engines": { "node": ">=22" }` to `package.json`. +- Copy `.nvmrc` from construct's repo root (it contains `22`). +- In `.github/workflows/ci.yml` (and any other workflow), replace each `actions/setup-node` `node-version: 22` with `node-version-file: .nvmrc` so the pin and CI can't drift. + +### 2. Complete the TanStack overrides (exact pins) + +Replace the instance's `pnpm.overrides` block with construct's. It now lists **all 25** `@tanstack/*` router/start internal packages pinned to **exact** versions (`1.114.29`–`1.114.35`), not the `~1.114.3` range. See `docs/agents/dependencies.md`. Do not run `pnpm install` yet. + +### 3. Wire up TanStack Query (SSR-aware) + +Copy from construct, integrating with any instance customizations: + +- `app/router.tsx` — creates a `QueryClient` and wraps the router with `routerWithQueryClient`, providing it via router `context` and a `QueryClientProvider`. +- `app/routes/__root.tsx` — uses `createRootRouteWithContext<{ queryClient: QueryClient }>()` so loaders get a typed `context.queryClient`. +- `app/routes/index.tsx` — the live example: a server-function-backed query prefetched with `ensureQueryData` and read via `useSuspenseQuery`. +- Add `@tanstack/react-router-with-query` (exact `1.114.35`) to `dependencies` and to `pnpm.overrides`. + +### 4. Bundle the stylesheet via a `?url` import + +In `app/routes/__root.tsx`, import the stylesheet as `import appCss from "~/styles/app.css?url"` and reference `appCss` in `head().links`. Referencing the source path works in dev but 404s after `vinxi build`. + +### 5. Make `env` and `db` lazy and pure + +Copy `app/env.ts` and `db/client.ts` from construct. Env validation runs lazily on first `getEnv()` (memoized), and `getDb()` constructs the client on first call — so importing these modules (e.g. transitively in tests) no longer requires a live `DATABASE_URL`. + +### 6. Add the `--color-muted-foreground` theme token + +In `app/styles/app.css`, add an `@theme` block defining `--color-muted-foreground` (construct uses `oklch(0.556 0 0)`). Without it, the `text-muted-foreground` utility emits nothing in Tailwind v4. + +### 7. Type-check `db/` and fix the `~/db/*` alias + +- Add `"db"` to the `include` array in `tsconfig.json`. +- Add the `"~/db/*": ["./db/*"]` path mapping (more specific than `"~/*"`) so the documented `~/db/client` import resolves in tsc, Vite, and Vitest. + +### 8. Harden the Hono API layer + +Copy from construct: + +- `app/routes/api.$.ts` — the catch-all now forwards `OPTIONS`/`HEAD` to Hono. +- `app/server/index.ts` — centralizes a JSON `404` and a `500` handler that doesn't leak internals; mounts the new `GET /api/health` route (`app/server/routes/health.ts` — copy it too). +- The request-validation example (`app/server/routes/example.ts`) stays by default. If you don't want it, copy `scripts/remove-example.mjs` and run it to delete the route and drop `@hono/zod-validator`. + +### 9. Fix starter-UI nits + +If the instance still uses construct's starter components, copy the fixes for the hydration mismatch, dropdown defaults, and CRLF handling (see `app/routes/__root.tsx` / `app/routes/index.tsx`). + +### 10. Add the `Counter` example and switch the jest-dom import + +- Copy `app/components/Counter.tsx` and `app/components/Counter.test.tsx` (the `@testing-library/user-event` example). Add `@testing-library/user-event` to `devDependencies` if absent. +- In `tests/setup.ts`, change `import "@testing-library/jest-dom"` to `import "@testing-library/jest-dom/vitest"` (the Vitest-specific entry). + +### 11. Add the unused-dependency check (knip) + +- Add `knip` to `devDependencies` and copy `knip.json` from construct (adjust the entry globs if your app structure differs). +- Add the `"knip": "knip --include dependencies,unlisted"` script and an "Unused dependency check" step (`pnpm knip`) to `ci.yml`. +- See `docs/agents/dependencies.md`. + +### 12. Adopt changelog fragments + +- Copy `scripts/changelog.mjs` and the `changelog.d/` directory (keep only `changelog.d/README.md`). +- Add the `changelog:preview` / `changelog:check` / `changelog:release` scripts. +- Add the `changelog:check` step and the `changelog-fragment` job (with the `skip-changelog` label escape hatch) to `ci.yml`; add the `skip-changelog` label (`scripts/labels.mjs`). +- Replace the hand-edited `## [Unreleased]` block in `CHANGELOG.md` with the pointer note. See `docs/agents/releases.md`. + +### 13. Propagate doc reconciliations + +Copy the updated `docs/agents/api.md`, `docs/agents/dependencies.md`, `docs/agents/environment.md` (Neon HTTP-driver guidance — pooled vs. direct doesn't matter for `neon-http`), `docs/agents/releases.md`, `docs/agents/styling.md`, and `.env.example`. Merge, don't overwrite, instance-owned content. + +### 14. Install + +Run `pnpm install` (not `--frozen-lockfile`). This updates the lockfile for the new/changed dependencies and runs `postinstall` (`tsr generate`) and `prepare` (`lefthook install`). + +### 15. Update `.construct` version + +Set `constructVersion` to `"0.3.0"` in the instance's `.construct` file. + +### 16. Verify + +Run `pnpm preflight`, `pnpm knip`, and `pnpm build`. Fix any issues before marking the migration complete. + +## Files Affected + +- `package.json` -- `engines.node`, `pnpm.overrides` (25 exact pins, `+react-router-with-query`), `knip` + `@testing-library/user-event` dev deps, `changelog:*` + `knip` scripts, version bump +- `.nvmrc` -- new file (Node pin) +- `app/router.tsx`, `app/routes/__root.tsx`, `app/routes/index.tsx` -- TanStack Query wiring + `?url` stylesheet import +- `app/env.ts`, `db/client.ts` -- lazy/pure construction +- `app/styles/app.css` -- `--color-muted-foreground` token +- `tsconfig.json` -- `include` adds `db`; `~/db/*` path mapping +- `app/routes/api.$.ts`, `app/server/index.ts`, `app/server/routes/health.ts`, `app/server/routes/example.ts` -- Hono hardening + health route +- `app/components/Counter.tsx` (+ test), `tests/setup.ts` -- user-event example + jest-dom/vitest entry +- `knip.json` -- new file (unused-dependency config) +- `scripts/changelog.mjs`, `scripts/remove-example.mjs`, `changelog.d/` -- new tooling +- `.github/workflows/ci.yml` -- `node-version-file`, knip step, changelog-fragment job + check +- `CHANGELOG.md` -- `[Unreleased]` pointer note; fragment workflow +- `docs/agents/*` (`api.md`, `dependencies.md`, `environment.md`, `releases.md`, `styling.md`), `.env.example` -- doc reconciliations +- `.construct` -- version bump to `0.3.0` + +Template-only (do not copy to instances): + +- The production-build smoke job, CI workflow hardening, scaffold internals, and label-recovery fixes — these affect construct itself, not instances. diff --git a/package.json b/package.json index 52f7d0e..332048f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "construct", - "version": "0.2.0", + "version": "0.3.0", "private": true, "type": "module", "packageManager": "pnpm@9.15.0",