create-vitnode-app 2.0.0-canary.1 → 2.0.0-canary.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +9 -2
  2. package/copy-of-vitnode-app/README.md +22 -0
  3. package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +10 -11
  4. package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +4 -26
  5. package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +1 -12
  6. package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +13 -13
  7. package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +3 -12
  8. package/copy-of-vitnode-app/docker/docker-compose.yml +6 -4
  9. package/copy-of-vitnode-app/monorepo/apps/api/.env.example +1 -1
  10. package/copy-of-vitnode-app/monorepo/apps/web/.env.example +1 -12
  11. package/copy-of-vitnode-app/monorepo/turbo.json +8 -3
  12. package/copy-of-vitnode-app/root/.env.example +1 -8
  13. package/copy-of-vitnode-app/root/global.d.ts +1 -19
  14. package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +2 -53
  15. package/copy-of-vitnode-app/root/src/components/main-footer.tsx +44 -0
  16. package/copy-of-vitnode-app/root/src/components/main-header.tsx +1 -30
  17. package/copy-of-vitnode-app/root/src/lib/i18n.ts +37 -0
  18. package/copy-of-vitnode-app/root/src/locales/app.ts +1 -25
  19. package/copy-of-vitnode-app/root/src/locales/packages.ts +1 -34
  20. package/copy-of-vitnode-app/root/src/router.tsx +16 -165
  21. package/copy-of-vitnode-app/root/src/routes/__root.tsx +12 -115
  22. package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +4 -54
  23. package/copy-of-vitnode-app/root/src/routes/_admin.tsx +5 -198
  24. package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +182 -46
  25. package/copy-of-vitnode-app/root/src/routes/_main.tsx +12 -47
  26. package/copy-of-vitnode-app/root/src/server/messages.server.ts +2 -23
  27. package/copy-of-vitnode-app/root/src/start.ts +3 -86
  28. package/copy-of-vitnode-app/root/src/styles.css +70 -92
  29. package/copy-of-vitnode-app/root/src/vitnode.config.ts +15 -53
  30. package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +12 -0
  31. package/copy-of-vitnode-app/root/tsconfig.json +8 -3
  32. package/copy-of-vitnode-app/root/vite.config.ts +2 -99
  33. package/copy-of-vitnode-plugin/root/global.d.ts +6 -6
  34. package/dist/src/create/create-package-json.js +5 -4
  35. package/dist/src/create/create-vitnode.js +15 -1
  36. package/dist/src/create/package-versions.js +22 -22
  37. package/dist/src/helpers/init-git.js +38 -0
  38. package/dist/src/helpers/init-vitnode.js +2 -3
  39. package/dist/src/helpers/install-dependencies.js +2 -3
  40. package/dist/src/helpers/spawn-command.js +7 -0
  41. package/dist/src/index.js +1 -0
  42. package/dist/src/plugin/create/add-plugin-to-config.js +231 -0
  43. package/dist/src/plugin/create/create-package-json.js +12 -10
  44. package/dist/src/plugin/create/create-plugin-vitnode.js +23 -2
  45. package/dist/src/plugin/create/route-templates.js +103 -61
  46. package/dist/src/questions.js +7 -0
  47. package/dist/tsconfig.build.tsbuildinfo +1 -1
  48. package/package.json +6 -6
  49. package/copy-of-vitnode-app/api/src/i18n.ts +0 -38
  50. package/copy-of-vitnode-app/root/src/i18n.ts +0 -36
  51. package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +0 -11
  52. package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +0 -43
  53. package/copy-of-vitnode-app/root/src/lib/admin-search.ts +0 -37
  54. package/copy-of-vitnode-app/root/src/lib/auth.ts +0 -66
  55. package/copy-of-vitnode-app/root/src/lib/content-registry.ts +0 -51
  56. package/copy-of-vitnode-app/root/src/lib/document-headers.ts +0 -128
  57. package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +0 -75
  58. package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +0 -20
  59. package/copy-of-vitnode-app/root/src/lib/navigation.ts +0 -24
  60. package/copy-of-vitnode-app/root/src/lib/page-head.ts +0 -19
  61. package/copy-of-vitnode-app/root/src/vitnode.shell.config.ts +0 -35
