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.
- package/README.md +9 -2
- package/copy-of-vitnode-app/README.md +22 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +10 -11
- package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +4 -26
- package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +1 -12
- package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +13 -13
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +3 -12
- package/copy-of-vitnode-app/docker/docker-compose.yml +6 -4
- package/copy-of-vitnode-app/monorepo/apps/api/.env.example +1 -1
- package/copy-of-vitnode-app/monorepo/apps/web/.env.example +1 -12
- package/copy-of-vitnode-app/monorepo/turbo.json +8 -3
- package/copy-of-vitnode-app/root/.env.example +1 -8
- package/copy-of-vitnode-app/root/global.d.ts +1 -19
- package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +2 -53
- package/copy-of-vitnode-app/root/src/components/main-footer.tsx +44 -0
- package/copy-of-vitnode-app/root/src/components/main-header.tsx +1 -30
- package/copy-of-vitnode-app/root/src/lib/i18n.ts +37 -0
- package/copy-of-vitnode-app/root/src/locales/app.ts +1 -25
- package/copy-of-vitnode-app/root/src/locales/packages.ts +1 -34
- package/copy-of-vitnode-app/root/src/router.tsx +16 -165
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +12 -115
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +4 -54
- package/copy-of-vitnode-app/root/src/routes/_admin.tsx +5 -198
- package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +182 -46
- package/copy-of-vitnode-app/root/src/routes/_main.tsx +12 -47
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +2 -23
- package/copy-of-vitnode-app/root/src/start.ts +3 -86
- package/copy-of-vitnode-app/root/src/styles.css +70 -92
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +15 -53
- package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +12 -0
- package/copy-of-vitnode-app/root/tsconfig.json +8 -3
- package/copy-of-vitnode-app/root/vite.config.ts +2 -99
- package/copy-of-vitnode-plugin/root/global.d.ts +6 -6
- package/dist/src/create/create-package-json.js +5 -4
- package/dist/src/create/create-vitnode.js +15 -1
- package/dist/src/create/package-versions.js +22 -22
- package/dist/src/helpers/init-git.js +38 -0
- package/dist/src/helpers/init-vitnode.js +2 -3
- package/dist/src/helpers/install-dependencies.js +2 -3
- package/dist/src/helpers/spawn-command.js +7 -0
- package/dist/src/index.js +1 -0
- package/dist/src/plugin/create/add-plugin-to-config.js +231 -0
- package/dist/src/plugin/create/create-package-json.js +12 -10
- package/dist/src/plugin/create/create-plugin-vitnode.js +23 -2
- package/dist/src/plugin/create/route-templates.js +103 -61
- package/dist/src/questions.js +7 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/copy-of-vitnode-app/api/src/i18n.ts +0 -38
- package/copy-of-vitnode-app/root/src/i18n.ts +0 -36
- package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +0 -11
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +0 -43
- package/copy-of-vitnode-app/root/src/lib/admin-search.ts +0 -37
- package/copy-of-vitnode-app/root/src/lib/auth.ts +0 -66
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +0 -51
- package/copy-of-vitnode-app/root/src/lib/document-headers.ts +0 -128
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +0 -75
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +0 -20
- package/copy-of-vitnode-app/root/src/lib/navigation.ts +0 -24
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +0 -19
- 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.
|
|
63
|
-
in the host’s `vitnode.config.ts
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
2
|
|
|
3
|
-
import { apiBridge } from
|
|
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
|
|
2
|
-
import { OpenAPIHono } from
|
|
3
|
-
import { VitNodeAPI } from
|
|
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
|
|
6
|
-
import { vitNodeApiConfig } from
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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,12 +1 @@
|
|
|
1
|
-
|
|
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", "
|
|
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", "
|
|
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
|
-
|
|
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 "
|
|
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"),
|