Skip to content

Commit f5d0b24

Browse files
Merge pull request #809 from aXenDeveloper/perf/plugin_install_and_config
feat(plugin): Add API support and enhance plugin registration process
2 parents cf2d7e0 + 9a1b672 commit f5d0b24

149 files changed

Lines changed: 10005 additions & 7174 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
name: upgrade-npm-packages
3+
description: A skill that helps users upgrade their npm packages to the latest versions.
4+
---
5+
6+
# Upgrade NPM Packages Skill
7+
8+
## Instructions
9+
10+
[Clear, step-by-step guidance for Claude to follow]
11+
12+
1. Open `packages/create-vitnode-app/src/create/package-versions.ts` file and read it.
13+
2. Run `pnpm outdated` to check for outdated packages in the project - do it for all projects in the turborepo (monorepo) including with root.
14+
3. For each outdated package, run `pnpm up <package-name>@<version>` to upgrade it.
15+
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.
16+
5. After upgrading all packages, update the `package-versions.ts` file with the new versions of the packages.
17+
6. Run `pnpm install` to ensure all dependencies are correctly installed.
18+
19+
## Examples
20+
21+
I'm working on a project that uses a monorepo structure with multiple packages. I want to ensure that all my npm packages are up-to-date. I will use the "upgrade-npm-packages" skill to check for outdated packages, upgrade them, and handle any breaking changes appropriately.

‎.github/copilot-instructions.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The repository is a monorepo for the VitNode framework, which includes a backend
1616
- TanStack Start on Vite, file-based routes under `apps/web/src/routes/`
1717
- Navigation: use `@vitnode/core/tanstack/layout`'s `RouterLink`, or TanStack
1818
Router's own `Link` / `useNavigate`.
19-
- Forms: Use `react-hook-form@7`, `createServerFn` for mutations
19+
- Forms: Use `@tanstack/react-form@1`, `createServerFn` for mutations
2020
- UI: Shadcn UI, Tailwind CSS 4, dark/light mode with system detection
2121
- i18n: Use `use-intl`, `t('key')` for translations, `createTranslator`
2222
(server), `useTranslations` (client)
@@ -53,7 +53,7 @@ The repository is a monorepo for the VitNode framework, which includes a backend
5353
## Integration & Conventions
5454

5555
- **External:**
56-
- TanStack Start, TanStack Router, TanStack Query, Hono.js, Drizzle ORM, Zod, react-hook-form, Shadcn UI, Tailwind, use-intl
56+
- TanStack Start, TanStack Router, TanStack Query, TanStack Form, Hono.js, Drizzle ORM, Zod, Shadcn UI, Tailwind, use-intl
5757
- **Internal:**
5858
- Navigation, config, API, middleware, plugin system
5959
- **Security:**

