create-vitnode-app 2.0.0-canary.1 → 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/README.md +8 -2
- package/copy-of-vitnode-app/README.md +22 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +25 -8
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +7 -8
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +7 -7
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +7 -6
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +5 -5
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +4 -4
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +2 -2
- package/copy-of-vitnode-app/root/src/locales/app.ts +3 -3
- package/copy-of-vitnode-app/root/src/locales/packages.ts +2 -2
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +2 -2
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +6 -7
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +5 -13
- package/copy-of-vitnode-app/root/src/start.ts +17 -81
- package/copy-of-vitnode-app/root/src/styles.css +3 -35
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +52 -37
- package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +23 -0
- package/copy-of-vitnode-app/root/vite.config.ts +11 -24
- package/package.json +2 -2
- 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/document-headers.ts +0 -128
- 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
|
|
|
@@ -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
|
});
|
|
@@ -15,14 +13,33 @@ export const POSTGRES_URL =
|
|
|
15
13
|
export const vitNodeApiConfig = buildApiConfig({
|
|
16
14
|
plugins: [],
|
|
17
15
|
/**
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
16
|
+
* The languages this installation serves.
|
|
17
|
+
*
|
|
18
|
+
* The API half of a split deployment. `apps/web/src/vitnode.config.ts`
|
|
19
|
+
* declares the same list, and the two are one declaration in two places by
|
|
20
|
+
* necessity rather than by design: they are separate packages, so neither can
|
|
21
|
+
* import the other's. Nothing walks the filesystem looking for the web app's
|
|
22
|
+
* config either - a bootstrap that guessed at a sibling application is exactly
|
|
23
|
+
* what that replaced, and it guessed wrong the moment the two were not laid
|
|
24
|
+
* out the way it expected.
|
|
20
25
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
26
|
+
* They have to agree, and this is the copy that matters most: this app owns
|
|
27
|
+
* the schema, so `vitnode db:prepare` seeds `core_languages` from *this* list.
|
|
28
|
+
* A language that is here and not in the web app's renders nowhere; one that
|
|
29
|
+
* is in the web app's and not here has no row in the database.
|
|
30
|
+
*
|
|
31
|
+
* Packages ship their own translations, so a new locale needs no `messages`
|
|
32
|
+
* entry - anything untranslated falls back to `defaultLocale` key by key.
|
|
24
33
|
*/
|
|
25
|
-
i18n
|
|
34
|
+
i18n: {
|
|
35
|
+
defaultLocale: "en",
|
|
36
|
+
locales: [{ code: "en", name: "English" }],
|
|
37
|
+
/**
|
|
38
|
+
* Explicit, because this API renders emails on a server: without one, dates
|
|
39
|
+
* format in whatever zone the host happens to run in.
|
|
40
|
+
*/
|
|
41
|
+
timeZone: "UTC",
|
|
42
|
+
},
|
|
26
43
|
dbProvider: drizzle({
|
|
27
44
|
connection: POSTGRES_URL,
|
|
28
45
|
relations: coreRelations,
|
|
@@ -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";
|
|
@@ -14,16 +14,15 @@ export const vitNodeApiConfig = buildApiConfig({
|
|
|
14
14
|
},
|
|
15
15
|
plugins: [],
|
|
16
16
|
/**
|
|
17
|
-
* The
|
|
18
|
-
* one
|
|
19
|
-
* `vitnode.shell.config.ts` and passed here.
|
|
17
|
+
* The site's own locale declaration, because this app is both: `vitnode.config.ts`
|
|
18
|
+
* is the one statement of which languages exist, and both configs read it.
|
|
20
19
|
*
|
|
21
20
|
* It is also what `vitnode db:prepare` seeds `core_languages` from - this app
|
|
22
|
-
* owns the schema - so adding a language
|
|
23
|
-
* row. Leave it out and the seed falls back to `en` alone, whatever the
|
|
24
|
-
* serves.
|
|
21
|
+
* owns the schema - so adding a language there and re-running `dev` inserts
|
|
22
|
+
* its row. Leave it out and the seed falls back to `en` alone, whatever the
|
|
23
|
+
* site serves.
|
|
25
24
|
*/
|
|
26
|
-
i18n,
|
|
25
|
+
i18n: vitNodeConfig.i18n,
|
|
27
26
|
dbProvider: drizzle({
|
|
28
27
|
connection: POSTGRES_URL,
|
|
29
28
|
relations: coreRelations,
|
|
@@ -17,13 +17,13 @@ import { pluginAdminNav } from "#/admin-nav.gen";
|
|
|
17
17
|
*
|
|
18
18
|
* ## Why it is not read from `vitnode.config.ts`
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* actually renders, through `src/lib/content-registry.ts
|
|
20
|
+
* The config does carry each plugin's registration, and a Next.js host walks it
|
|
21
|
+
* in its render pass. Reading it here would make every configured plugin's
|
|
22
|
+
* editing screens reachable from the module the document shell imports, which is
|
|
23
|
+
* the one graph that is never lazy. The generated projection carries exactly
|
|
24
|
+
* what a sidebar needs instead - ids, hrefs, permissions, icons and content type
|
|
25
|
+
* definitions, all plain data - and the Content Engine's UI arrives separately,
|
|
26
|
+
* when a content screen actually renders, through `src/lib/content-registry.ts`.
|
|
27
27
|
*
|
|
28
28
|
* ## Module scope, and why that matters twice
|
|
29
29
|
*
|
|
@@ -20,12 +20,13 @@ import { pluginContentTypes } from "#/content-registry.gen";
|
|
|
20
20
|
*
|
|
21
21
|
* ## Why it is not read from `vitnode.config.ts`
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `src/lib/admin-nav.ts` uses for the sidebar, one
|
|
23
|
+
* The config does carry each plugin's registration, and a Next.js host reads it
|
|
24
|
+
* from there. What it cannot give is *when*: it is imported by the document
|
|
25
|
+
* shell, so anything reachable from it is reachable eagerly. This module is
|
|
26
|
+
* loaded behind a dynamic `import()` in `src/router.tsx` instead, so the
|
|
27
|
+
* definitions, the icons and the override components arrive with the content
|
|
28
|
+
* route - the same arrangement `src/lib/admin-nav.ts` uses for the sidebar, one
|
|
29
|
+
* layer deeper.
|
|
29
30
|
*
|
|
30
31
|
* ## Registration, and where it belongs in the import graph
|
|
31
32
|
*
|
|
@@ -2,8 +2,8 @@ import { createServerFn } from "@tanstack/react-start";
|
|
|
2
2
|
import { configureIntl, validateIntlInput } from "@vitnode/core/tanstack/i18n";
|
|
3
3
|
import { IntlProvider } from "use-intl";
|
|
4
4
|
|
|
5
|
-
import { i18n } from "#/i18n";
|
|
6
5
|
import { loadIntlMessages } from "#/server/messages.server";
|
|
6
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* One language's messages for one set of namespaces, fetched on the server.
|
|
@@ -29,9 +29,9 @@ export const getIntlMessages = createServerFn()
|
|
|
29
29
|
* Everything in `@vitnode/core/tanstack/i18n` reads what this registers, so a
|
|
30
30
|
* route file imports `RouteMessages` and `intlQueryOptions` straight from the
|
|
31
31
|
* package. What must not happen is a route running before this module has been
|
|
32
|
-
* evaluated - so
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* evaluated - so `src/router.tsx`, which owns the route tree, imports from
|
|
33
|
+
* here. The request pipeline needs no such guarantee: `createVitNodeStart`
|
|
34
|
+
* derives its own locale routing from the config it is handed.
|
|
35
35
|
*
|
|
36
36
|
* The registration is at module scope but reads `getIntlMessages` above only by
|
|
37
37
|
* reference, so the order within this file does not matter: the validator and
|
|
@@ -61,7 +61,7 @@ export const {
|
|
|
61
61
|
* back to client rendering, and the page still appears.
|
|
62
62
|
*/
|
|
63
63
|
hostIntlProvider: IntlProvider,
|
|
64
|
-
i18n,
|
|
64
|
+
i18n: vitNodeConfig.i18n,
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
/**
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { vitNodeConfig } from "#/vitnode.config";
|
|
2
2
|
|
|
3
3
|
import { localeRouting } from "#/lib/i18n/runtime";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* A language this app serves, as a type. `"en" | "
|
|
7
|
-
* rather than written twice.
|
|
6
|
+
* A language this app serves, as a type. `"en"`, or `"en" | "de"` once a second
|
|
7
|
+
* one is declared - derived from the config rather than written twice.
|
|
8
8
|
*
|
|
9
9
|
* The one i18n thing this app still owns, and it has to: `@vitnode/core` is
|
|
10
10
|
* installed by apps with different language lists, so it types a locale as
|
|
11
11
|
* `string` and takes this union as a type argument where the value originates
|
|
12
12
|
* (`useLocale<Locale>()`, `resolveLocale<Locale>()`).
|
|
13
13
|
*/
|
|
14
|
-
export type Locale = (typeof i18n.locales)[number]["code"];
|
|
14
|
+
export type Locale = (typeof vitNodeConfig.i18n.locales)[number]["code"];
|
|
15
15
|
|
|
16
16
|
export { defaultLocale, localeRouting } from "#/lib/i18n/runtime";
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRouteHead } from "@vitnode/core/tanstack/metadata";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A route's `head`, bound to this app's name.
|
|
@@ -16,4 +16,4 @@ import { vitNodeShellConfig } from "#/vitnode.shell.config";
|
|
|
16
16
|
*
|
|
17
17
|
* head: ({ loaderData }) => pageHead({ robots: 'index, follow', ...loaderData })
|
|
18
18
|
*/
|
|
19
|
-
export const pageHead = createRouteHead(
|
|
19
|
+
export const pageHead = createRouteHead(vitNodeConfig.metadata);
|
|
@@ -21,8 +21,8 @@ import type { AppMessagesMap } from "@vitnode/core/lib/i18n/types";
|
|
|
21
21
|
* everything it leaves out falls back to the package's, and then to the default
|
|
22
22
|
* locale, key by key.
|
|
23
23
|
*
|
|
24
|
-
* Server-side only, and kept out of `src/
|
|
25
|
-
* functions, and
|
|
26
|
-
*
|
|
24
|
+
* Server-side only, and kept out of `src/vitnode.config.ts` on purpose: these
|
|
25
|
+
* are functions, and the shared config crosses to the browser and has to stay
|
|
26
|
+
* serializable. `src/vitnode.server.config.ts` is what registers this map.
|
|
27
27
|
*/
|
|
28
28
|
export const appMessages: AppMessagesMap = {};
|
|
@@ -33,8 +33,8 @@ import { CONFIG_PLUGIN as CORE } from "@vitnode/core/config";
|
|
|
33
33
|
* en: async () => await import('@acme/blog/locales/en.json'),
|
|
34
34
|
* },
|
|
35
35
|
*
|
|
36
|
-
* This is the app's
|
|
37
|
-
*
|
|
36
|
+
* This is the app's only copy of that list - `vitnode.server.config.ts` reads it
|
|
37
|
+
* from here.
|
|
38
38
|
*/
|
|
39
39
|
export const packageMessages: Record<string, LocaleMessagesMap> = {
|
|
40
40
|
[CORE.pluginId]: {
|
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
|
|
25
25
|
import type { Locale } from "#/lib/i18n/shared";
|
|
26
26
|
|
|
27
|
-
import {
|
|
27
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
28
28
|
|
|
29
29
|
import appCss from "../styles.css?url";
|
|
30
30
|
|
|
31
|
-
const { debug, i18n, metadata, theme } =
|
|
31
|
+
const { debug, i18n, metadata, theme } = vitNodeConfig;
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* What the router itself provides, before any route has run.
|
|
@@ -41,13 +41,12 @@ import {
|
|
|
41
41
|
*
|
|
42
42
|
* ## `pluginWidgets` is not passed
|
|
43
43
|
*
|
|
44
|
-
* A plugin
|
|
45
|
-
* `getVitNodeConfig()
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the same seam `AdminShell` leaves open for nav `declarations
|
|
50
|
-
* here when plugin AdminCP registration moves over.
|
|
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`.
|
|
51
50
|
*/
|
|
52
51
|
export const Route = createFileRoute("/_admin/admin/core/")({
|
|
53
52
|
loader: async ({ context }) => await loadAdminDashboardRoute(context),
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import "@tanstack/react-start/server-only";
|
|
2
2
|
import { createIntlMessagesLoader } from "@vitnode/core/tanstack/i18n/server";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { vitNodeConfig } from "#/vitnode.config";
|
|
4
|
+
import { vitNodeServerConfig } from "#/vitnode.server.config";
|
|
6
5
|
|
|
7
6
|
export type { IntlMessages } from "@vitnode/core/tanstack/i18n/server";
|
|
8
7
|
|
|
@@ -15,15 +14,8 @@ export type { IntlMessages } from "@vitnode/core/tanstack/i18n/server";
|
|
|
15
14
|
* place, the per-key fallback to the default locale, and the namespace pick that
|
|
16
15
|
* keeps every plugin's AdminCP copy out of a page that renders none of it.
|
|
17
16
|
*
|
|
18
|
-
* What is this app's
|
|
19
|
-
* it declares, and
|
|
20
|
-
* package's own locale barrel
|
|
21
|
-
* `import("./en.json", { with: { type: "json" } })` is a specifier Rollup will
|
|
22
|
-
* not follow. See `src/locales/packages.ts` for the reproduction.
|
|
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.
|
|
23
20
|
*/
|
|
24
|
-
export const loadIntlMessages = createIntlMessagesLoader(
|
|
25
|
-
appMessages: vitNodeConfig.i18n.messages,
|
|
26
|
-
defaultLocale: vitNodeConfig.i18n.defaultLocale,
|
|
27
|
-
packageMessages,
|
|
28
|
-
plugins: vitNodeConfig.plugins,
|
|
29
|
-
});
|
|
21
|
+
export const loadIntlMessages = createIntlMessagesLoader(vitNodeServerConfig);
|
|
@@ -1,88 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createCsrfMiddleware,
|
|
3
|
-
createMiddleware,
|
|
4
|
-
createStart,
|
|
5
|
-
} from "@tanstack/react-start";
|
|
6
|
-
import { handleLocaleRequest } from "@vitnode/core/tanstack/i18n/server";
|
|
1
|
+
import { createVitNodeStart } from "@vitnode/core/tanstack/start";
|
|
7
2
|
|
|
8
|
-
import {
|
|
9
|
-
applyDocumentCacheControl,
|
|
10
|
-
applyRedirectCacheControl,
|
|
11
|
-
} from "#/lib/document-headers";
|
|
12
|
-
import { localeRouting } from "#/lib/i18n/runtime";
|
|
3
|
+
import { vitNodeConfig } from "#/vitnode.config";
|
|
13
4
|
|
|
14
5
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* A global request middleware rather than a wrapper around the server entry:
|
|
18
|
-
* Start runs these before route matching and before SSR, which is exactly where
|
|
19
|
-
* a canonical redirect belongs - the alternative is rendering a page and then
|
|
20
|
-
* throwing it away.
|
|
21
|
-
*
|
|
22
|
-
* `handlerType` narrows it to page requests. Server function calls arrive on
|
|
23
|
-
* `/_serverFn/*` with `handlerType: "serverFn"`, and redirecting an RPC to a
|
|
24
|
-
* canonical URL would break it rather than tidy it.
|
|
25
|
-
*
|
|
26
|
-
* `/api/*` reaches here too and is deliberately ignored by
|
|
27
|
-
* `handleLocaleRequest`, so the Stage 1 Hono bridge sees the request exactly as
|
|
28
|
-
* the client sent it.
|
|
29
|
-
*
|
|
30
|
-
* `localeRouting` is handed in rather than read from the package's registered
|
|
31
|
-
* runtime: Start runs request middleware before route matching, so this is the
|
|
32
|
-
* one caller that cannot assume the router entry has been evaluated. Importing
|
|
33
|
-
* it from `#/lib/i18n/runtime` is also what guarantees this app's i18n is
|
|
34
|
-
* configured before the first request touches it.
|
|
6
|
+
* This app's Start instance.
|
|
35
7
|
*
|
|
36
|
-
*
|
|
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.
|
|
37
12
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* permission set - so none of them may be stored by a shared cache. See
|
|
44
|
-
* `#/lib/document-headers`, which owns the rule and says why; this file only
|
|
45
|
-
* applies it.
|
|
46
|
-
*/
|
|
47
|
-
const localeMiddleware = createMiddleware().server(
|
|
48
|
-
async ({ handlerType, next, request }) => {
|
|
49
|
-
if (handlerType !== "router") return await next();
|
|
50
|
-
|
|
51
|
-
const { redirect, setCookie } = handleLocaleRequest(request, localeRouting);
|
|
52
|
-
if (redirect) {
|
|
53
|
-
applyRedirectCacheControl(redirect);
|
|
54
|
-
|
|
55
|
-
return redirect;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const result = await next();
|
|
59
|
-
|
|
60
|
-
// `append`, not `set`: the API mounted at `/api/*` and the auth flow both
|
|
61
|
-
// mint their own cookies, and overwriting the header would sign people out.
|
|
62
|
-
if (setCookie) result.response.headers.append("set-cookie", setCookie);
|
|
63
|
-
|
|
64
|
-
// After the cookie, so a document that just wrote one is covered by the
|
|
65
|
-
// same directive as one that did not. Only an HTML response is touched -
|
|
66
|
-
// `/api/*` reaches here too, and the API's own caching is not this
|
|
67
|
-
// middleware's to decide.
|
|
68
|
-
applyDocumentCacheControl(result.response);
|
|
69
|
-
|
|
70
|
-
return result;
|
|
71
|
-
},
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* This app's Start instance.
|
|
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.
|
|
76
18
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* declared first so it runs before anything else.
|
|
19
|
+
* export const startInstance = createVitNodeStart({
|
|
20
|
+
* config: vitNodeConfig,
|
|
21
|
+
* requestMiddleware: [myMiddleware],
|
|
22
|
+
* })
|
|
82
23
|
*/
|
|
83
|
-
export const startInstance =
|
|
84
|
-
requestMiddleware: [
|
|
85
|
-
createCsrfMiddleware({ filter: ctx => ctx.handlerType === "serverFn" }),
|
|
86
|
-
localeMiddleware,
|
|
87
|
-
],
|
|
88
|
-
}));
|
|
24
|
+
export const startInstance = createVitNodeStart({ config: vitNodeConfig });
|
|
@@ -1,37 +1,11 @@
|
|
|
1
|
-
@import
|
|
2
|
-
@import
|
|
3
|
-
@import
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "shadcn/tailwind.css";
|
|
3
|
+
@import "tw-animate-css";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
/*
|
|
7
|
-
* VitNode's components ship compiled, so Tailwind cannot find their classes by
|
|
8
|
-
* scanning this app's source. Pointing it at core's build output is what makes
|
|
9
|
-
* the toaster, the tooltip and everything else that follows look like VitNode
|
|
10
|
-
* instead of unstyled markup.
|
|
11
|
-
*
|
|
12
|
-
* `dist/src/tanstack` is the line that is easiest to leave off and hardest to
|
|
13
|
-
* diagnose. `@vitnode/core/tanstack/*` is markup as much as it is orchestration
|
|
14
|
-
* - the header bar, the language switcher, the debug ribbon, the not-found
|
|
15
|
-
* screen - and a class written *there* exists nowhere else, because this app
|
|
16
|
-
* inherits those components rather than writing their markup. Without it a class
|
|
17
|
-
* like `.w-34` is never generated, an `<svg>` with a viewBox and no width
|
|
18
|
-
* collapses, and the header renders with no logo at all.
|
|
19
|
-
*
|
|
20
|
-
* Add a line per plugin that ships compiled components of its own.
|
|
21
|
-
*
|
|
22
|
-
* The paths are relative to this file. In a monorepo where the package manager
|
|
23
|
-
* hoists to the repository root, that is `../../../node_modules/...` - the
|
|
24
|
-
* generator rewrites them when it knows.
|
|
25
|
-
*/
|
|
26
5
|
@source "../node_modules/@vitnode/core/dist/src/components";
|
|
27
6
|
@source "../node_modules/@vitnode/core/dist/src/tanstack";
|
|
28
7
|
@source "../node_modules/@vitnode/core/dist/src/views";
|
|
29
8
|
|
|
30
|
-
/*
|
|
31
|
-
* The design tokens every VitNode component reads. `.dark` is what
|
|
32
|
-
* `ThemeScript` and `ThemeProvider` put on `<html>`, so the whole palette
|
|
33
|
-
* switches from that one class.
|
|
34
|
-
*/
|
|
35
9
|
:root:not(.dark) {
|
|
36
10
|
--background: oklch(0.97 0 0);
|
|
37
11
|
--foreground: oklch(0.145 0 0);
|
|
@@ -149,12 +123,6 @@
|
|
|
149
123
|
--radius-4xl: calc(var(--radius) * 2.6);
|
|
150
124
|
}
|
|
151
125
|
|
|
152
|
-
/*
|
|
153
|
-
* Last, so that a stylesheet a package brings in cannot outrank it in this
|
|
154
|
-
* layer: `@layer base` rules are ordered by where they appear, and a library
|
|
155
|
-
* that sets its own `body { background-color: ... }` would otherwise repaint
|
|
156
|
-
* whichever pages happen to load it.
|
|
157
|
-
*/
|
|
158
126
|
@layer base {
|
|
159
127
|
* {
|
|
160
128
|
@apply border-border outline-ring/50;
|
|
@@ -1,58 +1,73 @@
|
|
|
1
1
|
import { buildConfig } from "@vitnode/core/vitnode.config";
|
|
2
2
|
|
|
3
|
-
import { appMessages } from "./locales/app";
|
|
4
|
-
import { vitNodeShellConfig } from "./vitnode.shell.config";
|
|
5
|
-
|
|
6
3
|
/**
|
|
7
|
-
* This app's
|
|
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
|
|
8
17
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
23
|
*
|
|
12
|
-
*
|
|
13
|
-
* import { CONFIG_PLUGIN as BLOG } from '@acme/blog/const'
|
|
24
|
+
* `vitnode i18n:create de Deutsch` does all of it for you.
|
|
14
25
|
*
|
|
15
|
-
*
|
|
26
|
+
* ## Adding a plugin
|
|
16
27
|
*
|
|
17
|
-
*
|
|
18
|
-
* buildPlugin({
|
|
19
|
-
* messages: packageMessages[BLOG.pluginId],
|
|
20
|
-
* pluginId: BLOG.pluginId,
|
|
21
|
-
* }),
|
|
22
|
-
* ]
|
|
28
|
+
* With the plugin's own factory:
|
|
23
29
|
*
|
|
24
|
-
*
|
|
25
|
-
* reader takes `pluginId` and `messages` off each plugin and nothing else, so a
|
|
26
|
-
* full registration would add every content type's editing screen to a
|
|
27
|
-
* server-only graph where nothing would ever look at them. The AdminCP gets its
|
|
28
|
-
* content types from `src/content-registry.gen.ts` instead - one literal import
|
|
29
|
-
* per configured plugin - so a browser loads them with the content route and not
|
|
30
|
-
* before.
|
|
30
|
+
* import { blogPlugin } from '@acme/blog/config'
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
36
44
|
*
|
|
37
45
|
* A plugin's *pages* need nothing in this file at all. It declares them in its
|
|
38
46
|
* own `src/routes.ts`, and this app's Vite build compiles them into
|
|
39
47
|
* `src/plugin-routes.gen.ts`, which `src/router.tsx` mounts under the shell the
|
|
40
48
|
* plugin's `area` names. No page is ever copied into `src/routes`.
|
|
41
49
|
*
|
|
42
|
-
* Server-side only, and deliberately so - see `vitnode.shell.config.ts`.
|
|
43
|
-
* `src/server/messages.server.ts` is the only importer, and it carries the
|
|
44
|
-
* `server-only` guard that keeps it that way.
|
|
45
|
-
*
|
|
46
50
|
* `buildConfig` also registers this object process-wide, which is how core's own
|
|
47
51
|
* route files find it without being handed it as a prop.
|
|
48
52
|
*/
|
|
49
53
|
export const vitNodeConfig = buildConfig({
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
},
|
|
57
69
|
plugins: [],
|
|
70
|
+
theme: {
|
|
71
|
+
defaultTheme: "system",
|
|
72
|
+
},
|
|
58
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
|
+
});
|
|
@@ -2,11 +2,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|
|
2
2
|
import { devtools } from "@tanstack/devtools-vite";
|
|
3
3
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
4
4
|
import viteReact from "@vitejs/plugin-react";
|
|
5
|
-
import {
|
|
6
|
-
vitNodeEnv,
|
|
7
|
-
vitNodeOptimizeDeps,
|
|
8
|
-
vitNodePluginRoutes,
|
|
9
|
-
} from "@vitnode/core/framework/vite";
|
|
5
|
+
import { vitnode } from "@vitnode/core/framework/vite";
|
|
10
6
|
import { nitro } from "nitro/vite";
|
|
11
7
|
import { defineConfig } from "vite";
|
|
12
8
|
|
|
@@ -85,29 +81,20 @@ const config = defineConfig({
|
|
|
85
81
|
},
|
|
86
82
|
plugins: [
|
|
87
83
|
/**
|
|
88
|
-
*
|
|
84
|
+
* Environment handling, the dev server's dependency pre-bundling and the
|
|
85
|
+
* plugin route registry, in the order they have to run.
|
|
89
86
|
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* without this list a screen that is opened for the first time discovers a
|
|
94
|
-
* dependency mid-session, the optimizer re-runs, and a page holding modules
|
|
95
|
-
* from the previous run ends up with two copies of React.
|
|
87
|
+
* `appRoot` is `import.meta.dirname` because a Vite config is loaded with
|
|
88
|
+
* the working directory set to wherever the command ran, which in a
|
|
89
|
+
* monorepo is regularly the repository root.
|
|
96
90
|
*
|
|
97
|
-
* `
|
|
98
|
-
*
|
|
91
|
+
* `clientEnv` is not passed: this app publishes nothing to the browser
|
|
92
|
+
* beyond the two keys VitNode inlines for every install
|
|
99
93
|
* (`NEXT_PUBLIC_API_URL`, `NEXT_PUBLIC_WEB_URL`). Anything named there is
|
|
100
|
-
* compiled into JavaScript anyone can read, so
|
|
101
|
-
*
|
|
102
|
-
* reads it.
|
|
103
|
-
*
|
|
104
|
-
* `appRoot` is `import.meta.dirname` because a Vite config is loaded with the
|
|
105
|
-
* working directory set to wherever the command ran, which in this monorepo
|
|
106
|
-
* is regularly the repository root.
|
|
94
|
+
* compiled into JavaScript anyone can read, so a key is added only when
|
|
95
|
+
* something in the browser genuinely reads it.
|
|
107
96
|
*/
|
|
108
|
-
|
|
109
|
-
vitNodeOptimizeDeps(),
|
|
110
|
-
vitNodePluginRoutes({ appRoot: import.meta.dirname }),
|
|
97
|
+
vitnode({ appRoot: import.meta.dirname }),
|
|
111
98
|
devtools(),
|
|
112
99
|
nitro({ rollupConfig: { external: [/^@sentry\//] } }),
|
|
113
100
|
tailwindcss(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-vitnode-app",
|
|
3
|
-
"version": "2.0.0-canary.
|
|
3
|
+
"version": "2.0.0-canary.2",
|
|
4
4
|
"description": "Create a new VitNode app in seconds.",
|
|
5
5
|
"author": "VitNode Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"eslint": "^10.7.0",
|
|
43
43
|
"typescript": "^6.0.3",
|
|
44
44
|
"vitest": "^4.1.10",
|
|
45
|
-
"@vitnode/config": "2.0.0-canary.
|
|
45
|
+
"@vitnode/config": "2.0.0-canary.2"
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build:scripts": "tsc -p tsconfig.build.json && node dist/src/prepare/prepare.js",
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { VitNodeI18nConfig } from "@vitnode/core/lib/i18n/types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The languages this installation serves.
|
|
5
|
-
*
|
|
6
|
-
* The API half of a split deployment. `apps/web` has a file of the same name,
|
|
7
|
-
* and the two are one declaration in two places by necessity rather than by
|
|
8
|
-
* design: they are separate packages, so neither can import the other's.
|
|
9
|
-
*
|
|
10
|
-
* They have to agree, and this is the copy that matters most - this app owns the
|
|
11
|
-
* schema, so `vitnode db:prepare` seeds `core_languages` from *this* list. A
|
|
12
|
-
* language that is here and not in the web app's renders nowhere; one that is in
|
|
13
|
-
* the web app's and not here has no row in the database, and everything keyed on
|
|
14
|
-
* a language row has nowhere to put it.
|
|
15
|
-
*
|
|
16
|
-
* Deliberately not discovered: nothing walks the filesystem looking for the web
|
|
17
|
-
* app's config. A bootstrap that guessed at a sibling application is exactly
|
|
18
|
-
* what this replaced, and it guessed wrong the moment the two were not laid out
|
|
19
|
-
* the way it expected.
|
|
20
|
-
*
|
|
21
|
-
* Packages ship their own translations, so a new locale needs no `messages`
|
|
22
|
-
* entry - anything untranslated falls back to `defaultLocale` key by key.
|
|
23
|
-
*/
|
|
24
|
-
export const i18n = {
|
|
25
|
-
defaultLocale: "en",
|
|
26
|
-
/**
|
|
27
|
-
* Explicit, because this API renders emails on a server: without one, dates
|
|
28
|
-
* format in whatever zone the host happens to run in.
|
|
29
|
-
*/
|
|
30
|
-
timeZone: "UTC",
|
|
31
|
-
locales: [
|
|
32
|
-
{
|
|
33
|
-
code: "en",
|
|
34
|
-
name: "English",
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
messages: {},
|
|
38
|
-
} satisfies VitNodeI18nConfig;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { VitNodeI18nConfig } from "@vitnode/core/lib/i18n/types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* The languages this app serves.
|
|
5
|
-
*
|
|
6
|
-
* Its own module so the web config and the API config can point at the same
|
|
7
|
-
* object instead of drifting apart - the site and the emails it sends have to
|
|
8
|
-
* agree on which languages exist.
|
|
9
|
-
*
|
|
10
|
-
* Add a language by adding an entry. Packages ship their own translations, so a
|
|
11
|
-
* new locale needs no `messages` here: anything a package has not translated
|
|
12
|
-
* falls back to `defaultLocale` key by key. Register the package's own file for
|
|
13
|
-
* that language in `src/locales/packages.ts`, and put your own rewording in
|
|
14
|
-
* `src/locales/app.ts`.
|
|
15
|
-
*/
|
|
16
|
-
export const i18n = {
|
|
17
|
-
defaultLocale: "en" as const,
|
|
18
|
-
/**
|
|
19
|
-
* Explicit, because the app renders on a server: without one, `use-intl`
|
|
20
|
-
* formats dates in whatever zone the server happens to run in and warns that
|
|
21
|
-
* the client will disagree. Stage 3, which owns the locale runtime, is where a
|
|
22
|
-
* per-visitor zone would come from.
|
|
23
|
-
*/
|
|
24
|
-
timeZone: "UTC",
|
|
25
|
-
/**
|
|
26
|
-
* `as const` on each code, and nothing else: it keeps `"en" | "pl"` out of
|
|
27
|
-
* the widening `satisfies` would otherwise do, which is what makes `Locale`
|
|
28
|
-
* in `lib/i18n/shared.ts` a real union rather than an alias for `string`.
|
|
29
|
-
*/
|
|
30
|
-
locales: [
|
|
31
|
-
{
|
|
32
|
-
code: "en" as const,
|
|
33
|
-
name: "English",
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
} satisfies VitNodeI18nConfig;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a VitNode document response may say about being stored.
|
|
3
|
-
*
|
|
4
|
-
* `private, no-store`, and it is not a precaution - it is a description of what
|
|
5
|
-
* is in the body. Every page this application renders streams a dehydrated Query
|
|
6
|
-
* cache into its HTML, and that cache always holds `["vitnode","session"]`: the
|
|
7
|
-
* visitor's own name, avatar and `isAdmin` flag. Inside `/admin` it also holds
|
|
8
|
-
* `["vitnode","admin-session"]`, which is that administrator's entire permission
|
|
9
|
-
* set. `tanstack/auth/session-query` and `tanstack/admin/session-query` both say
|
|
10
|
-
* so in their own words - "that document is personalised and must not be served
|
|
11
|
-
* from a shared cache" - and until now nothing on the response said it back.
|
|
12
|
-
*
|
|
13
|
-
* Nothing caches these documents today. That is a property of the current
|
|
14
|
-
* deployment, not of the application: the moment a CDN, a reverse proxy or a
|
|
15
|
-
* `Cache-Control`-respecting edge sits in front of the Node server, an absent
|
|
16
|
-
* directive is an invitation to store one visitor's HTML and serve it to the
|
|
17
|
-
* next. Stage 15 is when that becomes likely, so the header belongs here now.
|
|
18
|
-
*
|
|
19
|
-
* ## It is an invariant, not a default
|
|
20
|
-
*
|
|
21
|
-
* This started as a fallback for documents that said nothing, and that was
|
|
22
|
-
* wrong. A default is something a route may override, and at this architecture
|
|
23
|
-
* level there is no override a route could correctly choose: the dehydrated
|
|
24
|
-
* cache is written into the stream by `setupRouterSsrQueryIntegration` for
|
|
25
|
-
* *every* document, so a route opting into `public, max-age=60` would be
|
|
26
|
-
* publishing whichever visitor rendered first to everyone who asked next. The
|
|
27
|
-
* route cannot know that, because the private payload is not something the route
|
|
28
|
-
* put there.
|
|
29
|
-
*
|
|
30
|
-
* So the directive is forced rather than filled in, and a route that sets its
|
|
31
|
-
* own is overwritten rather than obeyed. That is the whole difference between
|
|
32
|
-
* this being a hardening measure and it being a security invariant.
|
|
33
|
-
*
|
|
34
|
-
* Public document caching is not forbidden forever - it is forbidden *while the
|
|
35
|
-
* session is dehydrated into the document*. Introducing it later is a separate,
|
|
36
|
-
* explicit piece of architecture in which the private state is kept out of the
|
|
37
|
-
* shared body (a public shell fetching its session client-side, say), and the
|
|
38
|
-
* invariant here would move with it rather than being quietly relaxed by a route
|
|
39
|
-
* that wanted a faster page.
|
|
40
|
-
*
|
|
41
|
-
* `private` bars a shared cache. `no-store` bars every cache, including the
|
|
42
|
-
* browser's own disk cache - which is the half that matters on a shared machine,
|
|
43
|
-
* where the previous person's permission set should not be recoverable from
|
|
44
|
-
* `chrome://cache` after they sign out. The pair is the standard spelling for
|
|
45
|
-
* "this body belongs to exactly one person, once".
|
|
46
|
-
*
|
|
47
|
-
* The known cost is the back/forward cache. `no-store` used to make a page
|
|
48
|
-
* outright ineligible for bfcache in Chromium; current versions keep such pages
|
|
49
|
-
* eligible but evict them when cookies change - which for this app means a
|
|
50
|
-
* sign-in or sign-out invalidates a back-navigation that would otherwise have
|
|
51
|
-
* restored a page rendered for the previous session. That is the correct trade
|
|
52
|
-
* and the outcome anybody would want, but it is a real difference and it is
|
|
53
|
-
* worth a look during a manual pass rather than a surprise later.
|
|
54
|
-
*/
|
|
55
|
-
export const DOCUMENT_CACHE_CONTROL = "private, no-store";
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Whether this response is one of the documents the rule above describes.
|
|
59
|
-
*
|
|
60
|
-
* One question, and it is what keeps the API out of it. `/api/*` is served by
|
|
61
|
-
* the Hono bridge through this same middleware, and a bare `GET` from it carries
|
|
62
|
-
* no `Cache-Control` of its own - so a rule that applied to every response would
|
|
63
|
-
* quietly forbid clients from caching the API. An HTML content-type is the
|
|
64
|
-
* honest way to ask "is this a page", it needs no path list to be kept in step
|
|
65
|
-
* with the router, and it cannot be wrong about a response that has already been
|
|
66
|
-
* produced.
|
|
67
|
-
*
|
|
68
|
-
* It deliberately does *not* ask whether a directive is already present. That
|
|
69
|
-
* used to be the second half of this predicate, and it is exactly the exemption
|
|
70
|
-
* the invariant above cannot afford - see {@link applyDocumentCacheControl}.
|
|
71
|
-
*
|
|
72
|
-
* A redirect is deliberately not matched here - it has no content type - and is
|
|
73
|
-
* handled by {@link applyRedirectCacheControl} instead, which wants a narrower
|
|
74
|
-
* rule.
|
|
75
|
-
*/
|
|
76
|
-
const isRenderedDocument = (headers: Headers): boolean =>
|
|
77
|
-
(headers.get("content-type") ?? "").toLowerCase().startsWith("text/html");
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Says what a rendered document is, on the response about to be sent.
|
|
81
|
-
*
|
|
82
|
-
* `set` rather than a conditional fill, and that is the fix: whatever the
|
|
83
|
-
* response was carrying is replaced. A route cannot opt out, because a route is
|
|
84
|
-
* not in a position to know what is in the body it is opting out for.
|
|
85
|
-
*
|
|
86
|
-
* Only `text/html` is touched. Everything else the middleware sees - the API,
|
|
87
|
-
* assets, client chunks, a `204` with no content type at all - keeps whatever it
|
|
88
|
-
* had, including nothing.
|
|
89
|
-
*
|
|
90
|
-
* Mutates rather than returning a new `Response`, because the middleware already
|
|
91
|
-
* holds the one Start produced and rebuilding it would mean copying a stream.
|
|
92
|
-
* The same reason `set-cookie` is appended in place a few lines away.
|
|
93
|
-
*/
|
|
94
|
-
export const applyDocumentCacheControl = (response: Response): void => {
|
|
95
|
-
if (!isRenderedDocument(response.headers)) return;
|
|
96
|
-
|
|
97
|
-
response.headers.set("cache-control", DOCUMENT_CACHE_CONTROL);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* The same for a locale redirect, but only when it is carrying a cookie.
|
|
102
|
-
*
|
|
103
|
-
* A `308` from `/en/discover` to `/discover` is a fact about URLs, identical for
|
|
104
|
-
* every visitor, and permanently cacheable - which is most of the point of
|
|
105
|
-
* answering with one. So it keeps that property by default.
|
|
106
|
-
*
|
|
107
|
-
* The exception is the redirect that also writes the locale cookie, which is
|
|
108
|
-
* what `/pl/admin` produces: a stored copy of that would hand the next visitor
|
|
109
|
-
* through the same shared cache a `Set-Cookie` chosen by somebody else, and
|
|
110
|
-
* quietly switch their language. Shared caches are generally expected to refuse
|
|
111
|
-
* a `Set-Cookie` response, but "generally expected" is not a property this
|
|
112
|
-
* application can assert about somebody else's proxy, and one visitor's cookie
|
|
113
|
-
* reaching another's browser is not the kind of thing to leave to convention.
|
|
114
|
-
*
|
|
115
|
-
* So the cookie-carrying case is forced, for the same reason the document is: a
|
|
116
|
-
* `public` directive already on such a redirect is overwritten rather than
|
|
117
|
-
* respected, because the thing that makes it unsafe to share is the `Set-Cookie`
|
|
118
|
-
* beside it and not whatever the directive claims.
|
|
119
|
-
*
|
|
120
|
-
* The cookie-less case keeps its existing semantics untouched, directive and
|
|
121
|
-
* all. It carries no private state, so there is nothing here to protect and
|
|
122
|
-
* nothing to override.
|
|
123
|
-
*/
|
|
124
|
-
export const applyRedirectCacheControl = (response: Response): void => {
|
|
125
|
-
if (!response.headers.has("set-cookie")) return;
|
|
126
|
-
|
|
127
|
-
response.headers.set("cache-control", DOCUMENT_CACHE_CONTROL);
|
|
128
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { VitNodeConfig } from "@vitnode/core/vitnode.config";
|
|
2
|
-
|
|
3
|
-
import { i18n } from "./i18n";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The VitNode config the browser is allowed to see.
|
|
7
|
-
*
|
|
8
|
-
* Everything in `VitNodeConfig` except `plugins`, and that omission is the whole
|
|
9
|
-
* point. The plugin registry carries each plugin's translations as `import()`s
|
|
10
|
-
* of JSON inside its `dist`, and eventually its AdminCP components - neither of
|
|
11
|
-
* which a browser bundle should hold. In Next.js the boundary is drawn for you:
|
|
12
|
-
* `vitnode.config.ts` is only ever read by Server Components, so none of it
|
|
13
|
-
* reaches the client. TanStack Start has no such boundary - anything the root
|
|
14
|
-
* route imports is in the browser bundle - so the split is made here instead, by
|
|
15
|
-
* hand.
|
|
16
|
-
*
|
|
17
|
-
* `vitnode.config.ts` spreads this into `buildConfig` with the plugins added, so
|
|
18
|
-
* there is one source for the metadata, the theme and the locales rather than
|
|
19
|
-
* two that agree until they don't.
|
|
20
|
-
*
|
|
21
|
-
* Everything here is plain, serializable data. That is a rule, not a
|
|
22
|
-
* coincidence: this module is imported by the document shell, which renders on
|
|
23
|
-
* both sides of hydration.
|
|
24
|
-
*/
|
|
25
|
-
export const vitNodeShellConfig = {
|
|
26
|
-
debug: false,
|
|
27
|
-
i18n,
|
|
28
|
-
metadata: {
|
|
29
|
-
shortTitle: "VitNode",
|
|
30
|
-
title: "VitNode",
|
|
31
|
-
},
|
|
32
|
-
theme: {
|
|
33
|
-
defaultTheme: "system",
|
|
34
|
-
},
|
|
35
|
-
} satisfies Omit<VitNodeConfig, "plugins">;
|