vestrycms 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +348 -2
- package/dist/config/fields.d.ts +30 -0
- package/dist/config/fields.js +75 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +18 -0
- package/dist/config/types.d.ts +403 -0
- package/dist/config/types.js +10 -0
- package/dist/core/api.d.ts +67 -0
- package/dist/core/api.js +538 -0
- package/dist/core/auth.d.ts +1561 -0
- package/dist/core/auth.js +234 -0
- package/dist/core/cache.d.ts +27 -0
- package/dist/core/cache.js +52 -0
- package/dist/core/entries.d.ts +19 -0
- package/dist/core/entries.js +127 -0
- package/dist/core/index-types.d.ts +20 -0
- package/dist/core/index-types.js +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/mail.d.ts +28 -0
- package/dist/core/mail.js +62 -0
- package/dist/core/paths.d.ts +35 -0
- package/dist/core/paths.js +148 -0
- package/dist/core/roles.d.ts +24 -0
- package/dist/core/roles.js +15 -0
- package/dist/core/service.d.ts +102 -0
- package/dist/core/service.js +478 -0
- package/dist/core/source.d.ts +82 -0
- package/dist/core/source.js +75 -0
- package/dist/core/sources/github-app.d.ts +26 -0
- package/dist/core/sources/github-app.js +161 -0
- package/dist/core/sources/github.d.ts +40 -0
- package/dist/core/sources/github.js +278 -0
- package/dist/core/sources/local.d.ts +16 -0
- package/dist/core/sources/local.js +98 -0
- package/dist/core/values.d.ts +22 -0
- package/dist/core/values.js +123 -0
- package/dist/env.d.ts +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +167 -0
- package/dist/integration/build-index.d.ts +8 -0
- package/dist/integration/build-index.js +67 -0
- package/dist/integration/client-deps.d.ts +10 -0
- package/dist/integration/client-deps.js +43 -0
- package/dist/integration/dev-middleware.d.ts +6 -0
- package/dist/integration/dev-middleware.js +48 -0
- package/dist/integration/formatter.d.ts +7 -0
- package/dist/integration/formatter.js +56 -0
- package/dist/mdx/expressions.d.ts +14 -0
- package/dist/mdx/expressions.js +141 -0
- package/dist/mdx/index.d.ts +4 -0
- package/dist/mdx/index.js +4 -0
- package/dist/mdx/parse.d.ts +6 -0
- package/dist/mdx/parse.js +378 -0
- package/dist/mdx/serialize.d.ts +9 -0
- package/dist/mdx/serialize.js +396 -0
- package/dist/mdx/types.d.ts +40 -0
- package/dist/mdx/types.js +6 -0
- package/dist/pages/admin.astro +27 -0
- package/dist/pages/api.d.ts +3 -0
- package/dist/pages/api.js +31 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +70 -0
- package/dist/ui/api.d.ts +83 -0
- package/dist/ui/api.js +97 -0
- package/dist/ui/auth-client.d.ts +27 -0
- package/dist/ui/auth-client.js +29 -0
- package/dist/ui/components/AssetPicker.d.ts +27 -0
- package/dist/ui/components/AssetPicker.js +97 -0
- package/dist/ui/components/Icon.d.ts +11 -0
- package/dist/ui/components/Icon.js +46 -0
- package/dist/ui/components/Layout.d.ts +15 -0
- package/dist/ui/components/Layout.js +87 -0
- package/dist/ui/components/editor/EditorContext.d.ts +15 -0
- package/dist/ui/components/editor/EditorContext.js +9 -0
- package/dist/ui/components/editor/NodeViews.d.ts +4 -0
- package/dist/ui/components/editor/NodeViews.js +127 -0
- package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
- package/dist/ui/components/editor/RichTextEditor.js +253 -0
- package/dist/ui/components/editor/extensions.d.ts +20 -0
- package/dist/ui/components/editor/extensions.js +156 -0
- package/dist/ui/components/fields/CodeInput.d.ts +10 -0
- package/dist/ui/components/fields/CodeInput.js +8 -0
- package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
- package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
- package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
- package/dist/ui/components/fields/FieldRenderer.js +299 -0
- package/dist/ui/components/fields/ImageTools.d.ts +31 -0
- package/dist/ui/components/fields/ImageTools.js +296 -0
- package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
- package/dist/ui/components/fields/MarkdownField.js +16 -0
- package/dist/ui/components/fields/TreeInput.d.ts +13 -0
- package/dist/ui/components/fields/TreeInput.js +221 -0
- package/dist/ui/components/ui.d.ts +63 -0
- package/dist/ui/components/ui.js +113 -0
- package/dist/ui/context.d.ts +17 -0
- package/dist/ui/context.js +13 -0
- package/dist/ui/lib/format.d.ts +5 -0
- package/dist/ui/lib/format.js +11 -0
- package/dist/ui/lib/utils.d.ts +24 -0
- package/dist/ui/lib/utils.js +255 -0
- package/dist/ui/main.d.ts +1 -0
- package/dist/ui/main.js +11 -0
- package/dist/ui/pages/Assets.d.ts +1 -0
- package/dist/ui/pages/Assets.js +69 -0
- package/dist/ui/pages/CollectionList.d.ts +6 -0
- package/dist/ui/pages/CollectionList.js +115 -0
- package/dist/ui/pages/Dashboard.d.ts +1 -0
- package/dist/ui/pages/Dashboard.js +57 -0
- package/dist/ui/pages/EntryEditor.d.ts +3 -0
- package/dist/ui/pages/EntryEditor.js +313 -0
- package/dist/ui/pages/Login.d.ts +6 -0
- package/dist/ui/pages/Login.js +88 -0
- package/dist/ui/pages/Users.d.ts +1 -0
- package/dist/ui/pages/Users.js +102 -0
- package/dist/ui/styles.css +2536 -0
- package/package.json +121 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Spencer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,349 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Vestry CMS
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A git-backed CMS for [Astro](https://astro.build), built as an Astro
|
|
4
|
+
integration. Content, assets and configuration live in your repository;
|
|
5
|
+
content needs no database and there is no external asset storage. The control panel is a
|
|
6
|
+
React app served from your site at `/admin`, and every save is a commit.
|
|
7
|
+
|
|
8
|
+
- **Astro-native content.** Collections map onto the files Astro already
|
|
9
|
+
reads: Markdown/MDX with frontmatter, JSON and YAML, one file per entry or
|
|
10
|
+
one file holding many. Images are referenced the way Astro's `image()`
|
|
11
|
+
schema helper and `public/` expect.
|
|
12
|
+
- **MDX with components.** A Tiptap rich text editor reads and writes MDX.
|
|
13
|
+
Astro components you register show up in an "Insert" menu and are edited
|
|
14
|
+
as blocks with a form for their props (Statamic's Bard sets, for MDX). A
|
|
15
|
+
Source tab exposes the raw MDX.
|
|
16
|
+
- **GitHub backend, swappable.** Reads and writes go through a small
|
|
17
|
+
`ContentSource` interface. The GitHub implementation uses the Git Data API
|
|
18
|
+
with ETag revalidation, per-SHA caching and a build-time index, so the
|
|
19
|
+
panel stays snappy; the only database is the small one sign-in needs. `astro dev` edits your working
|
|
20
|
+
copy directly.
|
|
21
|
+
- **Prettier-clean commits.** When the project has Prettier installed, files
|
|
22
|
+
are formatted with the project's own config before they are committed.
|
|
23
|
+
|
|
24
|
+
## Setup
|
|
25
|
+
|
|
26
|
+
Vestry needs Astro 6 or 7 and Node 22.12 or later. On Cloudflare, use Astro
|
|
27
|
+
7.3.4 or later: earlier 7.x releases streamed on-demand pages in workerd as
|
|
28
|
+
`[object Object]`, which breaks the `/admin` shell.
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pnpm add vestrycms
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// vestry.config.ts
|
|
36
|
+
import { collection, component, defineConfig, fields } from "vestrycms/config";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
name: "My Site",
|
|
40
|
+
siteUrl: "https://example.com",
|
|
41
|
+
repo: { owner: "me", name: "my-site", branch: "main" },
|
|
42
|
+
collections: {
|
|
43
|
+
posts: collection({
|
|
44
|
+
label: "Posts",
|
|
45
|
+
path: "src/content/posts/*.mdx",
|
|
46
|
+
titleField: "title",
|
|
47
|
+
sort: { field: "date", direction: "desc" },
|
|
48
|
+
status: { field: "draft", publishedWhen: false },
|
|
49
|
+
previewUrl: "/posts/{slug}",
|
|
50
|
+
schema: {
|
|
51
|
+
title: fields.text({ required: true }),
|
|
52
|
+
date: fields.date({ sidebar: true, default: "now" }),
|
|
53
|
+
cover: fields.image({ directory: "src/assets/covers" }),
|
|
54
|
+
draft: fields.boolean({ default: false }),
|
|
55
|
+
},
|
|
56
|
+
body: { components: ["Callout"] },
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
components: {
|
|
60
|
+
Callout: component({
|
|
61
|
+
import: "@/components/Callout.astro",
|
|
62
|
+
children: true,
|
|
63
|
+
props: {
|
|
64
|
+
tone: fields.select({ options: [{ label: "Info", value: "info" }] }),
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
},
|
|
68
|
+
assets: {
|
|
69
|
+
covers: { label: "Covers", directory: "src/assets/covers" },
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
// astro.config.mjs
|
|
76
|
+
import vestry from "vestrycms";
|
|
77
|
+
import vestryConfig from "./vestry.config";
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
integrations: [vestry(vestryConfig, { base: "/admin" })],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The config is plain data (it is serialised into the server bundle and sent to
|
|
85
|
+
the browser), so it cannot contain functions.
|
|
86
|
+
|
|
87
|
+
`name` titles the control panel; `logo` (a repository path such as
|
|
88
|
+
`src/assets/logo-reverse.svg`, or an absolute URL) replaces it in the dark
|
|
89
|
+
top bar, so a light or reverse variant works best.
|
|
90
|
+
|
|
91
|
+
### Sign-in and users
|
|
92
|
+
|
|
93
|
+
Sign-in runs on [Better Auth](https://www.better-auth.com) with its email OTP
|
|
94
|
+
and admin plugins. Editors sign in with a six-digit code emailed to them;
|
|
95
|
+
there are no passwords. `auth.admins` lists the addresses that are always
|
|
96
|
+
admins (they can sign in before anyone has invited them, which bootstraps the
|
|
97
|
+
user list). Admins invite everyone else from the **Users** page and choose a
|
|
98
|
+
role:
|
|
99
|
+
|
|
100
|
+
| Role | Can |
|
|
101
|
+
| -------- | ------------------------------------------------------------ |
|
|
102
|
+
| `editor` | Create, edit, publish and delete content; upload assets |
|
|
103
|
+
| `admin` | Everything an editor can, plus invite, edit and remove users |
|
|
104
|
+
|
|
105
|
+
Users, sessions (30 days) and pending codes live in a SQL database that
|
|
106
|
+
Better Auth accepts directly: a Cloudflare D1 binding named `VESTRY_DB` in
|
|
107
|
+
production, a `node:sqlite` file in dev, its memory adapter in tests. The
|
|
108
|
+
tables are created on first use with Better Auth's migrator, so there is no
|
|
109
|
+
migration step. Codes expire after 10 minutes and are burned after five
|
|
110
|
+
wrong guesses; Better Auth rate-limits the sign-in routes per IP (counters in
|
|
111
|
+
the database, so they hold across Worker isolates). Emails go through
|
|
112
|
+
`config.mail` (`resend`, with the key in `VESTRY_RESEND_API_KEY`, or `console`
|
|
113
|
+
for development). Vestry adds invitations, the config-listed admins, a
|
|
114
|
+
`lastLoginAt` field and the last-admin guards on top of the plugins.
|
|
115
|
+
|
|
116
|
+
Commits are made through a GitHub App installed on the repository with
|
|
117
|
+
**Contents: read and write**. Commits are made by the app's bot, with the
|
|
118
|
+
editor credited as a co-author (`Co-authored-by: Name <email>`), so GitHub
|
|
119
|
+
shows both and the editor's account never needs push access.
|
|
120
|
+
|
|
121
|
+
| Variable | Purpose |
|
|
122
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
123
|
+
| `VESTRY_GITHUB_APP_ID` | GitHub App id |
|
|
124
|
+
| `VESTRY_GITHUB_INSTALLATION_ID` | The app's installation id on the repository |
|
|
125
|
+
| `VESTRY_GITHUB_PRIVATE_KEY` | The app's PEM private key (PKCS#1 as issued, PKCS#8, `\n`-escaped or base64 also accepted) |
|
|
126
|
+
| `VESTRY_RESEND_API_KEY` | Resend API key (when `mail.provider` is `resend`) |
|
|
127
|
+
| `VESTRY_SECRET` | Signs session cookies (`openssl rand -hex 32`); dev modes fall back to a fixed value |
|
|
128
|
+
| `VESTRY_MODE` | `auth` or `github` to turn sign-in on in `astro dev` |
|
|
129
|
+
|
|
130
|
+
```jsonc
|
|
131
|
+
// wrangler.jsonc
|
|
132
|
+
"d1_databases": [{ "binding": "VESTRY_DB", "database_name": "my-site-vestry" }]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
In `astro dev` no sign-in is needed by default: the panel edits the project
|
|
136
|
+
directory and you commit with git. `VESTRY_MODE=auth` keeps the local source
|
|
137
|
+
but requires sign-in, with users in git-ignored `.vestry/auth.sqlite`, for
|
|
138
|
+
working on the flow itself. Emails print to the terminal unless
|
|
139
|
+
`mail.provider` is `resend` and `VESTRY_RESEND_API_KEY` is set, in which case
|
|
140
|
+
they are really sent. `VESTRY_MODE=github` additionally commits through the
|
|
141
|
+
GitHub App.
|
|
142
|
+
|
|
143
|
+
## How it works
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
src
|
|
147
|
+
├── index.ts Astro integration: routes, Vite plugin, dev middleware
|
|
148
|
+
├── config/ defineConfig / collection / component / fields
|
|
149
|
+
├── core/
|
|
150
|
+
│ ├── source.ts ContentSource interface + git blob hashing
|
|
151
|
+
│ ├── sources/ LocalSource (fs), GitHubSource (Git Data API), GitHub App tokens
|
|
152
|
+
│ ├── cache.ts Cache interface, MemoryCache, kvCache adapter
|
|
153
|
+
│ ├── service.ts Collections → entries, singletons, assets
|
|
154
|
+
│ ├── api.ts HTTP API (runtime-agnostic Request → Response)
|
|
155
|
+
│ ├── auth.ts Better Auth setup (email OTP + admin), invitations, bootstrap admins
|
|
156
|
+
│ ├── roles.ts Role list and the PublicUser shape shared with the UI
|
|
157
|
+
│ ├── mail.ts Mailer interface, Resend + console adapters
|
|
158
|
+
│ └── values.ts Asset path normalisation, auto fields
|
|
159
|
+
├── mdx/ MDX ⇄ ProseMirror document conversion
|
|
160
|
+
├── pages/ Injected routes: admin.astro shell, api.ts endpoint
|
|
161
|
+
├── integration/ Build-time index, formatter module, dev bridge, client deps
|
|
162
|
+
└── ui/ React control panel (Tiptap, TanStack Query, react-router)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Routes injected into the site:
|
|
166
|
+
|
|
167
|
+
- `/admin/[...path]` – the control panel shell (on-demand, no auth needed to
|
|
168
|
+
load the shell; data comes from the API).
|
|
169
|
+
- `/admin/api/[...path]` – JSON API. `config`, `status`, `collections/:key`,
|
|
170
|
+
`collections/:key/entries/:slug`, `singletons/:key`, `assets/:container`,
|
|
171
|
+
`file?path=`, `users` (admins), and `auth/*` (Better Auth's routes).
|
|
172
|
+
|
|
173
|
+
Virtual modules provided by the Vite plugin: `virtual:vestry/config`,
|
|
174
|
+
`virtual:vestry/runtime`, `virtual:vestry/index` (build-time frontmatter
|
|
175
|
+
index keyed by git blob SHA) and `virtual:vestry/formatter` (Prettier, when
|
|
176
|
+
installed).
|
|
177
|
+
|
|
178
|
+
### Dev vs production
|
|
179
|
+
|
|
180
|
+
With the Cloudflare adapter, on-demand routes run inside workerd even in
|
|
181
|
+
`astro dev`, where Node's `fs` is unavailable. The integration therefore
|
|
182
|
+
serves the API from a Vite dev-server middleware (Node) in development, and
|
|
183
|
+
from the injected Astro endpoint in production. Both call the same
|
|
184
|
+
`createApi()`.
|
|
185
|
+
|
|
186
|
+
### Editing model
|
|
187
|
+
|
|
188
|
+
- Entry values are normalised for the editor: image/file fields always hold
|
|
189
|
+
repository paths; on save they are written back in the configured form
|
|
190
|
+
(`relative` to the entry file, bare `filename`, `public` URL or `repo`
|
|
191
|
+
path).
|
|
192
|
+
- The body editor parses MDX into a ProseMirror document. JSX components
|
|
193
|
+
become `mdxComponent` nodes with their attributes converted to JSON where
|
|
194
|
+
they are literals (anything else is kept verbatim as an expression).
|
|
195
|
+
Imports for known components are regenerated on save; other ESM is kept as
|
|
196
|
+
a raw block.
|
|
197
|
+
- Saving composes the full file (YAML frontmatter + body) in the browser,
|
|
198
|
+
formats it with Prettier when available, and sends it with the blob SHA
|
|
199
|
+
the editor loaded. A changed SHA is reported as a conflict.
|
|
200
|
+
- Uploads chosen from an image/file field are committed together with the
|
|
201
|
+
entry in one commit.
|
|
202
|
+
|
|
203
|
+
## Editor sections
|
|
204
|
+
|
|
205
|
+
By default the editor shows a "Details" card with every field, the content
|
|
206
|
+
editor, and a rail for fields marked `sidebar: true`. `sections` groups
|
|
207
|
+
fields into titled cards instead:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
sections: [
|
|
211
|
+
{ label: "Hero", fields: ["heroImage", "heroFocalPoint", "heroTitle"] },
|
|
212
|
+
"body", // the content editor goes here
|
|
213
|
+
{
|
|
214
|
+
label: "Events",
|
|
215
|
+
description: "Shown as structured data.",
|
|
216
|
+
fields: ["events"],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
label: "Search engines",
|
|
220
|
+
sidebar: true,
|
|
221
|
+
fields: ["seoTitle", "seoDescription"],
|
|
222
|
+
},
|
|
223
|
+
];
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Sections render in order; `sidebar: true` puts a card in the rail. Fields
|
|
227
|
+
not named by any section still fall into "Details" or the rail, and the
|
|
228
|
+
content editor renders after the sections unless `"body"` places it.
|
|
229
|
+
|
|
230
|
+
## Trees (menus)
|
|
231
|
+
|
|
232
|
+
`fields.tree` edits nested data such as a navigation menu. Each node has the
|
|
233
|
+
given fields plus a `children` array (configurable via `childrenKey`):
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
navigation: singleton({
|
|
237
|
+
label: "Navigation",
|
|
238
|
+
path: "src/data/navigation.json",
|
|
239
|
+
body: false,
|
|
240
|
+
schema: {
|
|
241
|
+
main: fields.tree({
|
|
242
|
+
maxDepth: 2,
|
|
243
|
+
itemDetail: "url",
|
|
244
|
+
fields: {
|
|
245
|
+
title: fields.text({ required: true }),
|
|
246
|
+
url: fields.text({ required: true }),
|
|
247
|
+
external: fields.boolean(),
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The editor shows an indented tree with collapse toggles, inline editing of a
|
|
255
|
+
node's fields, "add child" and remove actions, and drag-and-drop that both
|
|
256
|
+
reorders and re-parents: drag a node up or down to move it and sideways to
|
|
257
|
+
change its depth, within `maxDepth`. A dashed slot shows where it will land
|
|
258
|
+
and descendants travel with their parent.
|
|
259
|
+
|
|
260
|
+
## Dashboard
|
|
261
|
+
|
|
262
|
+
Each collection gets a tile with its entry count. Collections whose `sort`
|
|
263
|
+
field is a date or datetime also get a "Recent …" list; set `recent: false`
|
|
264
|
+
to hide it, `recent: true` to show it for other collections, or a number to
|
|
265
|
+
change how many entries appear.
|
|
266
|
+
|
|
267
|
+
## Image crop and focal point
|
|
268
|
+
|
|
269
|
+
Two fields edit _how_ a sibling image field is displayed, so the site keeps
|
|
270
|
+
one original file in git and decides the framing in templates:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
cover: fields.image({ directory: "src/assets/covers" }),
|
|
274
|
+
coverCrop: fields.crop({ image: "cover", aspect: 8 / 9 }),
|
|
275
|
+
hero: fields.image({ directory: "src/assets/images", storeAs: "filename" }),
|
|
276
|
+
heroFocalPoint: fields.focalPoint({ image: "hero", previews: ["16:9", "1:1"] }),
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
- `crop` stores `{ x, y, width, height }` in the image's natural pixel
|
|
280
|
+
coordinates. The editor shows a draggable, resizable rectangle (with the
|
|
281
|
+
aspect ratio locked when `aspect` is set), keyboard nudging, and a "Fit"
|
|
282
|
+
button for the largest centred crop. Use it with sharp's `extract()` or a
|
|
283
|
+
CSS transform.
|
|
284
|
+
- `focalPoint` stores `{ x, y }` as percentages (0–100) for CSS
|
|
285
|
+
`object-position`. The editor shows a crosshair you click or drag, plus
|
|
286
|
+
small previews at a few aspect ratios so you can see what survives a crop.
|
|
287
|
+
|
|
288
|
+
Both read the sibling field named by `image` at the same level of the
|
|
289
|
+
schema (top level, or inside the same object/array item), and preview
|
|
290
|
+
freshly uploaded images before they are committed.
|
|
291
|
+
|
|
292
|
+
## Field types
|
|
293
|
+
|
|
294
|
+
`text`, `textarea`, `slug`, `number`, `boolean`, `date` (optional time),
|
|
295
|
+
`datetime` (optional `autoUpdate`), `select`, `multiselect`, `color`,
|
|
296
|
+
`image`, `file`, `reference`, `object`, `array`, `markdown`, `code`, `json`,
|
|
297
|
+
`crop`, `focalPoint`, `tree`.
|
|
298
|
+
Fields accept `label`, `description`, `required`, `sidebar`, `hidden`,
|
|
299
|
+
`readOnly` and `width` (`full` | `half` | `third`).
|
|
300
|
+
|
|
301
|
+
## Development
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
pnpm install
|
|
305
|
+
pnpm build # src/ → dist/ (tsc, plus admin.astro, styles.css, env.d.ts)
|
|
306
|
+
pnpm typecheck
|
|
307
|
+
pnpm test
|
|
308
|
+
pnpm format:check
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
The package is published as compiled JavaScript in `dist/`; `src/` is not
|
|
312
|
+
shipped. `scripts/build.mjs` runs `tsc -p tsconfig.build.json`, copies the
|
|
313
|
+
files tsc doesn't emit (pointing `admin.astro`'s client script at the
|
|
314
|
+
compiled UI), and fails if `src/integration/client-deps.ts` doesn't list
|
|
315
|
+
exactly the packages the control panel imports in the browser. The
|
|
316
|
+
integration pre-bundles those through Vite's optimizer: an installed Astro
|
|
317
|
+
integration is excluded from it, and dev would otherwise serve CommonJS
|
|
318
|
+
dependencies such as `react/jsx-runtime` raw.
|
|
319
|
+
|
|
320
|
+
To work on Vestry against a site, run `pnpm dev` here (an initial build, then
|
|
321
|
+
`tsc --watch`) and link the package from the site:
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
pnpm link ../vestrycms
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Tests
|
|
328
|
+
|
|
329
|
+
```sh
|
|
330
|
+
pnpm test # github-mock, auth, github-app
|
|
331
|
+
node --import tsx/esm test/auth.ts # sign-in, users and guards over HTTP (memory adapter)
|
|
332
|
+
node --import tsx/esm test/github-app.ts # GitHub App JWT + installation token cache
|
|
333
|
+
node --import tsx/esm test/roundtrip.ts path/to/file.mdx # MDX round-trip
|
|
334
|
+
node --import tsx/esm test/github-mock.ts # commit sequencing against a fake API
|
|
335
|
+
GH_TOKEN=… node --import tsx/esm test/github-read.ts # live read path
|
|
336
|
+
GH_TOKEN=… node --import tsx/esm test/github-write.ts # live commits on a throwaway branch
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Releasing
|
|
340
|
+
|
|
341
|
+
1. Bump `version` in `package.json` and commit.
|
|
342
|
+
2. Publish a GitHub release tagged `v<version>`. The **Publish** workflow
|
|
343
|
+
checks the tag against `package.json`, runs the checks and publishes to
|
|
344
|
+
npm with provenance through npm trusted publishing (configure it on
|
|
345
|
+
npmjs.com for this repository and `publish.yml`).
|
|
346
|
+
|
|
347
|
+
The first release has to be published by hand, since npm only offers trusted
|
|
348
|
+
publishing for a package that exists: `pnpm publish` (which builds first via
|
|
349
|
+
`prepack`).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ArrayField, BooleanField, CodeField, ColorField, CropField, DateField, DatetimeField, Field, FileField, FocalPointField, ImageField, JsonField, MarkdownField, MultiselectField, NumberField, ObjectField, ReferenceField, SelectField, SlugField, TextField, TextareaField, TreeField } from "./types.js";
|
|
2
|
+
type Opts<T extends Field> = Omit<T, "type">;
|
|
3
|
+
/**
|
|
4
|
+
* Field builders. Each returns a plain object; the `type` discriminant is all
|
|
5
|
+
* the control panel and the server need.
|
|
6
|
+
*/
|
|
7
|
+
export declare const fields: {
|
|
8
|
+
text: (opts?: Opts<TextField>) => TextField;
|
|
9
|
+
textarea: (opts?: Opts<TextareaField>) => TextareaField;
|
|
10
|
+
slug: (opts?: Opts<SlugField>) => SlugField;
|
|
11
|
+
number: (opts?: Opts<NumberField>) => NumberField;
|
|
12
|
+
boolean: (opts?: Opts<BooleanField>) => BooleanField;
|
|
13
|
+
date: (opts?: Opts<DateField>) => DateField;
|
|
14
|
+
datetime: (opts?: Opts<DatetimeField>) => DatetimeField;
|
|
15
|
+
select: (opts: Opts<SelectField>) => SelectField;
|
|
16
|
+
multiselect: (opts: Opts<MultiselectField>) => MultiselectField;
|
|
17
|
+
color: (opts?: Opts<ColorField>) => ColorField;
|
|
18
|
+
image: (opts: Opts<ImageField>) => ImageField;
|
|
19
|
+
file: (opts: Opts<FileField>) => FileField;
|
|
20
|
+
reference: (opts: Opts<ReferenceField>) => ReferenceField;
|
|
21
|
+
object: (fieldsOrOpts: ObjectField["fields"] | Opts<ObjectField>, extra?: Omit<Opts<ObjectField>, "fields">) => ObjectField;
|
|
22
|
+
array: (of: Field, opts?: Omit<Opts<ArrayField>, "of">) => ArrayField;
|
|
23
|
+
markdown: (opts?: Opts<MarkdownField>) => MarkdownField;
|
|
24
|
+
code: (opts?: Opts<CodeField>) => CodeField;
|
|
25
|
+
json: (opts?: Opts<JsonField>) => JsonField;
|
|
26
|
+
crop: (opts: Opts<CropField>) => CropField;
|
|
27
|
+
focalPoint: (opts: Opts<FocalPointField>) => FocalPointField;
|
|
28
|
+
tree: (opts: Opts<TreeField>) => TreeField;
|
|
29
|
+
};
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field builders. Each returns a plain object; the `type` discriminant is all
|
|
3
|
+
* the control panel and the server need.
|
|
4
|
+
*/
|
|
5
|
+
export const fields = {
|
|
6
|
+
text: (opts = {}) => ({ type: "text", ...opts }),
|
|
7
|
+
textarea: (opts = {}) => ({
|
|
8
|
+
type: "textarea",
|
|
9
|
+
...opts,
|
|
10
|
+
}),
|
|
11
|
+
slug: (opts = {}) => ({ type: "slug", ...opts }),
|
|
12
|
+
number: (opts = {}) => ({
|
|
13
|
+
type: "number",
|
|
14
|
+
...opts,
|
|
15
|
+
}),
|
|
16
|
+
boolean: (opts = {}) => ({
|
|
17
|
+
type: "boolean",
|
|
18
|
+
...opts,
|
|
19
|
+
}),
|
|
20
|
+
date: (opts = {}) => ({ type: "date", ...opts }),
|
|
21
|
+
datetime: (opts = {}) => ({
|
|
22
|
+
type: "datetime",
|
|
23
|
+
...opts,
|
|
24
|
+
}),
|
|
25
|
+
select: (opts) => ({
|
|
26
|
+
type: "select",
|
|
27
|
+
...opts,
|
|
28
|
+
}),
|
|
29
|
+
multiselect: (opts) => ({
|
|
30
|
+
type: "multiselect",
|
|
31
|
+
...opts,
|
|
32
|
+
}),
|
|
33
|
+
color: (opts = {}) => ({
|
|
34
|
+
type: "color",
|
|
35
|
+
...opts,
|
|
36
|
+
}),
|
|
37
|
+
image: (opts) => ({ type: "image", ...opts }),
|
|
38
|
+
file: (opts) => ({ type: "file", ...opts }),
|
|
39
|
+
reference: (opts) => ({
|
|
40
|
+
type: "reference",
|
|
41
|
+
...opts,
|
|
42
|
+
}),
|
|
43
|
+
object: (fieldsOrOpts, extra = {}) => {
|
|
44
|
+
if ("fields" in fieldsOrOpts && isSchema(fieldsOrOpts.fields)) {
|
|
45
|
+
return { type: "object", ...fieldsOrOpts };
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
type: "object",
|
|
49
|
+
fields: fieldsOrOpts,
|
|
50
|
+
...extra,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
array: (of, opts = {}) => ({
|
|
54
|
+
type: "array",
|
|
55
|
+
of,
|
|
56
|
+
...opts,
|
|
57
|
+
}),
|
|
58
|
+
markdown: (opts = {}) => ({
|
|
59
|
+
type: "markdown",
|
|
60
|
+
...opts,
|
|
61
|
+
}),
|
|
62
|
+
code: (opts = {}) => ({ type: "code", ...opts }),
|
|
63
|
+
json: (opts = {}) => ({ type: "json", ...opts }),
|
|
64
|
+
crop: (opts) => ({ type: "crop", ...opts }),
|
|
65
|
+
focalPoint: (opts) => ({
|
|
66
|
+
type: "focalPoint",
|
|
67
|
+
...opts,
|
|
68
|
+
}),
|
|
69
|
+
tree: (opts) => ({ type: "tree", ...opts }),
|
|
70
|
+
};
|
|
71
|
+
function isSchema(value) {
|
|
72
|
+
return (typeof value === "object" &&
|
|
73
|
+
value !== null &&
|
|
74
|
+
Object.values(value).every((f) => typeof f === "object" && f !== null && "type" in f));
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AssetContainerConfig, CollectionConfig, ComponentConfig, SingletonConfig, VestryConfig } from "./types.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
export { fields } from "./fields.js";
|
|
4
|
+
/** Identity helper that gives you editor completions for the config shape. */
|
|
5
|
+
export declare function defineConfig(config: VestryConfig): VestryConfig;
|
|
6
|
+
export declare function collection(config: CollectionConfig): CollectionConfig;
|
|
7
|
+
export declare function singleton(config: SingletonConfig): SingletonConfig;
|
|
8
|
+
export declare function component(config?: ComponentConfig): ComponentConfig;
|
|
9
|
+
export declare function assetContainer(config: AssetContainerConfig): AssetContainerConfig;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export { fields } from "./fields.js";
|
|
3
|
+
/** Identity helper that gives you editor completions for the config shape. */
|
|
4
|
+
export function defineConfig(config) {
|
|
5
|
+
return config;
|
|
6
|
+
}
|
|
7
|
+
export function collection(config) {
|
|
8
|
+
return config;
|
|
9
|
+
}
|
|
10
|
+
export function singleton(config) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
export function component(config = {}) {
|
|
14
|
+
return config;
|
|
15
|
+
}
|
|
16
|
+
export function assetContainer(config) {
|
|
17
|
+
return config;
|
|
18
|
+
}
|