‎apps/api/package.json‎

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "api",
3-
"version": "2.0.0-canary.9",
3+
"version": "2.0.0-canary.12",
44
"private": true,
55
"type": "module",
66
"scripts": {
@@ -21,38 +21,38 @@
2121
"i18n:update:ai": "vitnode i18n:update:ai"
2222
},
2323
"dependencies": {
24-
"@ai-sdk/anthropic": "^4.0.20",
25-
"@ai-sdk/google": "^4.0.24",
26-
"@hono/zod-openapi": "^1.5.1",
27-
"@hono/zod-validator": "^0.9.0",
24+
"@ai-sdk/anthropic": "^4.0.53",
25+
"@ai-sdk/google": "^4.0.69",
26+
"@hono/zod-openapi": "^1.6.3",
27+
"@hono/zod-validator": "^0.9.1",
2828
"@vitnode/core": "workspace:*",
2929
"@vitnode/node-cron": "workspace:*",
3030
"@vitnode/supabase-storage": "workspace:*",
3131
"drizzle-kit": "1.0.0-rc.4",
3232
"drizzle-orm": "1.0.0-rc.4",
33-
"hono": "^4.12.31",
34-
"react": "^19.2.8",
35-
"react-dom": "^19.2.8",
36-
"use-intl": "^4.13.7",
37-
"ws": "^8.21.1",
38-
"zod": "^4.4.3"
33+
"hono": "^4.13.7",
34+
"react": "^19.3.0",
35+
"react-dom": "^19.3.0",
36+
"use-intl": "^4.14.4",
37+
"ws": "^8.21.3",
38+
"zod": "^4.6.2"
3939
},
4040
"devDependencies": {
41-
"@hono/node-server": "^2.0.11",
42-
"@react-email/ui": "^6.9.0",
43-
"@types/node": "^26.1.1",
44-
"@types/react": "^19.2.17",
45-
"@types/react-dom": "^19.2.3",
41+
"@hono/node-server": "^2.1.1",
42+
"@react-email/ui": "^6.9.5",
43+
"@types/node": "^26.5.1",
44+
"@types/react": "^19.3.0",
45+
"@types/react-dom": "^19.3.0",
4646
"@types/ws": "^8.18.1",
4747
"@vitnode/blog": "workspace:*",
48-
"@vitnode/example": "workspace:*",
4948
"@vitnode/config": "workspace:*",
49+
"@vitnode/example": "workspace:*",
5050
"@vitnode/nodemailer": "workspace:*",
5151
"dotenv": "^17.4.2",
52-
"eslint": "^10.7.0",
53-
"react-email": "^6.9.0",
54-
"tsc-alias": "^1.9.1",
55-
"tsx": "^4.23.1",
52+
"eslint": "^10.10.0",
53+
"react-email": "^6.9.5",
54+
"tsc-alias": "^1.9.5",
55+
"tsx": "^4.23.13",
5656
"typescript": "^6.0.3"
5757
}
5858
}

‎apps/web/content/docs/dev/configuration.mdx‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ of those modules, and that is the part worth knowing:
9090
| `src/plugin-routes.gen.ts` | the plugin's `src/routes.ts` | per route, behind `lazy()` |
9191
| `src/admin-nav.gen.ts` | the plugin's `admin/nav` | with the AdminCP shell |
9292
| `src/content-registry.gen.ts` | the plugin's `admin/content` | behind a dynamic `import()` in `src/router.tsx` |
93+
| `src/package-messages.gen.ts` | the factory's `localeFiles` | per request, one locale at a time |
9394

9495
One literal import per configured plugin, written at build time. So a content
9596
type's editing screen - a Tiptap field, a form layout, a table cell - arrives
@@ -99,12 +100,13 @@ whole editing stack rides along with every public page. See
99100
[Plugin routes](/docs/dev/plugins/routes) and
100101
[Plugin frontend modules](/docs/dev/content-engine/plugin-registration).
101102

102-
<Callout type="info" title="Also register its locale files">
103+
<Callout type="info" title="Where its locale files come from">
103104
The `messages` a factory carries is the plugin's own locale barrel, which loads
104105
its JSON with `import('./en.json', { with: { type: 'json' } })` - a specifier
105-
no bundler follows. VitNode reads translations from
106-
`src/locales/packages.ts` instead, so add a line there per language the plugin
107-
ships. [Languages & Localization](/docs/dev/i18n) has the detail.
106+
no bundler follows. The factory's `localeFiles` is the same list spelled as
107+
package subpaths, and that is what your app's translations are loaded from:
108+
your build writes the loaders into `src/package-messages.gen.ts` for you.
109+
[Languages & Localization](/docs/dev/i18n) has the detail.
108110
</Callout>
109111

110112
<Callout type="warn" title="Build-time cost">
@@ -127,7 +129,7 @@ import '@tanstack/react-start/server-only'
127129
import { buildServerConfig } from '@vitnode/core/vitnode.config'
128130

129131
import { appMessages } from '@/locales/app'
130-
import { packageMessages } from '@/locales/packages'
132+
import { packageMessages } from '@/package-messages.gen'
131133
import { vitNodeConfig } from '@/vitnode.config'
132134

