diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 8347b75..1448731 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -14,7 +14,16 @@ jobs: fail-fast: false matrix: pkg-manager: [npm, yarn, pnpm] - template: [vanilla, vanilla-ts, react, react-ts, vue, vue-ts] + template: [ + vanilla, + vanilla-ts, + react, + react-ts, + vue, + vue-ts, + nextjs, + nextjs-ts, + ] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -136,8 +145,12 @@ jobs: echo "Running format check..." ${{ matrix.pkg-manager }} run format - echo "Running tests..." - ${{ matrix.pkg-manager }} run test + # Run tests if the template defines a test script (the Next.js templates don't ship + # unit tests — their runtime smoke below is their coverage). + if grep -q '"test":' package.json; then + echo "Running tests..." + ${{ matrix.pkg-manager }} run test + fi # Check for build script and run it if exists if grep -q '"build":' package.json; then @@ -145,15 +158,25 @@ jobs: ${{ matrix.pkg-manager }} run build fi - # Boot the generated app under a real Harper instance and verify both the REST - # resources and the frontend are reachable (guards against the static handler - # swallowing REST GETs). Once per template — the package manager doesn't matter here. + # Boot the generated app under a real Harper instance and verify its HTTP surface. Once + # per template — the package manager doesn't matter here. The Next.js templates route + # through the @harperfast/nextjs plugin (no REST resources), so they use a dedicated + # smoke that serves the prebuilt build (produced by the `build` step above) and checks the + # Harper-backed counter reads and increments; every other template uses runtimeSmoke, + # which also guards against the static handler swallowing REST GETs. if [ "${{ matrix.pkg-manager }}" = "npm" ]; then echo "Installing Harper..." npm install --global harper echo "Running runtime smoke test..." - node "$REPO_PATH/template.tests/runtimeSmoke.js" . + case "${{ matrix.template }}" in + nextjs | nextjs-ts) + node "$REPO_PATH/template.tests/nextSmoke.js" . + ;; + *) + node "$REPO_PATH/template.tests/runtimeSmoke.js" . + ;; + esac fi echo "Integration test passed for ${{ matrix.pkg-manager }} with ${{ matrix.template }}!" diff --git a/README.md b/README.md index eaea8c8..2e703e3 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,8 @@ Currently supported template presets include: - `vanilla-ts` - `react` - `react-ts` +- `nextjs` +- `nextjs-ts` You can use `.` for the project name to scaffold in the current directory. diff --git a/lib/constants/frameworks.js b/lib/constants/frameworks.js index 8429087..2108594 100644 --- a/lib/constants/frameworks.js +++ b/lib/constants/frameworks.js @@ -5,6 +5,7 @@ const { blue, cyan, green, + magenta, yellow, } = colors; @@ -28,6 +29,7 @@ const frameworkMeta = { vanilla: { display: 'Vanilla', color: yellow }, react: { display: 'React', color: cyan }, vue: { display: 'Vue', color: green }, + nextjs: { display: 'Next.js', color: magenta }, }; /** diff --git a/lib/constants/templates.d.ts b/lib/constants/templates.d.ts index 0090253..cebe5d7 100644 --- a/lib/constants/templates.d.ts +++ b/lib/constants/templates.d.ts @@ -1,4 +1,4 @@ -export type Framework = 'vanilla' | 'react' | 'vue'; +export type Framework = 'vanilla' | 'react' | 'vue' | 'nextjs'; export type TemplateName = | 'vanilla-ts' @@ -10,7 +10,9 @@ export type TemplateName = | 'vue-ts' | 'vue' | 'vue-ts-ssr' - | 'vue-ssr'; + | 'vue-ssr' + | 'nextjs-ts' + | 'nextjs'; export interface TemplateInfo { /** The canonical template name (e.g. 'vanilla', 'react-ts'). Used to scaffold. */ @@ -27,6 +29,11 @@ export interface TemplateInfo { typescript: boolean; /** Whether the template is server-side rendered. */ ssr: boolean; + /** + * Whether a Studio template package is built and published for this template. Defaults to true; + * set to false for templates that don't (yet) run in the Studio's deploy-only model. + */ + studio?: boolean; /** The published Studio template package for this template. */ npmPackage: string; /** A link to the template's source on GitHub. */ @@ -51,4 +58,13 @@ export declare const templateNames: readonly [ 'vue', 'vue-ts-ssr', 'vue-ssr', + 'nextjs-ts', + 'nextjs', ]; + +/** + * The subset of {@link templateNames} for which a Studio template package is built and published + * (every template whose catalog entry does not set `studio: false`). Consumed by the Studio + * build/publish scripts. + */ +export declare const studioTemplateNames: readonly TemplateName[]; diff --git a/lib/constants/templates.js b/lib/constants/templates.js index 1a4ffa2..411354c 100644 --- a/lib/constants/templates.js +++ b/lib/constants/templates.js @@ -1,7 +1,7 @@ const CREATE_HARPER_TREE = 'https://github.com/HarperFast/create-harper/tree/main'; /** - * @typedef {'vanilla' | 'react' | 'vue'} Framework + * @typedef {'vanilla' | 'react' | 'vue' | 'nextjs'} Framework */ /** @@ -13,6 +13,9 @@ const CREATE_HARPER_TREE = 'https://github.com/HarperFast/create-harper/tree/mai * @property {string[]} tags - Tags describing the template's stack. * @property {boolean} typescript - Whether the template uses TypeScript. * @property {boolean} ssr - Whether the template is server-side rendered. + * @property {boolean} [studio] - Whether a Studio template package is built and published for this + * template. Defaults to true; set to false for templates that don't (yet) run in the Studio's + * deploy-only model. See {@link studioTemplateNames}. * @property {string} npmPackage - The published Studio template package for this template. * @property {string} githubUrl - A link to the template's source on GitHub. */ @@ -131,6 +134,31 @@ export const templates = [ typescript: false, ssr: true, }), + template({ + name: 'nextjs-ts', + framework: 'nextjs', + title: 'Next.js + TypeScript', + description: 'A type-safe Next.js app that reads and writes Harper tables directly from server actions.', + tags: ['Next.js', 'TypeScript', 'React', 'App Router'], + typescript: true, + // Next.js renders on the server (the app is force-dynamic and reads Harper per request), so + // this is SSR even though the name has no `-ssr` suffix (that suffix marks the Vite variants). + ssr: true, + // Not built/published as a Studio template: the plugin needs a prebuilt deploy (its on-startup + // build races on multi-thread clusters — HarperFast/nextjs#52) and Studio's edit-in-place + // model doesn't rebuild, so it isn't a fit today. + studio: false, + }), + template({ + name: 'nextjs', + framework: 'nextjs', + title: 'Next.js', + description: "A Next.js app wired to Harper's Resource API, reading and writing tables from server actions.", + tags: ['Next.js', 'React', 'App Router'], + typescript: false, + ssr: true, + studio: false, + }), ]; /** @@ -140,3 +168,12 @@ export const templates = [ * @type {string[]} */ export const templateNames = templates.map((t) => t.name); + +/** + * The subset of {@link templateNames} for which a Studio template package is built and published + * (i.e. every template except those with `studio: false`). The Studio build/publish scripts iterate + * this instead of {@link templateNames} so opt-out templates are skipped. + * + * @type {string[]} + */ +export const studioTemplateNames = templates.filter((t) => t.studio !== false).map((t) => t.name); diff --git a/lib/constants/templates.test.js b/lib/constants/templates.test.js index d78a826..4783c94 100644 --- a/lib/constants/templates.test.js +++ b/lib/constants/templates.test.js @@ -1,9 +1,9 @@ import colors from 'picocolors'; import { describe, expect, test } from 'vitest'; import { frameworks } from './frameworks.js'; -import { templateNames, templates as catalog } from './templates.js'; +import { studioTemplateNames, templateNames, templates as catalog } from './templates.js'; -const { blue, cyan, green, yellow } = colors; +const { blue, cyan, green, magenta, yellow } = colors; describe('templates catalog', () => { test('templateNames is the exact, ordered list of names (keeps templates.d.ts honest)', () => { @@ -18,6 +18,8 @@ describe('templates catalog', () => { 'vue', 'vue-ts-ssr', 'vue-ssr', + 'nextjs-ts', + 'nextjs', ]); }); @@ -32,17 +34,36 @@ describe('templates catalog', () => { } }); - test('typescript and ssr flags match the template name suffixes', () => { + test('typescript and ssr flags are correct per template', () => { for (const t of catalog) { expect(t.typescript).toBe(t.name.includes('-ts')); - expect(t.ssr).toBe(t.name.endsWith('-ssr')); + // The `-ssr` suffix marks the Vite SSR variants; Next.js is always server-rendered. + const expectedSsr = t.framework === 'nextjs' ? true : t.name.endsWith('-ssr'); + expect(t.ssr).toBe(expectedSsr); } }); + + test('studioTemplateNames excludes the studio: false templates (nextjs)', () => { + expect(studioTemplateNames).toEqual([ + 'vanilla-ts', + 'vanilla', + 'react-ts', + 'react', + 'react-ts-ssr', + 'react-ssr', + 'vue-ts', + 'vue', + 'vue-ts-ssr', + 'vue-ssr', + ]); + expect(studioTemplateNames).not.toContain('nextjs'); + expect(studioTemplateNames).not.toContain('nextjs-ts'); + }); }); describe('frameworks (derived from the catalog)', () => { test('groups templates by framework in catalog order', () => { - expect(frameworks.map((f) => f.name)).toEqual(['vanilla', 'react', 'vue']); + expect(frameworks.map((f) => f.name)).toEqual(['vanilla', 'react', 'vue', 'nextjs']); }); test('matches the expected display names and colors', () => { @@ -78,6 +99,15 @@ describe('frameworks (derived from the catalog)', () => { { name: 'vue-ssr', display: 'JavaScript + SSR', color: yellow }, ], }, + { + name: 'nextjs', + display: 'Next.js', + color: magenta, + variants: [ + { name: 'nextjs-ts', display: 'TypeScript + SSR', color: blue }, + { name: 'nextjs', display: 'JavaScript + SSR', color: yellow }, + ], + }, ]); }); }); diff --git a/package.json b/package.json index e5acb61..b4750ec 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,9 @@ "template-vue/", "template-vue-ts/", "template-vue-ssr/", - "template-vue-ts-ssr/" + "template-vue-ts-ssr/", + "template-nextjs/", + "template-nextjs-ts/" ], "scripts": { "commitlint": "commitlint --edit", diff --git a/template-nextjs-ts/README.md b/template-nextjs-ts/README.md new file mode 100644 index 0000000..6f96d52 --- /dev/null +++ b/template-nextjs-ts/README.md @@ -0,0 +1,79 @@ +# your-project-name-here + +A type-safe [Next.js](https://nextjs.org) app running on Harper via [`@harperfast/nextjs`](https://github.com/HarperFast/nextjs). Your new app is now ready for development! + +Because the app runs _inside_ Harper, server-side code (server actions and server components) reads and writes your database directly through the injected `tables` global — no separate API server and no network round-trip. + +The starter ships one tiny end-to-end example: a counter stored in a Harper table, read by a server component and incremented by a server action. + +## Installation + +Make sure you have [installed Harper](https://docs.harperdb.io/docs/deployments/install-harper): + +```sh +npm install -g harper +``` + +## Development + +Start the app: + +```sh +npm run dev +``` + +Then open [http://localhost:9926](http://localhost:9926) 🎉 + +Click the button — the count persists in Harper across reloads and restarts. + +### Define Your Schema + +Your tables live in [`schema.graphql`](./schema.graphql). The starter defines a single `Count` table; add your own `@table` types there, then mirror their shape in [`harper.d.ts`](./harper.d.ts) so your server code stays type-safe. (The `@harperfast/schema-codegen` component can also generate these types for you.) + +### Access Harper From Server Code + +Harper injects `tables` and `transaction` globals into server-side code, so server actions and server components read and write your database directly — no import needed (their types come from [`harper.d.ts`](./harper.d.ts)). Use an atomic `addTo` inside a `transaction` for writes that stay correct when requests overlap across worker threads and replicated nodes (a read-then-write would lose concurrent increments): + +```ts +'use server'; + +export async function getCount(): Promise { + const record = await tables.Count.get('count'); + return record?.value ?? 0; +} + +export async function increment(): Promise { + await transaction(async () => { + const record = await tables.Count.update('count'); + record.addTo('value', 1); + }); +} +``` + +> **Don't** add a top-level `import 'harper'` in these modules. It runs during the Next.js production build (when Next collects page data) and conflicts with the running database — use the injected globals instead. + +Put data access in **server actions** (see [`app/actions.ts`](./app/actions.ts)) so that both server _and_ client components can share the same functions. Any action a client can reach is a public endpoint, so add your own authorization checks before shipping mutations that matter. + +## Deployment + +When you are ready, head to [https://fabric.harper.fast/](https://fabric.harper.fast/), log in to your account, and create a cluster. + +Come back and log your local CLI into your cluster: + +```sh +harper login +``` + +Then deploy your app: + +```sh +npm run deploy +``` + +`npm run deploy` runs `next build` locally and ships the prebuilt `.next` output, then Harper serves it — no build runs on the cluster. (Building on the cluster currently fails; see the note in [`config.yaml`](./config.yaml).) + +## Keep Going! + +For more on building Harper applications, see the [getting started guide](https://docs.harperdb.io/docs). + +For more on Harper Components, see the [Components documentation](https://docs.harperdb.io/docs/reference/components). diff --git a/template-nextjs-ts/_aiignore b/template-nextjs-ts/_aiignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/template-nextjs-ts/_aiignore @@ -0,0 +1 @@ +.env diff --git a/template-nextjs-ts/_claude/launch.json b/template-nextjs-ts/_claude/launch.json new file mode 100644 index 0000000..a2d9719 --- /dev/null +++ b/template-nextjs-ts/_claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "harper", + "runtimeExecutable": "your-package-manager-here", + "runtimeArgs": ["run", "dev"], + "port": 9926 + } + ] +} diff --git a/template-nextjs-ts/_env b/template-nextjs-ts/_env new file mode 100644 index 0000000..9f42293 --- /dev/null +++ b/template-nextjs-ts/_env @@ -0,0 +1 @@ +CLI_TARGET='your-fabric.harper.fast-cluster-url-here' diff --git a/template-nextjs-ts/_env.example b/template-nextjs-ts/_env.example new file mode 100644 index 0000000..d071546 --- /dev/null +++ b/template-nextjs-ts/_env.example @@ -0,0 +1 @@ +CLI_TARGET='YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE' diff --git a/template-nextjs-ts/_github/workflow/deploy.yaml b/template-nextjs-ts/_github/workflow/deploy.yaml new file mode 100644 index 0000000..2dc4dc5 --- /dev/null +++ b/template-nextjs-ts/_github/workflow/deploy.yaml @@ -0,0 +1,31 @@ +name: Deploy to Harper Fabric +on: + workflow_dispatch: +# push: +# branches: +# - main + +concurrency: + group: main + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + fetch-tags: true + - name: Set up Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + cache: 'npm' + node-version-file: '.nvmrc' + - name: Install dependencies + run: npm ci + - name: Run lint + run: npm run lint + - name: Build & deploy + run: npm run deploy diff --git a/template-nextjs-ts/_gitignore b/template-nextjs-ts/_gitignore new file mode 100644 index 0000000..2b71457 --- /dev/null +++ b/template-nextjs-ts/_gitignore @@ -0,0 +1,34 @@ +.DS_Store + +# dependencies +node_modules/ +.pnp +.pnp.* + +# next.js +/.next/ +/out/ +/build + +# debug logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# testing +/coverage + +# typescript +*.tsbuildinfo +next-env.d.ts + +# dotenv environment variable files +.env +.env.* +!.env.example + +# misc +*.pem +.vercel diff --git a/template-nextjs-ts/_nvmrc b/template-nextjs-ts/_nvmrc new file mode 100644 index 0000000..32f8c50 --- /dev/null +++ b/template-nextjs-ts/_nvmrc @@ -0,0 +1 @@ +24.13.1 diff --git a/template-nextjs-ts/app/actions.ts b/template-nextjs-ts/app/actions.ts new file mode 100644 index 0000000..4555886 --- /dev/null +++ b/template-nextjs-ts/app/actions.ts @@ -0,0 +1,25 @@ +'use server'; + +import { revalidatePath } from 'next/cache'; + +// Server actions run *inside* Harper, so they read and write tables directly through the injected +// globals — no separate API and no network round-trip. Harper provides `tables` and `transaction`, +// so use them directly; do NOT add a top-level `import 'harper'` — that import runs during the +// Next.js production build and conflicts with the running database. + +// The whole counter lives in a single row keyed 'count', so reads are a cheap point lookup. +export async function getCount(): Promise { + const record = await tables.Count.get('count'); + return record?.value ?? 0; +} + +export async function increment(): Promise { + // Atomic increment: `addTo` inside a transaction is safe when requests overlap across worker + // threads and replicated nodes — a read-then-write would lose concurrent increments. The + // transaction also creates the row on first use. + await transaction(async () => { + const record = await tables.Count.update('count'); + record.addTo('value', 1); + }); + revalidatePath('/'); +} diff --git a/template-nextjs-ts/app/layout.tsx b/template-nextjs-ts/app/layout.tsx new file mode 100644 index 0000000..29dcd24 --- /dev/null +++ b/template-nextjs-ts/app/layout.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from 'react'; + +export const metadata = { + title: 'Next.js on Harper', + description: 'A Next.js app powered by Harper', +}; + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/template-nextjs-ts/app/page.tsx b/template-nextjs-ts/app/page.tsx new file mode 100644 index 0000000..ea1470b --- /dev/null +++ b/template-nextjs-ts/app/page.tsx @@ -0,0 +1,55 @@ +import { getCount, increment } from './actions'; + +// Read the count fresh on every request rather than caching it at build time. +export const dynamic = 'force-dynamic'; + +export default async function Page() { + const count = await getCount(); + + return ( +
+

