Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <version>`) 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
Expand Down
1 change: 0 additions & 1 deletion changelog.d/42-example-routes.added.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/42-testing-example.added.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/42-unused-deps.added.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/43-label-recovery.fixed.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/44-overrides-exact.changed.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/46-node-pin.added.md

This file was deleted.

3 changes: 0 additions & 3 deletions changelog.d/47-polish-bundle.fixed.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/53-changelog-fragments.added.md

This file was deleted.

7 changes: 4 additions & 3 deletions changelog.d/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
122 changes: 122 additions & 0 deletions docs/migrations/v0.3.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "construct",
"version": "0.2.0",
"version": "0.3.0",
"private": true,
"type": "module",
"packageManager": "pnpm@9.15.0",
Expand Down
Loading