133135
export const vitNodeServerConfig = buildServerConfig({
@@ -145,7 +147,7 @@ the document shell read. Hand the whole thing to the loader:
145147
export const loadIntlMessages = createIntlMessagesLoader(vitNodeServerConfig)
146148
```
147149

148-
`packageMessages` is one line per language a package ships, and `messages` is
150+
`packageMessages` is generated from the plugins you configured, and `messages` is
149151
where you reword a string a package translates differently to how you want it.
150152
[Languages & Localization](/docs/dev/i18n) covers both.
151153

‎apps/web/content/docs/dev/fetcher.mdx‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,31 @@ choose a transport.
2424
### Define it in your plugin
2525

2626
Keep this in one plugin file. Features import `notesApi`; they never set up a
27-
module reference themselves.
27+
module reference themselves. `create-vitnode-app --plugin` writes this file for
28+
you—the shape below is what it generates.
2829

2930
```ts title="plugins/site-notes/src/api/client.ts"
30-
import type { notesModule } from "../api/notes.module"
31+
import type { ApiClient } from "@vitnode/core/tanstack/fetcher"
3132

3233
import { createApiClient } from "@vitnode/core/tanstack/fetcher"
3334

34-
export const notesApi = createApiClient<typeof notesModule>("@acme/site-notes")
35+
import type { notesModule } from "./modules/notes/notes.module"
36+
37+
export const notesApi: ApiClient<typeof notesModule> =
38+
createApiClient<typeof notesModule>("@acme/site-notes")
3539
```
3640

41+
<Callout type="warn" title="Both halves of that line earn their keep">
42+
`import type` is what keeps Hono and your handlers out of the browser bundle—a
43+
value import would ship the whole API to every visitor, and nothing would fail
44+
to compile.
45+
46+
The `ApiClient` annotation is what keeps your plugin's `.d.ts` small. Without
47+
it, declaration emit resolves the client's type in full and writes every route
48+
the module serves into your build output: 200KB for a single route, and every
49+
app that installs the plugin type-checks it.
50+
</Callout>
51+
3752
</Step>
3853

3954
<Step>

‎apps/web/content/docs/dev/i18n/index.mdx‎

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,16 @@ Because `buildConfig` keeps those codes as literal types, `'de'` is now part of
6767

6868
A `() => import('./de.json')` reads a file out of a package's build output, so it is the one part of i18n that must never reach a browser. Two files own it, and both are registered through the **server-only** config:
6969

70-
| File | Holds |
71-
| :------------------------ | :--------------------------------------------------- |
72-
| `src/locales/packages.ts` | one loader per language each installed package ships |
73-
| `src/locales/app.ts` | your own rewordings, merged last |
70+
| File | Holds | Who writes it |
71+
| :---------------------------- | :--------------------------------------------------- | :------------ |
72+
| `src/package-messages.gen.ts` | one loader per language each installed package ships | your build |
73+
| `src/locales/app.ts` | your own rewordings, merged last | you |
7474

7575
```ts title="apps/web/src/vitnode.server.config.ts"
7676
export const vitNodeServerConfig = buildServerConfig({
7777
config: vitNodeConfig, // the locale list above
7878
messages: appMessages, // src/locales/app.ts
79-
packageMessages, // src/locales/packages.ts
79+
packageMessages, // src/package-messages.gen.ts
8080
})
8181
```
8282

@@ -86,20 +86,39 @@ export const vitNodeServerConfig = buildServerConfig({
8686
writes to the right file for you.
8787
</Callout>
8888

89-
Adding a language to a package that ships it needs one line in `src/locales/packages.ts`:
89+
### The generated half
9090

91-
```ts title="apps/web/src/locales/packages.ts"
92-
[CORE.pluginId]: {
93-
en: async () => await import('@vitnode/core/locales/en.json'),
94-
de: async () => await import('@vitnode/core/locales/de.json'), // [!code ++]
95-
},
91+
Registering a plugin is the whole step. Every VitNode build reads the `plugins` in your `vitnode.config.ts`, takes the `localeFiles` each factory declares, and writes `src/package-messages.gen.ts` - core's own languages plus one block per plugin:
92+
93+
```ts title="apps/web/src/package-messages.gen.ts"
94+
export const packageMessages: Record<string, LocaleMessagesMap> = {
95+
'@vitnode/core': {
96+
en: async () => await import('@vitnode/core/locales/en.json'),
97+
},
98+
'@acme/blog': {
99+
en: async () => await import('@acme/blog/locales/en.json'),
100+
},
101+
}
96102
```
97103

104+
Don't edit it - it is rewritten on every `dev` and `build`, which is why it sits in your `.gitignore`. Every specifier is a literal because that is the only kind a bundler can resolve: `import(pkg + '/locales/' + locale + '.json')` resolves to nothing. Every loader stays dynamic, so a language's JSON is a chunk of its own and the server loads only the locale a request asked for.
105+
106+
<Callout type="info" title="Packages ship English">
107+
`@vitnode/core` and the plugins in this repository ship `en` and nothing else.
108+
Every other language is the install's own, which is what the next section is
109+
for - and it is why a language you add is a file in **your** app rather than a
110+
pull request against a package.
111+
</Callout>
112+
98113
---
99114

100115
## Overriding Strings
101116

102-
To customize existing text from core or a third-party plugin, add an override file in `apps/web/src/locales/{pluginId}/{locale}.json` and register it in `src/locales/app.ts`:
117+
Your own translations live in `apps/web/src/locales/{pluginId}/{locale}.json`, registered in `src/locales/app.ts`. The same file does both jobs: a whole language a package does not ship, and a reword of a string it does.
118+
119+
One file per package per language, holding that package's web **and** email strings together - an app keeps them in one tree where a package ships two, so the copy in an email cannot drift from the copy on the page.
120+
121+
To reword something core already says:
103122

104123
```json title="apps/web/src/locales/@vitnode/core/en.json"
105124
{
@@ -122,15 +141,35 @@ export const appMessages: AppMessagesMap = {
122141

123142
Because your app overrides are merged last, only the keys you specify are overwritten. Everything else continues to fall back to the package defaults.
124143

144+
A whole language looks exactly the same, because it is the same mechanism - this repository's own Polish is a pair of files nobody's `node_modules` contains:
145+
146+
```ts title="apps/web/src/locales/app.ts"
147+
export const appMessages: AppMessagesMap = {
148+
pl: {
149+
'@vitnode/blog': async () => await import('./@vitnode/blog/pl.json'),
150+
'@vitnode/core': async () => await import('./@vitnode/core/pl.json'),
151+
},
152+
}
153+
```
154+
155+
If your app also serves the API, register the same map there so emails speak the language too - `i18n.messages` in `vitnode.api.config.ts`:
156+
157+
```ts title="apps/web/src/vitnode.api.config.ts"
158+
export const vitNodeApiConfig = buildApiConfig({
159+
i18n: { ...vitNodeConfig.i18n, messages: appMessages }, // [!code highlight]
160+
// ...
161+
})
162+
```
163+
125164
---
126165

127166
## Translation Architecture
128167

129-
| Source | Role | Order |
130-
| :------------------- | :------------------------------------------------- | :---------------------- |
131-
| `@vitnode/core` | Base strings for auth, admin shells, and dialogs | Base layer |
132-
| **Plugins** | Domain strings declared in `plugins/*/src/locales` | Second layer |
133-
| **Host Application** | Custom overrides in `apps/web/src/locales` | Highest priority (wins) |
168+
| Source | Role | Order |
169+
| :------------------- | :---------------------------------------------------------- | :---------------------- |
170+
| `@vitnode/core` | Base strings for auth, admin shells, and dialogs | Base layer |
171+
| **Plugins** | Domain strings declared in `plugins/*/src/locales` | Second layer |
172+
| **Host Application** | Your own languages and rewordings in `apps/web/src/locales` | Highest priority (wins) |
134173

135174
Missing keys automatically fall back to `defaultLocale` (`en`), preventing raw key paths from displaying in production.
136175

‎apps/web/content/docs/dev/i18n/server.mdx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,25 @@ export default function WelcomeEmail({ i18n }: DefaultTemplateEmailProps) {
149149

150150
A plugin owns its languages, and it splits them the same way the framework does: frontend strings in `src/locales/`, server strings (emails) in `src/locales/api/`. Each tree gets a barrel and is registered with the matching config - the frontend tree with `buildPlugin` in `config.tsx`, the server tree with `buildApiPlugin` in `config.api.ts`.
151151

152-
Most plugins render nothing server-side, so they ship only the frontend tree and register `messages` in `config.tsx` alone. Add the `api/` tree only when your plugin sends email:
152+
Most plugins render nothing server-side, so they ship only the frontend tree. That one is registered twice, and the two halves are not interchangeable:
153+
154+
```ts title="plugins/{your_plugin}/src/config.tsx"
155+
import messages from './locales'
156+
157+
export const yourPlugin = () =>
158+
buildPlugin({
159+
pluginId: CONFIG_PLUGIN.pluginId,
160+
localeFiles: {
161+
// [!code ++:2]
162+
en: '@acme/your-plugin/locales/en.json',
163+
},
164+
messages,
165+
})
166+
```
167+
168+
`messages` is your own barrel, whose `import('./en.json')` is relative to your build output - right for anything running inside your package, and a specifier no host bundler can follow. `localeFiles` is the same list written as the subpaths your `package.json` exports, and it is what an app's translations are actually loaded from: the app's build turns it into literal imports in `src/package-messages.gen.ts`. Ship both, and keep them in step.
169+
170+
Add the `api/` tree only when your plugin sends email:
153171

154172
```ts title="plugins/{your_plugin}/src/locales/api/index.ts"
155173
import type { LocaleMessagesMap } from '@vitnode/core/lib/i18n/types'
@@ -172,7 +190,7 @@ export const yourApiPlugin = () =>
172190
})
173191
```
174192

175-
Adding a language later is a new file plus one line in the barrel - apps pick it up on their next install, and can translate your plugin without forking it by dropping a file in their own `src/locales/{your_plugin}/`.
193+
Adding a language later is a new file plus one line in each list that names it - apps pick it up on their next install, and can translate your plugin without forking it by dropping a file in their own `src/locales/{your_plugin}/`.
176194

177195
## Typing the keys
178196

‎apps/web/content/docs/dev/plugins/api/modules.mdx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ Start with [a plugin](/docs/dev/plugins/create), not a host endpoint. A module
1010
groups the plugin's Hono routes under one URL prefix and gives OpenAPI a tidy
1111
place to describe them.
1212

13+
<Callout type="info" title="A generated plugin already has one">
14+
`create-vitnode-app --plugin` writes a `hello` module, its `config.api.ts` and
15+
a page that calls it. Read on for what each piece does—then rename them, or add
16+
a second module beside them.
17+
</Callout>
18+
1319
{/* Image prompt: Dark-theme API ownership diagram. A Site notes plugin contains a Hono route, notes module, and config.api file; the app API configuration composes the plugin once. Show resulting GET endpoint, 1600x900. */}
1420

1521
<Steps>
@@ -71,6 +77,10 @@ export const siteNotesApiPlugin = () =>
7177
})
7278
```
7379

80+
This file, and not `config.tsx`: the API config reaches your handlers, database
81+
and secrets, while `config.tsx` is read by the browser build. A module
82+
registered in the wrong one is shipped to every visitor.
83+
7484
</Step>
7585
<Step>
7686

0 commit comments

Comments
 (0)