Next.js + Harper

+

+ This counter is stored in a Harper table. Clicking the button calls a{' '} + + server action + {' '} + that reads and writes the table directly — no separate API. +

+ { + /* A plain form calling a server action: no client component needed, and the browser + shows its own pending state while the action runs. */ + } +
+ +
+

+ Edit the schema in schema.graphql and the logic in app/actions.ts. +

+
+ ); +} diff --git a/template-nextjs-ts/config.yaml b/template-nextjs-ts/config.yaml new file mode 100644 index 0000000..42e3a34 --- /dev/null +++ b/template-nextjs-ts/config.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=./node_modules/harper/config-app.schema.json + +# This is the configuration file for the application. +# It specifies built-in Harper components that will load the specified feature and files. +# For more information, see https://docs.harperdb.io/docs/reference/components/built-in-extensions + +# Load Environment Variables from the specified file +# loadEnv: +# files: '.env' + +# Reads GraphQL schemas to define the schema of database/tables/attributes. +graphqlSchema: + files: 'schema.graphql' + +# Runs the Next.js app as a Harper component. The npm scripts run `next build` first, then this +# component serves the prebuilt `.next` output (`prebuilt: true` below); `harper dev` builds once +# and then serves with hot-module reloading. Server-side code (server actions and server components) +# reads and writes Harper tables directly through the injected `tables` and `transaction` globals — +# see app/actions.ts. Because this plugin owns HTTP routing for the app, there is no `static` or +# `rest` handler here. +'@harperfast/nextjs': + package: '@harperfast/nextjs' + # Serve a prebuilt `.next` (the npm scripts run `next build` for you) instead of building on + # startup. Building on a Harper cluster currently fails — the Turbopack build crashes inside a + # worker thread, and a webpack build overruns the component-load timeout — so the app never + # serves. Prebuilding locally sidesteps both. See + # https://github.com/HarperFast/nextjs/issues/57 and https://github.com/HarperFast/nextjs/issues/58 + prebuilt: true diff --git a/template-nextjs-ts/eslint.config.mjs b/template-nextjs-ts/eslint.config.mjs new file mode 100644 index 0000000..2c10208 --- /dev/null +++ b/template-nextjs-ts/eslint.config.mjs @@ -0,0 +1,19 @@ +import next from 'eslint-config-next'; + +const eslintConfig = [ + ...next, + { + // `tables` and `transaction` are globals injected by the Harper runtime for server-side code. + languageOptions: { + globals: { + tables: 'readonly', + transaction: 'readonly', + }, + }, + }, + { + ignores: ['.next/', 'node_modules/'], + }, +]; + +export default eslintConfig; diff --git a/template-nextjs-ts/graphql.config.yml b/template-nextjs-ts/graphql.config.yml new file mode 100644 index 0000000..68d14fb --- /dev/null +++ b/template-nextjs-ts/graphql.config.yml @@ -0,0 +1,3 @@ +schema: schema.graphql +include: node_modules/harper/schema.graphql +documents: '**/*.graphql' diff --git a/template-nextjs-ts/harper.d.ts b/template-nextjs-ts/harper.d.ts new file mode 100644 index 0000000..2a8fe5a --- /dev/null +++ b/template-nextjs-ts/harper.d.ts @@ -0,0 +1,30 @@ +// Ambient type declarations for the Harper runtime. +// +// Harper injects globals into server-side code: `tables` (one entry per `@table` in schema.graphql) +// and `transaction` (runs a callback in a committing transaction). These hand-written declarations +// give you type-safe access. As you add tables and columns, mirror them here. (The +// `@harperfast/schema-codegen` component can also generate the table types for you.) + +export interface CountRecord { + id: string; + value: number; +} + +/** An updatable record from `tables..update(id)`, mutated inside a `transaction(...)`. */ +interface UpdatableRecord { + addTo(property: keyof T, value: number): void; + subtractFrom(property: keyof T, value: number): void; +} + +interface HarperTable { + get(id: string): Promise; + update(id: string): Promise>; +} + +declare global { + const tables: { + Count: HarperTable; + }; + /** Run `callback` in a Harper transaction, committing its writes when it resolves. */ + function transaction(callback: () => T | Promise): Promise; +} diff --git a/template-nextjs-ts/next.config.mjs b/template-nextjs-ts/next.config.mjs new file mode 100644 index 0000000..68f2254 --- /dev/null +++ b/template-nextjs-ts/next.config.mjs @@ -0,0 +1,6 @@ +import { withHarper } from '@harperfast/nextjs'; + +// `withHarper` wires this Next.js app into Harper: it marks the `harper` package as a server +// external so `import 'harper'` resolves to the running Harper runtime (rather than being bundled), +// giving server-side code access to the `tables` global. Add your own Next.js config inside. +export default withHarper({}); diff --git a/template-nextjs-ts/package.json b/template-nextjs-ts/package.json new file mode 100644 index 0000000..2c62548 --- /dev/null +++ b/template-nextjs-ts/package.json @@ -0,0 +1,31 @@ +{ + "name": "your-package-name-here", + "version": "0.0.0", + "private": true, + "repository": "github:HarperFast/create-harper", + "scripts": { + "agent:run": "npx -y @harperfast/agent@latest", + "agent:skills:update": "npx -y skills@latest add harperfast/skills --all --yes", + "start": "next build && harper run .", + "dev": "next build && harper dev .", + "build": "next build", + "lint": "eslint .", + "format": "prettier --write .", + "deploy": "next build && harper deploy_component . restart=true replicated=true" + }, + "dependencies": { + "@harperfast/nextjs": "^2.2.3", + "next": "^16.2.11", + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "@types/react": "^19.2.10", + "@types/react-dom": "^19.2.3", + "eslint": "^9.27.0", + "eslint-config-next": "^16.2.11", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + } +} diff --git a/template-nextjs-ts/pnpm-workspace.yaml b/template-nextjs-ts/pnpm-workspace.yaml new file mode 100644 index 0000000..87361f1 --- /dev/null +++ b/template-nextjs-ts/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +# Pre-approve the build scripts for Next.js's native dependencies so `pnpm install` doesn't halt +# on them (pnpm 10.12+/11 gate dependency build scripts by default). npm and yarn ignore this file. +allowBuilds: + sharp: true + unrs-resolver: true diff --git a/template-nextjs-ts/schema.graphql b/template-nextjs-ts/schema.graphql new file mode 100644 index 0000000..a5fbc69 --- /dev/null +++ b/template-nextjs-ts/schema.graphql @@ -0,0 +1,4 @@ +type Count @table @export { + id: ID @primaryKey + value: Int +} diff --git a/template-nextjs-ts/tsconfig.json b/template-nextjs-ts/tsconfig.json new file mode 100644 index 0000000..37f533a --- /dev/null +++ b/template-nextjs-ts/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/template-nextjs/README.md b/template-nextjs/README.md new file mode 100644 index 0000000..f274945 --- /dev/null +++ b/template-nextjs/README.md @@ -0,0 +1,79 @@ +# your-project-name-here + +A [Next.js](https://nextjs.org) app running on Harper via [`@harperfast/nextjs`](https://github.com/HarperFast/nextjs). Your new app is now ready for development! + +Because the app runs _inside_ Harper, server-side code (server actions and server components) reads and writes your database directly through the injected `tables` global — no separate API server and no network round-trip. + +The starter ships one tiny end-to-end example: a counter stored in a Harper table, read by a server component and incremented by a server action. + +## Installation + +Make sure you have [installed Harper](https://docs.harperdb.io/docs/deployments/install-harper): + +```sh +npm install -g harper +``` + +## Development + +Start the app: + +```sh +npm run dev +``` + +Then open [http://localhost:9926](http://localhost:9926) 🎉 + +Click the button — the count persists in Harper across reloads and restarts. + +### Define Your Schema + +Your tables live in [`schema.graphql`](./schema.graphql). The starter defines a single `Count` table; add your own `@table` types there and they become available on the `tables` global. + +### Access Harper From Server Code + +Harper injects `tables` and `transaction` globals into server-side code, so server actions and server components read and write your database directly — no import needed. Use an atomic `addTo` inside a `transaction` for writes that stay correct when requests overlap across worker threads and replicated nodes (a read-then-write would lose concurrent increments): + +```js +'use server'; + +export async function getCount() { + const record = await tables.Count.get('count'); + return record?.value ?? 0; +} + +export async function increment() { + await transaction(async () => { + const record = await tables.Count.update('count'); + record.addTo('value', 1); + }); +} +``` + +> **Don't** add a top-level `import 'harper'` in these modules. It runs during the Next.js production build (when Next collects page data) and conflicts with the running database — use the injected globals instead. + +Put data access in **server actions** (see [`app/actions.js`](./app/actions.js)) so that both server _and_ client components can share the same functions. Any action a client can reach is a public endpoint, so add your own authorization checks before shipping mutations that matter. + +## Deployment + +When you are ready, head to [https://fabric.harper.fast/](https://fabric.harper.fast/), log in to your account, and create a cluster. + +Come back and log your local CLI into your cluster: + +```sh +harper login +``` + +Then deploy your app: + +```sh +npm run deploy +``` + +`npm run deploy` runs `next build` locally and ships the prebuilt `.next` output, then Harper serves it — no build runs on the cluster. (Building on the cluster currently fails; see the note in [`config.yaml`](./config.yaml).) + +## Keep Going! + +For more on building Harper applications, see the [getting started guide](https://docs.harperdb.io/docs). + +For more on Harper Components, see the [Components documentation](https://docs.harperdb.io/docs/reference/components). diff --git a/template-nextjs/_aiignore b/template-nextjs/_aiignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/template-nextjs/_aiignore @@ -0,0 +1 @@ +.env diff --git a/template-nextjs/_claude/launch.json b/template-nextjs/_claude/launch.json new file mode 100644 index 0000000..a2d9719 --- /dev/null +++ b/template-nextjs/_claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "harper", + "runtimeExecutable": "your-package-manager-here", + "runtimeArgs": ["run", "dev"], + "port": 9926 + } + ] +} diff --git a/template-nextjs/_env b/template-nextjs/_env new file mode 100644 index 0000000..9f42293 --- /dev/null +++ b/template-nextjs/_env @@ -0,0 +1 @@ +CLI_TARGET='your-fabric.harper.fast-cluster-url-here' diff --git a/template-nextjs/_env.example b/template-nextjs/_env.example new file mode 100644 index 0000000..d071546 --- /dev/null +++ b/template-nextjs/_env.example @@ -0,0 +1 @@ +CLI_TARGET='YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE' diff --git a/template-nextjs/_github/workflow/deploy.yaml b/template-nextjs/_github/workflow/deploy.yaml new file mode 100644 index 0000000..2dc4dc5 --- /dev/null +++ b/template-nextjs/_github/workflow/deploy.yaml @@ -0,0 +1,31 @@ +name: Deploy to Harper Fabric +on: + workflow_dispatch: +# push: +# branches: +# - main + +concurrency: + group: main + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + fetch-tags: true + - name: Set up Node.js + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + cache: 'npm' + node-version-file: '.nvmrc' + - name: Install dependencies + run: npm ci + - name: Run lint + run: npm run lint + - name: Build & deploy + run: npm run deploy diff --git a/template-nextjs/_gitignore b/template-nextjs/_gitignore new file mode 100644 index 0000000..2b71457 --- /dev/null +++ b/template-nextjs/_gitignore @@ -0,0 +1,34 @@ +.DS_Store + +# dependencies +node_modules/ +.pnp +.pnp.* + +# next.js +/.next/ +/out/ +/build + +# debug logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# testing +/coverage + +# typescript +*.tsbuildinfo +next-env.d.ts + +# dotenv environment variable files +.env +.env.* +!.env.example + +# misc +*.pem +.vercel diff --git a/template-nextjs/_nvmrc b/template-nextjs/_nvmrc new file mode 100644 index 0000000..32f8c50 --- /dev/null +++ b/template-nextjs/_nvmrc @@ -0,0 +1 @@ +24.13.1 diff --git a/template-nextjs/app/actions.js b/template-nextjs/app/actions.js new file mode 100644 index 0000000..5158e0c --- /dev/null +++ b/template-nextjs/app/actions.js @@ -0,0 +1,26 @@ +'use server'; + +/* global tables, transaction */ +import { revalidatePath } from 'next/cache'; + +// Server actions run *inside* Harper, so they read and write tables directly through the injected +// globals — no separate API and no network round-trip. Harper provides `tables` and `transaction`, +// so use them directly; do NOT add a top-level `import 'harper'` — that import runs during the +// Next.js production build and conflicts with the running database. + +// The whole counter lives in a single row keyed 'count', so reads are a cheap point lookup. +export async function getCount() { + const record = await tables.Count.get('count'); + return record?.value ?? 0; +} + +export async function increment() { + // Atomic increment: `addTo` inside a transaction is safe when requests overlap across worker + // threads and replicated nodes — a read-then-write would lose concurrent increments. The + // transaction also creates the row on first use. + await transaction(async () => { + const record = await tables.Count.update('count'); + record.addTo('value', 1); + }); + revalidatePath('/'); +} diff --git a/template-nextjs/app/layout.js b/template-nextjs/app/layout.js new file mode 100644 index 0000000..8a05229 --- /dev/null +++ b/template-nextjs/app/layout.js @@ -0,0 +1,14 @@ +export const metadata = { + title: 'Next.js on Harper', + description: 'A Next.js app powered by Harper', +}; + +export default function RootLayout({ children }) { + return ( + + + {children} + + + ); +} diff --git a/template-nextjs/app/page.js b/template-nextjs/app/page.js new file mode 100644 index 0000000..224f61a --- /dev/null +++ b/template-nextjs/app/page.js @@ -0,0 +1,55 @@ +import { getCount, increment } from './actions'; + +// Read the count fresh on every request rather than caching it at build time. +export const dynamic = 'force-dynamic'; + +export default async function Page() { + const count = await getCount(); + + return ( +
+

