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
20 changes: 16 additions & 4 deletions .claude/skills/vitnode-upgrade-npm-packages/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,22 @@ description: A skill that helps users upgrade their npm packages to the latest v

1. Open `packages/create-vitnode-app/src/create/package-versions.ts` file and read it.
2. Run `pnpm outdated` to check for outdated packages in the project - do it for all projects in the turborepo (monorepo) including with root.
3. For each outdated package, run `pnpm up <package-name>@<version>` to upgrade it.
4. For each package, check if there are any breaking changes in the new version. If there are, ask the user if they want to proceed with the upgrade or skip it.
5. After upgrading all packages, update the `package-versions.ts` file with the new versions of the packages.
6. Run `pnpm install` to ensure all dependencies are correctly installed.
3. Check pre-release dependencies separately - `pnpm outdated` compares against the `latest` dist-tag only, so it never reports a newer rc/beta/alpha/canary (e.g. `drizzle-orm@1.0.0-rc.4`, `drizzle-kit`, `nitro@3.0.x-beta`).
- List them across every workspace `package.json` (root, `apps/*`, `packages/*`, `plugins/*`), including `npm:` aliases, plus the `nitro`/`drizzle*` entries in `package-versions.ts`:

```bash
for f in package.json apps/*/package.json packages/*/package.json plugins/*/package.json; do
jq -r --arg f "$f" '[.dependencies,.devDependencies,.peerDependencies,.optionalDependencies] | map(select(.)) | add // {} | to_entries[] | select(.value|test("-[a-z]")) | select(.value|test("workspace")|not) | "\(.key) \(.value) \($f)"' "$f"
done
```

- For each, run `npm view <package-name> dist-tags --json` and `npm view <package-name> versions --json`. Upgrade to the newest version on the same channel (the `rc`/`beta`/`latest` tag the current version belongs to, or a newer clean release such as `1.0.0-rc.5` / `1.0.0`). Ignore commit-hash snapshot builds like `1.0.0-rc.5-5935859` and feature-branch tags.
- Keep the pin style the file already uses (exact `1.0.0-rc.4` vs `^1.0.0-rc.4`) and bump every workspace that uses the package to the same version.
- A channel jump (rc → stable, beta → rc) counts as a possible breaking change - read the release notes and ask the user (step 5).
4. For each outdated package, run `pnpm up -r <package-name>@<version>` to upgrade it.
5. For each package, check if there are any breaking changes in the new version. If there are, ask the user if they want to proceed with the upgrade or skip it.
6. After upgrading all packages, update the `package-versions.ts` file with the new versions of the packages, including pre-release pins (`drizzleKit`, `drizzleOrm`, `nitro`).
7. Run `pnpm install` to ensure all dependencies are correctly installed.

## Examples

Expand Down
33 changes: 0 additions & 33 deletions .github/dependabot.yml

This file was deleted.

20 changes: 0 additions & 20 deletions .github/workflows/build-lint-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,6 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15

# Real Postgres for the integration tests that need row locks and
# `ON CONFLICT` (notifications). Each test file creates its own database.
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: root
POSTGRES_PASSWORD: root
POSTGRES_DB: postgres
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U root"
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
VITNODE_TEST_POSTGRES_URL: postgresql://root:root@localhost:5432/postgres

steps:
- name: Checkout
uses: actions/checkout@v7
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ npm i x
- Do not test database models or schema definitions (Drizzle tables, column types, indexes, foreign keys).
- Do not test static config, constants, color presets, or plugin configuration (`config.api`, content type definitions).
- Do not test third-party libraries or trivial helper/wrapper code.
- Always run "pnpm lint:fix" to fix formatting and linting issues before committing. Never commit a file with lint errors.

### Mocking

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ installable plugins instead of becoming permanent residents of one giant app.

## Start here

You need Node.js 22+ and Postgres (or Docker). Create an app with the package
manager you use every day:
You need Node.js 22.18+ (or 24.11+, or 26+) and Postgres (or Docker). Create an
app with the package manager you use every day:

### Bun

