astro-dev-edit 0.11.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 +125 -0
- package/package.json +52 -0
- package/src/client/admin-bar.ts +622 -0
- package/src/client/api.ts +370 -0
- package/src/client/classify-cache.ts +61 -0
- package/src/client/css-inspect.ts +345 -0
- package/src/client/editors/asset-picker.ts +155 -0
- package/src/client/editors/body-editor.ts +419 -0
- package/src/client/editors/collections-panel.ts +1532 -0
- package/src/client/editors/copy-panel.ts +73 -0
- package/src/client/editors/drawer.ts +95 -0
- package/src/client/editors/entry.ts +433 -0
- package/src/client/editors/expression.ts +77 -0
- package/src/client/editors/fields.ts +309 -0
- package/src/client/editors/image.ts +268 -0
- package/src/client/editors/markup-insert.ts +73 -0
- package/src/client/editors/markup.ts +125 -0
- package/src/client/editors/media-grid.ts +326 -0
- package/src/client/editors/media-modal.ts +588 -0
- package/src/client/editors/notice.ts +160 -0
- package/src/client/editors/peek.ts +135 -0
- package/src/client/editors/settings-panel.ts +457 -0
- package/src/client/editors/source-popup.ts +166 -0
- package/src/client/editors/text.ts +105 -0
- package/src/client/editors/unsplash-pane.ts +317 -0
- package/src/client/element-context.ts +308 -0
- package/src/client/features.ts +81 -0
- package/src/client/focus.ts +166 -0
- package/src/client/group.ts +186 -0
- package/src/client/highlight.ts +146 -0
- package/src/client/hover.ts +485 -0
- package/src/client/icons.ts +160 -0
- package/src/client/markdown.ts +319 -0
- package/src/client/overlay.ts +466 -0
- package/src/client/page-source.ts +143 -0
- package/src/client/router.ts +198 -0
- package/src/client/shadow.ts +111 -0
- package/src/client/source-map.ts +150 -0
- package/src/client/state.ts +153 -0
- package/src/client/styles.ts +3485 -0
- package/src/client/tree-model.ts +45 -0
- package/src/client/tree.ts +366 -0
- package/src/client/ui.ts +987 -0
- package/src/client/unsplash-search.ts +250 -0
- package/src/index.ts +299 -0
- package/src/patcher/astro.ts +792 -0
- package/src/patcher/content-config.ts +1035 -0
- package/src/patcher/dotenv.ts +121 -0
- package/src/patcher/expression-trace.ts +326 -0
- package/src/patcher/frontmatter.ts +249 -0
- package/src/patcher/registry.ts +11 -0
- package/src/patcher/types.ts +32 -0
- package/src/server/annotate.ts +173 -0
- package/src/server/assets.ts +167 -0
- package/src/server/collection-entries.ts +91 -0
- package/src/server/content-config.ts +210 -0
- package/src/server/editor.ts +15 -0
- package/src/server/entry-detect.ts +110 -0
- package/src/server/entry-resolve-routes.ts +218 -0
- package/src/server/entry-routes.ts +304 -0
- package/src/server/inspect-locate.ts +81 -0
- package/src/server/inspect-routes.ts +94 -0
- package/src/server/middleware.ts +480 -0
- package/src/server/options.ts +778 -0
- package/src/server/page-source-routes.ts +71 -0
- package/src/server/paths.ts +219 -0
- package/src/server/private-files.ts +116 -0
- package/src/server/route-manifest.ts +200 -0
- package/src/server/router.ts +94 -0
- package/src/server/schema-introspect.ts +233 -0
- package/src/server/schema-routes.ts +808 -0
- package/src/server/settings-routes.ts +246 -0
- package/src/server/settings.ts +382 -0
- package/src/server/text-writes.ts +105 -0
- package/src/server/unsplash-routes.ts +515 -0
- package/src/server/zod-adapt.ts +239 -0
- package/src/shared/asset-path.ts +132 -0
- package/src/shared/protocol.ts +935 -0
- package/src/shared/slug.ts +17 -0
- package/src/shared/unsplash.ts +51 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire protocol for the /__dev-edit endpoints — the single source of truth
|
|
3
|
+
* for every request/response shape exchanged between the browser overlay and
|
|
4
|
+
* the dev-server middleware.
|
|
5
|
+
*
|
|
6
|
+
* Types only, no runtime code: both sides import from here with `import type`
|
|
7
|
+
* (enforced by verbatimModuleSyntax), so this file can never reach the client
|
|
8
|
+
* bundle. If a future feature changes what one side sends, the other side
|
|
9
|
+
* fails `npm run typecheck` instead of failing at runtime.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A source location as captured from data-astro-source-file / -loc. */
|
|
13
|
+
export interface SourceLoc {
|
|
14
|
+
/** Path to the source file, relative to the project root. */
|
|
15
|
+
file: string;
|
|
16
|
+
/** "line:col" as emitted by Astro's dev annotations. */
|
|
17
|
+
loc: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** What an edit targets: the element's text content, its inner source when
|
|
21
|
+
* that text carries inline markup, the frontmatter string an `{expression}`
|
|
22
|
+
* renders, or an img attribute. */
|
|
23
|
+
export type TargetType = 'text' | 'markup' | 'expression' | 'src' | 'alt';
|
|
24
|
+
|
|
25
|
+
/** Whether an attribute can be patched: statically quoted, expression-driven,
|
|
26
|
+
* or absent from the source. */
|
|
27
|
+
export type AttrState = 'static' | 'dynamic' | 'missing';
|
|
28
|
+
|
|
29
|
+
export type ClassifyKind =
|
|
30
|
+
| 'text' // children are exclusively literal text → editable inline
|
|
31
|
+
| 'markup' // literal text plus safelisted inline tags → editable as raw source
|
|
32
|
+
| 'expression' // {expression} traced to a frontmatter string → editable by value
|
|
33
|
+
| 'image' // an element whose src/alt attrs may be editable (see attrs)
|
|
34
|
+
| 'empty' // no children; nothing to text-edit
|
|
35
|
+
| 'dynamic' // expression / child elements / component content
|
|
36
|
+
| 'ambiguous' // ≥2 elements share this annotation loc — cannot patch safely
|
|
37
|
+
| 'unresolved'; // no element matches this loc (stale DOM, edited file)
|
|
38
|
+
|
|
39
|
+
/** Why an apply was refused. Also used as ApplyResult codes server-side. */
|
|
40
|
+
export type RefusalCode = 'dynamic' | 'mismatch' | 'unresolved' | 'ambiguous' | 'unsupported';
|
|
41
|
+
|
|
42
|
+
// --- GET /health -------------------------------------------------------------
|
|
43
|
+
export interface HealthResponse {
|
|
44
|
+
ok: true;
|
|
45
|
+
name: string;
|
|
46
|
+
milestone: number;
|
|
47
|
+
/** Whether the hover-pill CSS class/ID inspector is enabled. The overlay
|
|
48
|
+
* reads this at boot and skips rendering the chips row when false. */
|
|
49
|
+
cssInspector: boolean;
|
|
50
|
+
/** Whether the "Open source" buttons and jump-to-file links should render.
|
|
51
|
+
* Absent from a server that predates the option editor. */
|
|
52
|
+
openInEditor?: boolean;
|
|
53
|
+
/** Whether the CMS entry drawer is enabled. Absent from a server that
|
|
54
|
+
* predates the option editor. */
|
|
55
|
+
entryEditor?: boolean;
|
|
56
|
+
/** Absolute project root. Astro's source annotations are absolute fsPaths,
|
|
57
|
+
* which the overlay only ever showed a basename of; the copied element
|
|
58
|
+
* context needs them repo-relative to be worth pasting anywhere, and this
|
|
59
|
+
* is the only way the client can strip the prefix exactly. Dev-only, and
|
|
60
|
+
* the annotations already carry the same information. */
|
|
61
|
+
root: string;
|
|
62
|
+
/** Whether the Unsplash photo source is both enabled AND holds a usable key.
|
|
63
|
+
* The overlay reads this at boot to decide whether the media modal renders
|
|
64
|
+
* its source tabs at all — a project that never opts in gets a single-source
|
|
65
|
+
* modal, not a tab that errors when clicked. */
|
|
66
|
+
unsplash: boolean;
|
|
67
|
+
/** The resolved `unsplash.importWidth` — where the picker's size select
|
|
68
|
+
* starts, so the project-wide setting is what you get unless you change it
|
|
69
|
+
* for one import. Absent from a server that predates the option. */
|
|
70
|
+
unsplashImportWidth?: UnsplashImportWidth;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- GET /assets -------------------------------------------------------------
|
|
74
|
+
/** One image under the configured asset dirs. Metadata rather than a bare path
|
|
75
|
+
* so the picker can sort by recency and caption a tile — an image uploaded a
|
|
76
|
+
* minute ago is otherwise buried in an alphabetical list. */
|
|
77
|
+
export interface AssetInfo {
|
|
78
|
+
/** Web-servable path, e.g. `/src/assets/hero.jpg`. */
|
|
79
|
+
path: string;
|
|
80
|
+
/** Size in bytes. */
|
|
81
|
+
size: number;
|
|
82
|
+
/** Last-modified time, epoch ms — the sort key behind "Newest first". */
|
|
83
|
+
mtime: number;
|
|
84
|
+
/**
|
|
85
|
+
* Whether a **production build** still serves the file at {@link path}.
|
|
86
|
+
*
|
|
87
|
+
* The asset dirs span two worlds on purpose — `src/assets` has to be listed
|
|
88
|
+
* so `image()` fields have somewhere to browse — but only the project's
|
|
89
|
+
* `publicDir` is copied into the output. `/src/assets/hero.svg` is a truthful
|
|
90
|
+
* *dev* URL and a 404 in the built site, so a picker filling a plain
|
|
91
|
+
* `<img src>` or a markdown destination has to refuse it at pick time. The
|
|
92
|
+
* server decides this from the configured public dir; no client re-derives it
|
|
93
|
+
* from the path.
|
|
94
|
+
*/
|
|
95
|
+
servable: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface AssetsResponse {
|
|
98
|
+
/** Images under the configured asset dirs, sorted by path. */
|
|
99
|
+
files: AssetInfo[];
|
|
100
|
+
/** The project's public directory, root-relative — what a picker names when
|
|
101
|
+
* it explains why a non-servable file cannot be used. */
|
|
102
|
+
publicDir: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- POST /upload ------------------------------------------------------------
|
|
106
|
+
export interface UploadRequest {
|
|
107
|
+
/** data: URL of the image to write. */
|
|
108
|
+
dataUrl: string;
|
|
109
|
+
/** Client-suggested filename; sanitised server-side. */
|
|
110
|
+
filename: string;
|
|
111
|
+
/**
|
|
112
|
+
* Marks the upload as backing an `assetRef: 'relative'` image field. Such
|
|
113
|
+
* assets are imported by Astro rather than served verbatim, so the write
|
|
114
|
+
* targets an importable `src/` dir and animated formats the image optimiser
|
|
115
|
+
* would flatten are refused.
|
|
116
|
+
*/
|
|
117
|
+
assetRef?: 'relative';
|
|
118
|
+
/**
|
|
119
|
+
* Root-relative directory to write into, so an upload lands beside the
|
|
120
|
+
* field's existing asset instead of a shared root. Confined server-side to
|
|
121
|
+
* the configured asset directories; omitted or rejected falls back to
|
|
122
|
+
* `imageUploadDir` (relative fields) or `uploadDir`.
|
|
123
|
+
*/
|
|
124
|
+
targetDir?: string;
|
|
125
|
+
}
|
|
126
|
+
export interface UploadResponse {
|
|
127
|
+
webPath: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- POST /open --------------------------------------------------------------
|
|
131
|
+
export interface OpenRequest {
|
|
132
|
+
file: string;
|
|
133
|
+
/** "line:col"; omitted or empty opens the file at its top. */
|
|
134
|
+
loc?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- POST /page-source -------------------------------------------------------
|
|
138
|
+
/**
|
|
139
|
+
* "Which file is this route written in?" — the admin bar's *Open page source*.
|
|
140
|
+
*
|
|
141
|
+
* Answered from Astro's own route manifest, because the DOM cannot answer it:
|
|
142
|
+
* component tags are never annotated, so counting annotated elements makes a
|
|
143
|
+
* markup-dense Nav.astro outrank a page that merely composes components.
|
|
144
|
+
*/
|
|
145
|
+
export interface PageSourceRequest {
|
|
146
|
+
/** `location.pathname` exactly as the browser has it, base prefix included —
|
|
147
|
+
* the server strips the configured base itself. */
|
|
148
|
+
pathname: string;
|
|
149
|
+
}
|
|
150
|
+
/** Why nothing resolved. `no-routes` — the manifest is empty (the routes hook
|
|
151
|
+
* never fired); `no-match` — no page route matches the pathname; `not-in-project`
|
|
152
|
+
* — the matched route's entrypoint belongs to a package or sits outside the
|
|
153
|
+
* root; `missing` — the entrypoint is not on disk (Astro's injected default
|
|
154
|
+
* 404 page). */
|
|
155
|
+
export type PageSourceRefusal = 'no-routes' | 'no-match' | 'not-in-project' | 'missing';
|
|
156
|
+
export interface PageSourceResponse {
|
|
157
|
+
/** Root-relative path of the route's entrypoint, or null when unresolved. */
|
|
158
|
+
file: string | null;
|
|
159
|
+
/** The route pattern that matched, e.g. "/articles/[...slug]"; null when
|
|
160
|
+
* unresolved. */
|
|
161
|
+
pattern: string | null;
|
|
162
|
+
/** Null on success. Set means nothing was opened — the panel says so rather
|
|
163
|
+
* than guessing at a file. */
|
|
164
|
+
refusal: PageSourceRefusal | null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- POST /inspect/open ------------------------------------------------------
|
|
168
|
+
/** Open the source of a CSS rule in the editor. The client resolves `file`
|
|
169
|
+
* from the stylesheet URL; the server best-effort locates `selector` inside it
|
|
170
|
+
* (for .astro, only within <style> blocks) and launches the editor there. */
|
|
171
|
+
export interface InspectOpenRequest {
|
|
172
|
+
/** Source file the rule came from — root-relative or absolute. */
|
|
173
|
+
file: string;
|
|
174
|
+
/** The class/id selector fragment to locate, e.g. ".hero-title" or "#masthead". */
|
|
175
|
+
selector: string;
|
|
176
|
+
}
|
|
177
|
+
export interface InspectOpenResponse {
|
|
178
|
+
ok: true;
|
|
179
|
+
/** "line:col" where the selector was found, or null when it wasn't (the file
|
|
180
|
+
* was opened at its top instead). */
|
|
181
|
+
loc: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- POST /peek --------------------------------------------------------------
|
|
185
|
+
export interface PeekRequest {
|
|
186
|
+
file: string;
|
|
187
|
+
/** "line:col"; the line the panel highlights and scrolls to. Omitted or
|
|
188
|
+
* empty peeks from the top of the file. */
|
|
189
|
+
loc?: string;
|
|
190
|
+
}
|
|
191
|
+
export interface PeekResponse {
|
|
192
|
+
file: string;
|
|
193
|
+
/** 1-based line number of `lines[0]` — 1 unless the huge-file cap cut the
|
|
194
|
+
* window down. */
|
|
195
|
+
startLine: number;
|
|
196
|
+
/** 1-based line the loc points at, clamped into the file. */
|
|
197
|
+
focusLine: number;
|
|
198
|
+
/** Total lines in the file, so the client can say "N more lines" when the
|
|
199
|
+
* cap applied. */
|
|
200
|
+
totalLines: number;
|
|
201
|
+
/** The file's source lines — the whole file, unless it exceeds the server's
|
|
202
|
+
* huge-file cap, in which case a window around `focusLine`. Empty when
|
|
203
|
+
* `refused` is set. */
|
|
204
|
+
lines: string[];
|
|
205
|
+
/** Set when the file exists but isn't the user's to look at through the
|
|
206
|
+
* panel — e.g. a package-owned path like the `astro:assets` `<Image>`
|
|
207
|
+
* component. The panel shows this sentence instead of source, and no file
|
|
208
|
+
* contents are returned. Absent on success. */
|
|
209
|
+
refused?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// --- POST /classify ----------------------------------------------------------
|
|
213
|
+
export interface ClassifyRequest extends SourceLoc {
|
|
214
|
+
/** Lowercased tag name of the clicked element. */
|
|
215
|
+
tag: string;
|
|
216
|
+
}
|
|
217
|
+
export interface ClassifyResult {
|
|
218
|
+
kind: ClassifyKind;
|
|
219
|
+
reason: string;
|
|
220
|
+
/** For `img` targets: whether src/alt are patchable. */
|
|
221
|
+
attrs?: { src: AttrState; alt: AttrState };
|
|
222
|
+
/**
|
|
223
|
+
* For `markup` targets: the element's inner *source*, outer whitespace
|
|
224
|
+
* trimmed. The popup edits this rather than the DOM's `innerHTML` — only the
|
|
225
|
+
* source knows how entities and quoting were spelled, and sending it back as
|
|
226
|
+
* the apply op's `original` keeps verify-then-patch comparing like with like.
|
|
227
|
+
*/
|
|
228
|
+
markup?: { html: string };
|
|
229
|
+
/**
|
|
230
|
+
* For `expression` targets: which frontmatter string the text was traced to.
|
|
231
|
+
* `property` is the key holding it (`title`); `label` names the whole path
|
|
232
|
+
* for the editor's title (`benefits[].title`). No value is sent — for a
|
|
233
|
+
* `.map()` loop every card shares this classification, and which item is
|
|
234
|
+
* being edited is only settled by the text the client sends on apply.
|
|
235
|
+
*/
|
|
236
|
+
expression?: { property: string; label: string };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- POST /apply -------------------------------------------------------------
|
|
240
|
+
/** One field edit within an apply request. */
|
|
241
|
+
export interface ApplyOp {
|
|
242
|
+
targetType: TargetType;
|
|
243
|
+
/** Rendered text / attr value the client saw — verified against the source
|
|
244
|
+
* before writing, so a stale page fails safe. */
|
|
245
|
+
original: string;
|
|
246
|
+
newText: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A verify-all-then-write-once batch of edits to ONE element (same file/loc/
|
|
250
|
+
* tag). The server applies each op to an in-memory copy of the source and only
|
|
251
|
+
* writes if every op verifies; a single failing op means nothing reaches disk,
|
|
252
|
+
* so a multi-field edit can never leave the file half-updated. Text edits send
|
|
253
|
+
* one op; the image panel sends up to two (src, alt). */
|
|
254
|
+
export interface ApplyRequestWire extends SourceLoc {
|
|
255
|
+
tag: string;
|
|
256
|
+
ops: ApplyOp[];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Error body shape shared by all endpoints (4xx/5xx). */
|
|
260
|
+
export interface ErrorResponse {
|
|
261
|
+
error: string;
|
|
262
|
+
code?: RefusalCode;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Entry editor (spec: CMS panel for content-collection entries) -----------
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* How a collection's `schema:` is written. Only `function` —
|
|
269
|
+
* `({ image }) => z.object({…})` — has Astro's `image()` helper in scope, which
|
|
270
|
+
* is the whole of the difference the designer cares about.
|
|
271
|
+
*/
|
|
272
|
+
export type SchemaForm = 'object' | 'function';
|
|
273
|
+
|
|
274
|
+
/** Widget/type a frontmatter field renders as in the entry panel. */
|
|
275
|
+
export type FieldType =
|
|
276
|
+
| 'text'
|
|
277
|
+
| 'textarea'
|
|
278
|
+
| 'date'
|
|
279
|
+
| 'number'
|
|
280
|
+
| 'boolean'
|
|
281
|
+
| 'select'
|
|
282
|
+
| 'tags'
|
|
283
|
+
| 'image'
|
|
284
|
+
| 'json'; // unrecognized shape → shown read-only
|
|
285
|
+
|
|
286
|
+
/** One frontmatter field, derived from the collection's zod schema (or, when
|
|
287
|
+
* no schema is resolvable, inferred from the entry's own values). */
|
|
288
|
+
export interface FieldDescriptor {
|
|
289
|
+
name: string;
|
|
290
|
+
/** Humanized name; overridable via entryEditor config. */
|
|
291
|
+
label: string;
|
|
292
|
+
type: FieldType;
|
|
293
|
+
/** No default and not optional/nullable. */
|
|
294
|
+
required: boolean;
|
|
295
|
+
/** Enum values, for `select`. */
|
|
296
|
+
options?: string[];
|
|
297
|
+
/**
|
|
298
|
+
* How an `image` field's value references its asset. Absent (the default)
|
|
299
|
+
* means a web-servable root-relative path, the shape a plain `<img src>`
|
|
300
|
+
* needs. `'relative'` marks a field backed by Astro's `image()` schema
|
|
301
|
+
* helper, whose values are paths relative to the *entry file* — the picker
|
|
302
|
+
* must resolve previews and write values in that shape instead.
|
|
303
|
+
*/
|
|
304
|
+
assetRef?: 'relative';
|
|
305
|
+
/** Schema default — shown as placeholder when the key is absent. */
|
|
306
|
+
defaultValue?: unknown;
|
|
307
|
+
/** Whether the key exists in the file's frontmatter. */
|
|
308
|
+
present: boolean;
|
|
309
|
+
source: 'schema' | 'inferred';
|
|
310
|
+
/** Render the control disabled. Used by the Settings panel for an option
|
|
311
|
+
* `astro.config.mjs` owns, where accepting input would be a lie. */
|
|
312
|
+
readOnly?: boolean;
|
|
313
|
+
/** One-line prose shown under the control. */
|
|
314
|
+
help?: string;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The *editor* half of a field: which control it renders as, what it is called,
|
|
319
|
+
* whether it shows at all. Stored in `.astro-dev-edit.json` (or set in
|
|
320
|
+
* `astro.config.mjs` under `entryEditor.collections.<name>.fields`) and never in
|
|
321
|
+
* the collection's zod schema — the collection designer keeps the two halves
|
|
322
|
+
* visibly apart, because one is committed source and the other is local.
|
|
323
|
+
*/
|
|
324
|
+
export interface FieldOverride {
|
|
325
|
+
widget?: FieldType;
|
|
326
|
+
label?: string;
|
|
327
|
+
hidden?: boolean;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A schema field as the designer *writes* it — the inverse of a
|
|
332
|
+
* {@link FieldDescriptor}, which is one read out of a live schema.
|
|
333
|
+
* `patcher/content-config.ts` renders these to zod expressions.
|
|
334
|
+
*/
|
|
335
|
+
export interface SchemaFieldSpec {
|
|
336
|
+
name: string;
|
|
337
|
+
type: FieldType;
|
|
338
|
+
/** False renders `.optional()`; a `defaultValue` implies optional. */
|
|
339
|
+
required: boolean;
|
|
340
|
+
/** Renders `.default(…)`. Not supported for `date` or `image`. */
|
|
341
|
+
defaultValue?: unknown;
|
|
342
|
+
/** Enum values, for `type: 'select'`. */
|
|
343
|
+
options?: string[];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// --- POST /entry -------------------------------------------------------------
|
|
347
|
+
export interface EntryRequest {
|
|
348
|
+
/** Repo-relative path — from the page-source meta tag, from `/entry/resolve`,
|
|
349
|
+
* or from an Items row. */
|
|
350
|
+
file: string;
|
|
351
|
+
}
|
|
352
|
+
export interface EntryResponse {
|
|
353
|
+
file: string;
|
|
354
|
+
/** sha256 of the full file content; sent back on every write. */
|
|
355
|
+
etag: string;
|
|
356
|
+
/** Matched collection name, when the file maps to one. */
|
|
357
|
+
collection: string | null;
|
|
358
|
+
/** Repo-relative directory holding the collection's entries, when matched.
|
|
359
|
+
* The create drawer resolves `assetRef: 'relative'` values against it,
|
|
360
|
+
* since a new entry has no path of its own yet. */
|
|
361
|
+
collectionDir: string | null;
|
|
362
|
+
fields: FieldDescriptor[];
|
|
363
|
+
/** Parsed frontmatter values (JSON-safe). */
|
|
364
|
+
values: Record<string, unknown>;
|
|
365
|
+
/** Markdown body, \n-normalized. */
|
|
366
|
+
body: string;
|
|
367
|
+
bodyEditable: boolean;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// --- POST /entry/resolve -----------------------------------------------------
|
|
371
|
+
/**
|
|
372
|
+
* "Which content entry backs the page I am looking at?" — the meta tag's job,
|
|
373
|
+
* done by the server so a project need not emit one.
|
|
374
|
+
*
|
|
375
|
+
* Three layers narrow the answer, and each may refuse:
|
|
376
|
+
*
|
|
377
|
+
* 1. Astro's route manifest maps the pathname to a page file and a pattern. A
|
|
378
|
+
* pattern with no dynamic segment renders a *set* of entries, so it is not
|
|
379
|
+
* a detail page and gets no entry.
|
|
380
|
+
* 2. That page file is **scanned** for `getCollection('x')` / `getEntry('x'`,
|
|
381
|
+
* which binds the route to a collection rather than inferring one from the
|
|
382
|
+
* other. A route that fetches through a helper names nothing, and every
|
|
383
|
+
* enabled collection stays a candidate.
|
|
384
|
+
* 3. The pathname's tail is matched against the candidates' entry ids.
|
|
385
|
+
*
|
|
386
|
+
* A refusal is an answer: the entry button stays hidden exactly as it does on a
|
|
387
|
+
* page with no meta tag today.
|
|
388
|
+
*/
|
|
389
|
+
export interface EntryResolveRequest {
|
|
390
|
+
/** `location.pathname` as the browser has it, base prefix included — the
|
|
391
|
+
* server strips the configured base itself, as `/page-source` does. */
|
|
392
|
+
pathname: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Why no entry was resolved.
|
|
396
|
+
* `disabled` — the entry editor is off; `no-routes` — the manifest is empty;
|
|
397
|
+
* `no-match` — no page route matches; `not-detail` — the route is static, so it
|
|
398
|
+
* renders a set rather than one entry; `not-enabled` — an entry *was* found and
|
|
399
|
+
* its collection has page editing switched off; `no-entry` — nothing on disk
|
|
400
|
+
* matches the pathname's tail (a dead URL a dynamic pattern still matched);
|
|
401
|
+
* `ambiguous` — two collections hold that id and nothing chose between them. */
|
|
402
|
+
export type EntryResolveRefusal =
|
|
403
|
+
| 'disabled'
|
|
404
|
+
| 'no-routes'
|
|
405
|
+
| 'no-match'
|
|
406
|
+
| 'not-detail'
|
|
407
|
+
| 'not-enabled'
|
|
408
|
+
| 'no-entry'
|
|
409
|
+
| 'ambiguous';
|
|
410
|
+
|
|
411
|
+
export interface EntryResolveResponse {
|
|
412
|
+
/** Repo-relative entry file — exactly what `POST /entry` takes. Null unless
|
|
413
|
+
* the entry resolved **and** its collection has page editing on. */
|
|
414
|
+
file: string | null;
|
|
415
|
+
/**
|
|
416
|
+
* The collection the page renders. Also set on `not-enabled`, together with
|
|
417
|
+
* {@link entryFile}, so the refusal notice can name what it is offering to
|
|
418
|
+
* switch on rather than saying "a collection".
|
|
419
|
+
*/
|
|
420
|
+
collection: string | null;
|
|
421
|
+
/** The entry that was found while refusing `not-enabled`. Never editable —
|
|
422
|
+
* it exists so the offer can name the file it would open. */
|
|
423
|
+
entryFile: string | null;
|
|
424
|
+
/** With `not-enabled`: whether `astro.config.mjs` owns the switch. True means
|
|
425
|
+
* the offer is not made, because the write behind it would be refused. */
|
|
426
|
+
pageEditingLocked: boolean;
|
|
427
|
+
/** The route pattern that matched, e.g. "/articles/[...slug]". */
|
|
428
|
+
pattern: string | null;
|
|
429
|
+
refusal: EntryResolveRefusal | null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// --- POST /entry/apply -------------------------------------------------------
|
|
433
|
+
export interface EntryApplyRequest {
|
|
434
|
+
file: string;
|
|
435
|
+
etag: string;
|
|
436
|
+
changes: {
|
|
437
|
+
/** ONLY changed keys. `null` clears an optional key from the file. */
|
|
438
|
+
frontmatter?: Record<string, unknown>;
|
|
439
|
+
body?: string;
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// --- POST /entry/create ------------------------------------------------------
|
|
444
|
+
export interface EntryCreateRequest {
|
|
445
|
+
collection: string;
|
|
446
|
+
/** Filename without extension; sanitized server-side. */
|
|
447
|
+
slug: string;
|
|
448
|
+
frontmatter: Record<string, unknown>;
|
|
449
|
+
body: string;
|
|
450
|
+
}
|
|
451
|
+
export interface EntryCreateResponse {
|
|
452
|
+
/** Repo-relative path of the new file. */
|
|
453
|
+
file: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// --- POST /entry/delete ------------------------------------------------------
|
|
457
|
+
export interface EntryDeleteRequest {
|
|
458
|
+
file: string;
|
|
459
|
+
etag: string;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Entry endpoints extend the shared error shape with two more codes and
|
|
463
|
+
* optional per-field validation messages. */
|
|
464
|
+
export interface EntryErrorResponse {
|
|
465
|
+
error: string;
|
|
466
|
+
code?: RefusalCode | 'conflict' | 'validation' | 'exists';
|
|
467
|
+
/** Field name → message, for 422 validation failures. */
|
|
468
|
+
fieldErrors?: Record<string, string>;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// --- The media modal ---------------------------------------------------------
|
|
472
|
+
/** What the shared media modal resolves with. Lives here rather than in the
|
|
473
|
+
* client because `origin` is a server-side distinction: the modal's three
|
|
474
|
+
* sources produce the same kind of path by different routes, and a caller may
|
|
475
|
+
* want to know which (an Unsplash pick has already been downloaded and
|
|
476
|
+
* attributed by the time it reaches this shape). */
|
|
477
|
+
export interface MediaPick {
|
|
478
|
+
/** The value to write — already relative-converted if the field needs it. */
|
|
479
|
+
webPath: string;
|
|
480
|
+
origin: 'existing' | 'upload' | 'unsplash';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// --- POST /unsplash/search ---------------------------------------------------
|
|
484
|
+
/**
|
|
485
|
+
* A photo, reshaped by the server. Unsplash's own object carries exif, tags,
|
|
486
|
+
* topics, sponsorship, the full user record and a dozen URL variants; none of
|
|
487
|
+
* that crosses the wire, so it can never become an accidental API surface.
|
|
488
|
+
*
|
|
489
|
+
* Deliberately carries **no** raw or download_location URL. The server keeps
|
|
490
|
+
* those in a bounded in-memory map keyed by `id`, so `/unsplash/import` never
|
|
491
|
+
* fetches a URL the browser supplied — that removes an SSRF-shaped capability
|
|
492
|
+
* from the dev server. The cost is that an `id` minted before a restart imports
|
|
493
|
+
* as `409 expired`.
|
|
494
|
+
*/
|
|
495
|
+
export interface UnsplashPhoto {
|
|
496
|
+
/** Unsplash's id — the only handle `/unsplash/import` accepts. */
|
|
497
|
+
id: string;
|
|
498
|
+
/** Grid thumbnail (urls.small), hotlinked in the picker UI only. */
|
|
499
|
+
thumbUrl: string;
|
|
500
|
+
/** Average colour, painted behind the tile while the thumb loads. */
|
|
501
|
+
color: string;
|
|
502
|
+
width: number;
|
|
503
|
+
height: number;
|
|
504
|
+
description: string;
|
|
505
|
+
photographer: string;
|
|
506
|
+
/** Profile URL with the required utm params already attached, so the client
|
|
507
|
+
* cannot forget them and the app name lives in one place. */
|
|
508
|
+
photographerUrl: string;
|
|
509
|
+
pageUrl: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export type UnsplashOrientation = 'any' | 'landscape' | 'portrait' | 'squarish';
|
|
513
|
+
|
|
514
|
+
export interface UnsplashSearchRequest {
|
|
515
|
+
query: string;
|
|
516
|
+
/** 1-based; clamped to >= 1 server-side. */
|
|
517
|
+
page?: number;
|
|
518
|
+
/** Clamped to Unsplash's own maximum of 30. */
|
|
519
|
+
perPage?: number;
|
|
520
|
+
orientation?: UnsplashOrientation;
|
|
521
|
+
}
|
|
522
|
+
export interface UnsplashSearchResponse {
|
|
523
|
+
photos: UnsplashPhoto[];
|
|
524
|
+
/** Total matches across all pages, for the "Load more (20 of 1,283)" label. */
|
|
525
|
+
total: number;
|
|
526
|
+
totalPages: number;
|
|
527
|
+
page: number;
|
|
528
|
+
/** Requests left this hour, when Unsplash reported it. Surfaced at the foot
|
|
529
|
+
* of the details rail — the demo tier allows only 50/hour. */
|
|
530
|
+
remaining?: number;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// --- POST /unsplash/import ---------------------------------------------------
|
|
534
|
+
/**
|
|
535
|
+
* How wide an imported photo is fetched. A closed set, not a number: the value
|
|
536
|
+
* reaches the URL the dev server fetches from Unsplash's CDN, so the server
|
|
537
|
+
* refuses anything off the list rather than clamping it. `'original'` sends no
|
|
538
|
+
* width at all — the raw file, bounded only by the 25 MB import cap.
|
|
539
|
+
*
|
|
540
|
+
* The runtime list lives in `shared/unsplash.ts`, since `protocol.ts` stays
|
|
541
|
+
* types-only — the same split `FIELD_TYPES` has.
|
|
542
|
+
*/
|
|
543
|
+
export type UnsplashImportWidth = 800 | 1600 | 2400 | 'original';
|
|
544
|
+
|
|
545
|
+
export interface UnsplashImportRequest {
|
|
546
|
+
/** An id from a search in this dev-server process; see UnsplashPhoto. */
|
|
547
|
+
id: string;
|
|
548
|
+
/** Fetch this width instead of the resolved `unsplash.importWidth` — the
|
|
549
|
+
* right size is per-slot (a card thumbnail, an avatar), not per-project.
|
|
550
|
+
* Validated against the safelist server-side; an unknown value is refused,
|
|
551
|
+
* so the import fails loudly rather than landing the wrong size. */
|
|
552
|
+
width?: UnsplashImportWidth;
|
|
553
|
+
/** Same semantics as UploadRequest: marks an `image()`-backed field, so the
|
|
554
|
+
* bytes land in an importable `src/` dir rather than the web-servable one. */
|
|
555
|
+
assetRef?: 'relative';
|
|
556
|
+
/** Root-relative directory, confined server-side exactly as /upload's is. */
|
|
557
|
+
targetDir?: string;
|
|
558
|
+
}
|
|
559
|
+
export interface UnsplashImportResponse {
|
|
560
|
+
/** Web-servable path of the downloaded file. */
|
|
561
|
+
webPath: string;
|
|
562
|
+
/** The name actually written, after sanitising and clash-suffixing. */
|
|
563
|
+
filename: string;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Why an Unsplash call failed. These routes answer with an explicit code rather
|
|
568
|
+
* than throwing, because a thrown error becomes a 400 — which would read as
|
|
569
|
+
* "your query was malformed" when the real cause is upstream. See the module
|
|
570
|
+
* header in server/unsplash-routes.ts.
|
|
571
|
+
*/
|
|
572
|
+
export type UnsplashErrorCode =
|
|
573
|
+
| 'disabled' // the integration option is off
|
|
574
|
+
| 'unconfigured' // enabled, but no access key resolved
|
|
575
|
+
| 'unauthorized' // Unsplash rejected the key
|
|
576
|
+
| 'rate-limited' // hourly quota exhausted (50/hr on the demo tier)
|
|
577
|
+
| 'expired' // photo id is no longer in the server's cache
|
|
578
|
+
| 'timeout' // the upstream call took too long
|
|
579
|
+
| 'too-large' // the download exceeded the size cap
|
|
580
|
+
| 'upstream'; // anything else on Unsplash's side
|
|
581
|
+
|
|
582
|
+
export interface UnsplashErrorResponse {
|
|
583
|
+
error: string;
|
|
584
|
+
code: UnsplashErrorCode;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// --- GET/POST /settings ------------------------------------------------------
|
|
588
|
+
/**
|
|
589
|
+
* Where a resolved access key came from, highest precedence first:
|
|
590
|
+
*
|
|
591
|
+
* - `config` — `unsplash.accessKey` in `astro.config.mjs`.
|
|
592
|
+
* - `env-shell` — an exported `UNSPLASH_ACCESS_KEY`. Vite's own env loader lets
|
|
593
|
+
* `process.env` override every `.env` file, so this outranks all of them.
|
|
594
|
+
* - `env-file` — `UNSPLASH_ACCESS_KEY` in a `.env` file. **This is the Settings
|
|
595
|
+
* panel's own store**: it writes `.env.local`, which Vite already refuses to
|
|
596
|
+
* serve. Within this source the files rank `.env` < `.env.local` <
|
|
597
|
+
* `.env.development` < `.env.development.local`, so the panel can override
|
|
598
|
+
* the first but not the last two — {@link SettingsResponse} reports which.
|
|
599
|
+
* - `file` — a legacy `unsplash.accessKey` in `.astro-dev-edit.json`. Read for
|
|
600
|
+
* back-compat only, and stripped the next time a key is saved.
|
|
601
|
+
*
|
|
602
|
+
* Not the same union as {@link OptionDescriptor.source}, which also has a
|
|
603
|
+
* `'file'` member — there it means the live settings file, here it means the
|
|
604
|
+
* legacy key inside it. `settings-panel.ts` consumes both.
|
|
605
|
+
*/
|
|
606
|
+
export type SettingsSource = 'config' | 'env-shell' | 'env-file' | 'file';
|
|
607
|
+
|
|
608
|
+
/** Which Settings tab an option is grouped under. */
|
|
609
|
+
export type OptionGroup = 'general' | 'editing' | 'media' | 'unsplash';
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* One integration option, described well enough for the panel to render a
|
|
613
|
+
* control for it without knowing the option exists.
|
|
614
|
+
*
|
|
615
|
+
* That genericity is the point: options are declared once in the server's
|
|
616
|
+
* `OPTION_SPECS` table, and adding one needs no client change. `type` reuses
|
|
617
|
+
* {@link FieldType} so the panel can build the control through the entry
|
|
618
|
+
* editor's own `buildControl` registry rather than a parallel one.
|
|
619
|
+
*/
|
|
620
|
+
export interface OptionDescriptor {
|
|
621
|
+
/** Flat wire key — what a {@link SettingsUpdateRequest} patch is keyed by. */
|
|
622
|
+
key: string;
|
|
623
|
+
label: string;
|
|
624
|
+
/** One-line prose shown under the control. */
|
|
625
|
+
help: string;
|
|
626
|
+
type: FieldType;
|
|
627
|
+
group: OptionGroup;
|
|
628
|
+
/** Enum values, for `type: 'select'`. */
|
|
629
|
+
choices?: string[];
|
|
630
|
+
/** The effective value, after precedence. */
|
|
631
|
+
value: unknown;
|
|
632
|
+
/** Which layer supplied it. */
|
|
633
|
+
source: 'default' | 'file' | 'config';
|
|
634
|
+
/**
|
|
635
|
+
* `astro.config.mjs` sets it, so storing a value here would do nothing — the
|
|
636
|
+
* panel renders the control read-only and says why. Also true for options
|
|
637
|
+
* consumed before the dev server exists, which can only come from the config.
|
|
638
|
+
*/
|
|
639
|
+
locked: boolean;
|
|
640
|
+
/** Changing it needs a dev-server restart. */
|
|
641
|
+
restartRequired?: boolean;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The access key itself is **never** in this shape. Only whether one resolved,
|
|
646
|
+
* where from, whether the panel may change it, and a masked fragment.
|
|
647
|
+
*/
|
|
648
|
+
export interface SettingsResponse {
|
|
649
|
+
/** Every option, in the order the panel should render them. Absent from a
|
|
650
|
+
* server that predates the option editor, so the panel must tolerate it. */
|
|
651
|
+
options?: OptionDescriptor[];
|
|
652
|
+
/**
|
|
653
|
+
* Files holding settings or the access key that the project's `.gitignore`
|
|
654
|
+
* does not cover, project-relative, in the order the panel should name them.
|
|
655
|
+
* Absent when everything is covered.
|
|
656
|
+
*
|
|
657
|
+
* Top-level rather than inside `unsplash`, because it is a fact about files:
|
|
658
|
+
* `.astro-dev-edit.json` exists as soon as any tab saves an option and has
|
|
659
|
+
* nothing to do with the photo source, while `.env.local` is only listed once
|
|
660
|
+
* it exists. The panel repeats it because this integration cannot edit a
|
|
661
|
+
* consuming project's ignore rules.
|
|
662
|
+
*/
|
|
663
|
+
gitignoreWarning?: string[];
|
|
664
|
+
unsplash: {
|
|
665
|
+
/** Whether the option is enabled at all, independent of a key. */
|
|
666
|
+
enabled: boolean;
|
|
667
|
+
configured: boolean;
|
|
668
|
+
source: SettingsSource | null;
|
|
669
|
+
/** The file the key resolved from, project-relative — `.env.local`,
|
|
670
|
+
* `.env.development`, `.astro-dev-edit.json`. Absent for `config` and
|
|
671
|
+
* `env-shell`, which have no file, and when nothing is configured. */
|
|
672
|
+
sourceFile?: string;
|
|
673
|
+
/** Masked tail, e.g. `••••••••Ab3d`. Absent when nothing is configured. */
|
|
674
|
+
hint?: string;
|
|
675
|
+
/**
|
|
676
|
+
* Whether a save would actually take effect. False when something that
|
|
677
|
+
* outranks `.env.local` supplies the key — the Astro config, an exported
|
|
678
|
+
* shell variable, or `.env.development[.local]`.
|
|
679
|
+
*
|
|
680
|
+
* Server-computed rather than derived from `source`, because the answer
|
|
681
|
+
* depends on which *file* within `env-file` won; the panel cannot know the
|
|
682
|
+
* precedence and should not encode a copy of it.
|
|
683
|
+
*/
|
|
684
|
+
writable: boolean;
|
|
685
|
+
/** Whether **Clear** can actually clear it. False for a key in `.env`,
|
|
686
|
+
* which a save can override but a removal from `.env.local` cannot unset. */
|
|
687
|
+
clearable: boolean;
|
|
688
|
+
/** A legacy `unsplash.accessKey` is still in `.astro-dev-edit.json` while
|
|
689
|
+
* something else wins. The next key save strips it; until then the panel
|
|
690
|
+
* nudges, since that file sits in the project's own tree. */
|
|
691
|
+
staleStoredKey?: true;
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export interface SettingsUpdateRequest {
|
|
696
|
+
unsplash?: {
|
|
697
|
+
/** The access key to store, written to `.env.local`. An empty string clears
|
|
698
|
+
* it. Either may be **refused** — see `writable` / `clearable` on
|
|
699
|
+
* {@link SettingsResponse}. */
|
|
700
|
+
accessKey: string;
|
|
701
|
+
};
|
|
702
|
+
/**
|
|
703
|
+
* Sparse option patch — **only** the keys the panel changed, keyed by
|
|
704
|
+
* {@link OptionDescriptor.key}, mirroring `EntryApplyRequest.changes`. Values
|
|
705
|
+
* are `unknown` because the option set is server-declared; the server checks
|
|
706
|
+
* each one against its spec's type and refuses unknown, config-only and
|
|
707
|
+
* locked keys by name.
|
|
708
|
+
*/
|
|
709
|
+
options?: Record<string, unknown>;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** 422 from `POST /settings`: which keys were refused and why. Nothing is
|
|
713
|
+
* written when this comes back — the patch is all-or-nothing, so a rejected
|
|
714
|
+
* key cannot leave the file half-updated. */
|
|
715
|
+
export interface SettingsErrorResponse {
|
|
716
|
+
error: string;
|
|
717
|
+
code?: 'validation' | 'conflict' | 'disabled';
|
|
718
|
+
/** Option key → message. */
|
|
719
|
+
fieldErrors?: Record<string, string>;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// --- The collection designer -------------------------------------------------
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* A field row in the designer spans **two stores**, and the wire shape keeps
|
|
726
|
+
* them apart on purpose:
|
|
727
|
+
*
|
|
728
|
+
* - the **schema** half (`type`, `required`, `defaultValue`, add, remove) is
|
|
729
|
+
* written to the project's own `src/content.config.ts` — committed source that
|
|
730
|
+
* changes what `astro build` accepts;
|
|
731
|
+
* - the **editor** half ({@link FieldOverride}) is written to
|
|
732
|
+
* `.astro-dev-edit.json` — local, gitignored, and only affects the entry
|
|
733
|
+
* drawer.
|
|
734
|
+
*
|
|
735
|
+
* A request may carry both; the response says which half landed.
|
|
736
|
+
*/
|
|
737
|
+
|
|
738
|
+
// --- POST /collections -------------------------------------------------------
|
|
739
|
+
export interface CollectionSummary {
|
|
740
|
+
name: string;
|
|
741
|
+
/** Repo-relative directory holding the collection's entries. */
|
|
742
|
+
dir: string;
|
|
743
|
+
/** Whether that directory exists yet. A collection declared in the config
|
|
744
|
+
* without its directory is a real and common state. */
|
|
745
|
+
dirExists: boolean;
|
|
746
|
+
/** Entry files found in it (.md/.mdx, recursive). */
|
|
747
|
+
entryCount: number;
|
|
748
|
+
/** The collection's fields — schema-derived when the schema resolved. */
|
|
749
|
+
fields: FieldDescriptor[];
|
|
750
|
+
/**
|
|
751
|
+
* Where {@link fields} came from. `'source'` means the config module didn't
|
|
752
|
+
* load (usually because it has an error), so the names come from the config
|
|
753
|
+
* text and the types are unknown — the panel says so rather than showing an
|
|
754
|
+
* authoritative-looking field table built from a guess.
|
|
755
|
+
*/
|
|
756
|
+
fieldSource: 'schema' | 'source';
|
|
757
|
+
/** The zod expression for each field, keyed by name, as it stands in the
|
|
758
|
+
* config. Shown for anything the designer can't model. */
|
|
759
|
+
expressions: Record<string, string>;
|
|
760
|
+
/** How the `schema:` is written. Null when the designer couldn't read it — the
|
|
761
|
+
* panel then offers "open source" instead of controls. */
|
|
762
|
+
schemaForm: SchemaForm | null;
|
|
763
|
+
/** Why the schema isn't patchable, when it isn't. */
|
|
764
|
+
unrecognized?: string;
|
|
765
|
+
/** Whether the const is registered in `export const collections`. */
|
|
766
|
+
registered: boolean;
|
|
767
|
+
/** 1-based line of the collection's block in the config, for "open source". */
|
|
768
|
+
configLine?: number;
|
|
769
|
+
/** Editor-only overrides in force for this collection. */
|
|
770
|
+
overrides: Record<string, FieldOverride>;
|
|
771
|
+
/**
|
|
772
|
+
* Fields whose override `astro.config.mjs` owns. Storing one from the panel
|
|
773
|
+
* would resolve to nothing, so the editor half renders read-only — the same
|
|
774
|
+
* `locked` honesty {@link OptionDescriptor} has, applied per field.
|
|
775
|
+
*/
|
|
776
|
+
lockedFields: string[];
|
|
777
|
+
/**
|
|
778
|
+
* Whether this collection's detail pages offer the entry drawer. Off by
|
|
779
|
+
* default: switching it on is what replaces hand-emitting the page-source
|
|
780
|
+
* meta tag. Editor-half state — it lives in `.astro-dev-edit.json` beside the
|
|
781
|
+
* field overrides, never in the project's committed content config.
|
|
782
|
+
*/
|
|
783
|
+
pageEditing: boolean;
|
|
784
|
+
/** True when `astro.config.mjs` owns {@link pageEditing}, so the row's switch
|
|
785
|
+
* renders read-only for the same reason a config-set widget does. */
|
|
786
|
+
pageEditingLocked: boolean;
|
|
787
|
+
/**
|
|
788
|
+
* The route pattern whose page file names this collection, e.g.
|
|
789
|
+
* "/articles/[...slug]". Null when no dynamic page route was found to render
|
|
790
|
+
* it — a data collection, or a route that fetches through a helper. The panel
|
|
791
|
+
* says so rather than implying the switch will do nothing.
|
|
792
|
+
*/
|
|
793
|
+
detailRoute: string | null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
export interface CollectionsResponse {
|
|
797
|
+
/** Repo-relative content config the designer reads and patches, or null when
|
|
798
|
+
* the project has none. Discovered server-side; a request never names it. */
|
|
799
|
+
configPath: string | null;
|
|
800
|
+
/** sha256 of that file, required by every write below. Null with no config. */
|
|
801
|
+
etag: string | null;
|
|
802
|
+
/** False when `schemaEditor` is off: the panel stays read-only for the schema
|
|
803
|
+
* half and still saves the editor half. */
|
|
804
|
+
schemaEditor: boolean;
|
|
805
|
+
collections: CollectionSummary[];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// --- POST /collection/schema/apply -------------------------------------------
|
|
809
|
+
export interface CollectionSchemaApplyRequest {
|
|
810
|
+
collection: string;
|
|
811
|
+
/** {@link CollectionsResponse.etag} as the panel read it. Required whenever
|
|
812
|
+
* `schema` is present; a stale one is refused rather than merged. */
|
|
813
|
+
etag?: string;
|
|
814
|
+
/** Schema edits. Applied form → removes → updates → adds, all against one
|
|
815
|
+
* in-memory copy, and written once — so a refusal anywhere leaves the file
|
|
816
|
+
* untouched. The form goes first because turning image support on and adding
|
|
817
|
+
* an image field are one save, and the field cannot render until it has. */
|
|
818
|
+
schema?: {
|
|
819
|
+
/** Switch the `schema:` between `z.object({…})` and `({ image }) =>
|
|
820
|
+
* z.object({…})`. Omitted means leave it as it is. */
|
|
821
|
+
form?: SchemaForm;
|
|
822
|
+
add?: SchemaFieldSpec[];
|
|
823
|
+
update?: SchemaFieldSpec[];
|
|
824
|
+
remove?: string[];
|
|
825
|
+
};
|
|
826
|
+
/** Editor-only overrides, merged per field. `null` clears one. */
|
|
827
|
+
overrides?: Record<string, FieldOverride | null>;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export interface CollectionApplyResponse {
|
|
831
|
+
/** True when everything asked for landed. */
|
|
832
|
+
ok: boolean;
|
|
833
|
+
/** Whether the config file was rewritten. */
|
|
834
|
+
schemaWritten: boolean;
|
|
835
|
+
/** Whether the settings file was rewritten. */
|
|
836
|
+
overridesWritten: boolean;
|
|
837
|
+
/** Fresh config etag after a schema write. */
|
|
838
|
+
etag?: string;
|
|
839
|
+
/** Why a half didn't land. */
|
|
840
|
+
error?: string;
|
|
841
|
+
code?: CollectionRefusal;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** Why a designer write was refused.
|
|
845
|
+
* `conflict` — the config changed on disk since the panel read it.
|
|
846
|
+
* `disabled` — `schemaEditor: false`.
|
|
847
|
+
* `unrecognized` — a schema shape the patcher won't guess at.
|
|
848
|
+
* The rest name the collection or field. */
|
|
849
|
+
export type CollectionRefusal =
|
|
850
|
+
| 'conflict'
|
|
851
|
+
| 'disabled'
|
|
852
|
+
| 'unrecognized'
|
|
853
|
+
| 'missing'
|
|
854
|
+
| 'exists'
|
|
855
|
+
| 'unsupported';
|
|
856
|
+
|
|
857
|
+
// --- POST /collection/entries ------------------------------------------------
|
|
858
|
+
export interface CollectionEntriesRequest {
|
|
859
|
+
collection: string;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** One entry as the Items view lists it. Enough to identify and open it, and
|
|
863
|
+
* nothing more — the body is never read into this response. */
|
|
864
|
+
export interface CollectionEntryItem {
|
|
865
|
+
/** Repo-relative path, which is exactly what `POST /entry` takes. */
|
|
866
|
+
file: string;
|
|
867
|
+
/** Filename without its extension. */
|
|
868
|
+
slug: string;
|
|
869
|
+
/** A title-ish frontmatter value, when the entry has one. */
|
|
870
|
+
title: string | null;
|
|
871
|
+
/** Last-modified time, ms since the epoch. */
|
|
872
|
+
mtime: number;
|
|
873
|
+
/** `draft: true` in the frontmatter. */
|
|
874
|
+
draft: boolean;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
export interface CollectionEntriesResponse {
|
|
878
|
+
collection: string;
|
|
879
|
+
dir: string;
|
|
880
|
+
entries: CollectionEntryItem[];
|
|
881
|
+
/** True when the directory held more entries than the endpoint will read. The
|
|
882
|
+
* panel says so rather than presenting a partial list as complete. */
|
|
883
|
+
truncated?: boolean;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// --- POST /collection/page-editing -------------------------------------------
|
|
887
|
+
/**
|
|
888
|
+
* Switch a collection's in-page entry drawer on or off.
|
|
889
|
+
*
|
|
890
|
+
* Its own route rather than a corner of `/collection/schema/apply`, whose
|
|
891
|
+
* `overrides` are keyed **per field**: this is one flag about the collection.
|
|
892
|
+
* It writes the editor half (`.astro-dev-edit.json`), so it is gated on
|
|
893
|
+
* `entryEditor` and deliberately **not** on `schemaEditor` — no committed
|
|
894
|
+
* source is touched.
|
|
895
|
+
*/
|
|
896
|
+
export interface CollectionPageEditingRequest {
|
|
897
|
+
collection: string;
|
|
898
|
+
enabled: boolean;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export interface CollectionPageEditingResponse {
|
|
902
|
+
ok: boolean;
|
|
903
|
+
/** The value now in force, re-resolved after the write. */
|
|
904
|
+
pageEditing: boolean;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// --- POST /collection/open ---------------------------------------------------
|
|
908
|
+
/** Launch the editor on the content config. Carries **no path**: the server
|
|
909
|
+
* opens the config it discovered, optionally at a collection's own line. */
|
|
910
|
+
export interface CollectionOpenRequest {
|
|
911
|
+
collection?: string;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// --- POST /collection/create -------------------------------------------------
|
|
915
|
+
export interface CollectionCreateRequest {
|
|
916
|
+
/** Identifier-safe; it becomes a `const` name and a registry key. */
|
|
917
|
+
name: string;
|
|
918
|
+
/** Repo-relative entry directory. Defaults to `src/content/<name>`. */
|
|
919
|
+
dir?: string;
|
|
920
|
+
/** Glob pattern for the loader. Defaults to `**\/*.md`. */
|
|
921
|
+
pattern?: string;
|
|
922
|
+
/** Which form to write the schema in. `function` puts Astro's `image()`
|
|
923
|
+
* helper in scope. Defaults to `object`; a spec holding an image field is
|
|
924
|
+
* promoted whatever this says, since the other combination cannot compile. */
|
|
925
|
+
schemaForm?: SchemaForm;
|
|
926
|
+
fields: SchemaFieldSpec[];
|
|
927
|
+
etag?: string;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
export interface CollectionCreateResponse {
|
|
931
|
+
name: string;
|
|
932
|
+
/** The directory that was created. */
|
|
933
|
+
dir: string;
|
|
934
|
+
etag: string;
|
|
935
|
+
}
|