package/README.md CHANGED
@@ -59,8 +59,14 @@ pnpm create vitnode-app@latest --plugin
59
59
  npm create vitnode-app@latest -- --plugin
60
60
  ```
61
61
 
62
- The generator creates the package and adds its workspace dependency. Register it
63
- in the host’s `vitnode.config.ts` to enable the feature.
62
+ The generator creates the package and adds its workspace dependency. Enable the
63
+ feature by registering it in the host’s `vitnode.config.ts`:
64
+
65
+ ```ts
66
+ import { myPlugin } from '@acme/my-plugin/config'
67
+
68
+ plugins: [myPlugin()]
69
+ ```
64
70
 
65
71
  ## Options
66
72
 
@@ -69,6 +75,7 @@ in the host’s `vitnode.config.ts` to enable the feature.
69
75
  | `--package-manager` | Choose `npm` or `pnpm` for the generated project. |
70
76
  | `--eslint` | Include ESLint and Prettier configuration. |
71
77
  | `--skip-install` | Skip dependency installation after scaffolding. |
78
+ | `--skip-git` | Skip initializing a git repository (created on `main` after install). |
72
79
  | `--mode` | Choose `singleApp`, `apiMonorepo`, or `onlyApi`. |
73
80
  | `--monorepo` | Create a workspace layout for plugins and multiple applications. |
74
81
  | `--docker` | Include local Docker services. |
@@ -38,3 +38,25 @@ To start the development server, run the following command:
38
38
  ```bash
39
39
  pnpm dev