Next.js + Harper

+

+ This counter is stored in a Harper table. Clicking the button calls a{' '} + + server action + {' '} + that reads and writes the table directly — no separate API. +

+ { + /* A plain form calling a server action: no client component needed, and the browser + shows its own pending state while the action runs. */ + } +
+ + +

+ Edit the schema in schema.graphql and the logic in app/actions.js. +

+
+ ); +} diff --git a/template-nextjs/config.yaml b/template-nextjs/config.yaml new file mode 100644 index 0000000..deb38ac --- /dev/null +++ b/template-nextjs/config.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=./node_modules/harper/config-app.schema.json + +# This is the configuration file for the application. +# It specifies built-in Harper components that will load the specified feature and files. +# For more information, see https://docs.harperdb.io/docs/reference/components/built-in-extensions + +# Load Environment Variables from the specified file +# loadEnv: +# files: '.env' + +# Reads GraphQL schemas to define the schema of database/tables/attributes. +graphqlSchema: + files: 'schema.graphql' + +# Runs the Next.js app as a Harper component. The npm scripts run `next build` first, then this +# component serves the prebuilt `.next` output (`prebuilt: true` below); `harper dev` builds once +# and then serves with hot-module reloading. Server-side code (server actions and server components) +# reads and writes Harper tables directly through the injected `tables` and `transaction` globals — +# see app/actions.js. Because this plugin owns HTTP routing for the app, there is no `static` or +# `rest` handler here. +'@harperfast/nextjs': + package: '@harperfast/nextjs' + # Serve a prebuilt `.next` (the npm scripts run `next build` for you) instead of building on + # startup. Building on a Harper cluster currently fails — the Turbopack build crashes inside a + # worker thread, and a webpack build overruns the component-load timeout — so the app never + # serves. Prebuilding locally sidesteps both. See + # https://github.com/HarperFast/nextjs/issues/57 and https://github.com/HarperFast/nextjs/issues/58 + prebuilt: true diff --git a/template-nextjs/eslint.config.mjs b/template-nextjs/eslint.config.mjs new file mode 100644 index 0000000..2c10208 --- /dev/null +++ b/template-nextjs/eslint.config.mjs @@ -0,0 +1,19 @@ +import next from 'eslint-config-next'; + +const eslintConfig = [ + ...next, + { + // `tables` and `transaction` are globals injected by the Harper runtime for server-side code. + languageOptions: { + globals: { + tables: 'readonly', + transaction: 'readonly', + }, + }, + }, + { + ignores: ['.next/', 'node_modules/'], + }, +]; + +export default eslintConfig; diff --git a/template-nextjs/graphql.config.yml b/template-nextjs/graphql.config.yml new file mode 100644 index 0000000..68d14fb --- /dev/null +++ b/template-nextjs/graphql.config.yml @@ -0,0 +1,3 @@ +schema: schema.graphql +include: node_modules/harper/schema.graphql +documents: '**/*.graphql' diff --git a/template-nextjs/jsconfig.json b/template-nextjs/jsconfig.json new file mode 100644 index 0000000..5c64b06 --- /dev/null +++ b/template-nextjs/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/template-nextjs/next.config.mjs b/template-nextjs/next.config.mjs new file mode 100644 index 0000000..68f2254 --- /dev/null +++ b/template-nextjs/next.config.mjs @@ -0,0 +1,6 @@ +import { withHarper } from '@harperfast/nextjs'; + +// `withHarper` wires this Next.js app into Harper: it marks the `harper` package as a server +// external so `import 'harper'` resolves to the running Harper runtime (rather than being bundled), +// giving server-side code access to the `tables` global. Add your own Next.js config inside. +export default withHarper({}); diff --git a/template-nextjs/package.json b/template-nextjs/package.json new file mode 100644 index 0000000..219de5b --- /dev/null +++ b/template-nextjs/package.json @@ -0,0 +1,28 @@ +{ + "name": "your-package-name-here", + "version": "0.0.0", + "private": true, + "repository": "github:HarperFast/create-harper", + "scripts": { + "agent:run": "npx -y @harperfast/agent@latest", + "agent:skills:update": "npx -y skills@latest add harperfast/skills --all --yes", + "start": "next build && harper run .", + "dev": "next build && harper dev .", + "build": "next build", + "lint": "eslint .", + "format": "prettier --write .", + "deploy": "next build && harper deploy_component . restart=true replicated=true" + }, + "dependencies": { + "@harperfast/nextjs": "^2.2.3", + "next": "^16.2.11", + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "eslint": "^9.27.0", + "eslint-config-next": "^16.2.11", + "prettier": "^3.8.1", + "typescript": "^5.9.3" + } +} diff --git a/template-nextjs/pnpm-workspace.yaml b/template-nextjs/pnpm-workspace.yaml new file mode 100644 index 0000000..87361f1 --- /dev/null +++ b/template-nextjs/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +# Pre-approve the build scripts for Next.js's native dependencies so `pnpm install` doesn't halt +# on them (pnpm 10.12+/11 gate dependency build scripts by default). npm and yarn ignore this file. +allowBuilds: + sharp: true + unrs-resolver: true diff --git a/template-nextjs/schema.graphql b/template-nextjs/schema.graphql new file mode 100644 index 0000000..a5fbc69 --- /dev/null +++ b/template-nextjs/schema.graphql @@ -0,0 +1,4 @@ +type Count @table @export { + id: ID @primaryKey + value: Int +} diff --git a/template.tests/nextSmoke.js b/template.tests/nextSmoke.js new file mode 100644 index 0000000..9799873 --- /dev/null +++ b/template.tests/nextSmoke.js @@ -0,0 +1,195 @@ +#!/usr/bin/env node +/** + * Runtime smoke test for a generated Next.js-on-Harper template application. + * + * The `@harperfast/nextjs` plugin owns HTTP routing, so these apps don't expose the REST resource + * surface that runtimeSmoke.js checks. The templates ship `prebuilt: true`, so `harper run` serves + * a prebuilt `.next` rather than building on startup (an on-cluster build currently fails — + * @harperfast/nextjs#57 and #58). This smoke mirrors the real deploy flow: it builds the app (if it + * isn't already built), then boots it under a real Harper instance (isolated root, throwaway admin + * user, multi-threaded) and verifies the two things every Next.js template must do: + * + * 1. The app serves — `GET /` returns HTML rendering the Harper-backed counter (a server + * component reading the Count table). A missing or broken prebuilt leaves the plugin unable to + * serve, and this catches that. + * 2. The write path works — invoking the increment server action (transaction + addTo) advances + * the persisted count, verified by a fresh reload. GET-only would still pass if the write half + * were broken. + * + * Usage: node template.tests/nextSmoke.js + * + * The app must already be installed (this script runs `next build` if `.next` is absent). Requires + * the `harper` CLI on PATH (or set HARPER_BIN). POSIX only. Ports override via SMOKE_HTTP_PORT / + * SMOKE_OPS_PORT. + */ +import { spawn, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; + +const appDir = process.argv[2] && path.resolve(process.argv[2]); +if (!appDir || !fs.existsSync(path.join(appDir, 'config.yaml'))) { + console.error('Usage: node template.tests/nextSmoke.js (must contain a config.yaml)'); + process.exit(2); +} + +// The templates ship `prebuilt: true`: `harper run` serves an existing `.next` and refuses to build +// on startup, so make sure the app is built first. Skip when it already is (CI runs the template's +// `build` script before this smoke); otherwise build here so the script also works standalone. +if (!fs.existsSync(path.join(appDir, '.next', 'BUILD_ID'))) { + console.log('No prebuilt .next found — running `next build`...'); + const nextBin = path.join(appDir, 'node_modules', '.bin', 'next'); + const build = spawnSync(nextBin, ['build'], { cwd: appDir, stdio: 'inherit', env: process.env }); + if (build.status !== 0) { + console.error('`next build` failed; cannot run the prebuilt smoke.'); + process.exit(1); + } +} + +const httpPort = Number(process.env.SMOKE_HTTP_PORT ?? 19926); +const opsPort = Number(process.env.SMOKE_OPS_PORT ?? 19925); +const baseUrl = `http://127.0.0.1:${httpPort}`; + +// Harper's operations server listens on a unix domain socket inside ROOTPATH, and socket paths +// are limited to ~104 characters on macOS — so keep the scratch root short and in /tmp. +const scratchDir = fs.mkdtempSync('/tmp/harper-next-smoke-'); +const rootPath = path.join(scratchDir, 'hdb'); +const homeDir = path.join(scratchDir, 'home'); +fs.mkdirSync(rootPath); +fs.mkdirSync(homeDir); + +const harperBin = process.env.HARPER_BIN ?? 'harper'; +console.log(`Starting ${harperBin} run ${appDir} (root: ${rootPath}, port: ${httpPort})...`); +const harper = spawn(harperBin, ['run', appDir], { + env: { + ...process.env, + // HOME controls where Harper looks for the boot-properties file of an existing + // installation; pointing it at a scratch dir guarantees an isolated, fresh install. + HOME: homeDir, + TC_AGREEMENT: 'yes', + HDB_ADMIN_USERNAME: 'smoke-admin', + HDB_ADMIN_PASSWORD: 'smoke-password', + ROOTPATH: rootPath, + HTTP_PORT: String(httpPort), + OPERATIONSAPI_NETWORK_PORT: String(opsPort), + }, + // Own process group so cleanup can kill Harper's worker threads/children along with it. + detached: true, + stdio: ['ignore', 'pipe', 'pipe'], +}); + +let harperOutput = ''; +harper.stdout.on('data', (chunk) => (harperOutput += chunk)); +harper.stderr.on('data', (chunk) => (harperOutput += chunk)); +let harperExited = false; +harper.on('exit', () => (harperExited = true)); +harper.on('error', (error) => { + harperOutput += `\nFailed to spawn ${harperBin}: ${error.message}\n`; + harperExited = true; +}); + +function cleanup() { + try { + if (harper.pid) { + process.kill(-harper.pid, 'SIGTERM'); + } + } catch {} + try { + fs.rmSync(scratchDir, { recursive: true, force: true }); + } catch {} +} + +process.on('SIGINT', () => { + cleanup(); + process.exit(130); +}); +process.on('SIGTERM', () => { + cleanup(); + process.exit(143); +}); + +async function waitForServer(timeoutMs = 300_000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + if (harperExited) { + throw new Error(`Harper exited before the HTTP server came up. Output:\n${harperOutput.slice(-4000)}`); + } + try { + const response = await fetch(baseUrl + '/', { signal: AbortSignal.timeout(2000) }); + // The plugin serves once the build finishes; a pre-build request can 404, so wait for 2xx. + if (response.ok) { + return; + } + } catch {} + await new Promise((resolve) => setTimeout(resolve, 1500)); + } + throw new Error(`Harper did not serve the app within ${timeoutMs}ms. Output:\n${harperOutput.slice(-4000)}`); +} + +const failures = []; + +// Pull the counter value out of the rendered HTML ("count is "; React can split that text node). +function parseCount(html) { + const m = html.replace(/<[^>]+>/g, '').match(/count is\s*(\d+)/i); + return m ? Number(m[1]) : null; +} + +try { + await waitForServer(); + + // 1. The app builds + serves, and the home page renders the Harper-backed counter (a server + // component reading the Count table). + const response = await fetch(baseUrl + '/', { signal: AbortSignal.timeout(10_000) }); + const html = await response.text(); + const contentType = response.headers.get('content-type') ?? ''; + const countBefore = parseCount(html); + // Next renders `
` with a hidden `$ACTION_ID_` field; posting it + // as multipart/form-data is the no-JS progressive-enhancement path that invokes the server action. + const actionField = html.match(/name="(\$ACTION_ID_[a-f0-9]+)"/)?.[1]; + + if (!response.ok) { + failures.push(`expected 200 from GET /, got ${response.status} (build likely failed → not served)`); + } else if (!contentType.includes('text/html')) { + failures.push(`expected a text/html response from GET /, got ${contentType}`); + } else if (!html.toLowerCase().includes('") — server-side table read may have failed'); + } else if (!actionField) { + failures.push("GET / HTML is missing the increment form's server-action field"); + } else { + console.log(`✓ app builds, serves, and renders the counter (count is ${countBefore})`); + + // 2. Exercise the write half end to end: invoke the increment server action, then confirm a + // fresh reload reflects the persisted, incremented value (transaction + addTo + revalidate). + const form = new FormData(); + form.append(actionField, ''); + const post = await fetch(baseUrl + '/', { method: 'POST', body: form, signal: AbortSignal.timeout(15_000) }); + if (!post.ok) { + failures.push(`increment server action POST failed: ${post.status}`); + } else { + const reload = await fetch(baseUrl + '/', { signal: AbortSignal.timeout(10_000) }); + const countAfter = parseCount(await reload.text()); + if (countAfter === countBefore + 1) { + console.log(`✓ increment server action advances the persisted count (${countBefore} → ${countAfter})`); + } else { + failures.push( + `increment did not advance the count on reload: ${countBefore} → ${countAfter} ` + + '(server action, transaction/addTo, or revalidation may be broken)', + ); + } + } + } +} catch (error) { + failures.push(String(error?.message ?? error)); +} finally { + cleanup(); +} + +if (failures.length > 0) { + console.error(`\nNext.js runtime smoke test FAILED for ${appDir}:\n\n${failures.join('\n\n')}`); + process.exit(1); +} +console.log(`\nNext.js runtime smoke test passed for ${appDir}.`); +process.exit(0); diff --git a/templates-studio/buildStudioTemplates.js b/templates-studio/buildStudioTemplates.js index 9ff2ccf..dc0467f 100644 --- a/templates-studio/buildStudioTemplates.js +++ b/templates-studio/buildStudioTemplates.js @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import { templateNames } from '../lib/constants/templates.js'; +import { studioTemplateNames } from '../lib/constants/templates.js'; import { copyDir } from '../lib/fs/copyDir.js'; import { emptyDir } from '../lib/fs/emptyDir.js'; import { renameFile } from '../lib/fs/renameFile.js'; @@ -11,7 +11,7 @@ import { getOwnVersion } from '../lib/pkg/packageInformation.js'; import { run } from '../lib/run.js'; (async function() { - for (const templateName of templateNames) { + for (const templateName of studioTemplateNames) { const targetTemplate = 'template-' + templateName; const fromTemplate = path.resolve(import.meta.dirname, '..', targetTemplate); const toTemplate = path.resolve(import.meta.dirname, targetTemplate); diff --git a/templates-studio/publishStudioTemplates.js b/templates-studio/publishStudioTemplates.js index 086f1d1..c300159 100644 --- a/templates-studio/publishStudioTemplates.js +++ b/templates-studio/publishStudioTemplates.js @@ -3,11 +3,11 @@ import spawn from 'cross-spawn'; import fs from 'node:fs'; import path from 'node:path'; -import { templateNames } from '../lib/constants/templates.js'; +import { studioTemplateNames } from '../lib/constants/templates.js'; (async function() { let hitError = 0; - for (const templateName of templateNames) { + for (const templateName of studioTemplateNames) { const targetTemplate = 'template-' + templateName; const toTemplate = path.resolve(import.meta.dirname, targetTemplate); if (!fs.existsSync(toTemplate)) {