create-vitnode-app 1.2.0-canary.9 → 2.0.0-canary.2
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/LICENSE.md +1 -1
- package/README.md +58 -18
- package/copy-of-vitnode-app/.vscode/settings.json +10 -0
- package/copy-of-vitnode-app/README.md +62 -0
- package/copy-of-vitnode-app/api/.gitignore_template +32 -0
- package/copy-of-vitnode-app/api/drizzle.config.ts +12 -0
- package/copy-of-vitnode-app/api/src/index.ts +27 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +57 -0
- package/copy-of-vitnode-app/api/tsconfig.json +17 -0
- package/copy-of-vitnode-app/api-bun/src/index.ts +16 -0
- package/copy-of-vitnode-app/api-single-app/drizzle.config.ts +12 -0
- package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +32 -0
- package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +22 -0
- package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +29 -0
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +36 -0
- package/copy-of-vitnode-app/docker/docker-compose.yml +30 -0
- package/copy-of-vitnode-app/eslint/.prettierrc.mjs +1 -1
- package/copy-of-vitnode-app/eslint/eslint.config.mjs +18 -2
- package/copy-of-vitnode-app/{root → eslint-react}/.prettierrc.mjs +1 -1
- package/copy-of-vitnode-app/eslint-react/eslint.config.mjs +19 -0
- package/copy-of-vitnode-app/monorepo/.gitignore_template +41 -0
- package/copy-of-vitnode-app/monorepo/apps/api/.env.example +12 -0
- package/copy-of-vitnode-app/monorepo/apps/web/.env.example +12 -0
- package/copy-of-vitnode-app/monorepo/turbo.json +53 -0
- package/copy-of-vitnode-app/root/.env.example +16 -5
- package/copy-of-vitnode-app/root/.gitignore_template +41 -0
- package/copy-of-vitnode-app/root/global.d.ts +28 -0
- package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +72 -0
- package/copy-of-vitnode-app/root/src/components/main-header.tsx +33 -0
- package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +11 -0
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +43 -0
- package/copy-of-vitnode-app/root/src/lib/admin-search.ts +37 -0
- package/copy-of-vitnode-app/root/src/lib/auth.ts +66 -0
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +52 -0
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +75 -0
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +20 -0
- package/copy-of-vitnode-app/root/src/lib/navigation.ts +24 -0
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +19 -0
- package/copy-of-vitnode-app/root/src/locales/app.ts +28 -0
- package/copy-of-vitnode-app/root/src/locales/packages.ts +43 -0
- package/copy-of-vitnode-app/root/src/router.tsx +227 -0
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +212 -0
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +69 -0
- package/copy-of-vitnode-app/root/src/routes/_admin.tsx +258 -0
- package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +60 -0
- package/copy-of-vitnode-app/root/src/routes/_main.tsx +64 -0
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +21 -0
- package/copy-of-vitnode-app/root/src/start.ts +24 -0
- package/copy-of-vitnode-app/root/src/styles.css +139 -0
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +73 -0
- package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +23 -0
- package/copy-of-vitnode-app/root/tsconfig.json +24 -16
- package/copy-of-vitnode-app/root/tsr.config.json +3 -0
- package/copy-of-vitnode-app/root/vite.config.ts +106 -0
- package/copy-of-vitnode-plugin/root/.swcrc +26 -0
- package/copy-of-vitnode-plugin/root/global.d.ts +21 -0
- package/copy-of-vitnode-plugin/root/npmignore.template +17 -0
- package/copy-of-vitnode-plugin/root/tsconfig.build.json +5 -0
- package/copy-of-vitnode-plugin/root/tsconfig.json +25 -0
- package/dist/src/create/create-package-json.js +266 -56
- package/dist/src/create/create-vitnode.js +177 -28
- package/dist/src/create/package-versions.js +49 -0
- package/dist/src/helpers/get-available-package-managers.js +6 -6
- package/dist/src/helpers/get-package-json.js +2 -2
- package/dist/src/helpers/get-package-manager-from-root.js +15 -0
- package/dist/src/helpers/get-vitnode-package-version.js +9 -0
- package/dist/src/helpers/init-vitnode.js +34 -0
- package/dist/src/helpers/install-dependencies.js +55 -14
- package/dist/src/helpers/is-folder-empty.js +25 -26
- package/dist/src/helpers/is-online.js +16 -16
- package/dist/src/helpers/is-writeable.js +3 -3
- package/dist/src/helpers/validate-pkg.js +1 -1
- package/dist/src/helpers/with-If.js +1 -0
- package/dist/src/index.js +29 -26
- package/dist/src/plugin/create/add-plugin-to-workspace.js +74 -0
- package/dist/src/plugin/create/create-package-json.js +54 -0
- package/dist/src/plugin/create/create-plugin-vitnode.js +80 -0
- package/dist/src/plugin/create/route-templates.js +144 -0
- package/dist/src/plugin/index.js +17 -6
- package/dist/src/plugin/questions.js +14 -0
- package/dist/src/plugin/validation.js +76 -0
- package/dist/src/prepare/prepare.js +11 -8
- package/dist/src/questions.js +52 -15
- package/dist/src/validation.js +15 -15
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +22 -12
- package/copy-of-vitnode-app/root/README.md +0 -1
- package/copy-of-vitnode-app/root/drizzle.config.ts +0 -12
- package/copy-of-vitnode-app/root/gitignore_template +0 -43
- package/copy-of-vitnode-app/root/next.config.ts +0 -11
- package/copy-of-vitnode-app/root/postcss.config.mjs +0 -8
- package/copy-of-vitnode-app/root/src/app/[locale]/(main)/[...rest]/page.tsx +0 -5
- package/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx +0 -10
- package/copy-of-vitnode-app/root/src/app/[locale]/(main)/not-found.tsx +0 -5
- package/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx +0 -113
- package/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/layout.tsx +0 -10
- package/copy-of-vitnode-app/root/src/app/[locale]/admin/not-found.tsx +0 -5
- package/copy-of-vitnode-app/root/src/app/[locale]/admin/page.tsx +0 -5
- package/copy-of-vitnode-app/root/src/app/[locale]/layout.tsx +0 -36
- package/copy-of-vitnode-app/root/src/app/api/[...route]/route.ts +0 -18
- package/copy-of-vitnode-app/root/src/app/favicon.ico +0 -0
- package/copy-of-vitnode-app/root/src/app/global-error.tsx +0 -27
- package/copy-of-vitnode-app/root/src/app/global.css +0 -128
- package/copy-of-vitnode-app/root/src/app/layout.tsx +0 -9
- package/copy-of-vitnode-app/root/src/app/not-found.tsx +0 -13
- package/copy-of-vitnode-app/root/src/vitnode.api.config.ts +0 -5
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
|
2
|
+
import {
|
|
3
|
+
ADMIN_ENTRY_PATH,
|
|
4
|
+
AdminNotFound,
|
|
5
|
+
adminReturnToFor,
|
|
6
|
+
canEnterAdmin,
|
|
7
|
+
ensureAdminAccess,
|
|
8
|
+
loadAdminMessages,
|
|
9
|
+
preloadAdminAccess,
|
|
10
|
+
} from "@vitnode/core/tanstack/admin";
|
|
11
|
+
import { ErrorActions } from "@vitnode/core/tanstack/layout";
|
|
12
|
+
|
|
13
|
+
import { AdminShell } from "#/components/admin-shell";
|
|
14
|
+
import { pageHead } from "#/lib/page-head";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The boundary every AdminCP page sits under - the admin session guard, and the
|
|
18
|
+
* shell.
|
|
19
|
+
*
|
|
20
|
+
* Pathless: the leading underscore means it contributes no URL segment, so
|
|
21
|
+
* `routes/_admin/core/index.tsx` is `/admin/core`, guarded, and the guard is
|
|
22
|
+
* this file. A page joins the AdminCP by *where its file lives*, not by
|
|
23
|
+
* remembering to check a session.
|
|
24
|
+
*
|
|
25
|
+
* ## It is not under `_authenticated`, and must not be
|
|
26
|
+
*
|
|
27
|
+
* The AdminCP has its own session, under its own cookie (`vitnode_auth_admin`),
|
|
28
|
+
* with its own model and its own endpoint. Stacking the public guard above this
|
|
29
|
+
* one would bounce an administrator to `/login` for a session the AdminCP does
|
|
30
|
+
* not use - and `AuthState.isAdmin`, which lives on the *public* session, means
|
|
31
|
+
* "may be offered the AdminCP", not "is inside it". Two cookies, two questions.
|
|
32
|
+
*
|
|
33
|
+
* ## Exactly one splat may live under here, and nothing wider
|
|
34
|
+
*
|
|
35
|
+
* `admin.content.$.tsx` claims `/admin/content/*` - the Content Engine's own
|
|
36
|
+
* namespace, which Stage 13 moved into this router. That one is deliberate and
|
|
37
|
+
* narrow; a second is how the AdminCP breaks.
|
|
38
|
+
*
|
|
39
|
+
* `$.tsx`, `admin.$.tsx` or any other catch-all beside it would consume every
|
|
40
|
+
* *remaining* admin URL, including the ones no route declares. The AdminCP's own
|
|
41
|
+
* not-found would stop being reachable, a plugin's admin route would be shadowed
|
|
42
|
+
* by whichever splat sat above it, and nothing would report either - silently,
|
|
43
|
+
* and all at once. `src/tests/admin-routes.test.ts` pins both halves: that the
|
|
44
|
+
* content splat sits at exactly that path, and that it is the only one.
|
|
45
|
+
*
|
|
46
|
+
* ## Why the check is in `beforeLoad`
|
|
47
|
+
*
|
|
48
|
+
* It runs before the loader and long before React, so a visitor without admin
|
|
49
|
+
* access never receives a byte of an AdminCP page - not a flash, not a
|
|
50
|
+
* hydration, not a `useEffect` that redirects afterwards. A component-level
|
|
51
|
+
* check would render the page first and then take it away, which on the server
|
|
52
|
+
* means admin markup already written into the stream.
|
|
53
|
+
*
|
|
54
|
+
* ## A hover is not a navigation
|
|
55
|
+
*
|
|
56
|
+
* `defaultPreload: 'intent'` means the router runs this whole chain when a
|
|
57
|
+
* pointer rests on an admin link, and router-core has no staleness gate on
|
|
58
|
+
* `beforeLoad` the way it has on a loader - it re-runs it from the top of the
|
|
59
|
+
* branch every time. With `ADMIN_SESSION_STALE_TIME` at zero that made crossing
|
|
60
|
+
* the sidebar one server-function POST and one Hono call *per link the mouse
|
|
61
|
+
* passed over*, for a question nobody had asked yet.
|
|
62
|
+
*
|
|
63
|
+
* So the read is chosen by the `preload` flag the router already hands in.
|
|
64
|
+
* `preloadAdminAccess` is the same query, the same key and the same rejection
|
|
65
|
+
* behaviour, trusted for thirty seconds - the identical window the public
|
|
66
|
+
* session has always used for the identical reason. `ensureAdminAccess` and its
|
|
67
|
+
* `staleTime: 0` are untouched on the path that matters, and that is the whole
|
|
68
|
+
* of the revocation guarantee: entering a screen is a real navigation, this runs
|
|
69
|
+
* again with `preload: false`, and the API is asked. Router-core never lets a
|
|
70
|
+
* preloaded `beforeLoad` result stand in for a navigation's.
|
|
71
|
+
*
|
|
72
|
+
* The redirect below is deliberately *not* conditional on `preload`. It is not a
|
|
73
|
+
* navigation - `preloadClientRoute` catches it and preloads the sign-in page
|
|
74
|
+
* instead of moving anybody - and skipping it would leave a denied preload
|
|
75
|
+
* walking into the screen loaders underneath, which would then send admin
|
|
76
|
+
* requests the API is going to refuse.
|
|
77
|
+
*
|
|
78
|
+
* ## A failed read is not a denial
|
|
79
|
+
*
|
|
80
|
+
* `ensureAdminAccess` resolves only for an answer the API actually gave - `200`
|
|
81
|
+
* or `403`. A `429` from the rate limiter, a `500`, an API that is not listening:
|
|
82
|
+
* all three reject, and that rejection is deliberately left to propagate as an
|
|
83
|
+
* ordinary route error.
|
|
84
|
+
*
|
|
85
|
+
* Only `canEnterAdmin` answering `false` sends anybody to `/admin`. Catching the
|
|
86
|
+
* rejection and redirecting instead would sign every administrator out of the
|
|
87
|
+
* AdminCP during an outage and present them with a sign-in form for a session
|
|
88
|
+
* they already hold - which is precisely what the Next.js `getSessionAdminApi()`
|
|
89
|
+
* does today, and precisely what this shape exists to stop.
|
|
90
|
+
*
|
|
91
|
+
* ## What it is not
|
|
92
|
+
*
|
|
93
|
+
* A navigation guard, and only that. `api/config.ts` puts
|
|
94
|
+
* `globalAdminMiddleware()` in front of every request whose path contains
|
|
95
|
+
* `/admin/`, each handler re-checks the staff tables, and
|
|
96
|
+
* `SessionAdminModel.getUser()` re-runs `checkIfUserIsAdmin` against the
|
|
97
|
+
* database on every request - deleting the session the moment the answer turns
|
|
98
|
+
* false. So an administrator who edits this app's cached permission set in
|
|
99
|
+
* devtools gets a visible button and an API that still refuses them. Nothing
|
|
100
|
+
* here is, or may become, the security boundary.
|
|
101
|
+
*/
|
|
102
|
+
export const Route = createFileRoute("/_admin")({
|
|
103
|
+
beforeLoad: async ({ context, location, preload }) => {
|
|
104
|
+
const access = preload
|
|
105
|
+
? await preloadAdminAccess(context.queryClient)
|
|
106
|
+
: await ensureAdminAccess(context.queryClient);
|
|
107
|
+
|
|
108
|
+
if (!canEnterAdmin(access)) {
|
|
109
|
+
// TanStack Router's own control-flow signal: `redirect()` returns a typed
|
|
110
|
+
// redirect object that the router catches and turns into a navigation
|
|
111
|
+
// (or, during SSR, a 302). Throwing it is what stops the guard.
|
|
112
|
+
//
|
|
113
|
+
// `/admin` is a sibling leaf, not a child of this route, so this cannot
|
|
114
|
+
// loop: the sign-in page's own guard only redirects *away* on a granted
|
|
115
|
+
// session, and `sanitizeAdminReturnTo` rejects `/admin` as a target.
|
|
116
|
+
//
|
|
117
|
+
// Cast because `/admin` is not in this router's type table: it is
|
|
118
|
+
// `@vitnode/core`'s code-based route now, mounted by `withCoreRootRoutes`,
|
|
119
|
+
// and code-based routes are outside the generated tree's types. The
|
|
120
|
+
// *runtime* is unaffected, and `ADMIN_ENTRY_PATH` is the package's own
|
|
121
|
+
// constant - so the path and the sign-in route that serves it are still one
|
|
122
|
+
// fact in one place.
|
|
123
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
124
|
+
throw redirect({
|
|
125
|
+
search: { returnTo: adminReturnToFor(location) },
|
|
126
|
+
to: ADMIN_ENTRY_PATH,
|
|
127
|
+
} as unknown as Parameters<typeof redirect>[0]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Merged into the context of everything below, so a child route reads
|
|
131
|
+
// `context.adminAccess` already narrowed to the granted half of the union.
|
|
132
|
+
// It is the same object the guard decided on, from the same cache entry, so
|
|
133
|
+
// a page cannot disagree with the guard that let it render.
|
|
134
|
+
return { adminAccess: access };
|
|
135
|
+
},
|
|
136
|
+
/**
|
|
137
|
+
* The shell's strings, warmed before React renders.
|
|
138
|
+
*
|
|
139
|
+
* `adminNav.namespaces` is the same array `AdminShell` hands the shell, and it
|
|
140
|
+
* has to be: the provider reads back the identical `intlQueryOptions` entry
|
|
141
|
+
* this fills, so warming a different namespace set would fill an entry nobody
|
|
142
|
+
* looks at and cost a round trip on the first paint anyway. It is not a fixed
|
|
143
|
+
* list because it cannot be - a plugin group's headings live under that
|
|
144
|
+
* plugin's own id, and which plugins this installation configured is decided
|
|
145
|
+
* in `src/admin-nav.gen.ts`.
|
|
146
|
+
*
|
|
147
|
+
* ## Imported inside the loader, not above it
|
|
148
|
+
*
|
|
149
|
+
* A route file's `loader` body runs only for a navigation into this shell; the
|
|
150
|
+
* file itself is evaluated in the client entry, on every page of the site. The
|
|
151
|
+
* generated navigation is not small - it carries every configured plugin's
|
|
152
|
+
* sidebar icons and its content type definitions, and through those the whole
|
|
153
|
+
* Content Engine and `zod` - so a static import here put ~45 KB of AdminCP
|
|
154
|
+
* data, plus `zod`, in front of the front page's first paint. `AdminShell`
|
|
155
|
+
* imports the same module, and `AdminShell` is this route's `component`, which
|
|
156
|
+
* is code-split: the two land in the same chunk, so an administrator pays for
|
|
157
|
+
* it once and everybody else not at all.
|
|
158
|
+
*/
|
|
159
|
+
loader: async ({ context }) => {
|
|
160
|
+
const { adminNav } = await import("#/lib/admin-nav");
|
|
161
|
+
|
|
162
|
+
await loadAdminMessages({ ...context, namespaces: adminNav.namespaces });
|
|
163
|
+
},
|
|
164
|
+
/**
|
|
165
|
+
* Stated once for the whole panel rather than on each screen.
|
|
166
|
+
*
|
|
167
|
+
* The AdminCP is behind a session and must never be indexed. Router merges the
|
|
168
|
+
* `head` of every matched route and dedupes `meta` by `name`, preferring the
|
|
169
|
+
* deepest, so an admin screen inherits this by saying nothing - which is
|
|
170
|
+
* exactly what `RouteHeadOptions` describes. A screen's own `pageHead` adds
|
|
171
|
+
* only its title and description.
|
|
172
|
+
*/
|
|
173
|
+
head: () => pageHead({ robots: "noindex, nofollow" }),
|
|
174
|
+
/**
|
|
175
|
+
* The AdminCP's 404, rendered inside the shell.
|
|
176
|
+
*
|
|
177
|
+
* What reaches it is a screen whose loader called `requireAdminPermission`
|
|
178
|
+
* and was refused - the same answer `app/[locale]/admin/(auth)/not-found.tsx`
|
|
179
|
+
* gives in the Next.js AdminCP. Declared here rather than per screen so every
|
|
180
|
+
* one of them answers a missing permission identically.
|
|
181
|
+
*
|
|
182
|
+
* A content URL that resolves to no content type reaches it too, and by the
|
|
183
|
+
* same route: `loadContentAdminRoute` throws `notFound()` for a splat its
|
|
184
|
+
* registry cannot name, which is the answer the Next.js catch-all's
|
|
185
|
+
* `notFound()` gives one navigation later.
|
|
186
|
+
*
|
|
187
|
+
* A URL under `/admin` that matches no route at all does *not* reach this one.
|
|
188
|
+
* Outside `/admin/content`, `_admin` has only declared children - see above -
|
|
189
|
+
* so such a path matches nothing in this subtree and the router falls back to
|
|
190
|
+
* its own not-found at the root. That is the correct trade, and the cost is
|
|
191
|
+
* paid deliberately: rendering an unmigrated admin URL inside this shell would
|
|
192
|
+
* mean claiming it. (The root has no `notFoundComponent` of its own yet, so
|
|
193
|
+
* that fallback is currently the router's bare one - for every unmatched URL
|
|
194
|
+
* in this application, not only admin ones. Giving the root a real 404 page is
|
|
195
|
+
* its own piece of work.)
|
|
196
|
+
*/
|
|
197
|
+
notFoundComponent: AdminNotFoundScreen,
|
|
198
|
+
component: AdminLayout,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The refusal, wearing the panel it was refused inside.
|
|
203
|
+
*
|
|
204
|
+
* ## It mounts the shell itself, and has to
|
|
205
|
+
*
|
|
206
|
+
* A `notFoundComponent` renders *instead of* the component of the route that
|
|
207
|
+
* handles the error, not inside it - so this replaces `AdminLayout`, sidebar
|
|
208
|
+
* and header and palette included. An administrator who opened a screen they
|
|
209
|
+
* lack the permission for would otherwise be dropped onto a bare 404 page with
|
|
210
|
+
* no way back into the AdminCP but the browser's back button, which is not what
|
|
211
|
+
* the Next.js AdminCP does: its `not-found.tsx` sits *under*
|
|
212
|
+
* `admin/(auth)/layout.tsx` and keeps the panel around the message.
|
|
213
|
+
*
|
|
214
|
+
* Mounting `AdminShell` here restores that, and it costs nothing: `beforeLoad`
|
|
215
|
+
* has already resolved the admin session and the loader above has already
|
|
216
|
+
* warmed the shell's messages, so the providers inside read the same two cache
|
|
217
|
+
* entries the working screens read and nothing suspends or fetches again.
|
|
218
|
+
*
|
|
219
|
+
* ## What is bound rather than defaulted
|
|
220
|
+
*
|
|
221
|
+
* `ErrorActions` is this app's binding rather than core's default: `/` is served
|
|
222
|
+
* by the Next.js application on some installs and by this one on others, and
|
|
223
|
+
* only the route tree knows which. It is passed as an element from module scope
|
|
224
|
+
* so the type is stable across renders.
|
|
225
|
+
*/
|
|
226
|
+
function AdminNotFoundScreen() {
|
|
227
|
+
return (
|
|
228
|
+
<AdminShell>
|
|
229
|
+
<AdminNotFound actions={<ErrorActions />} />
|
|
230
|
+
</AdminShell>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The AdminCP shell.
|
|
236
|
+
*
|
|
237
|
+
* `AdminShell` is this app's binding of `AdminShellContent` - the sidebar, the
|
|
238
|
+
* command palette, the breadcrumb area, the user menu and the one `<main>` every
|
|
239
|
+
* admin page renders inside. It mounts `AdminPermissionsProvider` itself, from
|
|
240
|
+
* the same admin session query the guard above has already filled, so
|
|
241
|
+
* `AdminStaffPermissionGate` and `useAdminStaffPermission` work identically here
|
|
242
|
+
* and in the Next.js AdminCP. It cannot suspend in practice and nothing below it
|
|
243
|
+
* can suspend at all; see the note on the provider.
|
|
244
|
+
*
|
|
245
|
+
* What `#/components/admin-shell` adds on top is only what a package cannot
|
|
246
|
+
* answer for a particular installation: where the user lookup runs, which
|
|
247
|
+
* plugins this app configured, and how the command palette moves without a link.
|
|
248
|
+
*
|
|
249
|
+
* The guard, the loader and the route options above are the parts that must not
|
|
250
|
+
* move.
|
|
251
|
+
*/
|
|
252
|
+
function AdminLayout() {
|
|
253
|
+
return (
|
|
254
|
+
<AdminShell>
|
|
255
|
+
<Outlet />
|
|
256
|
+
</AdminShell>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
|
|
3
|
+
import { pageHead } from "#/lib/page-head";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The front page.
|
|
7
|
+
*
|
|
8
|
+
* One route file serving every public URL of the home page: `/` and `/pl` both
|
|
9
|
+
* match here, because the locale is stripped before matching and written back
|
|
10
|
+
* into every link the router builds. There is no `routes/pl/index.tsx` and there
|
|
11
|
+
* does not need to be one.
|
|
12
|
+
*
|
|
13
|
+
* **`head` must be written after `loader`** on routes that have one -
|
|
14
|
+
* `loaderData` is inferred from `loader` in the same object literal, and
|
|
15
|
+
* TypeScript reads a literal's members in order. This route has no loader: it
|
|
16
|
+
* fetches nothing, and the shell above it already warmed everything the header
|
|
17
|
+
* reads.
|
|
18
|
+
*
|
|
19
|
+
* `robots: 'index, follow'` is stated rather than assumed. `_main` says nothing
|
|
20
|
+
* about indexing, and a page that wants to be found should say so where somebody
|
|
21
|
+
* editing it will see it.
|
|
22
|
+
*/
|
|
23
|
+
export const Route = createFileRoute("/_main/")({
|
|
24
|
+
head: () =>
|
|
25
|
+
pageHead({
|
|
26
|
+
description: "A VitNode application.",
|
|
27
|
+
robots: "index, follow",
|
|
28
|
+
title: "Home",
|
|
29
|
+
}),
|
|
30
|
+
component: HomeRoute,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replace this with your own home page.
|
|
35
|
+
*
|
|
36
|
+
* No `<main>`: the shell owns the document's one `main` landmark, and a page
|
|
37
|
+
* that renders a second gives a screen reader two to choose between. A page owns
|
|
38
|
+
* its container - width, padding, vertical rhythm - and nothing above it.
|
|
39
|
+
*
|
|
40
|
+
* The copy is hard-coded because a new app has no strings of its own yet. To
|
|
41
|
+
* translate it, add the keys to `src/locales/` and read them with
|
|
42
|
+
* `useTranslations` from `use-intl` - the same hook every VitNode component
|
|
43
|
+
* uses, on every host.
|
|
44
|
+
*/
|
|
45
|
+
function HomeRoute() {
|
|
46
|
+
return (
|
|
47
|
+
<div className="container mx-auto flex max-w-2xl flex-col gap-4 p-4">
|
|
48
|
+
<h1 className="text-3xl font-semibold tracking-tight text-balance">
|
|
49
|
+
Welcome to VitNode
|
|
50
|
+
</h1>
|
|
51
|
+
|
|
52
|
+
<p className="text-muted-foreground leading-relaxed text-pretty">
|
|
53
|
+
Edit <code>src/routes/_main/index.tsx</code> to change this page. The
|
|
54
|
+
header, the footer and the shell around it are{" "}
|
|
55
|
+
<code>@vitnode/core</code>’s; everything under{" "}
|
|
56
|
+
<code>src/routes</code> is yours.
|
|
57
|
+
</p>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
|
2
|
+
import { MainBreadcrumb } from "@vitnode/core/tanstack/breadcrumb";
|
|
3
|
+
import {
|
|
4
|
+
loadMainShell,
|
|
5
|
+
ThemeLayoutContent,
|
|
6
|
+
} from "@vitnode/core/tanstack/layout";
|
|
7
|
+
|
|
8
|
+
import { MainHeader } from "#/components/main-header";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The main application shell - the header, the breadcrumb area and the one
|
|
12
|
+
* `<main>` landmark that every public page renders inside.
|
|
13
|
+
*
|
|
14
|
+
* Pathless, so it contributes no URL segment: `/discover` is `/discover`, not
|
|
15
|
+
* `/_main/discover`. A page joins the shell by *where its file lives*, which is
|
|
16
|
+
* the same rule `_authenticated` uses for the session guard - and the reason
|
|
17
|
+
* `_authenticated` lives underneath this one: a signed-in page is still a page
|
|
18
|
+
* on the public site, so it wants the shell *and* the guard rather than a second
|
|
19
|
+
* copy of the shell.
|
|
20
|
+
*
|
|
21
|
+
* ## What is deliberately outside it
|
|
22
|
+
*
|
|
23
|
+
* The four auth screens: `/login`, `/login/sso/$providerId`, `/register` and
|
|
24
|
+
* `/login/reset-password`. An auth screen is a full-height card on an otherwise
|
|
25
|
+
* empty document, and the header it would render is a header whose only
|
|
26
|
+
* interesting control is "sign in". Keeping them out is what makes this a shell
|
|
27
|
+
* that routes opt into rather than one every route is subject to.
|
|
28
|
+
* `routes/api/$` is outside for a different reason: it is a server route and
|
|
29
|
+
* renders no document at all. Note this is a visual difference from the Next.js
|
|
30
|
+
* app, where all four sit inside `(main)` and do render the header.
|
|
31
|
+
*
|
|
32
|
+
* `src/tests/main-shell.test.ts` asserts both halves - the settings paths
|
|
33
|
+
* inside, the four auth screens outside.
|
|
34
|
+
*
|
|
35
|
+
* ## The slots
|
|
36
|
+
*
|
|
37
|
+
* `ThemeLayoutContent`'s, and two of the same three the Next.js `ThemeLayout`
|
|
38
|
+
* fills: `header` and `breadcrumb`.
|
|
39
|
+
*
|
|
40
|
+
* `listeners` is deliberately left empty here. The Next.js app puts the
|
|
41
|
+
* notification toasts and the WebSocket's sign-in resync in it because its
|
|
42
|
+
* `/login` is inside the main shell; this app's is not, so a sync mounted here
|
|
43
|
+
* would not exist during the sign-in it has to notice. They are mounted by
|
|
44
|
+
* `__root` instead, next to the connection whose lifetime they share.
|
|
45
|
+
*
|
|
46
|
+
* ## What it is not
|
|
47
|
+
*
|
|
48
|
+
* A provider. Every technical provider this app has - the QueryClient, the two
|
|
49
|
+
* intl records, the theme, the WebSocket - is mounted once by `__root`, above
|
|
50
|
+
* every route, because a login screen needs them just as much as a page under
|
|
51
|
+
* this shell does. What lives here is structure: markup, and where the slots go.
|
|
52
|
+
*/
|
|
53
|
+
export const Route = createFileRoute("/_main")({
|
|
54
|
+
loader: async ({ context }) => await loadMainShell(context),
|
|
55
|
+
component: MainLayout,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
function MainLayout() {
|
|
59
|
+
return (
|
|
60
|
+
<ThemeLayoutContent breadcrumb={<MainBreadcrumb />} header={<MainHeader />}>
|
|
61
|
+
<Outlet />
|
|
62
|
+
</ThemeLayoutContent>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import "@tanstack/react-start/server-only";
|
|
2
|
+
import { createIntlMessagesLoader } from "@vitnode/core/tanstack/i18n/server";
|
|
3
|
+
|
|
4
|
+
import { vitNodeServerConfig } from "#/vitnode.server.config";
|
|
5
|
+
|
|
6
|
+
export type { IntlMessages } from "@vitnode/core/tanstack/i18n/server";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The messages one page needs, in one language - this app's loader.
|
|
10
|
+
*
|
|
11
|
+
* Everything about *how* messages load is core's: the source order (core, then
|
|
12
|
+
* each plugin, then this app's overrides, later ones winning), the `web` scope
|
|
13
|
+
* stamped on each so the API's tree in this same process cannot be served in its
|
|
14
|
+
* place, the per-key fallback to the default locale, and the namespace pick that
|
|
15
|
+
* keeps every plugin's AdminCP copy out of a page that renders none of it.
|
|
16
|
+
*
|
|
17
|
+
* What is this app's is the server config it is handed - the plugins it
|
|
18
|
+
* registered, the languages it declares, and the loaders that stand in for each
|
|
19
|
+
* package's own locale barrel.
|
|
20
|
+
*/
|
|
21
|
+
export const loadIntlMessages = createIntlMessagesLoader(vitNodeServerConfig);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createVitNodeStart } from "@vitnode/core/tanstack/start";
|
|
2
|
+
|
|
3
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* This app's Start instance.
|
|
7
|
+
*
|
|
8
|
+
* The whole request pipeline is VitNode's, and it is mandatory rather than a
|
|
9
|
+
* default: CSRF protection on server functions, canonical locale redirects with
|
|
10
|
+
* the remembered-locale cookie, and the `private, no-store` directive every
|
|
11
|
+
* document carrying a dehydrated session needs.
|
|
12
|
+
*
|
|
13
|
+
* Add this app's own request middleware - a request id, a tracing span, a
|
|
14
|
+
* maintenance gate - with `requestMiddleware`. It runs after all of the above,
|
|
15
|
+
* which is the only safe place for it: a redirect ends the request, so anything
|
|
16
|
+
* in front of the locale rule would run twice for every visitor arriving at a
|
|
17
|
+
* non-canonical URL.
|
|
18
|
+
*
|
|
19
|
+
* export const startInstance = createVitNodeStart({
|
|
20
|
+
* config: vitNodeConfig,
|
|
21
|
+
* requestMiddleware: [myMiddleware],
|
|
22
|
+
* })
|
|
23
|
+
*/
|
|
24
|
+
export const startInstance = createVitNodeStart({ config: vitNodeConfig });
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "shadcn/tailwind.css";
|
|
3
|
+
@import "tw-animate-css";
|
|
4
|
+
|
|
5
|
+
@source "../node_modules/@vitnode/core/dist/src/components";
|
|
6
|
+
@source "../node_modules/@vitnode/core/dist/src/tanstack";
|
|
7
|
+
@source "../node_modules/@vitnode/core/dist/src/views";
|
|
8
|
+
|
|
9
|
+
:root:not(.dark) {
|
|
10
|
+
--background: oklch(0.97 0 0);
|
|
11
|
+
--foreground: oklch(0.145 0 0);
|
|
12
|
+
--card: oklch(1 0 0);
|
|
13
|
+
--card-foreground: oklch(0.145 0 0);
|
|
14
|
+
--popover: oklch(1 0 0);
|
|
15
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
16
|
+
--primary: oklch(0.51 0.16 262.61);
|
|
17
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
18
|
+
--secondary: oklch(0.967 0.001 286.375);
|
|
19
|
+
--secondary-foreground: oklch(0.21 0.006 285.885);
|
|
20
|
+
--muted: oklch(0.967 0.001 286.375);
|
|
21
|
+
--muted-foreground: oklch(0.552 0.016 285.938);
|
|
22
|
+
--accent: oklch(0.967 0.001 286.375);
|
|
23
|
+
--accent-foreground: oklch(0.21 0.006 285.885);
|
|
24
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
25
|
+
--warn: oklch(0.54 0.12 82.58);
|
|
26
|
+
--border: oklch(0.92 0.004 286.32);
|
|
27
|
+
--input: oklch(0.92 0.004 286.32);
|
|
28
|
+
--ring: oklch(0.705 0.015 286.067);
|
|
29
|
+
--chart-1: oklch(0.871 0.006 286.286);
|
|
30
|
+
--chart-2: oklch(0.552 0.016 285.938);
|
|
31
|
+
--chart-3: oklch(0.442 0.017 285.786);
|
|
32
|
+
--chart-4: oklch(0.37 0.013 285.805);
|
|
33
|
+
--chart-5: oklch(0.274 0.006 286.033);
|
|
34
|
+
--sidebar: oklch(0.985 0 0);
|
|
35
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
36
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
37
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
38
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
39
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
40
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
41
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dark {
|
|
45
|
+
--background: oklch(0.14 0 0);
|
|
46
|
+
--foreground: oklch(0.985 0 0);
|
|
47
|
+
--card: oklch(0.21 0.006 285.885);
|
|
48
|
+
--card-foreground: oklch(0.985 0 0);
|
|
49
|
+
--popover: oklch(0.21 0.006 285.885);
|
|
50
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
51
|
+
--primary: oklch(0.6 0.18 262.65);
|
|
52
|
+
--primary-foreground: oklch(0.98 0 0);
|
|
53
|
+
--secondary: oklch(0.274 0.006 286.033);
|
|
54
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
55
|
+
--muted: oklch(0.274 0.006 286.033);
|
|
56
|
+
--muted-foreground: oklch(0.705 0.015 286.067);
|
|
57
|
+
--accent: oklch(0.274 0.006 286.033);
|
|
58
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
59
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
60
|
+
--warn: oklch(0.76 0.18 81.84);
|
|
61
|
+
--border: oklch(1 0 0 / 10%);
|
|
62
|
+
--input: oklch(1 0 0 / 15%);
|
|
63
|
+
--ring: oklch(0.552 0.016 285.938);
|
|
64
|
+
--chart-1: oklch(0.871 0.006 286.286);
|
|
65
|
+
--chart-2: oklch(0.552 0.016 285.938);
|
|
66
|
+
--chart-3: oklch(0.442 0.017 285.786);
|
|
67
|
+
--chart-4: oklch(0.37 0.013 285.805);
|
|
68
|
+
--chart-5: oklch(0.274 0.006 286.033);
|
|
69
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
70
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
71
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
72
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
73
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
74
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
75
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
76
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:root {
|
|
80
|
+
--radius: 0.625rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@theme inline {
|
|
84
|
+
--font-heading: var(--font-sans);
|
|
85
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
86
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
87
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
88
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
89
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
90
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
91
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
92
|
+
--color-sidebar: var(--sidebar);
|
|
93
|
+
--color-chart-5: var(--chart-5);
|
|
94
|
+
--color-chart-4: var(--chart-4);
|
|
95
|
+
--color-chart-3: var(--chart-3);
|
|
96
|
+
--color-chart-2: var(--chart-2);
|
|
97
|
+
--color-chart-1: var(--chart-1);
|
|
98
|
+
--color-ring: var(--ring);
|
|
99
|
+
--color-input: var(--input);
|
|
100
|
+
--color-border: var(--border);
|
|
101
|
+
--color-warn: var(--warn);
|
|
102
|
+
--color-destructive: var(--destructive);
|
|
103
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
104
|
+
--color-accent: var(--accent);
|
|
105
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
106
|
+
--color-muted: var(--muted);
|
|
107
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
108
|
+
--color-secondary: var(--secondary);
|
|
109
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
110
|
+
--color-primary: var(--primary);
|
|
111
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
112
|
+
--color-popover: var(--popover);
|
|
113
|
+
--color-card-foreground: var(--card-foreground);
|
|
114
|
+
--color-card: var(--card);
|
|
115
|
+
--color-foreground: var(--foreground);
|
|
116
|
+
--color-background: var(--background);
|
|
117
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
118
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
119
|
+
--radius-lg: var(--radius);
|
|
120
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
121
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
122
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
123
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@layer base {
|
|
127
|
+
* {
|
|
128
|
+
@apply border-border outline-ring/50;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
html,
|
|
132
|
+
body {
|
|
133
|
+
min-height: 100%;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
body {
|
|
137
|
+
@apply bg-background text-foreground antialiased;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { buildConfig } from "@vitnode/core/vitnode.config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This app's configuration - the one file every VitNode app edits first.
|
|
5
|
+
*
|
|
6
|
+
* **Browser-safe, and everything here has to stay that way.** Three very
|
|
7
|
+
* different readers depend on it: `routes/__root.tsx` renders the document shell
|
|
8
|
+
* from `metadata`, `theme` and `debug`; `lib/i18n/runtime.ts` derives the locale
|
|
9
|
+
* routing from `i18n`; and Vite's own plugin registry loads this file with
|
|
10
|
+
* `jiti` while it is still resolving its config, to find out which plugins to
|
|
11
|
+
* generate route, navigation and content-registry imports for. So it is plain
|
|
12
|
+
* data and plugin *identity* - never a `() => import(...)` message loader, never
|
|
13
|
+
* a module that reaches a database. Anything like that goes in
|
|
14
|
+
* `vitnode.server.config.ts`.
|
|
15
|
+
*
|
|
16
|
+
* ## Adding a language
|
|
17
|
+
*
|
|
18
|
+
* Add an entry to `locales`. Packages ship their own translations, so a new
|
|
19
|
+
* locale needs nothing else: anything a package has not translated falls back to
|
|
20
|
+
* `defaultLocale` key by key. Register the package's file for that language in
|
|
21
|
+
* `src/locales/packages.ts`, and put your own rewording in
|
|
22
|
+
* `src/locales/app.ts` - both of which `vitnode.server.config.ts` picks up.
|
|
23
|
+
*
|
|
24
|
+
* `vitnode i18n:create de Deutsch` does all of it for you.
|
|
25
|
+
*
|
|
26
|
+
* ## Adding a plugin
|
|
27
|
+
*
|
|
28
|
+
* With the plugin's own factory:
|
|
29
|
+
*
|
|
30
|
+
* import { blogPlugin } from '@acme/blog/config'
|
|
31
|
+
*
|
|
32
|
+
* plugins: [blogPlugin()]
|
|
33
|
+
*
|
|
34
|
+
* That is the whole registration - the factory carries the plugin's content
|
|
35
|
+
* types, its AdminCP navigation and its translations. Register its locale files
|
|
36
|
+
* in `src/locales/packages.ts` as well, which is what the message loader
|
|
37
|
+
* actually reads.
|
|
38
|
+
*
|
|
39
|
+
* What the AdminCP renders comes back through `src/admin-nav.gen.ts` and
|
|
40
|
+
* `src/content-registry.gen.ts` rather than out of this object: the build writes
|
|
41
|
+
* one literal import per configured plugin, and `src/router.tsx` loads the
|
|
42
|
+
* content registry behind a dynamic `import()`, so a content type's editing
|
|
43
|
+
* screen arrives with the route that renders it.
|
|
44
|
+
*
|
|
45
|
+
* A plugin's *pages* need nothing in this file at all. It declares them in its
|
|
46
|
+
* own `src/routes.ts`, and this app's Vite build compiles them into
|
|
47
|
+
* `src/plugin-routes.gen.ts`, which `src/router.tsx` mounts under the shell the
|
|
48
|
+
* plugin's `area` names. No page is ever copied into `src/routes`.
|
|
49
|
+
*
|
|
50
|
+
* `buildConfig` also registers this object process-wide, which is how core's own
|
|
51
|
+
* route files find it without being handed it as a prop.
|
|
52
|
+
*/
|
|
53
|
+
export const vitNodeConfig = buildConfig({
|
|
54
|
+
debug: false,
|
|
55
|
+
i18n: {
|
|
56
|
+
defaultLocale: "en",
|
|
57
|
+
locales: [{ code: "en", name: "English" }],
|
|
58
|
+
/**
|
|
59
|
+
* Explicit, because the app renders on a server: without one, `use-intl`
|
|
60
|
+
* formats dates in whatever zone the server happens to run in and warns
|
|
61
|
+
* that the client will disagree.
|
|
62
|
+
*/
|
|
63
|
+
timeZone: "UTC",
|
|
64
|
+
},
|
|
65
|
+
metadata: {
|
|
66
|
+
shortTitle: "VitNode",
|
|
67
|
+
title: "VitNode",
|
|
68
|
+
},
|
|
69
|
+
plugins: [],
|
|
70
|
+
theme: {
|
|
71
|
+
defaultTheme: "system",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import "@tanstack/react-start/server-only";
|
|
2
|
+
import { buildServerConfig } from "@vitnode/core/vitnode.config";
|
|
3
|
+
|
|
4
|
+
import { appMessages } from "#/locales/app";
|
|
5
|
+
import { packageMessages } from "#/locales/packages";
|
|
6
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The half of this app's configuration a browser may never hold.
|
|
10
|
+
*
|
|
11
|
+
* Both entries are `() => import(...)` loaders that read JSON out of a package's
|
|
12
|
+
* build output, which is exactly what `vitnode.config.ts` cannot carry - so they
|
|
13
|
+
* live here, beside the shared config rather than duplicating any of it. The
|
|
14
|
+
* `server-only` marker makes an accidental import from a component a build error
|
|
15
|
+
* instead of a browser bundle with every plugin's AdminCP copy in it.
|
|
16
|
+
*
|
|
17
|
+
* `src/server/messages.server.ts` is the only reader.
|
|
18
|
+
*/
|
|
19
|
+
export const vitNodeServerConfig = buildServerConfig({
|
|
20
|
+
config: vitNodeConfig,
|
|
21
|
+
messages: appMessages,
|
|
22
|
+
packageMessages,
|
|
23
|
+
});
|