Skip to content

feat(templates): add Next.js templates (JavaScript + TypeScript) - #123

Merged
dawsontoth merged 15 commits into
mainfrom
claude/create-harper-nextjs-scaffold-851671
Jul 27, 2026
Merged

feat(templates): add Next.js templates (JavaScript + TypeScript)#123
dawsontoth merged 15 commits into
mainfrom
claude/create-harper-nextjs-scaffold-851671

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two templates — nextjs (JavaScript) and nextjs-ts (TypeScript) — that scaffold a Next.js App Router app running on Harper via @harperfast/nextjs. Because the app runs inside Harper, server actions and server components read and write tables directly through the injected tables global — no separate API server and no network round-trip.

They slot in as a new Next.js framework family in the interactive picker and --template flag.

What the templates are

A bare starter at the same altitude as the existing vanilla/react/vue templates — not a full app. The only Harper touch is a counter persisted in a Count table: a server component reads it, and a <form action> server action increments it. That shows the one thing unique to Next-on-Harper (server-side tables access) while staying minimal and safe — single known-key record (no scans), non-destructive (no delete), no user input (no validation surface).

schema.graphql            # a single Count table
app/page.(tsx|js)         # server component: reads the count, renders a form-action button
app/actions.(ts|js)       # getCount / increment via the `tables` global
app/layout.(tsx|js)       # minimal root layout
config.yaml               # graphqlSchema + @harperfast/nextjs (prebuilt: true)
next.config.mjs           # withHarper({})
harper.d.ts               # (TS) ambient types for the `tables` global

Catalog wiring: lib/constants/templates.js + .d.ts, frameworks.js (new family + color), templates.test.js, package.json files, and the root README.

Key decisions (a few are non-obvious — details below)

  • Use the injected tables global; do not import 'harper' in server code. A top-level import 'harper' breaks the production build (Turbopack can't resolve it, and even under webpack it opens the DB during Next's build-time page-data collection). This matches the plugin's own next-16 fixture.
  • Deploy prebuilt (prebuilt: true + build-first scripts). On a multi-threaded cluster the plugin's on-startup build races across worker threads and 404s (see below); shipping a prebuilt .next avoids it. Verified on a stage cluster.
  • Schema-only config (graphqlSchema + the plugin). The plugin owns HTTP routing, so there's no static/rest handler.
  • studio: false. No Studio template package is built/published for these (new optional catalog flag → derived studioTemplateNames). Studio's edit-in-place model also doesn't fit Next.js (verified — see below).

Validated on a real stage cluster

  • Deploy without a build → 404s (multi-thread build race).
  • Prebuilt deploy → serves; the counter reads and writes live (confirmed in-browser).
  • Editing a file in the Studio editor + Restart → no effect for a prebuilt app (and a rebuild would just re-trigger the race).

Root cause filed upstream as HarperFast/nextjs#52 (concurrent next build across worker threads → ENOENT on .next/BUILD_ID), with @harperfast/vite's withBuildLock as the fix to port. Also filed #51 (Turbopack can't resolve import 'harper') and a repro on #37 (build vs. RocksDB lock).

CI coverage