40
40
  ```
41
+
42
+ ## Configuration
43
+
44
+ Two files, and the line between them is one question: may a browser hold this?
45
+
46
+ | File | Holds |
47
+ | :----------------------------- | :----------------------------------------------------------------------------------------------------------------------- |
48
+ | `src/vitnode.config.ts` | locales, metadata, theme, `debug`, enabled plugin ids - plain data, read by the browser, the server, and your Vite build |
49
+ | `src/vitnode.server.config.ts` | message loaders - server only |
50
+
51
+ Add a language to `i18n.locales` in the shared config; register the files that
52
+ translate it in `src/locales/packages.ts` (a package's own translations) or
53
+ `src/locales/app.ts` (your rewordings). `pnpm vitnode i18n:create de Deutsch`
54
+ does all three.
55
+
56
+ `src/start.ts` is one call to `createVitNodeStart`, which installs CSRF
57
+ protection for server functions, canonical locale redirects with the
58
+ remembered-locale cookie, and the `private, no-store` directive every rendered
59
+ document needs. Add your own request middleware with `requestMiddleware` - it
60
+ runs after all of it.
61
+
62
+ See [Configuration](https://vitnode.com/docs/dev/configuration).
@@ -3,8 +3,6 @@ import { config } from "dotenv";
3
3
  import { coreRelations } from "@vitnode/core/database/relations";
4
4
  import { drizzle } from "drizzle-orm/postgres-js";
5
5
 
6
- import { i18n } from "./i18n.js";
7
-
8
6
  config({
9
7
  quiet: true,
10
8
  });
@@ -14,15 +12,16 @@ export const POSTGRES_URL =
14
12
 
15
13
  export const vitNodeApiConfig = buildApiConfig({
16
14
  plugins: [],
17
- /**
18
- * The installation's languages - see `src/i18n.ts`, and keep it in step with
19
- * the web app's file of the same name.
20
- *
21
- * This app owns the schema, so `vitnode db:prepare` seeds `core_languages`
22
- * from this list. Leave it out and the seed falls back to `en` alone, whatever
23
- * the site serves.
24
- */
25
- i18n,
15
+
16
+ i18n: {
17
+ defaultLocale: "en",
18
+ locales: [{ code: "en", name: "English" }],
19
+ /**
20
+ * Explicit, because this API renders emails on a server: without one, dates
21
+ * format in whatever zone the host happens to run in.
22
+ */
23
+ timeZone: "UTC",
24
+ },
26
25
  dbProvider: drizzle({
27
26
  connection: POSTGRES_URL,
28
27
  relations: coreRelations,
@@ -1,32 +1,10 @@
1
- import { createFileRoute } from '@tanstack/react-router'
1
+ import { createFileRoute } from "@tanstack/react-router";
2
2
 
3
- import { apiBridge } from '#/server/vitnode-api.server'
3
+ import { apiBridge } from "@/server/vitnode-api.server";
4
4
 
5
- /**
6
- * `/api/*` - the existing VitNode Hono application, mounted.
7
- *
8
- * `server` is the only option on this route on purpose. TanStack Start prunes a
9
- * route file whose sole option is `server` out of the client route tree
10
- * entirely (and its client code-splitter deletes the `server` node on top of
11
- * that), so none of the API - Hono, Drizzle, the plugins - can reach the browser
12
- * bundle. The `.server.ts` import is refused by import protection if that ever
13
- * stops being true.
14
- *
15
- * `ANY` rather than a handler per method: routing, OpenAPI, middleware, auth,
16
- * plugin mounting and error handling all stay inside Hono, exactly as they are
17
- * when the same app runs standalone in `apps/api` or under the Next.js catch-all
18
- * in `apps/docs`. `ANY` is part of the framework's `RouteMethod` union and is
19
- * what Start falls back to for any method it was given no handler for - `HEAD`
20
- * included, where it calls this handler and strips the response body itself.
21
- * So the API keeps answering for methods this file has never heard of, which is
22
- * the point: there is nothing here to keep in sync with the API's routes.
23
- *
24
- * `src/tests/api-server-route.test.ts` drives the real request handler over this
25
- * route to hold that to every method the API needs.
26
- */
27
- export const Route = createFileRoute('/api/$')({
5
+ export const Route = createFileRoute("/api/$")({
28
6
  server: {
29
7
  handlers: ({ createHandlers }) =>
30
8
  createHandlers({ ANY: async ({ request }) => apiBridge(request) }),
31
9
  },
32
- })
10
+ });
@@ -1,15 +1,4 @@
1
- /**
2
- * The seam between the web runtime and the Hono API.
3
- *
4
- * A bridge is handed the `Request` the browser (or an SSR loader) made to
5
- * `/api/*` on this origin and answers it with whatever Hono answers. It is one
6
- * line, and that is the point: status, body, every `Set-Cookie`, the cookie and
7
- * `x-forwarded-*` headers the API reads are all already correct on the request
8
- * the platform built, and stay correct exactly as long as nobody rebuilds them.
9
- *
10
- * `src/tests/api-bridge-contract.ts` holds this to that behaviour, including the
11
- * ways a rebuilt request loses it.
12
- */
1
+
13
2
  export type ApiBridge = (request: Request) => Promise<Response> | Response
14
3
 
15
4
  interface FetchableApp {
@@ -1,29 +1,29 @@
1
- import '@tanstack/react-start/server-only'
2
- import { OpenAPIHono } from '@hono/zod-openapi'
3
- import { VitNodeAPI } from '@vitnode/core/api/config'
1
+ import "@tanstack/react-start/server-only";
2
+ import { OpenAPIHono } from "@hono/zod-openapi";
3
+ import { VitNodeAPI } from "@vitnode/core/api/config";
4
4
 
5
- import { createApiBridge } from '#/server/api-bridge'
6
- import { vitNodeApiConfig } from '#/vitnode.api.config'
5
+ import { createApiBridge } from "@/server/api-bridge";
6
+ import { vitNodeApiConfig } from "@/vitnode.api.config";
7
7
 
8
8
  // The same two lines `apps/api` and `apps/docs` run. `basePath("/api")` is what
9
9
  // makes the mount point part of the API's own routing, so every path the plugins
10
10
  // register - `/api/@vitnode/core/...` - resolves identically here.
11
11
  const createVitNodeApi = () => {
12
- const app = new OpenAPIHono().basePath('/api')
12
+ const app = new OpenAPIHono().basePath("/api");
13
13
 
14
- VitNodeAPI({ app, vitNodeApiConfig })
14
+ VitNodeAPI({ app, vitNodeApiConfig });
15
15
 
16
- return app
17
- }
16
+ return app;
17
+ };
18
18
 