Expand Down
6 changes: 3 additions & 3 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
"i18n:update:ai": "vitnode i18n update-ai"
},
"dependencies": {
"@ai-sdk/anthropic": "^4.0.75",
"@ai-sdk/google": "^4.0.90",
"@ai-sdk/anthropic": "^4.0.77",
"@ai-sdk/google": "^4.0.92",
"@hono/zod-openapi": "^1.6.3",
"@hono/zod-validator": "^0.9.1",
"@vitnode/core": "workspace:*",
Expand All @@ -37,7 +37,7 @@
"zod": "^4.6.5"
},
"devDependencies": {
"@hono/node-server": "^2.1.3",
"@hono/node-server": "^2.1.4",
"@react-email/ui": "^6.11.1",
"@types/node": "^26.6.4",
"@types/react": "^19.3.0",
Expand Down
23 changes: 18 additions & 5 deletions apps/web/content/docs/dev/cli/build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,24 @@ Nothing is static until you ask for it. [Prerender static pages](/docs/dev/deplo

The same command builds other project types:

| Project | What `vitnode build` does |
| -------------- | --------------------------------------------------------------------------- |
| API app (Node) | Compiles with `tsc` and `tsc-alias` into `dist`, then lists the output |
| API app (Bun) | Nothing: Bun runs `src/index.ts` directly |
| Plugin package | Emits type declarations, compiles with `swc` and rewrites aliases in `dist` |
| Project | What `vitnode build` does |
| -------------- | --------------------------------------------------------------------------------- |
| API app (Node) | Compiles with `tsc` and `tsc-alias` into `dist`, then lists the output |
| API app (Bun) | Nothing: Bun runs `src/index.ts` directly |
| Plugin package | Compiles `src` into `dist/src` with tsdown and checks types with `tsc --noEmit` |

```txt title="vitnode build in a plugin package"
◆ VitNode
Building @acme/blog

✓ API registry generated
✓ Compiling sources 8.0s
✓ Checking types 7.3s

✓ Built in 8.0s
```

Compiling and type checking run at the same time, and both must pass. [Build and watch a plugin](/docs/dev/plugins/build) explains what ends up in `dist`.

## When a build fails

Expand Down
11 changes: 8 additions & 3 deletions apps/web/content/docs/dev/cli/dev.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: Start a VitNode app, API app or plugin package in development with
icon: Play
---

`vitnode dev` starts whatever the current folder is: an app gets a Vite dev server, an API app gets a watching Node or Bun process, and a plugin package gets its compilers in watch mode. Run it from the project's folder.
`vitnode dev` starts whatever the current folder is: an app gets a Vite dev server, an API app gets a watching Node or Bun process, and a plugin package gets tsdown and the type checker in watch mode. Run it from the project's folder.

```bash
vitnode dev
Expand Down Expand Up @@ -91,17 +91,22 @@ Otherwise it runs on Node with `tsx watch`.

## Develop a plugin package

In a plugin package, `vitnode dev` regenerates the plugin's API registry and runs `tsc`, `swc` and `tsc-alias` in watch mode. Apps that use the plugin import its `dist` folder, so they reload as you save.
In a plugin package, `vitnode dev` regenerates the plugin's API registry, then watches `src` with tsdown and runs `tsc` in watch mode to report type errors. Apps that use the plugin import its `dist` folder, so they reload as you save.

```txt
◆ VitNode
Plugin development - @acme/blog

✓ API registry generated
● Watching sources - apps using this plugin reload as dist/ changes

tsdown js Rebuilt in 71ms.
tsdown types Rebuilt in 4493ms.
```

If one compiler crashes, the CLI stops the others and exits with code `1`.
The JavaScript (`tsdown js`) is rebuilt within milliseconds of a save. Declarations (`tsdown types`) follow once TypeScript catches up, so your editor sees new types a few seconds later. A syntax error is printed and the next save rebuilds. If a watcher fails to start, for example because tsdown is not installed, the CLI stops the others and exits with code `1`.

[Build and watch a plugin](/docs/dev/plugins/build) covers added and deleted files, assets and what your app sees while a rebuild is written.

## Next steps

Expand Down
10 changes: 5 additions & 5 deletions apps/web/content/docs/dev/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ Run `vitnode` alone (or `vitnode --help`) for a short overview, `vitnode <comman

`dev`, `build` and `start` act on the project in the current folder. The CLI walks up to the nearest `package.json` and checks, in this order:

| The folder has | It is a |
| -------------------------------------- | -------------- |
| `vite.config.ts` (or `.mts`, `.js`...) | App |
| `src/vitnode.api.config.ts` | API app |
| `tsconfig.build.json` and `.swcrc` | Plugin package |
| The folder has | It is a |
| ---------------------------------------------------------------------- | -------------- |
| `vite.config.ts` (or `.mts`, `.js`...) | App |
| `src/vitnode.api.config.ts` | API app |
| `tsconfig.build.json` and `package.json` `exports` into `./dist/src/` | Plugin package |

Run it at a monorepo root and the CLI stops with a hint to `cd` into your app, such as `apps/web`, instead of guessing which project you meant.

Expand Down
10 changes: 0 additions & 10 deletions apps/web/content/docs/dev/notifications/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,3 @@ Every inbox change updates the count in the same transaction, so a wrong count i
## Items say "This notification is no longer available"

The item is a placeholder. The content was deleted, the member lost access, the plugin was uninstalled, or the stored data no longer matches the type's schema and `migrate` could not upgrade it. Members can still read or archive the item. Plugins can tidy these up with [`remove()`](/docs/dev/notifications/access#remove-notifications-when-content-goes-away).

## Run the notification integration tests

The notification integration tests need a real PostgreSQL database for row locks and concurrent transactions. They run when `VITNODE_TEST_POSTGRES_URL` is set and are skipped otherwise:

```bash
VITNODE_TEST_POSTGRES_URL=postgresql://root:root@localhost:5432/postgres pnpm --filter @vitnode/core test
```

Each test file creates its own temporary database and drops it afterwards.
141 changes: 141 additions & 0 deletions apps/web/content/docs/dev/plugins/build.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
title: Build and watch a plugin
description: How vitnode build compiles a plugin package with tsdown, how vitnode dev watches it, and what ends up in dist.
icon: Hammer
---

import { File, Files, Folder } from 'fumadocs-ui/components/files'

`vitnode build` turns a plugin's `src` folder into `dist/src`: one JavaScript file, one declaration file (`.d.ts`) and one declaration map per module. `vitnode dev` does the same in watch mode, so an app using the plugin reloads as you save. There is no build config to write. VitNode owns it, and every plugin and adapter package is built the same way.

## What tsdown does

[tsdown](https://tsdown.dev) is the compiler behind both commands. It runs in **unbundle mode**: every module is compiled on its own instead of being merged into one bundle, so `src/pages/home-page.tsx` becomes `dist/src/pages/home-page.js`.

<Files>
<Folder name="plugins/site-notes" defaultOpen>
<Folder name="src" defaultOpen>
<Folder name="locales">
<File name="en.json" />
<File name="index.ts" />
</Folder>
<File name="config.tsx" />
<File name="routes.ts" />
</Folder>
<Folder name="dist/src" defaultOpen>
<Folder name="locales">
<File name="en.json" />
<File name="index.js" />
<File name="index.d.ts" />
<File name="index.d.ts.map" />
</Folder>
<File name="config.js" />
<File name="config.d.ts" />
<File name="config.d.ts.map" />
<File name="routes.js" />
<File name="routes.d.ts" />
<File name="routes.d.ts.map" />
</Folder>
</Folder>
</Files>

Every `.ts` and `.tsx` file under `src` is compiled, not only what an `index.ts` imports, because the plugin's `"./*"` export makes each module public. Test files (`*.test.ts`, `*.test.tsx`, `*.test-d.ts`), `src/tests/`, `__tests__/` and `__fixtures__/` folders stay out of `dist`.

The output is ES modules that Node and Vite load as they are:

- **Imports of packages stay imports.** React, Hono, Drizzle, `@vitnode/core` and every other package are left for the app to install, exactly as written.
- **Relative imports get their `.js` extension**, and dynamic imports such as `lazy(() => import("./pages/home-page"))` keep working.
- **`@/` aliases become relative paths**, in the JavaScript and in the declarations. The aliases come from `paths` in `tsconfig.build.json`.
- **JSX** uses React's automatic runtime (`react/jsx-runtime`).
- **File names are kept**, so `auth.server.ts` stays `auth.server.js`. Modules keep their side effects; nothing is tree-shaken away.

A production build is minified and ships declaration maps, but no JavaScript source maps. `vitnode dev` writes readable JavaScript with source maps instead.

## Assets and aliases

Every file under `src` that is not code (locale JSON, CSS, images for emails) is copied to the same place under `dist/src`. Imports of those files stay imports, with their import attributes:

```ts title="src/locales/index.ts"
export default {
en: async () => await import("./en.json", { with: { type: "json" } }),
};
```

`dist/src/locales/index.js` imports `./en.json` next to it, so the compiled module finds its JSON the way the source did. An app loads your translations through the `"./locales/*.json"` export instead, which points at `src/locales`.

## Types and declarations

Two TypeScript jobs run side by side, and both must pass:

| Job | What it does |
| -------------- | ------------------------------------------------------------------------------ |
| tsdown | Writes the `.d.ts` files and their maps with the TypeScript compiler |
| `tsc --noEmit` | Type checks the whole package with `tsconfig.build.json` and fails on an error |

Declarations are written from every file `tsconfig.build.json` includes. That matters for files nothing imports, such as `global.d.ts` and the generated `types/api-registry.gen.d.ts`: they are what lets `fetcher({ plugin: "@acme/site-notes", ... })` infer its response, and that inferred type lands in your `.d.ts` files.

You don't need `isolatedDeclarations` or explicit return types. Inferred types come out the same as `tsc` would write them.

## What a plugin package needs

| File | Why |
| --------------------- | --------------------------------------------------------------------------------------- |
| `package.json` | `"type": "module"`, `exports` pointing into `./dist/src/`, and `tsdown` and `typescript` in `devDependencies` |
| `tsconfig.json` | Compiler options for your editor, with `"paths": { "@/*": ["./src/*"] }` |
| `tsconfig.build.json` | The same, without tests: what `vitnode build` compiles and type checks |

The CLI treats a folder as a plugin package when it has `tsconfig.build.json` and its `exports` point into `./dist/src/`. `create-vitnode-app` generates all three files. A `tsdown.config.ts` in a plugin is not read.

## How `vitnode dev` watches a plugin

`vitnode dev` runs three watchers:

- **tsdown js** rebuilds the JavaScript and copies changed assets, usually within 100 ms for a plugin.
- **tsdown types** rebuilds the declarations once TypeScript has caught up, a few seconds later.
- **`tsc --watch --noEmit`** prints type errors.

The two tsdown watchers run as separate processes, so the TypeScript compiler never holds back the JavaScript your app reloads.

Adding, deleting or renaming a file restarts both tsdown watchers with the new list of modules, after a short pause so a branch switch counts as one change. Output for a deleted module is removed from `dist/src`. A file that fails to compile is reported, the last good output stays in place, and the next save rebuilds.

<kbd>Ctrl</kbd>+<kbd>C</kbd> stops every watcher. If one of them exits on its own, the CLI stops the rest and exits with an error.

## How dist stays consistent

A build only writes to `dist/src`, and only files whose content changed. An app watching the plugin reloads for the module you edited, not for every file in the package. After a successful build, files under `dist/src` that no source produces anymore are deleted. Nothing outside `dist/src` is touched, so don't keep hand-written files in `dist/src`.

Writes are not atomic. A changed file is overwritten in place, so for a moment an app's dev server can see the old or a partly written file, and may reload once more when the write finishes. Since only changed files are written, this window covers the files you edited.

## Plugin, CLI and app builds

VitNode uses different builds for different things. This page is about the first one.

| Build | Who runs it | Tool |
| -------------- | ---------------------------------- | ----------------------------------------------------------- |
| Plugin package | `vitnode build` in a plugin | tsdown in unbundle mode, plus `tsc --noEmit` |
| VitNode CLI | `pnpm build:scripts` in `@vitnode/core` | tsdown with `packages/vitnode/tsdown.config.ts`, into `dist/scripts` |
| App | `vitnode build` in an app | Vite and TanStack Start, unchanged |

`@vitnode/core` itself is built as a plugin package into `dist/src`. Its CLI build writes `dist/scripts` and only cleans that folder, so the two never delete each other's output.

## Limitations

- tsdown needs Node.js `^22.18.0`, `^24.11.0` or `26` and later, which is also the range VitNode supports. Node.js 23 and 25 are not on the list.
- Declarations are generated by the TypeScript compiler, which is the slow part of a build. In watch mode they arrive a few seconds after the JavaScript. For a package as large as `@vitnode/core`, adding or deleting a file rebuilds all of its declarations.
- Each JavaScript rebuild renders every module of the package again and writes only the changed ones. That takes under 100 ms for a typical plugin and about 2 seconds for the roughly 1,600 modules of `@vitnode/core`.
- A re-export-only `index.ts` may get no `.d.ts.map`, so "Go to definition" stops at its `.d.ts` first.

## Next steps

<Cards>
<Card
title="Migrate a plugin from SWC"
description="Move an existing plugin from SWC and tsc-alias to tsdown"
href="/docs/dev/plugins/migrate-to-tsdown"
/>
<Card
title="Plugin CLI"
description="List and validate plugins"
href="/docs/dev/cli/plugins"
/>
</Cards>
2 changes: 2 additions & 0 deletions apps/web/content/docs/dev/plugins/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"defaultOpen": true,
"pages": [
"create",
"build",
"api",
"admin",
"navigation",
"migrate-to-tsdown",
"..."
]
}
Loading
Loading