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
@@ -0,0 +1,396 @@
1
+ import { gfmToMarkdown } from "mdast-util-gfm";
2
+ import { mdxToMarkdown } from "mdast-util-mdx";
3
+ import { toMarkdown } from "mdast-util-to-markdown";
4
+ import { isPlainString, valueToSource } from "./expressions.js";
5
+ export function serializeMdx(doc, options = {}) {
6
+ const isMdx = options.format !== "md";
7
+ const used = new Set();
8
+ const root = {
9
+ type: "root",
10
+ children: blocks(doc.content ?? [], used),
11
+ };
12
+ const imports = isMdx ? importStatements(used, options) : [];
13
+ if (imports.length) {
14
+ root.children.unshift({
15
+ type: "mdxjsEsm",
16
+ value: imports.join("\n"),
17
+ });
18
+ }
19
+ const mdx = mdxToMarkdown({ quote: '"' });
20
+ const libFlow = findHandler(mdx, "mdxJsxFlowElement");
21
+ let out = toMarkdown(root, {
22
+ extensions: isMdx ? [mdx, gfmToMarkdown()] : [gfmToMarkdown()],
23
+ bullet: "-",
24
+ bulletOther: "*",
25
+ emphasis: "_",
26
+ strong: "*",
27
+ fences: true,
28
+ rule: "-",
29
+ listItemIndent: "one",
30
+ resourceLink: true,
31
+ handlers: {
32
+ rawHtml: (node) => node.value,
33
+ rawInlineHtml: (node) => node.value,
34
+ ...(libFlow ? { mdxJsxFlowElement: flowElementHandler(libFlow) } : {}),
35
+ },
36
+ });
37
+ // Place a blank line after the import block like Prettier/MDX conventions.
38
+ out = out.replace(/\s+$/, "");
39
+ return out ? `${out}\n` : "";
40
+ }
41
+ function findHandler(ext, name) {
42
+ const own = ext.handlers?.[name];
43
+ if (typeof own === "function")
44
+ return own;
45
+ for (const sub of ext.extensions ?? []) {
46
+ const found = findHandler(sub, name);
47
+ if (found)
48
+ return found;
49
+ }
50
+ return undefined;
51
+ }
52
+ /**
53
+ * Block components with children are written with blank lines around the
54
+ * children and no indentation:
55
+ *
56
+ * <Article>
57
+ *
58
+ * ## Heading
59
+ *
60
+ * </Article>
61
+ *
62
+ * The library default indents children, which Prettier's MDX parser then
63
+ * reflows as JSX text.
64
+ */
65
+ function flowElementHandler(lib) {
66
+ return (node, parent, state, info) => {
67
+ const el = node;
68
+ if (!el.children?.length)
69
+ return lib(node, parent, state, info);
70
+ const open = lib({ ...el, children: [] }, parent, state, info).replace(/\s*\/>$/, ">");
71
+ const children = state.containerFlow(el, info);
72
+ return `${open}\n\n${children}\n\n</${el.name ?? ""}>`;
73
+ };
74
+ }
75
+ function importStatements(used, options) {
76
+ const lines = new Map();
77
+ for (const name of used) {
78
+ const reg = options.components?.[name];
79
+ const spec = reg?.import
80
+ ? { name, from: reg.import, named: reg.namedImport }
81
+ : options.existingImports?.[name];
82
+ if (!spec)
83
+ continue;
84
+ const line = spec.named
85
+ ? `import { ${spec.name} } from "${spec.from}";`
86
+ : `import ${spec.name} from "${spec.from}";`;
87
+ lines.set(name, line);
88
+ }
89
+ const originalOrder = Object.keys(options.existingImports ?? {});
90
+ return [...lines.entries()]
91
+ .sort(([a], [b]) => {
92
+ const ia = originalOrder.indexOf(a);
93
+ const ib = originalOrder.indexOf(b);
94
+ if (ia !== -1 && ib !== -1)
95
+ return ia - ib;
96
+ if (ia !== -1)
97
+ return -1;
98
+ if (ib !== -1)
99
+ return 1;
100
+ return a.localeCompare(b);
101
+ })
102
+ .map(([, l]) => l);
103
+ }
104
+ function blocks(nodes, used) {
105
+ const out = [];
106
+ for (const node of nodes) {
107
+ const converted = block(node, used);
108
+ if (converted)
109
+ out.push(converted);
110
+ }
111
+ return out;
112
+ }
113
+ function block(node, used) {
114
+ switch (node.type) {
115
+ case "paragraph": {
116
+ const children = inlines(node.content ?? [], used);
117
+ if (!children.length)
118
+ return null;
119
+ return { type: "paragraph", children: children };
120
+ }
121
+ case "heading":
122
+ return {
123
+ type: "heading",
124
+ depth: clampDepth(Number(node.attrs?.level ?? 1)),
125
+ children: inlines(node.content ?? [], used),
126
+ };
127
+ case "horizontalRule":
128
+ return { type: "thematicBreak" };
129
+ case "blockquote":
130
+ return {
131
+ type: "blockquote",
132
+ children: blocks(node.content ?? [], used),
133
+ };
134
+ case "bulletList":
135
+ case "orderedList": {
136
+ const ordered = node.type === "orderedList";
137
+ const items = (node.content ?? []).map((item) => {
138
+ const checked = item.attrs?.checked;
139
+ return {
140
+ type: "listItem",
141
+ spread: false,
142
+ checked: typeof checked === "boolean" ? checked : null,
143
+ children: blocks(item.content ?? [], used),
144
+ };
145
+ });
146
+ return {
147
+ type: "list",
148
+ ordered,
149
+ start: ordered ? Number(node.attrs?.start ?? 1) : null,
150
+ spread: false,
151
+ children: items,
152
+ };
153
+ }
154
+ case "codeBlock":
155
+ return {
156
+ type: "code",
157
+ lang: node.attrs?.language ?? null,
158
+ meta: node.attrs?.meta ?? null,
159
+ value: (node.content ?? []).map((t) => t.text ?? "").join(""),
160
+ };
161
+ case "rawHtml":
162
+ return { type: "html", value: String(node.attrs?.value ?? "") };
163
+ case "table":
164
+ return table(node, used);
165
+ case "mdxComponent":
166
+ return jsxElement(node, used, false);
167
+ case "mdxExpression":
168
+ return {
169
+ type: "mdxFlowExpression",
170
+ value: String(node.attrs?.value ?? ""),
171
+ };
172
+ case "mdxEsm":
173
+ return { type: "mdxjsEsm", value: String(node.attrs?.value ?? "") };
174
+ case "image":
175
+ return {
176
+ type: "paragraph",
177
+ children: inlines([node], used),
178
+ };
179
+ default: {
180
+ const children = inlines([node], used);
181
+ return children.length
182
+ ? { type: "paragraph", children: children }
183
+ : null;
184
+ }
185
+ }
186
+ }
187
+ function clampDepth(n) {
188
+ return Math.min(6, Math.max(1, Math.round(n)));
189
+ }
190
+ function table(node, used) {
191
+ const rows = node.content ?? [];
192
+ const align = [];
193
+ const children = rows.map((row, rowIdx) => ({
194
+ type: "tableRow",
195
+ children: (row.content ?? []).map((cell, colIdx) => {
196
+ if (rowIdx === 0)
197
+ align[colIdx] = cell.attrs?.align ?? null;
198
+ const paragraphs = cell.content ?? [];
199
+ const inline = [];
200
+ paragraphs.forEach((p, i) => {
201
+ if (i > 0)
202
+ inline.push({ type: "hardBreak" });
203
+ inline.push(...(p.type === "paragraph" ? (p.content ?? []) : [p]));
204
+ });
205
+ return {
206
+ type: "tableCell",
207
+ children: inlines(inline, used),
208
+ };
209
+ }),
210
+ }));
211
+ return { type: "table", align, children };
212
+ }
213
+ function propsToAttributes(node, inline = false) {
214
+ const props = (node.attrs?.props ?? {});
215
+ const spreads = (node.attrs?.spreads ?? []);
216
+ const attrs = [];
217
+ for (const [name, value] of Object.entries(props)) {
218
+ if (value === undefined)
219
+ continue;
220
+ if (value === true) {
221
+ attrs.push({ type: "mdxJsxAttribute", name, value: null });
222
+ }
223
+ else if (isPlainString(value)) {
224
+ attrs.push({ type: "mdxJsxAttribute", name, value });
225
+ }
226
+ else {
227
+ attrs.push({
228
+ type: "mdxJsxAttribute",
229
+ name,
230
+ value: {
231
+ type: "mdxJsxAttributeValueExpression",
232
+ // Block components print each attribute on its own line, indented
233
+ // two spaces, so nested lines are indented relative to that.
234
+ value: valueToSource(value, inline ? "" : " "),
235
+ },
236
+ });
237
+ }
238
+ }
239
+ for (const s of spreads)
240
+ attrs.push({ type: "mdxJsxExpressionAttribute", value: s });
241
+ return attrs;
242
+ }
243
+ function jsxElement(node, used, inline) {
244
+ const name = String(node.attrs?.name ?? "");
245
+ if (name && /^[A-Z]/.test(name))
246
+ used.add(name);
247
+ const attributes = propsToAttributes(node, inline);
248
+ if (inline) {
249
+ return {
250
+ type: "mdxJsxTextElement",
251
+ name: name || null,
252
+ attributes,
253
+ children: inlines(node.content ?? [], used),
254
+ };
255
+ }
256
+ return {
257
+ type: "mdxJsxFlowElement",
258
+ name: name || null,
259
+ attributes,
260
+ children: blocks(node.content ?? [], used),
261
+ };
262
+ }
263
+ const MARK_ORDER = [
264
+ "link",
265
+ "superscript",
266
+ "subscript",
267
+ "bold",
268
+ "italic",
269
+ "strike",
270
+ "code",
271
+ ];
272
+ /**
273
+ * Convert a flat run of inline PM nodes (text with marks) into nested mdast
274
+ * phrasing content. At each step the mark covering the longest run of
275
+ * following nodes becomes the outer wrapper, so `**a <sup>6</sup> b**`
276
+ * keeps bold outermost.
277
+ */
278
+ function inlines(nodes, used) {
279
+ return group(nodes, used);
280
+ }
281
+ function markKey(m) {
282
+ return JSON.stringify([m.type, m.attrs ?? {}]);
283
+ }
284
+ function hasMark(node, key) {
285
+ return (node.marks ?? []).some((m) => markKey(m) === key);
286
+ }
287
+ function stripMark(node, key) {
288
+ return {
289
+ ...node,
290
+ marks: (node.marks ?? []).filter((m) => markKey(m) !== key),
291
+ };
292
+ }
293
+ function group(nodes, used) {
294
+ const out = [];
295
+ let i = 0;
296
+ while (i < nodes.length) {
297
+ const node = nodes[i];
298
+ const marks = node.marks ?? [];
299
+ if (!marks.length) {
300
+ out.push(...leaf(node, used));
301
+ i++;
302
+ continue;
303
+ }
304
+ // Pick the mark with the longest run from here.
305
+ let best = null;
306
+ let bestLen = 0;
307
+ for (const m of marks) {
308
+ const key = markKey(m);
309
+ let j = i;
310
+ while (j < nodes.length && hasMark(nodes[j], key))
311
+ j++;
312
+ const len = j - i;
313
+ const better = len > bestLen ||
314
+ (len === bestLen &&
315
+ best !== null &&
316
+ MARK_ORDER.indexOf(m.type) <
317
+ MARK_ORDER.indexOf(best.type));
318
+ if (better) {
319
+ best = m;
320
+ bestLen = len;
321
+ }
322
+ }
323
+ const key = markKey(best);
324
+ const run = nodes.slice(i, i + bestLen).map((n) => stripMark(n, key));
325
+ const children = group(run, used);
326
+ out.push(wrap(best, children));
327
+ i += bestLen;
328
+ }
329
+ return out;
330
+ }
331
+ function wrap(mark, children) {
332
+ switch (mark.type) {
333
+ case "bold":
334
+ return { type: "strong", children };
335
+ case "italic":
336
+ return { type: "emphasis", children };
337
+ case "strike":
338
+ return { type: "delete", children };
339
+ case "link":
340
+ return {
341
+ type: "link",
342
+ url: String(mark.attrs?.href ?? ""),
343
+ title: mark.attrs?.title ?? null,
344
+ children,
345
+ };
346
+ case "superscript":
347
+ return {
348
+ type: "mdxJsxTextElement",
349
+ name: "sup",
350
+ attributes: [],
351
+ children,
352
+ };
353
+ case "subscript":
354
+ return {
355
+ type: "mdxJsxTextElement",
356
+ name: "sub",
357
+ attributes: [],
358
+ children,
359
+ };
360
+ case "code": {
361
+ const text = children.map((c) => ("value" in c ? c.value : "")).join("");
362
+ return { type: "inlineCode", value: text };
363
+ }
364
+ default:
365
+ return { type: "paragraph", children };
366
+ }
367
+ }
368
+ function leaf(node, used) {
369
+ switch (node.type) {
370
+ case "text":
371
+ return node.text ? [{ type: "text", value: node.text }] : [];
372
+ case "hardBreak":
373
+ return [{ type: "break" }];
374
+ case "image":
375
+ return [
376
+ {
377
+ type: "image",
378
+ url: String(node.attrs?.src ?? ""),
379
+ alt: node.attrs?.alt ?? null,
380
+ title: node.attrs?.title ?? null,
381
+ },
382
+ ];
383
+ case "rawInlineHtml":
384
+ return [{ type: "html", value: String(node.attrs?.value ?? "") }];
385
+ case "mdxInlineComponent":
386
+ return [jsxElement(node, used, true)];
387
+ case "mdxInlineExpression":
388
+ return [
389
+ { type: "mdxTextExpression", value: String(node.attrs?.value ?? "") },
390
+ ];
391
+ case "mdxComponent":
392
+ return [jsxElement(node, used, true)];
393
+ default:
394
+ return node.content ? inlines(node.content, used) : [];
395
+ }
396
+ }
@@ -0,0 +1,40 @@
1
+ /** Minimal ProseMirror JSON shapes, shared by the converter and the editor. */
2
+ export interface PMMark {
3
+ type: string;
4
+ attrs?: Record<string, unknown>;
5
+ }
6
+ export interface PMNode {
7
+ type: string;
8
+ attrs?: Record<string, unknown>;
9
+ content?: PMNode[];
10
+ marks?: PMMark[];
11
+ text?: string;
12
+ }
13
+ /** A non-literal JSX attribute value, kept verbatim as source. */
14
+ export interface RawExpression {
15
+ $raw: string;
16
+ }
17
+ export declare function isRawExpression(v: unknown): v is RawExpression;
18
+ export type PropValue = unknown;
19
+ export interface ImportSpec {
20
+ /** Local identifier, e.g. `Highlights`. */
21
+ name: string;
22
+ /** Module specifier. */
23
+ from: string;
24
+ /** Default or named import. */
25
+ named?: boolean;
26
+ }
27
+ export interface ParsedDocument {
28
+ doc: PMNode;
29
+ /** Imports found in the source, keyed by local name. */
30
+ imports: Record<string, ImportSpec>;
31
+ }
32
+ export interface ComponentRegistry {
33
+ /** Component name → import spec to emit when it is used. */
34
+ [name: string]: {
35
+ import?: string;
36
+ namedImport?: boolean;
37
+ inline?: boolean;
38
+ children?: boolean | "text";
39
+ };
40
+ }
@@ -0,0 +1,6 @@
1
+ export function isRawExpression(v) {
2
+ return (typeof v === "object" &&
3
+ v !== null &&
4
+ "$raw" in v &&
5
+ typeof v.$raw === "string");
6
+ }
@@ -0,0 +1,27 @@
1
+ ---
2
+ /// <reference path="../env.d.ts" />
3
+ import { basePath, name } from "virtual:vestry/runtime";
4
+
5
+ export const prerender = false;
6
+ ---
7
+
8
+ <!doctype html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ <meta name="robots" content="noindex, nofollow" />
14
+ <meta name="color-scheme" content="light" />
15
+ <title>{name}</title>
16
+ <link
17
+ rel="icon"
18
+ href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%231e293b'/%3E%3Cpath d='M9 9l7 14 7-14' stroke='%23f8fafc' stroke-width='3.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"
19
+ />
20
+ </head>
21
+ <body>
22
+ <div id="vestry-root" data-base={basePath} data-name={name}></div>
23
+ <script>
24
+ import "../ui/main.js";
25
+ </script>
26
+ </body>
27
+ </html>
@@ -0,0 +1,3 @@
1
+ import type { APIRoute } from "astro";
2
+ export declare const prerender = false;
3
+ export declare const ALL: APIRoute;
@@ -0,0 +1,31 @@
1
+ import config from "virtual:vestry/config";
2
+ import index from "virtual:vestry/index";
3
+ import { basePath } from "virtual:vestry/runtime";
4
+ import { createApi } from "../core/api.js";
5
+ export const prerender = false;
6
+ function readEnv(key) {
7
+ // Cloudflare (nodejs_compat populates process.env), Node adapters, and
8
+ // anything else that exposes a process.env object.
9
+ const fromProcess = globalThis.process?.env?.[key];
10
+ if (fromProcess !== undefined)
11
+ return fromProcess;
12
+ const fromMeta = import.meta.env?.[key];
13
+ return fromMeta;
14
+ }
15
+ const api = createApi({ config, index, basePath, env: readEnv });
16
+ /**
17
+ * Cloudflare bindings (the D1 database) come from `cloudflare:workers`; the
18
+ * adapter's `locals.runtime.env` was removed in Astro 6 and now throws.
19
+ * Imported lazily and by variable so other runtimes just get no bindings.
20
+ */
21
+ async function bindings() {
22
+ try {
23
+ const specifier = "cloudflare:workers";
24
+ const mod = (await import(/* @vite-ignore */ specifier));
25
+ return mod.env;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ export const ALL = async ({ request }) => api.handle(request, { bindings: await bindings() });
@@ -0,0 +1,4 @@
1
+ export declare function App({ basePath, name }: {
2
+ basePath: string;
3
+ name: string;
4
+ }): import("react").JSX.Element;
package/dist/ui/App.js ADDED
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { QueryClient, QueryClientProvider, useQuery, } from "@tanstack/react-query";
3
+ import { useEffect, useMemo } from "react";
4
+ import { BrowserRouter, Navigate, Route, Routes, useLocation, } from "react-router";
5
+ import { createClient, UNAUTHORIZED_EVENT } from "./api.js";
6
+ import { createVestryAuthClient } from "./auth-client.js";
7
+ import { Shell } from "./components/Layout.js";
8
+ import { Alert, Loading, ToastProvider } from "./components/ui.js";
9
+ import { VestryContext } from "./context.js";
10
+ import { AssetsPage } from "./pages/Assets.js";
11
+ import { CollectionListPage } from "./pages/CollectionList.js";
12
+ import { DashboardPage } from "./pages/Dashboard.js";
13
+ import { CollectionEntryPage, NewEntryPage, SingletonPage, } from "./pages/EntryEditor.js";
14
+ import { LoginPage } from "./pages/Login.js";
15
+ import { UsersPage } from "./pages/Users.js";
16
+ export function App({ basePath, name }) {
17
+ const queryClient = useMemo(() => new QueryClient({
18
+ defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
19
+ }), []);
20
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ToastProvider, { children: _jsx(BrowserRouter, { basename: basePath, children: _jsx(Root, { basePath: basePath, name: name }) }) }) }));
21
+ }
22
+ function Root({ basePath, name }) {
23
+ const api = useMemo(() => createClient(basePath), [basePath]);
24
+ const authClient = useMemo(() => createVestryAuthClient(basePath), [basePath]);
25
+ const location = useLocation();
26
+ const configQuery = useQuery({
27
+ queryKey: ["config"],
28
+ queryFn: () => api.config(),
29
+ staleTime: Infinity,
30
+ });
31
+ // When a session expires mid-use the API answers 401; reloading the config
32
+ // drops the user and the router falls back to the sign-in page.
33
+ const refetch = configQuery.refetch;
34
+ useEffect(() => {
35
+ const onUnauthorized = () => void refetch();
36
+ window.addEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
37
+ return () => window.removeEventListener(UNAUTHORIZED_EVENT, onUnauthorized);
38
+ }, [refetch]);
39
+ if (configQuery.isLoading)
40
+ return _jsx(Loading, { label: "Loading Vestry\u2026" });
41
+ if (configQuery.error || !configQuery.data) {
42
+ return (_jsx("div", { className: "v-page", children: _jsxs(Alert, { kind: "error", children: ["Could not load the Vestry configuration:", " ", configQuery.error?.message] }) }));
43
+ }
44
+ const client = configQuery.data;
45
+ const value = {
46
+ basePath,
47
+ name: client.config.name ?? name,
48
+ api,
49
+ authClient,
50
+ client,
51
+ config: client.config,
52
+ refresh: async () => {
53
+ await configQuery.refetch();
54
+ },
55
+ };
56
+ if (!client.user) {
57
+ return (_jsx(VestryContext.Provider, { value: value, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/login", element: _jsx(LoginPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: `/login?next=${encodeURIComponent(basePath + location.pathname + location.search)}`, replace: true }) })] }) }));
58
+ }
59
+ return (_jsx(VestryContext.Provider, { value: value, children: _jsxs(Routes, { children: [_jsx(Route, { path: "/login", element: _jsx(Navigate, { to: afterLogin(location.search, basePath), replace: true }) }), _jsx(Route, { path: "*", element: _jsx(Shell, { children: _jsxs(Routes, { children: [_jsx(Route, { index: true, element: _jsx(DashboardPage, {}) }), _jsx(Route, { path: "collections/:collection", element: _jsx(CollectionListPage, {}) }), _jsx(Route, { path: "collections/:collection/new", element: _jsx(NewEntryPage, {}) }), _jsx(Route, { path: "collections/:collection/*", element: _jsx(CollectionEntryPage, {}) }), _jsx(Route, { path: "singletons/:singleton", element: _jsx(SingletonPage, {}) }), _jsx(Route, { path: "assets/:container", element: _jsx(AssetsPage, {}) }), _jsx(Route, { path: "users", element: _jsx(UsersPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) })] }) }) })] }) }));
60
+ }
61
+ /** Where to go after sign-in: the `next` the login page was sent with, made router-relative. */
62
+ function afterLogin(search, basePath) {
63
+ const next = new URLSearchParams(search).get("next");
64
+ if (!next || !next.startsWith("/") || next.startsWith("//"))
65
+ return "/";
66
+ const base = basePath.replace(/\/$/, "");
67
+ const underBase = next === base || next.startsWith(`${base}/`) || next.startsWith(`${base}?`);
68
+ const rest = underBase ? next.slice(base.length) : next;
69
+ return rest.startsWith("/") ? rest : `/${rest}`;
70
+ }
@@ -0,0 +1,83 @@
1
+ import type { ClientConfig } from "../core/api.js";
2
+ import type { PublicUser, Role } from "../core/roles.js";
3
+ import type { AssetItem, EntryDetail, EntrySummary, SaveEntryInput } from "../core/service.js";
4
+ export type { ClientConfig, AssetItem, EntryDetail, EntrySummary, SaveEntryInput, PublicUser, Role, };
5
+ /** Fired on `window` when the API answers 401, so the app can return to sign-in. */
6
+ export declare const UNAUTHORIZED_EVENT = "vestry:unauthorized";
7
+ export declare class ApiError extends Error {
8
+ status: number;
9
+ code: string;
10
+ constructor(message: string, status: number, code: string);
11
+ }
12
+ export declare function createClient(basePath: string): {
13
+ prefix: string;
14
+ fileUrl: (path: string, sha?: string) => string;
15
+ config: () => Promise<ClientConfig>;
16
+ status: () => Promise<{
17
+ head: string;
18
+ }>;
19
+ listUsers: () => Promise<{
20
+ users: PublicUser[];
21
+ }>;
22
+ inviteUser: (input: {
23
+ email: string;
24
+ name?: string;
25
+ role: Role;
26
+ }) => Promise<{
27
+ user: PublicUser;
28
+ mailError?: string;
29
+ }>;
30
+ updateUser: (id: string, patch: {
31
+ name?: string;
32
+ role?: Role;
33
+ }) => Promise<{
34
+ user: PublicUser;
35
+ }>;
36
+ removeUser: (id: string) => Promise<{
37
+ ok: true;
38
+ }>;
39
+ resendInvite: (id: string) => Promise<{
40
+ user: PublicUser;
41
+ }>;
42
+ listEntries: (collection: string) => Promise<{
43
+ entries: EntrySummary[];
44
+ head: string;
45
+ }>;
46
+ getEntry: (collection: string, slug: string) => Promise<EntryDetail>;
47
+ createEntry: (collection: string, input: SaveEntryInput) => Promise<{
48
+ slug: string;
49
+ sha: string;
50
+ commit: string;
51
+ path: string;
52
+ }>;
53
+ updateEntry: (collection: string, slug: string, input: SaveEntryInput) => Promise<{
54
+ slug: string;
55
+ sha: string;
56
+ commit: string;
57
+ path: string;
58
+ }>;
59
+ deleteEntry: (collection: string, slug: string) => Promise<{
60
+ commit: string;
61
+ }>;
62
+ getSingleton: (key: string) => Promise<EntryDetail>;
63
+ saveSingleton: (key: string, input: SaveEntryInput) => Promise<{
64
+ sha: string;
65
+ commit: string;
66
+ }>;
67
+ listAssets: (container: string, dir?: string) => Promise<{
68
+ items: AssetItem[];
69
+ folders: string[];
70
+ }>;
71
+ uploadAssets: (container: string, dir: string, files: {
72
+ name: string;
73
+ base64: string;
74
+ }[]) => Promise<{
75
+ commit: string;
76
+ paths: string[];
77
+ }>;
78
+ deleteAsset: (container: string, path: string) => Promise<{
79
+ commit: string;
80
+ }>;
81
+ };
82
+ export type ApiClient = ReturnType<typeof createClient>;
83
+ export declare function fileToBase64(file: File): Promise<string>;