19
19
  // `VitNodeAPI` is a boot step, not a request step: it opens the Redis client,
20
20
  // starts the cron scheduler and registers every plugin route. Vite re-evaluates
21
21
  // server modules on HMR, so the instance is parked on `globalThis` to keep one
22
22
  // API per process instead of one per edit.
23
23
  const cache = globalThis as typeof globalThis & {
24
- __vitnodeApi?: ReturnType<typeof createVitNodeApi>
25
- }
24
+ __vitnodeApi?: ReturnType<typeof createVitNodeApi>;
25
+ };
26
26
 
27
- export const vitNodeApi = (cache.__vitnodeApi ??= createVitNodeApi())
27
+ export const vitNodeApi = (cache.__vitnodeApi ??= createVitNodeApi());
28
28
 
29
- export const apiBridge = createApiBridge(vitNodeApi)
29
+ export const apiBridge = createApiBridge(vitNodeApi);
@@ -2,7 +2,7 @@ import { buildApiConfig } from "@vitnode/core/vitnode.config";
2
2
  import { coreRelations } from "@vitnode/core/database/relations";
3
3
  import { drizzle } from "drizzle-orm/postgres-js";
4
4
 
5
- import { i18n } from "./i18n";
5
+ import { vitNodeConfig } from "./vitnode.config";
6
6
 
7
7
  export const POSTGRES_URL =
8
8
  process.env.POSTGRES_URL ?? "postgresql://root:root@localhost:5432/vitnode";
@@ -13,17 +13,8 @@ export const vitNodeApiConfig = buildApiConfig({
13
13
  shortTitle: "VitNode",
14
14
  },
15
15
  plugins: [],
16
- /**
17
- * The same `src/i18n.ts` the frontend config reads, because this app is both:
18
- * one declaration of which languages exist, spread into `buildConfig` through
19
- * `vitnode.shell.config.ts` and passed here.
20
- *
21
- * It is also what `vitnode db:prepare` seeds `core_languages` from - this app
22
- * owns the schema - so adding a language here and re-running `dev` inserts its
23
- * row. Leave it out and the seed falls back to `en` alone, whatever the site
24
- * serves.
25
- */
26
- i18n,
16
+
17
+ i18n: vitNodeConfig.i18n,
27
18
  dbProvider: drizzle({
28
19
  connection: POSTGRES_URL,
29
20
  relations: coreRelations,
@@ -1,5 +1,3 @@
1
- version: '3.8'
2
-
3
1
  services:
4
2
  database:
5
3
  container_name: vitnode_postgres_dev
@@ -12,7 +10,11 @@ services:
12
10
  volumes:
13
11
  - ./docker/dev:/var/lib/postgresql/data
14
12
  ports:
15
- - '5432:5432'
13
+ # Loopback only. These are development containers with a default password
14
+ # of `root`, and `'5432:5432'` publishes them on every interface the host
15
+ # has - which on a laptop is whatever café or office network it is joined
16
+ # to, and on a VPS is the internet.
17
+ - '127.0.0.1:5432:5432'
16
18
  networks:
17
19
  - vitnode_dev
18
20
 
@@ -22,7 +24,7 @@ services:
22
24
  restart: unless-stopped
23
25
  command: redis-server --requirepass ${REDIS_PASSWORD-root}
24
26
  ports:
25
- - '6379:6379'
27
+ - '127.0.0.1:6379:6379'
26
28
  networks:
27
29
  - vitnode_dev
28
30
 
@@ -1,7 +1,7 @@
1
1
  POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode
2
2
  REDIS_URL=redis://localhost:6379
3
3
 
4
- NEXT_PUBLIC_WEB_URL=http://localhost:3000
4
+ VITNODE_WEB_URL=http://localhost:3000
5
5
 
6
6
  # === Docker Database Postgres ===
7
7
  POSTGRES_USER=root
@@ -1,12 +1 @@
1
- NEXT_PUBLIC_API_URL=http://localhost:8000
2
-
3
- # Optional. Set these to back the Next.js caches with Redis, so cached pages and
4
- # `use cache` entries are shared between instances and a tag revalidation on one
5
- # instance is seen by all of them.
6
- #
7
- # The web process needs its own copy: the cache handlers are loaded by Next.js
8
- # itself and read the environment directly - they cannot see the API's
9
- # `vitnode.api.config.ts`. Point them at the same Redis the API uses.
10
- # https://vitnode.com/docs/dev/advanced/redis#caching-nextjs-output
11
- # REDIS_URL=redis://localhost:6379
12
- # REDIS_PASSWORD=root
1
+ VITNODE_API_URL=http://localhost:8000
@@ -3,6 +3,7 @@
3
3
  "ui": "tui",
4
4
  "tasks": {
5
5
  "db:prepare": {
6
+ "dependsOn": ["^build:plugins"],
6
7
  "cache": false,
7
8
  "inputs": ["$TURBO_DEFAULT$", ".env*"],
8
9
  "env": ["POSTGRES_URL"]
@@ -13,17 +14,21 @@
13
14
  "inputs": ["$TURBO_DEFAULT$", ".env*"],
14
15
  "env": ["POSTGRES_URL"]
15
16
  },
17
+ "build:plugins": {
18
+ "dependsOn": ["^build:plugins"],
19
+ "outputs": ["dist/**"]
20
+ },
16
21
  "build": {
17
- "dependsOn": ["^build"],
22
+ "dependsOn": ["^build:plugins", "^build"],
18
23
  "inputs": ["$TURBO_DEFAULT$", ".env*"],
19
24
  "outputs": [".output/**", "dist/**"],
20
- "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"]
25
+ "env": ["POSTGRES_URL", "VITNODE_API_URL", "VITNODE_WEB_URL"]
21
26
  },
22
27
  "dev": {
23
28
  "cache": false,
24
29
  "persistent": true,
25
30
  "inputs": ["$TURBO_DEFAULT$", ".env*"],
26
- "env": ["POSTGRES_URL", "NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_WEB_URL"]
31
+ "env": ["POSTGRES_URL", "VITNODE_API_URL", "VITNODE_WEB_URL"]
27
32
  },
28
33
  "start": {
29
34
  "dependsOn": ["^start"]
@@ -1,14 +1,7 @@
1
1
  POSTGRES_URL=postgresql://root:root@localhost:5432/vitnode
2
2
  REDIS_URL=redis://localhost:6379
3
3
 
4
- # Both name this app's own origin, and it has to be the port `dev` actually
5
- # serves. Neither is strictly required: on the server the API origin is taken
6
- # off the request being rendered, and in the browser it falls back to the origin
7
- # the page was served from - so a preview deployment on a generated hostname
8
- # needs no config at all. Set `NEXT_PUBLIC_API_URL` only to point at a separate
9
- # API server; `NEXT_PUBLIC_WEB_URL` is what the API stamps cookies with.
10
- NEXT_PUBLIC_WEB_URL=http://localhost:3000
11
- NEXT_PUBLIC_API_URL=http://localhost:3000
4
+ VITNODE_WEB_URL=http://localhost:3000
12
5
 
13
6
  # === CRON Secret for Internal API Calls ===
14
7
  CRON_SECRET=your-secure-cron-secret-key
@@ -2,25 +2,7 @@
2
2
 
3
3
  import core from '@vitnode/core/locales/en.json' with { type: 'json' }
4
4
 
5
- /**
6
- * What `useTranslations` is allowed to be asked for.
7
- *
8
- * Augmenting **`use-intl`** rather than `next-intl`. `AppConfig` is declared by
9
- * `use-intl`, which is what VitNode renders every string through on every host;
10
- * `next-intl` only ever re-exported it. This is a type-level dependency and so
11
- * it survives a grep for imports - which is exactly why it is worth naming.
12
- *
13
- * `use-intl` has to be a *direct* dependency of this app for the reference above
14
- * to resolve. Under pnpm's strict `node_modules` layout, reaching it through
15
- * `@vitnode/core` is not enough.
16
- *
17
- * Typed against core's English tree, which is the default locale: every other
18
- * language falls back to it key by key, so it is the one that defines which keys
19
- * exist. Add a plugin's tree to the intersection to get its keys checked too:
20
- *
21
- * import blog from '@acme/blog/locales/en.json' with { type: 'json' }
22
- * Messages: typeof core & typeof blog
23
- */
5
+
24
6
  declare module 'use-intl' {
25
7
  interface AppConfig {
26
8
  Messages: typeof core
@@ -1,58 +1,8 @@
1
- import type { AdminUserSearch } from "@vitnode/core/tanstack/admin";
2
-
3
1
  import { AdminShellContent } from "@vitnode/core/tanstack/admin";
2
+ import { useAppNavigate } from "@vitnode/core/tanstack/auth";
4
3
  import { LanguageSwitcher } from "@vitnode/core/tanstack/layout";
5
4
 
6
- import { adminNav } from "#/lib/admin-nav";
7
- import { adminUserSearchFn } from "#/lib/admin-search";
8
- import { useAppNavigate } from "#/lib/navigation";
9
-
10
- /**
11
- * The AdminCP shell, as this app mounts it.
12
- *
13
- * Everything the panel *is* - the sidebar, the header, the palette, the user
14
- * menu, the breadcrumb area and the one `<main>` - is `AdminShellContent`'s.
15
- * What is bound here is only what a package cannot answer for an application:
16
- *
17
- * onNavigate useAppNavigate the palette's Enter key
18
- * searchUsers adminUserSearchFn this app's own server function
19
- * languageSwitcher <LanguageSwitcher/> the router's
20
- * nav adminNav the plugins *this* app configured
21
- *
22
- * No `LinkComponent`: every sidebar destination is a route in this application's
23
- * own tree, so the shell's own default - `RouterLink` - is the right one. The
24
- * exception is handled a layer down rather than here: a plugin's `admin.nav`
25
- * entry may point at a docs site or a status page, and `adminLinkFor` renders
26
- * those as plain anchors, so an absolute URL is never handed to a router that
27
- * would try to match it. The sidebar and the command palette both go through it,
28
- * so an entry cannot behave one way when clicked and another when searched.
29
- *
30
- * `onNavigate` is still passed, because the palette moves the router *without a
31
- * link*: Enter on a highlighted entry is a navigation nobody clicked, and it has
32
- * to be handed the same de-localized destination a `<Link>` would build. See
33
- * `#/lib/navigation`.
34
- *
35
- * ## `nav` is a projection, not the plugin registry
36
- *
37
- * `src/vitnode.config.ts` is server-side and carries message loaders, which a
38
- * browser bundle has no business holding. So the sidebar is built from
39
- * `#/lib/admin-nav`, which reads the generated browser-safe projection: ids,
40
- * hrefs, permissions, icons and content type definitions, and nothing that
41
- * renders a screen. The screens have a projection of their own - see
42
- * `#/lib/content-registry`, which the content route imports so that a plugin's
43
- * editor fields and form layouts land in that route's chunk rather than in the
44
- * shell's.
45
- *
46
- * It carries the message namespaces with it, because a plugin group's headings
47
- * live under that plugin's own id and the shell would otherwise render them as
48
- * dotted identifiers. `_admin`'s loader warms the same list.
49
- *
50
- * No navigation is derived from a plugin's route tree, in either direction: the
51
- * navigation model is complete regardless of which screen a click lands on, and
52
- * a nav entry is a product decision rather than a consequence of the route tree.
53
- */
54
- const searchUsers: AdminUserSearch = async search =>
55
- await adminUserSearchFn({ data: { search } });
5
+ import { adminNav } from "@/admin-nav.gen";
56
6
 
57
7
  export const AdminShell = ({ children }: { children: React.ReactNode }) => {
58
8
  const navigate = useAppNavigate();
@@ -64,7 +14,6 @@ export const AdminShell = ({ children }: { children: React.ReactNode }) => {
64
14
  onNavigate={href => {
65
15
  void navigate(href);
66
16
  }}
67
- searchUsers={searchUsers}
68
17
  >
69
18
  {children}
70
19
  </AdminShellContent>
@@ -0,0 +1,44 @@
1
+ import {
2
+ VITNODE_DOCS_URL,
3
+ VITNODE_SPONSOR_URL,
4
+ VITNODE_WEBSITE_URL,
5
+ } from "@vitnode/core/lib/docs-links";
6
+ import { Heart } from "lucide-react";
7
+
8
+ export const MainFooter = () => (
9
+ <footer className="border-t">
10
+ <div className="text-muted-foreground container mx-auto flex flex-col items-center justify-between gap-3 px-4 py-6 text-sm sm:flex-row">
11
+ <p>
12
+ Powered by{" "}
13
+ <a
14
+ className="text-foreground font-medium underline-offset-4 hover:underline"
15
+ href={VITNODE_WEBSITE_URL}
16
+ rel="noopener noreferrer"
17
+ target="_blank"
18
+ >
19
+ VitNode
20
+ </a>
21
+ </p>
22
+
23
+ <nav aria-label="VitNode" className="flex items-center gap-5">
24
+ <a
25
+ className="hover:text-foreground transition-colors"
26
+ href={VITNODE_DOCS_URL}
27
+ rel="noopener noreferrer"
28
+ target="_blank"
29
+ >
30
+ Docs
31
+ </a>
32
+ <a
33
+ className="hover:text-foreground flex items-center gap-1.5 transition-colors"
34
+ href={VITNODE_SPONSOR_URL}
35
+ rel="noopener noreferrer"
36
+ target="_blank"
37
+ >
38
+ <Heart className="size-3.5" />
39
+ Sponsor
40
+ </a>
41
+ </nav>
42
+ </div>
43
+ </footer>
44
+ );
@@ -1,33 +1,4 @@
1
1
  import { MainHeader as MainHeaderContent } from "@vitnode/core/tanstack/layout";
2
2
 
3
- /**
4
- * The site header, as the main shell's slot for it.
5
- *
6
- * One line of application. The bar, the nav, the language and theme switchers,
7
- * the user area and both cache entries they read are
8
- * `@vitnode/core/tanstack/layout`'s `MainHeader`.
9
- *
10
- * **Your mark goes here.** Left alone the header renders VitNode's own, which is
11
- * the right default and the wrong answer for a real site. Pass your own:
12
- *
13
- * <MainHeaderContent logo={<YourLogo />} />
14
- *
15
- * This file exists so that choosing a mark is a sentence a site says out loud
16
- * rather than something it inherits without noticing.
17
- *
18
- * No `LinkComponent`. Every header destination is a route in this application's
19
- * own tree, so the header's own default - `RouterLink`, the router's `Link` in
20
- * the shape the shared views ask for - is the right one, and the prop stays
21
- * available for a host that needs to answer differently.
22
- *
23
- * ## What the shell owes it
24
- *
25
- * Two warm cache entries, both ensured by `_main`'s loader:
26
- *
27
- * headerIntlQueryOptions -> a `useSuspenseQuery`, so this is required
28
- * prefetchSession -> the first paint shows the visitor, not a gap
29
- *
30
- * See the loader in `routes/_main.tsx`, which states why one is `ensure` and the
31
- * other `prefetch`.
32
- */
3
+
33
4
  export const MainHeader = () => <MainHeaderContent />;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The one bootstrap adapter this application still owns.
3
+ *
4
+ * Auth, the AdminCP session and the AdminCP user search all reach the API
5
+ * through `@vitnode/core`'s own default transports now, so none of them needs a
6
+ * module here. Messages cannot work that way, for two reasons that only hold
7
+ * in app source:
8
+ *
9
+ * - `loadIntlMessages` reads *this* application's locale barrels, which are the
10
+ * app's own files plus whichever packages it installed. A package cannot know
11
+ * them.
12
+ * - `createServerFn` is extracted by the TanStack Start compiler, and the
13
+ * compiler only runs over the application's source. `@vitnode/core` ships
14
+ * precompiled, so a server function declared there would never have its
15
+ * handler extracted and would resolve to `undefined` at runtime.
16
+ *
17
+ * `configureIntl` is where the two meet: the app hands core the fetch and its
18
+ * configured languages, and everything else in the package reads them back
19
+ * through `getIntlRuntime()`.
20
+ */
21
+ import { createServerFn } from "@tanstack/react-start";
22
+ import { configureIntl, validateIntlInput } from "@vitnode/core/tanstack/i18n";
23
+ import { IntlProvider } from "use-intl";
24
+
25
+ import { loadIntlMessages } from "@/server/messages.server";
26
+ import { vitNodeConfig } from "@/vitnode.config";
27
+
28
+ export const getIntlMessages = createServerFn()
29
+ .validator(validateIntlInput)
30
+ .handler(async ({ data }) => await loadIntlMessages(data));
31
+
32
+ export const { localeRouting } = configureIntl({
33
+ fetchMessages: async input => await getIntlMessages({ data: input }),
34
+
35
+ hostIntlProvider: IntlProvider,
36
+ i18n: vitNodeConfig.i18n,
37
+ });
@@ -1,28 +1,4 @@
1
1
  import type { AppMessagesMap } from "@vitnode/core/lib/i18n/types";
2
2
 
3
- /**
4
- * Translations this app owns, on top of whatever the packages ship.
5
- *
6
- * Empty, and usually stays that way. Every VitNode package ships its own
7
- * translations and `locales/packages.ts` is what registers them - carrying a
8
- * second copy of a string a package already owns is how one product comes to
9
- * spell the same settings tab two different ways.
10
- *
11
- * What belongs here is a string this app *changes*. Add a locale, then the key
12
- * you are rewording:
13
- *
14
- * export const appMessages: AppMessagesMap = {
15
- * en: {
16
- * '@vitnode/core': async () => await import('./en.json'),
17
- * },
18
- * }
19
- *
20
- * Deep-merged last, so a file here only needs the keys it actually changes:
21
- * everything it leaves out falls back to the package's, and then to the default
22
- * locale, key by key.
23
- *
24
- * Server-side only, and kept out of `src/i18n.ts` on purpose: these are
25
- * functions, and `src/i18n.ts` is spread into the shell config, which crosses to
26
- * the browser and has to stay serializable.
27
- */
3
+
28
4
  export const appMessages: AppMessagesMap = {};
@@ -2,40 +2,7 @@ import type { LocaleMessagesMap } from "@vitnode/core/lib/i18n/types";
2
2
 
3
3
  import { CONFIG_PLUGIN as CORE } from "@vitnode/core/config";
4
4
 
5
- /**
6
- * Where this app reads each installed package's translations from.
7
- *
8
- * Every VitNode package ships a locale barrel - `@vitnode/core/locales/index` -
9
- * that loads its own files with a runtime
10
- * `import("./en.json", { with: { type: "json" } })`. Under Node that is exactly
11
- * right, and it is how an API app reads them.
12
- *
13
- * It cannot work here, and the reason is the import attribute rather than
14
- * anything about VitNode. Vite and Nitro inline `@vitnode/core`'s build output
15
- * into this app's server chunks - `ssr.external` applies to the SSR pass, not to
16
- * Nitro's own bundling - but Rollup will not follow a dynamic import that
17
- * carries `with: { type: "json" }`, so it neither emits the JSON nor rewrites
18
- * the specifier. What ships is a relative import pointing next to a chunk that
19
- * the JSON was never copied to, and every string on the page renders as its own
20
- * key.
21
- *
22
- * So the loaders are declared here instead, with static specifiers a bundler can
23
- * follow. Each resolves through the package's `./locales/*.json` export to the
24
- * real file and lands in the build as a chunk fetched on demand, which is the
25
- * same laziness the barrels wanted.
26
- *
27
- * **Add a line here for every plugin you install.** A plugin registered in
28
- * `vitnode.config.ts` with no entry in this map renders its own strings as keys:
29
- *
30
- * import { CONFIG_PLUGIN as BLOG } from '@acme/blog/const'
31
- *
32
- * [BLOG.pluginId]: {
33
- * en: async () => await import('@acme/blog/locales/en.json'),
34
- * },
35
- *
36
- * This is the app's *only* copy of that list - `vitnode.config.ts` and
37
- * `server/messages.server.ts` both read it from here.
38
- */
5
+
39
6
  export const packageMessages: Record<string, LocaleMessagesMap> = {
40
7
  [CORE.pluginId]: {
41
8
  en: async () => await import("@vitnode/core/locales/en.json"),