Both variants are in the integration.yaml matrix across npm / yarn / pnpm (scaffold → install → lint → format → build → runtime smoke). This caught real package-manager issues the npm-only path hid:

  • typescript must be a devDependency even in the JS template (eslint-config-nexttypescript-eslint requires it; npm hoists it, yarn/pnpm don't).
  • pnpm 11 needs allowBuilds in pnpm-workspace.yaml (it removed onlyBuiltDependencies) or pnpm install fails on Next's native build-script deps.
  • run test is now conditional (these templates ship no unit tests; existing templates unchanged).

A dedicated template.tests/nextSmoke.js boots the app under Harper (multi-threaded) and asserts it serves / and renders the Harper-backed counter — since the Next.js apps route through the plugin and have no REST resource surface for the existing runtimeSmoke.js.

Follow-ups (not blocking)

🤖 Generated with Claude Code

Adds `nextjs` and `nextjs-ts` templates that run a Next.js App Router app on
Harper via the @harperfast/nextjs plugin. Because the app runs inside Harper,
server actions and server components read and write tables directly through the
`tables` global — no separate API server and no network round-trip.

Ports the "Doggy Management System" demo (schema, server actions, list/detail
pages, client delete button) from the nextjs-example prior art, modernized to
the current harper CLI, @harperfast/nextjs 2.x, Next.js 16, and React 19.

Wires both templates into the catalog: templates.js, templates.d.ts,
frameworks.js (new "Next.js" family), templates.test.js, package.json files[],
and the root README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new Next.js and Next.js + TypeScript templates for the Harper platform, including the necessary configuration, schema definitions, and sample application code. The review identified several critical issues, including the use of unreleased versions for Next.js and Node.js in package files, a bug in the form validation logic where an age of 0 is incorrectly rejected, and a missing loading state for the delete action. These issues should be addressed to ensure the templates are stable and provide a good user experience.

Comment thread template-nextjs-ts/package.json
Comment thread template-nextjs/package.json
Comment thread template-nextjs-ts/_nvmrc
Comment thread template-nextjs/_nvmrc
Comment thread template-nextjs-ts/app/actions.ts Outdated
Comment thread template-nextjs/app/actions.js Outdated
Comment thread template-nextjs-ts/app/ui/DeleteButton.tsx Outdated
Comment thread template-nextjs/app/ui/DeleteButton.js Outdated
dawsontoth and others added 2 commits July 21, 2026 13:07
Adds an optional `studio` flag to the template catalog (default true) and a
derived `studioTemplateNames` list. The Studio build/publish scripts now iterate
`studioTemplateNames` instead of `templateNames`, and the two nextjs entries set
`studio: false`.

The @harperfast/nextjs plugin builds Next.js on load, and whether that works in
the Studio's deploy-only model is unverified — plus first-time publish perms /
npm provenance for the new packages aren't set up yet. Both are easy follow-ups
once this lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- createDog: validate the raw `age` form value instead of the coerced number so
  a legitimate age of 0 (a puppy) is no longer rejected as a missing field.
- DeleteButton: wrap the deleteDog server action in useTransition, disabling the
  button and showing a pending state while it runs — the recommended React 19 /
  Next.js pattern for server actions invoked from client events.

Verified: repo dprint/oxlint/vitest, plus scaffold tsc --noEmit and eslint for
both variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Thanks for the review, @gemini-code-assist. Triage below.

Applied (fixed in d2bb8ff)

  • age of 0 rejected (high): good catch — createDog now validates the raw form value so a valid age of 0 (a puppy) is accepted instead of being treated as a missing field.
  • DeleteButton has no pending state (medium): now wrapped in useTransition, which disables the button and shows a "Deleting…" state while the server action runs — the React 19 / Next.js pattern you described.

Not applied — the "unreleased version" flags are false positives

The four critical comments claim next@16, eslint-config-next@16, @types/node@24, and Node 24.13.1 don't exist yet. They're all released and current — this looks like a training-cutoff gap rather than a real issue:

  • npm view next version16.2.11; @harperfast/nextjs@2.2.1 declares peerDependencies.next: "^14 || ^15 || ^16" and ships next-16 in its own devDependencies.
  • npm view eslint-config-next version16.2.11 (installs cleanly; note it also required switching to its native flat config — the old FlatCompat shim throws on ESLint 9).
  • Node 24 is current; every existing template in this repo already pins .nvmrc to 24.13.1 and uses @types/node ^24.10.1 (see template-react-ts), so these values are intentional and consistent — not new to this PR.
  • npm install + npm run build/tsc --noEmit were run against both variants successfully with these versions.

Downgrading to Next 15 / Node 22 would regress the templates and diverge from the rest of the catalog, so I'm keeping them as-is. 🙂

@gemini-code-assist

Copy link
Copy Markdown

Thanks for the update, @dawsontoth. The changes look great:

  • The age validation logic is now correct for handling 0.
  • Using useTransition for the DeleteButton is the correct pattern for handling pending states in React 19 / Next.js.

Regarding the versioning: Thanks for the clarification. I understand that these versions are intentional and consistent with the existing catalog, and that downgrading would cause unnecessary divergence. I appreciate the context on the environment and the verification steps you've taken.

Everything looks solid. Nice work on these templates!

@dawsontoth
dawsontoth marked this pull request as ready for review July 21, 2026 17:24
@dawsontoth
dawsontoth requested a review from a team as a code owner July 21, 2026 17:24
Local `harper run`/`deploy` was leaving the app unbuilt (every route 404'd)
because the template's server modules had a top-level `import 'harper'`. That
import breaks the plugin's on-startup production build two ways: Turbopack
(Next 16's default) can't resolve the bare `harper` specifier, and even under
webpack the import opens Harper's database during Next's build-time page-data
collection, colliding with the running instance's LMDB lock. On failure the
plugin logs and skips serving — so the app "deploys" but serves nothing (the
symptom seen deploying to Studio).

Harper already injects `tables` as a global into server-side code, so the
import is unnecessary: drop it and use `tables` directly. With that gone the
default Turbopack build succeeds and the app builds + serves under `harper run`.

Verified end to end on Harper 5.1.22: both variants build + serve via
`harper run`, TS type-checks, and a full create/list/detail/delete round-trip
(server actions + tables read/write, incl. age 0) works through the UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these just copied from nextjs-example?

Comment thread template-nextjs-ts/app/actions.ts Outdated
Comment thread template-nextjs-ts/app/actions.ts Outdated
Comment thread template-nextjs-ts/app/ui/DeleteButton.tsx Outdated
Comment thread template-nextjs-ts/app/actions.ts Outdated
Comment thread template-nextjs-ts/package.json
…arter

Replaces the ported "Doggy Management System" CRUD app with a bare starter whose
only Harper touch is a counter persisted in a `Count` table: a server component
reads it and a server action increments it. This matches the altitude of the
other create-harper templates (blank starter + schema scaffolding) and shows the
one thing unique to Next-on-Harper — reading/writing `tables` from server code —
without shipping a full app.

Addresses the PR review (thanks @kriszyp): dropping the CRUD surface removes the
unbounded table scan, the unauthenticated destructive `deleteDog` action, the
no-confirm delete button, and the bypassable form validation. The remaining
mutation is a single-key, non-destructive counter increment (no scan, no user
input); the README notes that any client-reachable action needs its own authz.

Removed: app/dogs/**, app/ui/DeleteButton, app/not-found. Both JS and TS
variants updated (+ harper.d.ts, READMEs).

Verified on Harper 5.1.22: both variants build + serve under `harper run`, TS
type-checks, and the counter increments and survives a restart (0 -> 1 -> 2,
still 2 after restart) — confirming it's persisted in the table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Are these just copied from nextjs-example?

Yep — originally ported straight from nextjs-example, and you're right that it dragged in a full CRUD app that's the wrong altitude for a scaffold. I've reworked both variants (56ed077) down to a bare starter that matches the other templates: the only Harper touch is a counter persisted in a Count table — a server component reads it, a server action increments it. That's the one thing unique to Next-on-Harper (server-side tables access), minus the app.

That removes the surface behind each of your notes:

  • Unbounded listDogs scan → gone; the counter is a single-key tables.Count.get('count') point lookup.
  • Unauthenticated destructive deleteDog → gone; no delete anywhere. The one remaining mutation is a non-destructive counter increment (no user input), and the README now calls out that any client-reachable action needs its own authz.
  • No-confirm delete button → gone.
  • Bypassable createDog validation → gone; the increment takes no user input.
  • Generated-project CI coverage → per Dawson, expanding the integration matrix to the Next.js variants is a separate follow-up PR; the much smaller surface here lowers the risk in the meantime. Happy to add a Next-tailored job if you'd prefer it in this PR.

Also worth flagging: a separate fix (e6f048a) dropped the top-level import 'harper' that was silently breaking the production build — both variants now build + serve under harper run on 5.1.22 (verified the counter persists across a restart).

dawsontoth and others added 4 commits July 21, 2026 15:31
Adds `nextjs` and `nextjs-ts` to the integration test matrix so CI installs,
lints, formats, builds, and boots them like every other template.

Two accommodations for the Next.js templates:
- The `run test` step is now conditional on a `test` script existing — the
  Next.js templates ship no unit tests (the runtime smoke is their coverage),
  while every existing template still runs its tests unchanged.
- A dedicated template.tests/nextSmoke.js replaces runtimeSmoke.js for the
  Next.js variants: they route through the @harperfast/nextjs plugin (no REST
  resource surface), so the smoke boots the app under Harper and asserts the
  on-startup build serves `/` and renders the Harper-backed counter. It builds
  single-threaded for determinism and uses the same isolated-/tmp-root,
  process-group-cleanup conventions as runtimeSmoke.js.

Verified locally on Harper 5.1.22: scaffold → install → lint → format → build
(cold `next build`) → nextSmoke all pass for both variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…yarn

The integration matrix surfaced two package-manager-specific failures the npm
path masked:

- pnpm `install` exited non-zero with ERR_PNPM_IGNORED_BUILDS for Next.js's
  native deps (sharp, unrs-resolver). Added `pnpm.onlyBuiltDependencies` to
  approve exactly those builds (npm/yarn ignore the field).
- `eslint .` crashed under yarn/pnpm with "Cannot find module 'typescript'":
  eslint-config-next pulls typescript-eslint, which requires typescript. npm
  happened to hoist it transitively; yarn/pnpm don't. Declared `typescript` as
  a devDependency in the JS template (the TS template already had it) so the
  Next.js lint config resolves under every package manager.

Verified locally with pnpm 10 and yarn 1: scaffold → install → lint → build all
pass for both variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Builds

The previous package.json `pnpm.onlyBuiltDependencies` field was a no-op on the
CI's pnpm (11.13.1): pnpm v11 removed that setting and replaced it with
`allowBuilds` in pnpm-workspace.yaml, so `pnpm install` still hard-failed with
ERR_PNPM_IGNORED_BUILDS for sharp/unrs-resolver. (My local pnpm 10.32 only
warned, which masked it.)

Ship a `pnpm-workspace.yaml` with `allowBuilds` in both templates instead, and
drop the dead package.json field. Verified with the pinned CI version via
`npx pnpm@11.13.1`: a freshly scaffolded nextjs-ts installs, lints, and builds
clean (EXIT 0); pnpm 10.32 also tolerates the key; npm and yarn ignore the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Deploying the Next.js templates with the default server-side build 404s on a
multi-threaded Harper cluster: worker threads run `next build` into the same
`.next` concurrently and race (ENOENT on `.next/BUILD_ID`), so the plugin never
serves. See HarperFast/nextjs#52. Until the plugin coordinates the build across
threads, ship a prebuilt app.

- config.yaml: `prebuilt: true` on `@harperfast/nextjs` (serve a prebuilt `.next`
  instead of building on startup).
- scripts: `dev`/`start`/`deploy` now run `next build` first, so `.next` exists
  before Harper serves it (the plugin honors `prebuilt` even in dev, so a bare
  `harper dev` would otherwise bail on a missing `.next`). `deploy_component`
  uploads `.next` because it doesn't consult `.gitignore` — only excludes
  node_modules — so `.next` can stay gitignored.
- nextSmoke.js: drop the forced THREADS_COUNT=1 so CI exercises multi-threaded
  serving and would catch a regression back to an on-startup build.
- README/config comments updated to describe the prebuilt deploy.

Verified on the stage cluster: prebuilt deploy serves and the counter reads +
writes live (browser). Locally: scaffold → install → build → multi-thread
nextSmoke passes for both variants; `next build && harper dev` serves with HMR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread template-nextjs/app/actions.js Outdated
Comment thread lib/constants/templates.js Outdated
dawsontoth and others added 2 commits July 22, 2026 11:14
The counter's read-then-write (`get` then `put value+1`) loses updates when
requests overlap — two workers read 7 and both write 8. Since this starter
deploys replicated and CI now runs multiple Harper workers, switch to Harper's
atomic delta inside a transaction (thanks @kriszyp):

    await transaction(async () => {
      const record = await tables.Count.update('count');
      record.addTo('value', 1);
    });

`addTo` is safe across worker threads and replicated nodes, and the transaction
creates the row on first use. Both `transaction` and `tables` are Harper runtime
globals — no `import 'harper'` (which would break the production build).

Mirrored across JS + TS: server actions, `harper.d.ts` types (adds `transaction`
+ `update(id).addTo`), eslint globals (`transaction`), and the README snippet.

Verified on a multi-threaded local instance: increments 0 -> 1 -> 2 from an
empty table; fresh scaffolds install/lint/tsc/build clean for both variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Next.js templates render on the server — the page is `force-dynamic` and
reads Harper on every request — but the catalog entries had `ssr: false`, so the
exported `create-harper/templates` contract reported the wrong capability and
the picker omitted "+ SSR" (thanks @kriszyp).

Set `ssr: true` for both entries. The `-ssr` name suffix is a Vite-variant
convention that doesn't apply to Next.js (which is always server-rendered), so
the `templates.test.js` invariant is relaxed to expect ssr=true for the nextjs
framework and the suffix rule for the rest. The picker now shows the variants as
"TypeScript + SSR" / "JavaScript + SSR".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth requested a review from kriszyp July 22, 2026 16:07

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally LGTM but I think we should fix the nextjs plugin first so you don't have to ship this with workarounds in place.

Comment thread lib/constants/templates.js
Comment thread template-nextjs-ts/app/actions.ts
Comment thread template-nextjs-ts/app/actions.ts
Comment thread template-nextjs-ts/config.yaml
Comment thread template-nextjs-ts/harper.d.ts
Comment thread template-nextjs/app/actions.js
Comment thread template-nextjs-ts/pnpm-workspace.yaml
Comment thread template.tests/nextSmoke.js
Comment thread template-nextjs-ts/app/actions.ts
Comment thread template-nextjs-ts/app/actions.ts
dawsontoth and others added 3 commits July 24, 2026 11:50
@harperfast/nextjs 2.2.2 fixed the multi-thread build race (serializes the build
across worker threads — HarperFast/nextjs#52), which is the only reason these
templates shipped `prebuilt: true` + build-first scripts. Revert to the same
server-side-build model as the other templates:

- config.yaml: drop `prebuilt: true` (Harper builds on `harper run`/deploy).
- scripts: `dev` → `harper dev .`, `start` → `harper run .`, `deploy` →
  `harper deploy_component ...` (no `next build &&` prefix).
- bump `@harperfast/nextjs` to `^2.2.3` so the build-serialization fix is required.
- README deploy note back to "Harper builds on the server, no local build".

Verified on Harper 5.1.22 with @harperfast/nextjs 2.2.3: a fresh scaffold builds
and serves multi-threaded via `harper run` (no prebuilt, no race) and the counter
works. (Server code still uses the injected `tables`/`transaction` globals, not
`import 'harper'` — that stays until HarperFast/nextjs#41 + #51 land.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per review: nextSmoke only GET'd `/`, so the write half of the counter (the
`transaction` + `addTo` server action and its revalidation) was untested. It now
invokes the increment action via its no-JS progressive-enhancement form POST
(multipart with the rendered `$ACTION_ID_<hash>` field) and asserts a fresh
reload shows the persisted count advanced by one. Also updated the header to
reflect the server-side (not prebuilt) build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s#57/#58)

Re-instates `prebuilt: true` + build-first scripts, reversing f8e4e35. That
commit dropped prebuilt assuming @harperfast/nextjs 2.2.2's build-lock (#52)
made the on-startup server build viable — but re-testing on a real cluster
(stage, Harper 5.1.23) shows the server-side build still fails, regardless of
Harper version or bundler:

- Turbopack (Next 16 default): `next build` crashes the instant it runs inside
  a Harper worker thread — `uncaughtException: Worker creator already
  registered` → build aborts, no .next, app 404s/500s. Filed HarperFast/nextjs#57.
- `bundler: webpack`: no crash, but a cold build overruns Harper's 30s
  `handleApplication` timeout → component load aborts. Filed HarperFast/nextjs#58.
- `prebuilt: true`: works — verified live on stage (serves + atomic increment
  persists across the replicated cluster).

Local `harper run`/`dev` and CI stay green because a plain-CLI `next build`
(not inside a Harper worker thread) succeeds; only a multi-thread cluster hits
the crash.

- config.yaml: `prebuilt: true`; comment cites #57/#58 (not the fixed #52).
- scripts: `dev`/`start`/`deploy` prefix `next build` again.
- nextSmoke.js: build the app if `.next` is absent (harper run serves prebuilt,
  it won't build on startup); wording updated. Verified locally on 5.1.22.

Catalog unchanged — studio: false stays (without an on-cluster build the plugin
is a poor Studio experience).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth

Copy link
Copy Markdown
Contributor Author

Restored prebuilt: true — the on-cluster server build doesn't work (filed upstream).

Re-tested the server-side-build model on a real cluster (stage, Harper 5.1.23). It fails regardless of bundler, so prebuilt is back in d1a5466, reversing the earlier drop:

Local harper run/dev and CI stay green because a plain-CLI next build (not inside a Harper worker thread) succeeds in ~850ms; only a multi-thread cluster hits the crash — which is why this slipped through when prebuilt was first dropped.

Once HarperFast/nextjs#57 and #58 land we can revisit dropping prebuilt (and reconsider studio: true). For now studio: false stays — without a working on-cluster build, using the plugin directly in Studio is a poor experience.

Follow-up to d1a5466 (prebuilt restore): the integration workflow comment still
described the old "on-startup build" model. The Next.js smoke now serves the
prebuilt output produced by the `build` step. Split into its own commit because
pushing workflow files needs `workflow` OAuth scope (the HTTPS push used for
d1a5466 rejects `.github/workflows/**`).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dawsontoth
dawsontoth requested a review from kriszyp July 24, 2026 18:21
@dawsontoth
dawsontoth merged commit c9e36c1 into main Jul 27, 2026
33 checks passed
@dawsontoth
dawsontoth deleted the claude/create-harper-nextjs-scaffold-851671 branch July 27, 2026 15:43
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.11.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants