create-vitnode-app 2.0.0-canary.1 → 2.0.0-canary.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/copy-of-vitnode-app/README.md +22 -0
- package/copy-of-vitnode-app/api/src/vitnode.api.config.ts +10 -11
- package/copy-of-vitnode-app/api-single-app/src/routes/api/$.ts +4 -26
- package/copy-of-vitnode-app/api-single-app/src/server/api-bridge.ts +1 -12
- package/copy-of-vitnode-app/api-single-app/src/server/vitnode-api.server.ts +13 -13
- package/copy-of-vitnode-app/api-single-app/src/vitnode.api.config.ts +3 -12
- package/copy-of-vitnode-app/docker/docker-compose.yml +6 -4
- package/copy-of-vitnode-app/monorepo/apps/api/.env.example +1 -1
- package/copy-of-vitnode-app/monorepo/apps/web/.env.example +1 -12
- package/copy-of-vitnode-app/monorepo/turbo.json +2 -2
- package/copy-of-vitnode-app/root/.env.example +1 -8
- package/copy-of-vitnode-app/root/global.d.ts +1 -19
- package/copy-of-vitnode-app/root/src/components/admin-shell.tsx +2 -53
- package/copy-of-vitnode-app/root/src/components/main-footer.tsx +44 -0
- package/copy-of-vitnode-app/root/src/components/main-header.tsx +1 -30
- package/copy-of-vitnode-app/root/src/lib/i18n.ts +37 -0
- package/copy-of-vitnode-app/root/src/locales/app.ts +1 -25
- package/copy-of-vitnode-app/root/src/locales/packages.ts +1 -34
- package/copy-of-vitnode-app/root/src/router.tsx +9 -165
- package/copy-of-vitnode-app/root/src/routes/__root.tsx +12 -115
- package/copy-of-vitnode-app/root/src/routes/_admin/admin.core.index.tsx +4 -54
- package/copy-of-vitnode-app/root/src/routes/_admin.tsx +5 -198
- package/copy-of-vitnode-app/root/src/routes/_main/index.tsx +182 -46
- package/copy-of-vitnode-app/root/src/routes/_main.tsx +12 -47
- package/copy-of-vitnode-app/root/src/server/messages.server.ts +2 -23
- package/copy-of-vitnode-app/root/src/start.ts +3 -86
- package/copy-of-vitnode-app/root/src/styles.css +70 -92
- package/copy-of-vitnode-app/root/src/vitnode.config.ts +15 -53
- package/copy-of-vitnode-app/root/src/vitnode.server.config.ts +12 -0
- package/copy-of-vitnode-app/root/tsconfig.json +8 -3
- package/copy-of-vitnode-app/root/vite.config.ts +2 -99
- package/copy-of-vitnode-plugin/root/global.d.ts +6 -6
- package/dist/src/create/create-package-json.js +3 -3
- package/dist/src/create/create-vitnode.js +15 -1
- package/dist/src/create/package-versions.js +22 -22
- package/dist/src/helpers/init-git.js +38 -0
- package/dist/src/helpers/init-vitnode.js +2 -3
- package/dist/src/helpers/install-dependencies.js +2 -3
- package/dist/src/helpers/spawn-command.js +7 -0
- package/dist/src/index.js +1 -0
- package/dist/src/plugin/create/add-plugin-to-config.js +231 -0
- package/dist/src/plugin/create/create-package-json.js +2 -1
- package/dist/src/plugin/create/create-plugin-vitnode.js +23 -2
- package/dist/src/plugin/create/route-templates.js +103 -61
- package/dist/src/questions.js +7 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/copy-of-vitnode-app/api/src/i18n.ts +0 -38
- package/copy-of-vitnode-app/root/src/i18n.ts +0 -36
- package/copy-of-vitnode-app/root/src/lib/admin-auth.ts +0 -11
- package/copy-of-vitnode-app/root/src/lib/admin-nav.ts +0 -43
- package/copy-of-vitnode-app/root/src/lib/admin-search.ts +0 -37
- package/copy-of-vitnode-app/root/src/lib/auth.ts +0 -66
- package/copy-of-vitnode-app/root/src/lib/content-registry.ts +0 -51
- package/copy-of-vitnode-app/root/src/lib/document-headers.ts +0 -128
- package/copy-of-vitnode-app/root/src/lib/i18n/runtime.ts +0 -75
- package/copy-of-vitnode-app/root/src/lib/i18n/shared.ts +0 -20
- package/copy-of-vitnode-app/root/src/lib/navigation.ts +0 -24
- package/copy-of-vitnode-app/root/src/lib/page-head.ts +0 -19
- package/copy-of-vitnode-app/root/src/vitnode.shell.config.ts +0 -35
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { readAdminUserSearchOnApi } from "@vitnode/core/tanstack/admin/server";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* The AdminCP command palette's user lookup, as this app's server function.
|
|
7
|
-
*
|
|
8
|
-
* One handler, one line, exactly like `lib/auth.ts` - and here for the same
|
|
9
|
-
* reason. `createServerFn` needs the module it sits in to be transformed by the
|
|
10
|
-
* Start compiler on both sides of the render, and `@vitnode/core` reaches the
|
|
11
|
-
* server un-compiled (see the note in `lib/auth.ts`), so the declaration lives
|
|
12
|
-
* in the host and the behaviour lives in the package.
|
|
13
|
-
*
|
|
14
|
-
* ## Why it has a validator
|
|
15
|
-
*
|
|
16
|
-
* A server function is a public same-origin endpoint: its input is whatever a
|
|
17
|
-
* caller posts, not whatever the dialog typed. This value reaches an API query
|
|
18
|
-
* string, so it is parsed rather than trusted - trimmed, length-bounded, and a
|
|
19
|
-
* string at all.
|
|
20
|
-
*
|
|
21
|
-
* The bound is not a security control; the API authorizes the read from the
|
|
22
|
-
* admin cookie and pages it itself. It is a cheap refusal of the requests that
|
|
23
|
-
* could only ever be abuse - a megabyte of "search term" that the API would
|
|
24
|
-
* otherwise have to hand to Postgres.
|
|
25
|
-
*
|
|
26
|
-
* `POST` puts it behind `createCsrfMiddleware` in `src/start.ts`, which matters
|
|
27
|
-
* more here than it looks: this reads other people's names and email addresses,
|
|
28
|
-
* and a `GET` would be reachable from another origin's page with the
|
|
29
|
-
* administrator's cookies attached.
|
|
30
|
-
*/
|
|
31
|
-
const adminUserSearchInput = z.object({
|
|
32
|
-
search: z.string().trim().min(1).max(128),
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export const adminUserSearchFn = createServerFn({ method: "POST" })
|
|
36
|
-
.validator(adminUserSearchInput)
|
|
37
|
-
.handler(async ({ data }) => await readAdminUserSearchOnApi(data.search));
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import {
|
|
3
|
-
changePasswordInputSchema,
|
|
4
|
-
passwordResetRequestInputSchema,
|
|
5
|
-
setAuthTransport,
|
|
6
|
-
signInInputSchema,
|
|
7
|
-
signOutInputSchema,
|
|
8
|
-
signUpInputSchema,
|
|
9
|
-
ssoCallbackInputSchema,
|
|
10
|
-
ssoStartInputSchema,
|
|
11
|
-
} from "@vitnode/core/tanstack/auth";
|
|
12
|
-
import {
|
|
13
|
-
changePasswordFromResetOnApi,
|
|
14
|
-
completeSsoOnApi,
|
|
15
|
-
readSessionOnApi,
|
|
16
|
-
requestPasswordResetOnApi,
|
|
17
|
-
signInOnApi,
|
|
18
|
-
signOutOnApi,
|
|
19
|
-
signUpOnApi,
|
|
20
|
-
startSsoOnApi,
|
|
21
|
-
} from "@vitnode/core/tanstack/auth/server";
|
|
22
|
-
|
|
23
|
-
export const readSessionFn = createServerFn().handler(
|
|
24
|
-
async () => await readSessionOnApi(),
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
export const signInFn = createServerFn({ method: "POST" })
|
|
28
|
-
.validator(signInInputSchema)
|
|
29
|
-
.handler(async ({ data }) => await signInOnApi(data));
|
|
30
|
-
|
|
31
|
-
export const signOutFn = createServerFn({ method: "POST" })
|
|
32
|
-
.validator(signOutInputSchema)
|
|
33
|
-
.handler(async ({ data }) => await signOutOnApi(data));
|
|
34
|
-
|
|
35
|
-
export const startSsoFn = createServerFn({ method: "POST" })
|
|
36
|
-
.validator(ssoStartInputSchema)
|
|
37
|
-
.handler(async ({ data }) => await startSsoOnApi(data));
|
|
38
|
-
|
|
39
|
-
export const completeSsoFn = createServerFn({ method: "POST" })
|
|
40
|
-
.validator(ssoCallbackInputSchema)
|
|
41
|
-
.handler(async ({ data }) => await completeSsoOnApi(data));
|
|
42
|
-
|
|
43
|
-
export const signUpFn = createServerFn({ method: "POST" })
|
|
44
|
-
.validator(signUpInputSchema)
|
|
45
|
-
.handler(async ({ data }) => await signUpOnApi(data));
|
|
46
|
-
|
|
47
|
-
export const requestPasswordResetFn = createServerFn({ method: "POST" })
|
|
48
|
-
.validator(passwordResetRequestInputSchema)
|
|
49
|
-
.handler(async ({ data }) => await requestPasswordResetOnApi(data));
|
|
50
|
-
|
|
51
|
-
export const changePasswordFromResetFn = createServerFn({ method: "POST" })
|
|
52
|
-
.validator(changePasswordInputSchema)
|
|
53
|
-
.handler(async ({ data }) => await changePasswordFromResetOnApi(data));
|
|
54
|
-
|
|
55
|
-
setAuthTransport({
|
|
56
|
-
changePasswordFromReset: async input =>
|
|
57
|
-
await changePasswordFromResetFn({ data: input }),
|
|
58
|
-
completeSso: async input => await completeSsoFn({ data: input }),
|
|
59
|
-
readSession: async () => await readSessionFn(),
|
|
60
|
-
requestPasswordReset: async input =>
|
|
61
|
-
await requestPasswordResetFn({ data: input }),
|
|
62
|
-
signIn: async input => await signInFn({ data: input }),
|
|
63
|
-
signOut: async input => await signOutFn({ data: input }),
|
|
64
|
-
signUp: async input => await signUpFn({ data: input }),
|
|
65
|
-
startSso: async input => await startSsoFn({ data: input }),
|
|
66
|
-
});
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildContentFrontendRegistry,
|
|
3
|
-
setContentFrontendRegistry,
|
|
4
|
-
} from "@vitnode/core/content";
|
|
5
|
-
|
|
6
|
-
import { pluginContentTypes } from "#/content-registry.gen";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* This installation's Content Engine registry - every content type its
|
|
10
|
-
* configured plugins register, with their editing screens attached.
|
|
11
|
-
*
|
|
12
|
-
* Two lines, and both of them are host-only work by necessity. Which plugins are
|
|
13
|
-
* installed is a property of this application, and `src/content-registry.gen.ts`
|
|
14
|
-
* is where the build writes the answer: one literal import per configured plugin
|
|
15
|
-
* that exports an `admin/content` module. Every *rule* about what a registration
|
|
16
|
-
* is - which paths are legal, which two of them collide, how they are ordered,
|
|
17
|
-
* how one is found by id or by `admin.path` - is `@vitnode/core`'s, in
|
|
18
|
-
* `buildContentFrontendRegistry`. Nothing about the Content Engine is decided
|
|
19
|
-
* here.
|
|
20
|
-
*
|
|
21
|
-
* ## Why it is not read from `vitnode.config.ts`
|
|
22
|
-
*
|
|
23
|
-
* That config is server-side on purpose (`vitnode.shell.config.ts` explains the
|
|
24
|
-
* split): it carries message loaders and API wiring, which a browser bundle has
|
|
25
|
-
* no business holding. The generated projection is the browser-safe half - the
|
|
26
|
-
* definitions, the icons and the override components - so the AdminCP gets the
|
|
27
|
-
* content screens without the server config. The same arrangement
|
|
28
|
-
* `src/lib/admin-nav.ts` uses for the sidebar, one layer deeper.
|
|
29
|
-
*
|
|
30
|
-
* ## Registration, and where it belongs in the import graph
|
|
31
|
-
*
|
|
32
|
-
* `setContentFrontendRegistry` fills a module-scope slot in `@vitnode/core`, so
|
|
33
|
-
* the package's own content code finds the registry without being handed it as
|
|
34
|
-
* a prop - the same shape as `setAdminTransport`. Module scope means *per
|
|
35
|
-
* bundle*: the browser has one instance and the server has one, and each
|
|
36
|
-
* registers its own.
|
|
37
|
-
*
|
|
38
|
-
* This module is reached through a `() => import(...)` that `/admin/content`'s
|
|
39
|
-
* loader awaits, never through a static import, and that is deliberate.
|
|
40
|
-
* Registration only has to happen before a content screen runs, and deferring it
|
|
41
|
-
* to the route is what lets Rollup put this whole graph - every plugin's field
|
|
42
|
-
* components, table cells and form layouts, plus `zod` and the Content Engine
|
|
43
|
-
* itself - in that route's chunk instead of in the bundle every page of the site
|
|
44
|
-
* loads first. `router.tsx` holds the thunk because that is where the route tree
|
|
45
|
-
* is composed; what it does *not* hold is the value. A plugin's own heavier
|
|
46
|
-
* parts stay lazier still: `@vitnode/blog` draws a `React.lazy` boundary around
|
|
47
|
-
* its Tiptap editor, so even opening the article list does not fetch it.
|
|
48
|
-
*/
|
|
49
|
-
export const contentRegistry = buildContentFrontendRegistry(pluginContentTypes);
|
|
50
|
-
|
|
51
|
-
setContentFrontendRegistry(contentRegistry);
|
|
@@ -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,75 +0,0 @@
|
|
|
1
|
-
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
-
import { configureIntl, validateIntlInput } from "@vitnode/core/tanstack/i18n";
|
|
3
|
-
import { IntlProvider } from "use-intl";
|
|
4
|
-
|
|
5
|
-
import { i18n } from "#/i18n";
|
|
6
|
-
import { loadIntlMessages } from "#/server/messages.server";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* One language's messages for one set of namespaces, fetched on the server.
|
|
10
|
-
*
|
|
11
|
-
* The one piece of the i18n runtime that cannot live in `@vitnode/core`, and the
|
|
12
|
-
* reason is the compiler rather than the code. A server function has to be
|
|
13
|
-
* transformed by the Start plugin in *both* bundles; the package is externalised
|
|
14
|
-
* from this app's SSR pass, so its modules reach the server un-compiled and a
|
|
15
|
-
* `createServerFn` declared there resolves to `undefined` during SSR with no
|
|
16
|
-
* error. See `packages/vitnode/src/tanstack/boundary.test.ts`.
|
|
17
|
-
*
|
|
18
|
-
* So the wrapper is here and the body is not: `validateIntlInput` is core's, and
|
|
19
|
-
* `loadIntlMessages` delegates to core's loading engine. Start strips the
|
|
20
|
-
* handler - and `#/server/messages.server` with it - out of the client build.
|
|
21
|
-
*/
|
|
22
|
-
export const getIntlMessages = createServerFn()
|
|
23
|
-
.validator(validateIntlInput)
|
|
24
|
-
.handler(async ({ data }) => await loadIntlMessages(data));
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* This app's languages, handed to the package once.
|
|
28
|
-
*
|
|
29
|
-
* Everything in `@vitnode/core/tanstack/i18n` reads what this registers, so a
|
|
30
|
-
* route file imports `RouteMessages` and `intlQueryOptions` straight from the
|
|
31
|
-
* package. What must not happen is a route running before this module has been
|
|
32
|
-
* evaluated - so the two framework entry points, `src/router.tsx` and
|
|
33
|
-
* `src/start.ts`, both import from here, and `src/tests/intl-runtime.test.ts`
|
|
34
|
-
* fails if either stops doing so.
|
|
35
|
-
*
|
|
36
|
-
* The registration is at module scope but reads `getIntlMessages` above only by
|
|
37
|
-
* reference, so the order within this file does not matter: the validator and
|
|
38
|
-
* the fetcher are both called per request, long after it has finished
|
|
39
|
-
* evaluating.
|
|
40
|
-
*/
|
|
41
|
-
export const {
|
|
42
|
-
defaultLocale,
|
|
43
|
-
isLocale: isSupportedLocale,
|
|
44
|
-
localeRouting,
|
|
45
|
-
} = configureIntl({
|
|
46
|
-
fetchMessages: async input => await getIntlMessages({ data: input }),
|
|
47
|
-
/**
|
|
48
|
-
* This app's own `use-intl`, handed over so `RouteMessages` can provide it.
|
|
49
|
-
*
|
|
50
|
-
* `@vitnode/core` is external to this app's SSR pass (`vite.config.ts`), so
|
|
51
|
-
* under `vite dev` Node loads the package and Vite loads this app, and the two
|
|
52
|
-
* resolve `use-intl` to two different files - two `createContext` calls, two
|
|
53
|
-
* React contexts. Everything the package renders reads its own; anything this
|
|
54
|
-
* app renders with its own `useTranslations` - `routes/_main/index.tsx` does -
|
|
55
|
-
* reads this one, and nothing inside the package can import it.
|
|
56
|
-
*
|
|
57
|
-
* So it is registered rather than imported, and `RouteMessages` mounts it
|
|
58
|
-
* outermost. A production build resolves `use-intl` once and the providers
|
|
59
|
-
* collapse into one, which is why leaving this out is a dev-only failure: the
|
|
60
|
-
* route's server render throws "No intl context found", React quietly falls
|
|
61
|
-
* back to client rendering, and the page still appears.
|
|
62
|
-
*/
|
|
63
|
-
hostIntlProvider: IntlProvider,
|
|
64
|
-
i18n,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The router's half of locale routing, bound to this app's languages.
|
|
69
|
-
*
|
|
70
|
-
* Re-exported from here rather than imported straight from the package by
|
|
71
|
-
* `src/router.tsx`: it is the router entry's only i18n import, and routing it
|
|
72
|
-
* through this module is what makes `configureIntl` above run before the router
|
|
73
|
-
* - and therefore before any route, loader or component - exists.
|
|
74
|
-
*/
|
|
75
|
-
export { createLocaleRewrite } from "@vitnode/core/tanstack/i18n";
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { i18n } from "#/i18n";
|
|
2
|
-
|
|
3
|
-
import { localeRouting } from "#/lib/i18n/runtime";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A language this app serves, as a type. `"en" | "pl"`, derived from the config
|
|
7
|
-
* rather than written twice.
|
|
8
|
-
*
|
|
9
|
-
* The one i18n thing this app still owns, and it has to: `@vitnode/core` is
|
|
10
|
-
* installed by apps with different language lists, so it types a locale as
|
|
11
|
-
* `string` and takes this union as a type argument where the value originates
|
|
12
|
-
* (`useLocale<Locale>()`, `resolveLocale<Locale>()`).
|
|
13
|
-
*/
|
|
14
|
-
export type Locale = (typeof i18n.locales)[number]["code"];
|
|
15
|
-
|
|
16
|
-
export { defaultLocale, localeRouting } from "#/lib/i18n/runtime";
|
|
17
|
-
|
|
18
|
-
/** Narrows a string - a URL segment, a cookie, a `<select>` value - to a locale. */
|
|
19
|
-
export const isLocale = (value: null | string | undefined): value is Locale =>
|
|
20
|
-
localeRouting.isSupportedLocale(value);
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { createAuthNavigation } from "@vitnode/core/tanstack/auth";
|
|
2
|
-
|
|
3
|
-
import { localeRouting } from "#/lib/i18n/shared";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Going somewhere in this application from code, bound to this app's languages.
|
|
7
|
-
*
|
|
8
|
-
* One line of application, and everything else is
|
|
9
|
-
* `@vitnode/core/tanstack/auth`: the two questions a user-supplied target has to
|
|
10
|
-
* answer (may we send a browser there, and what does the router want to be
|
|
11
|
-
* handed), the reason a redirect carries `to` rather than `href`, and the fact
|
|
12
|
-
* that the same decision is made on a server and in a browser.
|
|
13
|
-
*
|
|
14
|
-
* What is left here is the only thing a package cannot answer - which languages
|
|
15
|
-
* this installation serves, which is what decides whether `/pl/discover` is a
|
|
16
|
-
* Polish page or a route called `pl`.
|
|
17
|
-
*
|
|
18
|
-
* `@vitnode/core/tanstack/routes` builds its own from the same factory, handed
|
|
19
|
-
* the same `localeRouting`, so core's auth screens and this app's AdminCP command
|
|
20
|
-
* palette navigate by one rule rather than two.
|
|
21
|
-
*/
|
|
22
|
-
export const { internalDestination, useAppNavigate } = createAuthNavigation({
|
|
23
|
-
localeRouting,
|
|
24
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { createRouteHead } from "@vitnode/core/tanstack/metadata";
|
|
2
|
-
|
|
3
|
-
import { vitNodeShellConfig } from "#/vitnode.shell.config";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A route's `head`, bound to this app's name.
|
|
7
|
-
*
|
|
8
|
-
* Two lines of application, and everything else is
|
|
9
|
-
* `@vitnode/core/tanstack/metadata`: the `"<page> - <site>"` title rule Next.js
|
|
10
|
-
* applies through `title.template`, the decision that a robots directive is
|
|
11
|
-
* stated rather than assumed, and the handling of a `loaderData` that is
|
|
12
|
-
* `undefined` on a route's first pass.
|
|
13
|
-
*
|
|
14
|
-
* What is left here is the only thing a package cannot answer - this site's own
|
|
15
|
-
* name, which is what every tab title ends with.
|
|
16
|
-
*
|
|
17
|
-
* head: ({ loaderData }) => pageHead({ robots: 'index, follow', ...loaderData })
|
|
18
|
-
*/
|
|
19
|
-
export const pageHead = createRouteHead(vitNodeShellConfig.metadata);
|
|
@@ -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">;
|