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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +348 -2
  3. package/dist/config/fields.d.ts +30 -0
  4. package/dist/config/fields.js +75 -0
  5. package/dist/config/index.d.ts +9 -0
  6. package/dist/config/index.js +18 -0
  7. package/dist/config/types.d.ts +403 -0
  8. package/dist/config/types.js +10 -0
  9. package/dist/core/api.d.ts +67 -0
  10. package/dist/core/api.js +538 -0
  11. package/dist/core/auth.d.ts +1561 -0
  12. package/dist/core/auth.js +234 -0
  13. package/dist/core/cache.d.ts +27 -0
  14. package/dist/core/cache.js +52 -0
  15. package/dist/core/entries.d.ts +19 -0
  16. package/dist/core/entries.js +127 -0
  17. package/dist/core/index-types.d.ts +20 -0
  18. package/dist/core/index-types.js +1 -0
  19. package/dist/core/index.d.ts +13 -0
  20. package/dist/core/index.js +13 -0
  21. package/dist/core/mail.d.ts +28 -0
  22. package/dist/core/mail.js +62 -0
  23. package/dist/core/paths.d.ts +35 -0
  24. package/dist/core/paths.js +148 -0
  25. package/dist/core/roles.d.ts +24 -0
  26. package/dist/core/roles.js +15 -0
  27. package/dist/core/service.d.ts +102 -0
  28. package/dist/core/service.js +478 -0
  29. package/dist/core/source.d.ts +82 -0
  30. package/dist/core/source.js +75 -0
  31. package/dist/core/sources/github-app.d.ts +26 -0
  32. package/dist/core/sources/github-app.js +161 -0
  33. package/dist/core/sources/github.d.ts +40 -0
  34. package/dist/core/sources/github.js +278 -0
  35. package/dist/core/sources/local.d.ts +16 -0
  36. package/dist/core/sources/local.js +98 -0
  37. package/dist/core/values.d.ts +22 -0
  38. package/dist/core/values.js +123 -0
  39. package/dist/env.d.ts +19 -0
  40. package/dist/index.d.ts +18 -0
  41. package/dist/index.js +167 -0
  42. package/dist/integration/build-index.d.ts +8 -0
  43. package/dist/integration/build-index.js +67 -0
  44. package/dist/integration/client-deps.d.ts +10 -0
  45. package/dist/integration/client-deps.js +43 -0
  46. package/dist/integration/dev-middleware.d.ts +6 -0
  47. package/dist/integration/dev-middleware.js +48 -0
  48. package/dist/integration/formatter.d.ts +7 -0
  49. package/dist/integration/formatter.js +56 -0
  50. package/dist/mdx/expressions.d.ts +14 -0
  51. package/dist/mdx/expressions.js +141 -0
  52. package/dist/mdx/index.d.ts +4 -0
  53. package/dist/mdx/index.js +4 -0
  54. package/dist/mdx/parse.d.ts +6 -0
  55. package/dist/mdx/parse.js +378 -0
  56. package/dist/mdx/serialize.d.ts +9 -0
  57. package/dist/mdx/serialize.js +396 -0
  58. package/dist/mdx/types.d.ts +40 -0
  59. package/dist/mdx/types.js +6 -0
  60. package/dist/pages/admin.astro +27 -0
  61. package/dist/pages/api.d.ts +3 -0
  62. package/dist/pages/api.js +31 -0
  63. package/dist/ui/App.d.ts +4 -0
  64. package/dist/ui/App.js +70 -0
  65. package/dist/ui/api.d.ts +83 -0
  66. package/dist/ui/api.js +97 -0
  67. package/dist/ui/auth-client.d.ts +27 -0
  68. package/dist/ui/auth-client.js +29 -0
  69. package/dist/ui/components/AssetPicker.d.ts +27 -0
  70. package/dist/ui/components/AssetPicker.js +97 -0
  71. package/dist/ui/components/Icon.d.ts +11 -0
  72. package/dist/ui/components/Icon.js +46 -0
  73. package/dist/ui/components/Layout.d.ts +15 -0
  74. package/dist/ui/components/Layout.js +87 -0
  75. package/dist/ui/components/editor/EditorContext.d.ts +15 -0
  76. package/dist/ui/components/editor/EditorContext.js +9 -0
  77. package/dist/ui/components/editor/NodeViews.d.ts +4 -0
  78. package/dist/ui/components/editor/NodeViews.js +127 -0
  79. package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
  80. package/dist/ui/components/editor/RichTextEditor.js +253 -0
  81. package/dist/ui/components/editor/extensions.d.ts +20 -0
  82. package/dist/ui/components/editor/extensions.js +156 -0
  83. package/dist/ui/components/fields/CodeInput.d.ts +10 -0
  84. package/dist/ui/components/fields/CodeInput.js +8 -0
  85. package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
  86. package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
  87. package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
  88. package/dist/ui/components/fields/FieldRenderer.js +299 -0
  89. package/dist/ui/components/fields/ImageTools.d.ts +31 -0
  90. package/dist/ui/components/fields/ImageTools.js +296 -0
  91. package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
  92. package/dist/ui/components/fields/MarkdownField.js +16 -0
  93. package/dist/ui/components/fields/TreeInput.d.ts +13 -0
  94. package/dist/ui/components/fields/TreeInput.js +221 -0
  95. package/dist/ui/components/ui.d.ts +63 -0
  96. package/dist/ui/components/ui.js +113 -0
  97. package/dist/ui/context.d.ts +17 -0
  98. package/dist/ui/context.js +13 -0
  99. package/dist/ui/lib/format.d.ts +5 -0
  100. package/dist/ui/lib/format.js +11 -0
  101. package/dist/ui/lib/utils.d.ts +24 -0
  102. package/dist/ui/lib/utils.js +255 -0
  103. package/dist/ui/main.d.ts +1 -0
  104. package/dist/ui/main.js +11 -0
  105. package/dist/ui/pages/Assets.d.ts +1 -0
  106. package/dist/ui/pages/Assets.js +69 -0
  107. package/dist/ui/pages/CollectionList.d.ts +6 -0
  108. package/dist/ui/pages/CollectionList.js +115 -0
  109. package/dist/ui/pages/Dashboard.d.ts +1 -0
  110. package/dist/ui/pages/Dashboard.js +57 -0
  111. package/dist/ui/pages/EntryEditor.d.ts +3 -0
  112. package/dist/ui/pages/EntryEditor.js +313 -0
  113. package/dist/ui/pages/Login.d.ts +6 -0
  114. package/dist/ui/pages/Login.js +88 -0
  115. package/dist/ui/pages/Users.d.ts +1 -0
  116. package/dist/ui/pages/Users.js +102 -0
  117. package/dist/ui/styles.css +2536 -0
  118. 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
- # Temporary Holding Version
1
+ # Vestry CMS
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }