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.
Files changed (107) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +58 -18
  3. package/copy-of-vitnode-app/.vscode/settings.json +10 -0
  4. package/copy-of-vitnode-app/README.md +62 -0
  5. package/copy-of-vitnode-app/api/.gitignore_template +32 -0
  6. package/copy-of-vitnode-app/api/drizzle.config.ts +12 -0
  7. package/copy-of-vitnode-app/api/src/index.ts +27 -0
  8. package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +57 -0
  9. package/copy-of-vitnode-app/api/tsconfig.json +17 -0
  10. package/copy-of-vitnode-app/api-bun/src/index.ts +16 -0
  11. package/copy-of-vitnode-app/api-single-app/drizzle.config.ts +12 -0
  12. package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +32 -0
  13. package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +22 -0
  14. package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +29 -0
  15. package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +36 -0
  16. package/copy-of-vitnode-app/docker/docker-compose.yml +30 -0
  17. package/copy-of-vitnode-app/eslint/.prettierrc.mjs +1 -1
  18. package/copy-of-vitnode-app/eslint/eslint.config.mjs +18 -2
  19. package/copy-of-vitnode-app/{root → eslint-react}/.prettierrc.mjs +1 -1
  20. package/copy-of-vitnode-app/eslint-react/eslint.config.mjs +19 -0
  21. package/copy-of-vitnode-app/monorepo/.gitignore_template +41 -0
  22. package/copy-of-vitnode-app/monorepo/apps/api/.env.example +12 -0
  23. package/copy-of-vitnode-app/monorepo/apps/web/.env.example +12 -0
  24. package/copy-of-vitnode-app/monorepo/turbo.json +53 -0
  25. package/copy-of-vitnode-app/root/.env.example +16 -5
  26. package/copy-of-vitnode-app/root/.gitignore_template +41 -0
  27. package/copy-of-vitnode-app/root/global.d.ts +28 -0
  28. package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +72 -0
  29. package/copy-of-vitnode-app/root/src/components/main-header.tsx +33 -0
  30. package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +11 -0
  31. package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +43 -0
  32. package/copy-of-vitnode-app/root/src/lib/admin-search.ts +37 -0
  33. package/copy-of-vitnode-app/root/src/lib/auth.ts +66 -0
  34. package/copy-of-vitnode-app/root/src/lib/content-registry.ts +52 -0
  35. package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +75 -0
  36. package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +20 -0
  37. package/copy-of-vitnode-app/root/src/lib/navigation.ts +24 -0
  38. package/copy-of-vitnode-app/root/src/lib/page-head.ts +19 -0
  39. package/copy-of-vitnode-app/root/src/locales/app.ts +28 -0
  40. package/copy-of-vitnode-app/root/src/locales/packages.ts +43 -0
  41. package/copy-of-vitnode-app/root/src/router.tsx +227 -0
  42. package/copy-of-vitnode-app/root/src/routes/__root.tsx +212 -0
  43. package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +69 -0
  44. package/copy-of-vitnode-app/root/src/routes/_admin.tsx +258 -0
  45. package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +60 -0
  46. package/copy-of-vitnode-app/root/src/routes/_main.tsx +64 -0
  47. package/copy-of-vitnode-app/root/src/server/messages.server.ts +21 -0
  48. package/copy-of-vitnode-app/root/src/start.ts +24 -0
  49. package/copy-of-vitnode-app/root/src/styles.css +139 -0
  50. package/copy-of-vitnode-app/root/src/vitnode.config.ts +73 -0
  51. package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +23 -0
  52. package/copy-of-vitnode-app/root/tsconfig.json +24 -16
  53. package/copy-of-vitnode-app/root/tsr.config.json +3 -0
  54. package/copy-of-vitnode-app/root/vite.config.ts +106 -0
  55. package/copy-of-vitnode-plugin/root/.swcrc +26 -0
  56. package/copy-of-vitnode-plugin/root/global.d.ts +21 -0
  57. package/copy-of-vitnode-plugin/root/npmignore.template +17 -0
  58. package/copy-of-vitnode-plugin/root/tsconfig.build.json +5 -0
  59. package/copy-of-vitnode-plugin/root/tsconfig.json +25 -0
  60. package/dist/src/create/create-package-json.js +266 -56
  61. package/dist/src/create/create-vitnode.js +177 -28
  62. package/dist/src/create/package-versions.js +49 -0
  63. package/dist/src/helpers/get-available-package-managers.js +6 -6
  64. package/dist/src/helpers/get-package-json.js +2 -2
  65. package/dist/src/helpers/get-package-manager-from-root.js +15 -0
  66. package/dist/src/helpers/get-vitnode-package-version.js +9 -0
  67. package/dist/src/helpers/init-vitnode.js +34 -0
  68. package/dist/src/helpers/install-dependencies.js +55 -14
  69. package/dist/src/helpers/is-folder-empty.js +25 -26
  70. package/dist/src/helpers/is-online.js +16 -16
  71. package/dist/src/helpers/is-writeable.js +3 -3
  72. package/dist/src/helpers/validate-pkg.js +1 -1
  73. package/dist/src/helpers/with-If.js +1 -0
  74. package/dist/src/index.js +29 -26
  75. package/dist/src/plugin/create/add-plugin-to-workspace.js +74 -0
  76. package/dist/src/plugin/create/create-package-json.js +54 -0
  77. package/dist/src/plugin/create/create-plugin-vitnode.js +80 -0
  78. package/dist/src/plugin/create/route-templates.js +144 -0
  79. package/dist/src/plugin/index.js +17 -6
  80. package/dist/src/plugin/questions.js +14 -0
  81. package/dist/src/plugin/validation.js +76 -0
  82. package/dist/src/prepare/prepare.js +11 -8
  83. package/dist/src/questions.js +52 -15
  84. package/dist/src/validation.js +15 -15
  85. package/dist/tsconfig.build.tsbuildinfo +1 -0
  86. package/package.json +22 -12
  87. package/copy-of-vitnode-app/root/README.md +0 -1
  88. package/copy-of-vitnode-app/root/drizzle.config.ts +0 -12
  89. package/copy-of-vitnode-app/root/gitignore_template +0 -43
  90. package/copy-of-vitnode-app/root/next.config.ts +0 -11
  91. package/copy-of-vitnode-app/root/postcss.config.mjs +0 -8
  92. package/copy-of-vitnode-app/root/src/app/[locale]/(main)/[...rest]/page.tsx +0 -5
  93. package/copy-of-vitnode-app/root/src/app/[locale]/(main)/layout.tsx +0 -10
  94. package/copy-of-vitnode-app/root/src/app/[locale]/(main)/not-found.tsx +0 -5
  95. package/copy-of-vitnode-app/root/src/app/[locale]/(main)/page.tsx +0 -113
  96. package/copy-of-vitnode-app/root/src/app/[locale]/admin/(auth)/layout.tsx +0 -10
  97. package/copy-of-vitnode-app/root/src/app/[locale]/admin/not-found.tsx +0 -5
  98. package/copy-of-vitnode-app/root/src/app/[locale]/admin/page.tsx +0 -5
  99. package/copy-of-vitnode-app/root/src/app/[locale]/layout.tsx +0 -36
  100. package/copy-of-vitnode-app/root/src/app/api/[...route]/route.ts +0 -18
  101. package/copy-of-vitnode-app/root/src/app/favicon.ico +0 -0
  102. package/copy-of-vitnode-app/root/src/app/global-error.tsx +0 -27
  103. package/copy-of-vitnode-app/root/src/app/global.css +0 -128
  104. package/copy-of-vitnode-app/root/src/app/layout.tsx +0 -9
  105. package/copy-of-vitnode-app/root/src/app/not-found.tsx +0 -13
  106. package/copy-of-vitnode-app/root/src/vitnode.api.config.ts +0 -5
  107. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,43 @@
1
+ import type { LocaleMessagesMap } from "@vitnode/core/lib/i18n/types";
2
+
3
+ import { CONFIG_PLUGIN as CORE } from "@vitnode/core/config";
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.server.config.ts` reads it
37
+ * from here.
38
+ */
39
+ export const packageMessages: Record<string, LocaleMessagesMap> = {
40
+ [CORE.pluginId]: {
41
+ en: async () => await import("@vitnode/core/locales/en.json"),
42
+ },
43
+ };
@@ -0,0 +1,227 @@
1
+ import type { AnyRouter } from "@tanstack/react-router";
2
+
3
+ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
4
+ import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query";
5
+ import { createVitNodeQueryClient } from "@vitnode/core/lib/query-client";
6
+ import { RoutePendingSpinner } from "@vitnode/core/tanstack/pending";
7
+ import {
8
+ pluginRouteSpecs,
9
+ withPluginRoutes,
10
+ } from "@vitnode/core/tanstack/plugin-routes";
11
+ import {
12
+ withCoreAdminRoutes,
13
+ withCoreMainRoutes,
14
+ withCoreRootRoutes,
15
+ } from "@vitnode/core/tanstack/routes";
16
+
17
+ /**
18
+ * The auth transport, registered by importing the module that declares it.
19
+ *
20
+ * `@vitnode/core/tanstack/auth` owns every auth decision this app makes but may
21
+ * not declare a `createServerFn` - uncompiled on the server, one silently
22
+ * resolves to `undefined` - so `lib/auth.ts` declares the eight wrappers and
23
+ * hands them over at module scope. A bare import because there is nothing to
24
+ * name: the registration *is* the module's effect.
25
+ *
26
+ * Here because a router is the one module both entry points load, so the
27
+ * registration has happened before any route, loader or component can reach for
28
+ * it, in the browser bundle and on the server alike.
29
+ */
30
+ import "./lib/auth";
31
+ /**
32
+ * The admin transport, registered the same way and for the same reason.
33
+ *
34
+ * One server function rather than eight, reading the AdminCP's own session under
35
+ * its own cookie. It is a separate registration from the auth one on purpose:
36
+ * they are two sessions, two cookies and two cache entries, and nothing in
37
+ * VitNode may let the public session answer an admin question.
38
+ */
39
+ import "./lib/admin-auth";
40
+ import { createLocaleRewrite, localeRouting } from "./lib/i18n/runtime";
41
+ import { pageHead } from "./lib/page-head";
42
+ import { pluginRouteSources } from "./plugin-routes.gen";
43
+ import { Route as adminShellRoute } from "./routes/_admin";
44
+ import { Route as mainShellRoute } from "./routes/_main";
45
+ import { routeTree as fileRouteTree } from "./routeTree.gen";
46
+
47
+ /**
48
+ * The Content Engine registry, behind a literal dynamic import.
49
+ *
50
+ * Awaited by the one loader that needs it - `/admin/content/*` - rather than
51
+ * imported here. Building the registry reaches `@vitnode/core/content` and
52
+ * every configured plugin's admin form components, and this module is the one
53
+ * the client entry evaluates on every page: as a static import it put `zod`,
54
+ * every plugin's content registrations, the content form primitives and
55
+ * `react-hook-form` in front of the front page's first paint. See
56
+ * `CoreAdminRouteContext.loadContentRegistry`.
57
+ */
58
+ const loadContentRegistry = async () =>
59
+ (await import("./lib/content-registry")).contentRegistry;
60
+
61
+ /**
62
+ * One route tree: this app's route files, plus the AdminCP screens `@vitnode/core`
63
+ * owns, plus the pages its plugins declare.
64
+ *
65
+ * At module scope rather than inside `getRouter`, because `getRouter` runs once
66
+ * per server request and mounting the plugin routes mutates the route tree - the
67
+ * generated tree is a module singleton. `withPluginRoutes` is idempotent anyway;
68
+ * doing it once is simply where it belongs.
69
+ *
70
+ * The plugin half comes from one generated file: a static import of each
71
+ * configured plugin's own route tree. No plugin page is copied into
72
+ * `src/routes`, no route path is written by hand, and nothing here knows which
73
+ * plugins are installed - see `@vitnode/core/tanstack/plugin-routes`.
74
+ *
75
+ * A page is reached only through the literal `lazy(() => import(...))` its route
76
+ * declared, so every one of them is a chunk of its own. The one part of a plugin
77
+ * route that is not lazy is a `search` schema: a router's `validateSearch` runs
78
+ * during path matching, before any chunk is fetched, so it lives in the tree
79
+ * rather than in the page.
80
+ *
81
+ * `mountUnder` names one route per shell, which is the whole of what "a plugin
82
+ * route renders in the application shell" amounts to here. A plugin declares
83
+ * `area: "main"` or `area: "admin"`; `_main` is the route that renders the
84
+ * public shell and `_admin` the one that renders the AdminCP, and being a child
85
+ * of one of them is what gives `/example` the header and the one `<main>` that
86
+ * `/discover` has, or gives `/admin/reports` the sidebar, the breadcrumb area
87
+ * and the admin session guard that `/admin/core` has. No new field, no per-route
88
+ * layout metadata and no second copy of either shell - route composition, which
89
+ * the area declaration already described.
90
+ *
91
+ * Neither shell changes a path: both are pathless, so `/example` stays
92
+ * `/example` and an admin plugin route's `/admin/…` is the path its own route
93
+ * spells out in full. An area VitNode knows and this app has not named here
94
+ * fails the composition rather than being mounted under the other one.
95
+ *
96
+ * `_main` and `_admin` are imported for their route objects, and they are the
97
+ * same objects the generated tree holds: `createFileRoute` produces one instance
98
+ * per module and `routeTree.gen.ts` mutates it in place.
99
+ *
100
+ * `withCoreMainRoutes`, `withCoreAdminRoutes` and `withCoreRootRoutes` mount
101
+ * core's own screens the same way, one per mount point: the public pages under
102
+ * the main shell, the AdminCP's under its own, and the shell-less ones - the auth
103
+ * cards and the AdminCP sign-in - straight under the root.
104
+ *
105
+ * They were twenty-nine route files in this application until
106
+ * `@vitnode/core/tanstack/routes` existed, every one of them pure wiring around
107
+ * something imported from the package - so an app carried a copy of VitNode's own
108
+ * routing table and core adding a screen meant an edit here. They are code-based
109
+ * rather than declared as plugin routes because they need the router's full
110
+ * option set: a real `validateSearch` that clamps `?page=999` before anything
111
+ * renders, a `beforeLoad` guard that runs before any chunk is fetched, and a
112
+ * splat path a plugin route path does not represent.
113
+ *
114
+ * `localeRouting` goes to the last of the three because a sign-in navigates to a
115
+ * path a *visitor* supplied: the route tree carries no locale, so the prefix has
116
+ * to be stripped before the router sees it, and which prefixes exist is this
117
+ * app's answer. It is the same object the `rewrite` below uses.
118
+ *
119
+ * `pageHead` is this app's own `createRouteHead(metadata)` binding, handed over
120
+ * because a package cannot know the site's name: a plugin page's `<title>` goes
121
+ * through the same `"<page> - <site>"` rule every other VitNode page's does,
122
+ * rather than through a second one the plugin invented.
123
+ */
124
+ const routeTree = withCoreRootRoutes(
125
+ withCoreAdminRoutes(
126
+ withCoreMainRoutes(
127
+ withPluginRoutes(
128
+ fileRouteTree,
129
+ pluginRouteSpecs(pluginRouteSources),
130
+ {
131
+ mountUnder: { admin: adminShellRoute, main: mainShellRoute },
132
+ pageHead,
133
+ },
134
+ ),
135
+ { mountUnder: mainShellRoute, pageHead },
136
+ ),
137
+ { loadContentRegistry, mountUnder: adminShellRoute, pageHead },
138
+ ),
139
+ { localeRouting, mountUnder: fileRouteTree, pageHead },
140
+ );
141
+
142
+ /**
143
+ * The app's router, and the QueryClient it owns.
144
+ *
145
+ * Start calls this once per server request and once in the browser, which is
146
+ * exactly the lifetime a QueryClient should have: created here, it is per
147
+ * request on the server - never a module-level client shared by every visitor
148
+ * being rendered at once - and a single long-lived one on the client.
149
+ *
150
+ * It goes into the router context, so a route loader reaches it as
151
+ * `context.queryClient` and can `ensureQueryData` before its component renders.
152
+ * That is the whole point of putting it here rather than in a provider: a
153
+ * loader runs before React does, so a client mounted by a component would be
154
+ * out of reach of the code that most wants it.
155
+ *
156
+ * `setupRouterSsrQueryIntegration` wires the two together: it dehydrates the
157
+ * cache into the SSR stream (including queries that resolve mid-render),
158
+ * hydrates it on the client before the first render, routes `redirect()` thrown
159
+ * inside a query or mutation through the router, and wraps the app in the one
160
+ * `QueryClientProvider` for this client. Nothing else in this app may create a
161
+ * `QueryClient` or a provider for one - two clients in a page means a query a
162
+ * loader cached is invisible to the component that reads it.
163
+ *
164
+ * `defaultPreloadStaleTime: 0` leaves caching to Query rather than having the
165
+ * router keep a second copy of the same data with its own expiry.
166
+ *
167
+ * `defaultStaleReloadMode: 'blocking'` is what makes a route's pending shape
168
+ * reachable at all once preloading is on. Router core's default is
169
+ * `'background'`, and a background reload never opens a pending window - but it
170
+ * still waits for the route's component chunk before it commits. So the common
171
+ * desktop path, hover a link and click it, took the one branch that renders
172
+ * nothing: the hover filled the loader, the click was therefore a background
173
+ * reload, and the chunk downloaded with the previous page still on screen and
174
+ * no skeleton in sight.
175
+ *
176
+ * It costs nothing here because a VitNode loader does not block on a warm cache:
177
+ * `ensureQueryData` with `revalidateIfStale` hands back the cached entry and
178
+ * refreshes behind it, so "blocking" describes a promise that resolves in a
179
+ * microtask. What changes is only that the router now marks the match pending
180
+ * while that happens, which is what `defaultPendingMs` is for.
181
+ *
182
+ * `defaultPendingMs: 150` is that threshold, and it is not zero for the same
183
+ * reason. At zero, every navigation opens a pending window, so
184
+ * `defaultPendingMinMs` holds a *fully cached* navigation behind a skeleton for
185
+ * 300ms - a page that could have been instant, made slow to look busy. At 150ms
186
+ * a cached navigation goes straight through with nothing shown and a slow one
187
+ * still gets its shape, in the content area and the breadcrumb together.
188
+ *
189
+ * `rewrite` is what makes one route tree serve two public URL shapes: `/pl/...`
190
+ * arrives, `/...` is matched, and every link the router builds gets the prefix
191
+ * back. No route file mentions a locale, so nothing here has to be duplicated
192
+ * per language - see `@vitnode/core/tanstack/i18n`.
193
+ */
194
+ export function getRouter() {
195
+ const queryClient = createVitNodeQueryClient();
196
+
197
+ // The rewrite reads the locale off the router's own current location, and the
198
+ // router needs the rewrite to parse that location - so it is handed a getter
199
+ // rather than the router itself. `output` only ever runs once a link is built,
200
+ // which is long after the assignment below.
201
+ const holder: { current?: AnyRouter } = {};
202
+
203
+ const router = createTanStackRouter({
204
+ context: { queryClient },
205
+ defaultPendingComponent: RoutePendingSpinner,
206
+ defaultPendingMs: 150,
207
+ defaultPendingMinMs: 300,
208
+ defaultPreload: "intent",
209
+ defaultPreloadStaleTime: 0,
210
+ defaultStaleReloadMode: "blocking",
211
+ rewrite: createLocaleRewrite(() => holder.current),
212
+ routeTree,
213
+ scrollRestoration: true,
214
+ });
215
+
216
+ holder.current = router;
217
+
218
+ setupRouterSsrQueryIntegration({ queryClient, router });
219
+
220
+ return router;
221
+ }
222
+
223
+ declare module "@tanstack/react-router" {
224
+ interface Register {
225
+ router: ReturnType<typeof getRouter>;
226
+ }
227
+ }
@@ -0,0 +1,212 @@
1
+ import type { QueryClient } from "@tanstack/react-query";
2
+
3
+ import { TanStackDevtools } from "@tanstack/react-devtools";
4
+ import { ReactQueryDevtoolsPanel } from "@tanstack/react-query-devtools";
5
+ import {
6
+ createRootRouteWithContext,
7
+ HeadContent,
8
+ Outlet,
9
+ Scripts,
10
+ } from "@tanstack/react-router";
11
+ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
12
+ import { ThemeScript } from "@vitnode/core/components/theme-script";
13
+ import {
14
+ intlQueryOptions,
15
+ publicPathnameOf,
16
+ resolveLocale,
17
+ useLocale,
18
+ } from "@vitnode/core/tanstack/i18n";
19
+ import {
20
+ ErrorActions,
21
+ NotFound,
22
+ VitNodeRootProviders,
23
+ } from "@vitnode/core/tanstack/layout";
24
+
25
+ import type { Locale } from "#/lib/i18n/shared";
26
+
27
+ import { vitNodeConfig } from "#/vitnode.config";
28
+
29
+ import appCss from "../styles.css?url";
30
+
31
+ const { debug, i18n, metadata, theme } = vitNodeConfig;
32
+
33
+ /**
34
+ * What the router itself provides, before any route has run.
35
+ *
36
+ * The QueryClient, and nothing else. `beforeLoad` below adds `locale` on top, so
37
+ * what a loader actually receives is `{ queryClient, locale }` - the language
38
+ * included, because a loader that fetches anything user-facing needs to know
39
+ * which one it is fetching.
40
+ */
41
+ export interface RootRouterContext {
42
+ queryClient: QueryClient;
43
+ }
44
+
45
+ export const Route = createRootRouteWithContext<RootRouterContext>()({
46
+ /**
47
+ * The request's language, resolved once and handed to every loader below.
48
+ *
49
+ * The same function the rewrite and the components use, so there is one answer
50
+ * per request rather than one per consumer. Note that a language switch does
51
+ * not change the *internal* URL - only the public one - so the switcher
52
+ * invalidates the router to bring this back in step.
53
+ */
54
+ beforeLoad: ({ location }) => ({
55
+ locale: resolveLocale<Locale>(publicPathnameOf(location)),
56
+ }),
57
+ component: RootComponent,
58
+ head: () => ({
59
+ links: [
60
+ { href: appCss, rel: "stylesheet" },
61
+ /*
62
+ * The tab icon, from `public/favicon.ico`.
63
+ *
64
+ * Stated rather than left to the browser's automatic `/favicon.ico`
65
+ * request, because that request is a 404 until the file exists and a
66
+ * silently missing icon is easy to never notice. Drop your own 32px `.ico`
67
+ * at `public/favicon.ico`; it lives there rather than being imported so
68
+ * that the URL is stable and the file is still reachable at the well-known
69
+ * path browsers ask for unprompted.
70
+ */
71
+ {
72
+ href: "/favicon.ico",
73
+ rel: "icon",
74
+ sizes: "32x32",
75
+ type: "image/x-icon",
76
+ },
77
+ ],
78
+ meta: [
79
+ { charSet: "utf-8" },
80
+ { content: "width=device-width, initial-scale=1", name: "viewport" },
81
+ // The default title, from the app's config. A route that names itself
82
+ // renders `"<page> - <shortTitle>"` instead, through `formatPageTitle` -
83
+ // the same rule Next.js applies through `title.template`.
84
+ { title: metadata.title },
85
+ ],
86
+ }),
87
+ /**
88
+ * Warm this language's shell strings before anything renders.
89
+ *
90
+ * `context.locale` rather than a default: `/pl` has to arrive with Polish
91
+ * already in the cache, or the first paint is English and the page flips after
92
+ * hydration.
93
+ */
94
+ loader: async ({ context }) => {
95
+ await context.queryClient.ensureQueryData(
96
+ intlQueryOptions({ locale: context.locale }),
97
+ );
98
+ },
99
+ /**
100
+ * Every URL this application does not serve.
101
+ *
102
+ * The last resort, and until now there was none: a path that matched no route
103
+ * at all fell through to TanStack Router's own `<p>Not Found</p>` - no shell,
104
+ * no strings, no way back - and the router warned about the missing option on
105
+ * every such navigation. `/admin/contents` and a hand-typed `/blog/post-30`
106
+ * both landed there.
107
+ *
108
+ * ## What reaches it, and what does not
109
+ *
110
+ * Only a path **no route matched**. A route that matched and then answered
111
+ * `notFound()` from its own loader is caught by the nearest
112
+ * `notFoundComponent` above it, which is why `/admin/content/nope` and
113
+ * `/admin/content/blog/articles/999999/edit` render the AdminCP's 404 inside
114
+ * the panel rather than this - see `_admin`'s, which mounts the shell around
115
+ * the same message.
116
+ *
117
+ * ## It is a 404, and not a redirect anywhere
118
+ *
119
+ * The route tree is the whole application, so a URL that reaches here is one
120
+ * somebody typed or a stale bookmark, and saying so is the honest answer.
121
+ * Bouncing an unmatched path at some other origin would hide a genuinely
122
+ * missing page behind a hop to a server that 404s it anyway. Where a URL is
123
+ * served from is a deployment question, and a proxy in front of the app
124
+ * answers it better than this route can.
125
+ */
126
+ notFoundComponent: RootNotFound,
127
+ shellComponent: RootDocument,
128
+ });
129
+
130
+ /**
131
+ * Rendered inside `RootComponent`, which is what makes the strings work: the
132
+ * providers it mounts include `RouteMessages` with `core.global`, and that is
133
+ * the namespace `NotFound` reads its two lines from. The root's loader has
134
+ * already warmed that entry, so nothing suspends.
135
+ */
136
+ function RootNotFound() {
137
+ return <NotFound actions={<ErrorActions />} />;
138
+ }
139
+
140
+ /**
141
+ * The VitNode provider tree, mounted once above every route.
142
+ *
143
+ * Every provider in it is shared with the Next.js app - the theme, the toaster,
144
+ * the tooltip provider, the WebSocket - plus the pair of `use-intl` records that
145
+ * only a package can name. `VitNodeRootProviders` owns all of it, including the
146
+ * argument for why the realtime listeners are inside it rather than in the main
147
+ * shell.
148
+ *
149
+ * What this route contributes is the two things an application owns: which
150
+ * languages it serves, and its theme defaults.
151
+ *
152
+ * Because the locale comes from router state, changing language re-renders this:
153
+ * new locale, new query key, new messages, no page reload.
154
+ */
155
+ function RootComponent() {
156
+ return (
157
+ <VitNodeRootProviders config={{ debug, locales: i18n.locales, theme }}>
158
+ <Outlet />
159
+ </VitNodeRootProviders>
160
+ );
161
+ }
162
+
163
+ /**
164
+ * The document itself.
165
+ *
166
+ * `lang` is the language this request actually resolved to - `en` for `/`, `pl`
167
+ * for `/pl`, and on a route outside the localized URL space (`/admin`) whatever
168
+ * the visitor's cookie says. It comes from the same router state the provider
169
+ * reads, so the two cannot disagree and hydration has nothing to complain about.
170
+ *
171
+ * `ThemeScript` has to be in the head, and it has to be inline: it applies the
172
+ * stored theme to `<html>` before the browser paints, so the first frame is the
173
+ * theme the visitor chose rather than a flash of the default one.
174
+ * `suppressHydrationWarning` covers the attributes it writes, which by design
175
+ * differ from what the server rendered.
176
+ */
177
+ function RootDocument({ children }: { children: React.ReactNode }) {
178
+ const locale = useLocale();
179
+
180
+ return (
181
+ <html lang={locale} suppressHydrationWarning>
182
+ <head>
183
+ <HeadContent />
184
+ <ThemeScript {...theme} />
185
+ </head>
186
+
187
+ <body suppressHydrationWarning>
188
+ {children}
189
+
190
+ {import.meta.env.DEV ? (
191
+ <TanStackDevtools
192
+ config={{
193
+ position: "bottom-right",
194
+ }}
195
+ plugins={[
196
+ {
197
+ name: "TanStack Router",
198
+ render: <TanStackRouterDevtoolsPanel />,
199
+ },
200
+ {
201
+ name: "TanStack Query",
202
+ render: <ReactQueryDevtoolsPanel />,
203
+ },
204
+ ]}
205
+ />
206
+ ) : null}
207
+
208
+ <Scripts />
209
+ </body>
210
+ </html>
211
+ );
212
+ }
@@ -0,0 +1,69 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { AdminBreadcrumb } from "@vitnode/core/tanstack/admin";
3
+ import {
4
+ AdminDashboardRouteContent,
5
+ loadAdminDashboardRoute,
6
+ } from "@vitnode/core/tanstack/admin/dashboard";
7
+
8
+ /**
9
+ * `/admin/core` - the AdminCP dashboard, and the one route file `_admin` keeps.
10
+ *
11
+ * Topology only. The layout query, the widget catalogue, the drag-and-drop
12
+ * board, the settings dialogs and the four mutations behind them are
13
+ * `@vitnode/core/tanstack/admin/dashboard`.
14
+ *
15
+ * ## Why this one file is still here
16
+ *
17
+ * Every other AdminCP screen is `@vitnode/core`'s, mounted by
18
+ * `withCoreAdminRoutes` as a code-based route - see `src/router.tsx`. This one
19
+ * stays because the file-based generator requires it to, and the requirement is
20
+ * sharp rather than stylistic:
21
+ *
22
+ * - A pathless layout with no file children is **dropped from the generated
23
+ * tree**. `buildRouteTreeConfig` skips it outright, so `_admin` would not be
24
+ * in `routeTree.gen.ts` at all and the route object `src/router.tsx` mounts
25
+ * core's screens under would be an orphan.
26
+ * - It also collapses to a full path of `/`, which collides with `_main/index.tsx`
27
+ * and fails the generator's uniqueness check by name.
28
+ *
29
+ * So `_admin` needs one file-based child with a real path in order to exist, and
30
+ * this is it - which is the same job it has always had. It was the shell's first
31
+ * child for exactly this reason, and now it is the only one.
32
+ *
33
+ * Deleting it does not remove a screen; it removes the AdminCP.
34
+ *
35
+ * ## No `head`, deliberately
36
+ *
37
+ * The Next.js page exports no `generateMetadata`, so the tab keeps the site's
38
+ * own name. Declaring `title: 'VitNode'` here would render "VitNode - VitNode"
39
+ * through `formatPageTitle`; saying nothing inherits the root's title and
40
+ * `_admin`'s `noindex`, which is the parity-preserving answer.
41
+ *
42
+ * ## `pluginWidgets` is not passed
43
+ *
44
+ * A plugin declares widgets in its `admin.dashboard.widgets`, which reaches a
45
+ * Next.js board through `getVitNodeConfig()`. This route does not read the
46
+ * config - the AdminCP takes its plugin data from the generated projections, and
47
+ * neither of those carries widgets - so the board shows core's own, which is the
48
+ * complete set for this install because no configured plugin declares any. It is
49
+ * the same seam `AdminShell` leaves open for nav `declarations`.
50
+ */
51
+ export const Route = createFileRoute("/_admin/admin/core/")({
52
+ loader: async ({ context }) => await loadAdminDashboardRoute(context),
53
+ component: AdminDashboardRoute,
54
+ /**
55
+ * The whole of how an admin route contributes to the trail in the shell's
56
+ * header: the route declares its own crumb next to its own component, the
57
+ * shell renders whichever matched route declared the deepest one, and there is
58
+ * no map from pathname to breadcrumb anywhere. The label comes from the
59
+ * *visible* navigation, so this reads "Core" in whatever language the
60
+ * administrator is using without this file naming a string.
61
+ */
62
+ staticData: {
63
+ breadcrumb: <AdminBreadcrumb segments={["core"]} />,
64
+ },
65
+ });
66
+
67
+ function AdminDashboardRoute() {
68
+ return <AdminDashboardRouteContent {...Route.useLoaderData()} />;
69
+ }