vestrycms 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +348 -2
- package/dist/config/fields.d.ts +30 -0
- package/dist/config/fields.js +75 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.js +18 -0
- package/dist/config/types.d.ts +403 -0
- package/dist/config/types.js +10 -0
- package/dist/core/api.d.ts +67 -0
- package/dist/core/api.js +538 -0
- package/dist/core/auth.d.ts +1561 -0
- package/dist/core/auth.js +234 -0
- package/dist/core/cache.d.ts +27 -0
- package/dist/core/cache.js +52 -0
- package/dist/core/entries.d.ts +19 -0
- package/dist/core/entries.js +127 -0
- package/dist/core/index-types.d.ts +20 -0
- package/dist/core/index-types.js +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +13 -0
- package/dist/core/mail.d.ts +28 -0
- package/dist/core/mail.js +62 -0
- package/dist/core/paths.d.ts +35 -0
- package/dist/core/paths.js +148 -0
- package/dist/core/roles.d.ts +24 -0
- package/dist/core/roles.js +15 -0
- package/dist/core/service.d.ts +102 -0
- package/dist/core/service.js +478 -0
- package/dist/core/source.d.ts +82 -0
- package/dist/core/source.js +75 -0
- package/dist/core/sources/github-app.d.ts +26 -0
- package/dist/core/sources/github-app.js +161 -0
- package/dist/core/sources/github.d.ts +40 -0
- package/dist/core/sources/github.js +278 -0
- package/dist/core/sources/local.d.ts +16 -0
- package/dist/core/sources/local.js +98 -0
- package/dist/core/values.d.ts +22 -0
- package/dist/core/values.js +123 -0
- package/dist/env.d.ts +19 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +167 -0
- package/dist/integration/build-index.d.ts +8 -0
- package/dist/integration/build-index.js +67 -0
- package/dist/integration/client-deps.d.ts +10 -0
- package/dist/integration/client-deps.js +43 -0
- package/dist/integration/dev-middleware.d.ts +6 -0
- package/dist/integration/dev-middleware.js +48 -0
- package/dist/integration/formatter.d.ts +7 -0
- package/dist/integration/formatter.js +56 -0
- package/dist/mdx/expressions.d.ts +14 -0
- package/dist/mdx/expressions.js +141 -0
- package/dist/mdx/index.d.ts +4 -0
- package/dist/mdx/index.js +4 -0
- package/dist/mdx/parse.d.ts +6 -0
- package/dist/mdx/parse.js +378 -0
- package/dist/mdx/serialize.d.ts +9 -0
- package/dist/mdx/serialize.js +396 -0
- package/dist/mdx/types.d.ts +40 -0
- package/dist/mdx/types.js +6 -0
- package/dist/pages/admin.astro +27 -0
- package/dist/pages/api.d.ts +3 -0
- package/dist/pages/api.js +31 -0
- package/dist/ui/App.d.ts +4 -0
- package/dist/ui/App.js +70 -0
- package/dist/ui/api.d.ts +83 -0
- package/dist/ui/api.js +97 -0
- package/dist/ui/auth-client.d.ts +27 -0
- package/dist/ui/auth-client.js +29 -0
- package/dist/ui/components/AssetPicker.d.ts +27 -0
- package/dist/ui/components/AssetPicker.js +97 -0
- package/dist/ui/components/Icon.d.ts +11 -0
- package/dist/ui/components/Icon.js +46 -0
- package/dist/ui/components/Layout.d.ts +15 -0
- package/dist/ui/components/Layout.js +87 -0
- package/dist/ui/components/editor/EditorContext.d.ts +15 -0
- package/dist/ui/components/editor/EditorContext.js +9 -0
- package/dist/ui/components/editor/NodeViews.d.ts +4 -0
- package/dist/ui/components/editor/NodeViews.js +127 -0
- package/dist/ui/components/editor/RichTextEditor.d.ts +24 -0
- package/dist/ui/components/editor/RichTextEditor.js +253 -0
- package/dist/ui/components/editor/extensions.d.ts +20 -0
- package/dist/ui/components/editor/extensions.js +156 -0
- package/dist/ui/components/fields/CodeInput.d.ts +10 -0
- package/dist/ui/components/fields/CodeInput.js +8 -0
- package/dist/ui/components/fields/CodeMirrorInput.d.ts +2 -0
- package/dist/ui/components/fields/CodeMirrorInput.js +44 -0
- package/dist/ui/components/fields/FieldRenderer.d.ts +41 -0
- package/dist/ui/components/fields/FieldRenderer.js +299 -0
- package/dist/ui/components/fields/ImageTools.d.ts +31 -0
- package/dist/ui/components/fields/ImageTools.js +296 -0
- package/dist/ui/components/fields/MarkdownField.d.ts +7 -0
- package/dist/ui/components/fields/MarkdownField.js +16 -0
- package/dist/ui/components/fields/TreeInput.d.ts +13 -0
- package/dist/ui/components/fields/TreeInput.js +221 -0
- package/dist/ui/components/ui.d.ts +63 -0
- package/dist/ui/components/ui.js +113 -0
- package/dist/ui/context.d.ts +17 -0
- package/dist/ui/context.js +13 -0
- package/dist/ui/lib/format.d.ts +5 -0
- package/dist/ui/lib/format.js +11 -0
- package/dist/ui/lib/utils.d.ts +24 -0
- package/dist/ui/lib/utils.js +255 -0
- package/dist/ui/main.d.ts +1 -0
- package/dist/ui/main.js +11 -0
- package/dist/ui/pages/Assets.d.ts +1 -0
- package/dist/ui/pages/Assets.js +69 -0
- package/dist/ui/pages/CollectionList.d.ts +6 -0
- package/dist/ui/pages/CollectionList.js +115 -0
- package/dist/ui/pages/Dashboard.d.ts +1 -0
- package/dist/ui/pages/Dashboard.js +57 -0
- package/dist/ui/pages/EntryEditor.d.ts +3 -0
- package/dist/ui/pages/EntryEditor.js +313 -0
- package/dist/ui/pages/Login.d.ts +6 -0
- package/dist/ui/pages/Login.js +88 -0
- package/dist/ui/pages/Users.d.ts +1 -0
- package/dist/ui/pages/Users.js +102 -0
- package/dist/ui/styles.css +2536 -0
- package/package.json +121 -3
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { basename, dirname, joinPath, normalizePath, relativePath, resolveRelative, } from "./paths.js";
|
|
2
|
+
export function walkValues(schema, data, fn) {
|
|
3
|
+
const out = { ...data };
|
|
4
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
5
|
+
if (!(key in out))
|
|
6
|
+
continue;
|
|
7
|
+
out[key] = walkValue(field, out[key], fn);
|
|
8
|
+
}
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
function walkValue(field, value, fn) {
|
|
12
|
+
if (field.type === "object" &&
|
|
13
|
+
value &&
|
|
14
|
+
typeof value === "object" &&
|
|
15
|
+
!Array.isArray(value)) {
|
|
16
|
+
return walkValues(field.fields, value, fn);
|
|
17
|
+
}
|
|
18
|
+
if (field.type === "array" && Array.isArray(value)) {
|
|
19
|
+
return value.map((v) => walkValue(field.of, v, fn));
|
|
20
|
+
}
|
|
21
|
+
if (field.type === "tree" && Array.isArray(value)) {
|
|
22
|
+
const key = field.childrenKey ?? "children";
|
|
23
|
+
const walkNodes = (nodes) => nodes.map((n) => {
|
|
24
|
+
if (!n || typeof n !== "object")
|
|
25
|
+
return n;
|
|
26
|
+
const node = walkValues(field.fields, n, fn);
|
|
27
|
+
const children = n[key];
|
|
28
|
+
if (Array.isArray(children))
|
|
29
|
+
node[key] = walkNodes(children);
|
|
30
|
+
return node;
|
|
31
|
+
});
|
|
32
|
+
return walkNodes(value);
|
|
33
|
+
}
|
|
34
|
+
return fn(field, value);
|
|
35
|
+
}
|
|
36
|
+
export function defaultStorage(field) {
|
|
37
|
+
if (field.storeAs)
|
|
38
|
+
return field.storeAs;
|
|
39
|
+
return normalizePath(field.directory).startsWith("public/")
|
|
40
|
+
? "public"
|
|
41
|
+
: "relative";
|
|
42
|
+
}
|
|
43
|
+
/** Stored reference (relative path, bare filename, public URL) → repository path. */
|
|
44
|
+
export function assetToRepoPath(field, value, filePath) {
|
|
45
|
+
const dir = normalizePath(field.directory);
|
|
46
|
+
if (/^https?:\/\//.test(value))
|
|
47
|
+
return value;
|
|
48
|
+
if (value.startsWith("/")) {
|
|
49
|
+
if (dir.startsWith("public/")) {
|
|
50
|
+
const publicRoot = dir.slice("public/".length);
|
|
51
|
+
const rel = value.replace(/^\//, "");
|
|
52
|
+
return rel.startsWith(publicRoot)
|
|
53
|
+
? `public/${rel}`
|
|
54
|
+
: joinPath(dir, basename(rel));
|
|
55
|
+
}
|
|
56
|
+
return normalizePath(value);
|
|
57
|
+
}
|
|
58
|
+
if (value.startsWith("."))
|
|
59
|
+
return resolveRelative(dirname(filePath), value);
|
|
60
|
+
if (value.startsWith(dir))
|
|
61
|
+
return normalizePath(value);
|
|
62
|
+
if (value.includes("/") &&
|
|
63
|
+
(value.startsWith("src/") || value.startsWith("public/"))) {
|
|
64
|
+
return normalizePath(value);
|
|
65
|
+
}
|
|
66
|
+
return joinPath(dir, value);
|
|
67
|
+
}
|
|
68
|
+
/** Repository path → the reference written into the content file. */
|
|
69
|
+
export function repoPathToAsset(field, repoPath, filePath) {
|
|
70
|
+
if (/^https?:\/\//.test(repoPath))
|
|
71
|
+
return repoPath;
|
|
72
|
+
const n = normalizePath(repoPath);
|
|
73
|
+
switch (defaultStorage(field)) {
|
|
74
|
+
case "filename":
|
|
75
|
+
return basename(n);
|
|
76
|
+
case "public":
|
|
77
|
+
return n.startsWith("public/")
|
|
78
|
+
? `/${n.slice("public/".length)}`
|
|
79
|
+
: `/${n}`;
|
|
80
|
+
case "repo":
|
|
81
|
+
return n;
|
|
82
|
+
case "relative":
|
|
83
|
+
default:
|
|
84
|
+
return relativePath(dirname(filePath), n);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Convert stored values to the editor's canonical representation (repo paths, ISO strings). */
|
|
88
|
+
export function toEditorValues(schema, data, filePath) {
|
|
89
|
+
return walkValues(schema, data, (field, value) => {
|
|
90
|
+
if ((field.type === "image" || field.type === "file") &&
|
|
91
|
+
typeof value === "string" &&
|
|
92
|
+
value) {
|
|
93
|
+
return assetToRepoPath(field, value, filePath);
|
|
94
|
+
}
|
|
95
|
+
if (field.type === "date" && value instanceof Date)
|
|
96
|
+
return value.toISOString().slice(0, 10);
|
|
97
|
+
if (field.type === "datetime" && value instanceof Date)
|
|
98
|
+
return value.toISOString();
|
|
99
|
+
return value;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
export function toStoredValues(schema, data, filePath) {
|
|
103
|
+
return walkValues(schema, data, (field, value) => {
|
|
104
|
+
if ((field.type === "image" || field.type === "file") &&
|
|
105
|
+
typeof value === "string" &&
|
|
106
|
+
value) {
|
|
107
|
+
return repoPathToAsset(field, value, filePath);
|
|
108
|
+
}
|
|
109
|
+
return value;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function applyAutoFields(schema, data, now = new Date()) {
|
|
113
|
+
const out = { ...data };
|
|
114
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
115
|
+
if (field.type === "datetime" && field.autoUpdate) {
|
|
116
|
+
out[key] =
|
|
117
|
+
field.storage === "local"
|
|
118
|
+
? now.toISOString().slice(0, 16)
|
|
119
|
+
: now.toISOString();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare module "virtual:vestry/config" {
|
|
2
|
+
const config: import("./config/types.js").VestryConfig;
|
|
3
|
+
export default config;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
declare module "virtual:vestry/index" {
|
|
7
|
+
const index: import("./core/index-types.js").BuildIndex;
|
|
8
|
+
export default index;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare module "virtual:vestry/runtime" {
|
|
12
|
+
export const basePath: string;
|
|
13
|
+
export const name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module "virtual:vestry/formatter" {
|
|
17
|
+
export const available: boolean;
|
|
18
|
+
export function formatFile(text: string, filepath: string): Promise<string>;
|
|
19
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AstroIntegration } from "astro";
|
|
2
|
+
import type { VestryConfig } from "./config/types.js";
|
|
3
|
+
export type { VestryConfig } from "./config/types.js";
|
|
4
|
+
export { defineConfig, collection, singleton, component, assetContainer, fields, } from "./config/index.js";
|
|
5
|
+
export interface VestryIntegrationOptions {
|
|
6
|
+
/** URL path the control panel is served from. Defaults to `/admin`. */
|
|
7
|
+
base?: string;
|
|
8
|
+
/**
|
|
9
|
+
* What `astro dev` does. `local` (the default) edits the project directory
|
|
10
|
+
* with no sign-in. `auth` edits the project directory but requires sign-in,
|
|
11
|
+
* with users and sessions kept in `.vestry/auth.sqlite` and emails printed to
|
|
12
|
+
* the terminal, for working on the sign-in flow itself. `github` goes
|
|
13
|
+
* through the repository with the GitHub App credentials, as production
|
|
14
|
+
* does. `VESTRY_MODE` in the environment sets this too.
|
|
15
|
+
*/
|
|
16
|
+
mode?: "local" | "auth" | "github";
|
|
17
|
+
}
|
|
18
|
+
export default function vestry(config: VestryConfig, options?: VestryIntegrationOptions): AstroIntegration;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { loadEnv } from "vite";
|
|
6
|
+
import { createApi } from "./core/api.js";
|
|
7
|
+
import { buildIndex } from "./integration/build-index.js";
|
|
8
|
+
import { formatterModule } from "./integration/formatter.js";
|
|
9
|
+
import { nodeToFetchHandler } from "./integration/dev-middleware.js";
|
|
10
|
+
import { LocalSource } from "./core/sources/local.js";
|
|
11
|
+
import { consoleMailer } from "./core/mail.js";
|
|
12
|
+
import { clientDependencies } from "./integration/client-deps.js";
|
|
13
|
+
export { defineConfig, collection, singleton, component, assetContainer, fields, } from "./config/index.js";
|
|
14
|
+
const VIRTUAL = {
|
|
15
|
+
config: "virtual:vestry/config",
|
|
16
|
+
index: "virtual:vestry/index",
|
|
17
|
+
runtime: "virtual:vestry/runtime",
|
|
18
|
+
formatter: "virtual:vestry/formatter",
|
|
19
|
+
};
|
|
20
|
+
export default function vestry(config, options = {}) {
|
|
21
|
+
const basePath = `/${(options.base ?? "/admin").replace(/^\/+|\/+$/g, "")}`;
|
|
22
|
+
validateConfig(config);
|
|
23
|
+
return {
|
|
24
|
+
name: "vestry",
|
|
25
|
+
hooks: {
|
|
26
|
+
"astro:config:setup": ({ command, config: astroConfig, injectRoute, updateConfig, addWatchFile, logger, }) => {
|
|
27
|
+
const root = fileURLToPath(astroConfig.root);
|
|
28
|
+
// The config arrives as an object, so Astro can't know which file it
|
|
29
|
+
// came from. Watch the conventional file name so edits restart dev.
|
|
30
|
+
for (const name of [
|
|
31
|
+
"vestry.config.ts",
|
|
32
|
+
"vestry.config.mts",
|
|
33
|
+
"vestry.config.js",
|
|
34
|
+
"vestry.config.mjs",
|
|
35
|
+
]) {
|
|
36
|
+
const candidate = path.join(root, name);
|
|
37
|
+
if (existsSync(candidate))
|
|
38
|
+
addWatchFile(candidate);
|
|
39
|
+
}
|
|
40
|
+
const isDev = command === "dev";
|
|
41
|
+
const fileEnv = loadEnv(isDev ? "development" : "production", root, "");
|
|
42
|
+
const readEnv = (key) => process.env[key] ?? fileEnv[key];
|
|
43
|
+
const envMode = readEnv("VESTRY_MODE");
|
|
44
|
+
const mode = options.mode ??
|
|
45
|
+
(envMode === "github" || envMode === "auth"
|
|
46
|
+
? envMode
|
|
47
|
+
: isDev
|
|
48
|
+
? "local"
|
|
49
|
+
: "github");
|
|
50
|
+
let indexPromise = null;
|
|
51
|
+
const getIndex = () => (indexPromise ??= buildIndex(root, config));
|
|
52
|
+
const plugin = {
|
|
53
|
+
name: "vestry",
|
|
54
|
+
resolveId(id) {
|
|
55
|
+
if (Object.values(VIRTUAL).includes(id))
|
|
56
|
+
return `\0${id}`;
|
|
57
|
+
return null;
|
|
58
|
+
},
|
|
59
|
+
async load(id) {
|
|
60
|
+
if (id === `\0${VIRTUAL.config}`)
|
|
61
|
+
return `export default ${JSON.stringify(config)};`;
|
|
62
|
+
if (id === `\0${VIRTUAL.runtime}`) {
|
|
63
|
+
return `export const basePath = ${JSON.stringify(basePath)};\nexport const name = ${JSON.stringify(config.name ?? "Vestry")};`;
|
|
64
|
+
}
|
|
65
|
+
if (id === `\0${VIRTUAL.formatter}`)
|
|
66
|
+
return formatterModule(root);
|
|
67
|
+
if (id === `\0${VIRTUAL.index}`) {
|
|
68
|
+
// Dev edits bypass the index (the local source reads disk), so
|
|
69
|
+
// an empty index keeps dev startup instant.
|
|
70
|
+
const index = isDev
|
|
71
|
+
? { builtAt: "", files: {} }
|
|
72
|
+
: await getIndex();
|
|
73
|
+
return `export default ${JSON.stringify(index)};`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
},
|
|
77
|
+
configureServer(server) {
|
|
78
|
+
if (!isDev)
|
|
79
|
+
return;
|
|
80
|
+
const localSource = mode === "github" ? undefined : new LocalSource(root);
|
|
81
|
+
// Dev keeps users and sessions in a SQLite file and, unless a real
|
|
82
|
+
// mail provider is configured, prints sign-in codes to the terminal.
|
|
83
|
+
let auth;
|
|
84
|
+
if (mode !== "local") {
|
|
85
|
+
const dir = path.join(root, ".vestry");
|
|
86
|
+
mkdirSync(dir, { recursive: true });
|
|
87
|
+
// Loaded only here, so builds and plain local dev never touch
|
|
88
|
+
// node:sqlite. A CJS require rather than import(): Astro runs
|
|
89
|
+
// this file through Vite's module runner, which intercepts
|
|
90
|
+
// dynamic imports and has already closed by the time this runs.
|
|
91
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
92
|
+
auth = {
|
|
93
|
+
database: new DatabaseSync(path.join(dir, "auth.sqlite")),
|
|
94
|
+
mailer: config.mail?.provider === "resend" &&
|
|
95
|
+
readEnv("VESTRY_RESEND_API_KEY")
|
|
96
|
+
? undefined
|
|
97
|
+
: consoleMailer((line) => logger.info(line)),
|
|
98
|
+
required: true,
|
|
99
|
+
dev: true,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const api = createApi({
|
|
103
|
+
config,
|
|
104
|
+
index: { builtAt: "", files: {} },
|
|
105
|
+
basePath,
|
|
106
|
+
env: readEnv,
|
|
107
|
+
localSource,
|
|
108
|
+
auth,
|
|
109
|
+
});
|
|
110
|
+
const handler = nodeToFetchHandler(api.handle);
|
|
111
|
+
server.middlewares.use((req, res, next) => {
|
|
112
|
+
if (!req.url?.startsWith(`${api.apiPrefix}/`) &&
|
|
113
|
+
req.url !== api.apiPrefix)
|
|
114
|
+
return next();
|
|
115
|
+
return handler(req, res, next);
|
|
116
|
+
});
|
|
117
|
+
logger.info(`Control panel at ${basePath} (${mode === "local"
|
|
118
|
+
? "editing local files, no sign-in"
|
|
119
|
+
: mode === "auth"
|
|
120
|
+
? "editing local files, sign-in required"
|
|
121
|
+
: `GitHub ${config.repo.owner}/${config.repo.name}`})`);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
updateConfig({
|
|
125
|
+
vite: {
|
|
126
|
+
plugins: [plugin],
|
|
127
|
+
optimizeDeps: {
|
|
128
|
+
include: clientDependencies.map((dep) => `vestrycms > ${dep}`),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
injectRoute({
|
|
133
|
+
pattern: `${basePath}/api/[...path]`,
|
|
134
|
+
entrypoint: "vestrycms/pages/api",
|
|
135
|
+
prerender: false,
|
|
136
|
+
});
|
|
137
|
+
injectRoute({
|
|
138
|
+
pattern: `${basePath}/[...path]`,
|
|
139
|
+
entrypoint: "vestrycms/pages/admin.astro",
|
|
140
|
+
prerender: false,
|
|
141
|
+
});
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function validateConfig(config) {
|
|
147
|
+
if (!config.repo?.owner || !config.repo?.name) {
|
|
148
|
+
throw new Error("[vestry] config.repo.owner and config.repo.name are required");
|
|
149
|
+
}
|
|
150
|
+
for (const [key, col] of Object.entries(config.collections ?? {})) {
|
|
151
|
+
if (!col.path)
|
|
152
|
+
throw new Error(`[vestry] collection "${key}" needs a path`);
|
|
153
|
+
if (col.body) {
|
|
154
|
+
for (const name of col.body.components ?? []) {
|
|
155
|
+
if (!config.components?.[name]) {
|
|
156
|
+
throw new Error(`[vestry] collection "${key}" references unknown component "${name}"`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
JSON.stringify(config);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new Error("[vestry] config must be JSON-serializable (no functions)");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { VestryConfig } from "../config/types.js";
|
|
2
|
+
import type { BuildIndex } from "../core/index-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Scan every file-per-entry collection and record blob SHA + frontmatter.
|
|
5
|
+
* Runs at build time inside the integration (Node), so it can read the
|
|
6
|
+
* checked-out repository directly.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildIndex(root: string, config: VestryConfig): Promise<BuildIndex>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseEntry } from "../core/entries.js";
|
|
4
|
+
import { collectionFormat, normalizePath, parsePathPattern, slugFromPath, } from "../core/paths.js";
|
|
5
|
+
import { gitBlobSha } from "../core/source.js";
|
|
6
|
+
/**
|
|
7
|
+
* Scan every file-per-entry collection and record blob SHA + frontmatter.
|
|
8
|
+
* Runs at build time inside the integration (Node), so it can read the
|
|
9
|
+
* checked-out repository directly.
|
|
10
|
+
*/
|
|
11
|
+
export async function buildIndex(root, config) {
|
|
12
|
+
const files = {};
|
|
13
|
+
for (const col of Object.values(config.collections ?? {})) {
|
|
14
|
+
const pattern = parsePathPattern(col.path);
|
|
15
|
+
if (pattern.single)
|
|
16
|
+
continue;
|
|
17
|
+
const format = collectionFormat(col);
|
|
18
|
+
const dir = path.join(root, pattern.directory);
|
|
19
|
+
for (const rel of await walk(dir)) {
|
|
20
|
+
const repoPath = normalizePath(`${pattern.directory}/${rel}`);
|
|
21
|
+
if (slugFromPath(pattern, repoPath) === null)
|
|
22
|
+
continue;
|
|
23
|
+
try {
|
|
24
|
+
const buf = await fs.readFile(path.join(dir, rel));
|
|
25
|
+
const text = buf.toString("utf8");
|
|
26
|
+
const parsed = parseEntry(format, text);
|
|
27
|
+
files[repoPath] = {
|
|
28
|
+
sha: await gitBlobSha(new Uint8Array(buf)),
|
|
29
|
+
data: JSON.parse(JSON.stringify(parsed.data)),
|
|
30
|
+
excerpt: excerpt(parsed.body),
|
|
31
|
+
hasBody: parsed.body.trim().length > 0,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.warn(`[vestry] Could not index ${repoPath}: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { builtAt: new Date().toISOString(), files };
|
|
40
|
+
}
|
|
41
|
+
async function walk(dir, rel = "") {
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const out = [];
|
|
50
|
+
for (const e of entries) {
|
|
51
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
52
|
+
if (e.isDirectory())
|
|
53
|
+
out.push(...(await walk(path.join(dir, e.name), r)));
|
|
54
|
+
else if (e.isFile())
|
|
55
|
+
out.push(r);
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
function excerpt(body) {
|
|
60
|
+
const text = body
|
|
61
|
+
.replace(/^import .*$/gm, "")
|
|
62
|
+
.replace(/<[^>]+>/g, " ")
|
|
63
|
+
.replace(/[#*_>`[\]]/g, "")
|
|
64
|
+
.replace(/\s+/g, " ")
|
|
65
|
+
.trim();
|
|
66
|
+
return text ? text.slice(0, 200) : undefined;
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every package the control panel imports in the browser (from `ui/main`,
|
|
3
|
+
* following relative imports). An installed integration is excluded from
|
|
4
|
+
* Vite's dependency optimizer, and the optimizer then skips its imports too,
|
|
5
|
+
* so dev would serve these raw: CommonJS ones such as `react/jsx-runtime`
|
|
6
|
+
* fail to load. The integration pre-bundles them explicitly as
|
|
7
|
+
* `vestrycms > dep`, which resolves each from this package. `pnpm build`
|
|
8
|
+
* checks this list against the compiled UI's imports.
|
|
9
|
+
*/
|
|
10
|
+
export declare const clientDependencies: readonly ["@codemirror/lang-markdown", "@codemirror/lang-yaml", "@codemirror/view", "@dnd-kit/core", "@dnd-kit/modifiers", "@dnd-kit/sortable", "@dnd-kit/utilities", "@tanstack/react-query", "@tiptap/core", "@tiptap/extension-image", "@tiptap/extension-subscript", "@tiptap/extension-superscript", "@tiptap/extension-table", "@tiptap/extensions", "@tiptap/react", "@tiptap/starter-kit", "@uiw/react-codemirror", "better-auth/client", "better-auth/client/plugins", "lucide-react", "mdast-util-from-markdown", "mdast-util-gfm", "mdast-util-mdx", "mdast-util-to-markdown", "micromark-extension-gfm", "micromark-extension-mdxjs", "react", "react-dom", "react-dom/client", "react-router", "react/jsx-runtime", "yaml"];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every package the control panel imports in the browser (from `ui/main`,
|
|
3
|
+
* following relative imports). An installed integration is excluded from
|
|
4
|
+
* Vite's dependency optimizer, and the optimizer then skips its imports too,
|
|
5
|
+
* so dev would serve these raw: CommonJS ones such as `react/jsx-runtime`
|
|
6
|
+
* fail to load. The integration pre-bundles them explicitly as
|
|
7
|
+
* `vestrycms > dep`, which resolves each from this package. `pnpm build`
|
|
8
|
+
* checks this list against the compiled UI's imports.
|
|
9
|
+
*/
|
|
10
|
+
export const clientDependencies = [
|
|
11
|
+
"@codemirror/lang-markdown",
|
|
12
|
+
"@codemirror/lang-yaml",
|
|
13
|
+
"@codemirror/view",
|
|
14
|
+
"@dnd-kit/core",
|
|
15
|
+
"@dnd-kit/modifiers",
|
|
16
|
+
"@dnd-kit/sortable",
|
|
17
|
+
"@dnd-kit/utilities",
|
|
18
|
+
"@tanstack/react-query",
|
|
19
|
+
"@tiptap/core",
|
|
20
|
+
"@tiptap/extension-image",
|
|
21
|
+
"@tiptap/extension-subscript",
|
|
22
|
+
"@tiptap/extension-superscript",
|
|
23
|
+
"@tiptap/extension-table",
|
|
24
|
+
"@tiptap/extensions",
|
|
25
|
+
"@tiptap/react",
|
|
26
|
+
"@tiptap/starter-kit",
|
|
27
|
+
"@uiw/react-codemirror",
|
|
28
|
+
"better-auth/client",
|
|
29
|
+
"better-auth/client/plugins",
|
|
30
|
+
"lucide-react",
|
|
31
|
+
"mdast-util-from-markdown",
|
|
32
|
+
"mdast-util-gfm",
|
|
33
|
+
"mdast-util-mdx",
|
|
34
|
+
"mdast-util-to-markdown",
|
|
35
|
+
"micromark-extension-gfm",
|
|
36
|
+
"micromark-extension-mdxjs",
|
|
37
|
+
"react",
|
|
38
|
+
"react-dom",
|
|
39
|
+
"react-dom/client",
|
|
40
|
+
"react-router",
|
|
41
|
+
"react/jsx-runtime",
|
|
42
|
+
"yaml",
|
|
43
|
+
];
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/**
|
|
3
|
+
* Bridge a Node HTTP request to the Fetch API handler and write the Response
|
|
4
|
+
* back. Used by the Vite dev server middleware.
|
|
5
|
+
*/
|
|
6
|
+
export declare function nodeToFetchHandler(handle: (request: Request) => Promise<Response>): (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => Promise<void>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Bridge a Node HTTP request to the Fetch API handler and write the Response
|
|
4
|
+
* back. Used by the Vite dev server middleware.
|
|
5
|
+
*/
|
|
6
|
+
export function nodeToFetchHandler(handle) {
|
|
7
|
+
return async (req, res, next) => {
|
|
8
|
+
try {
|
|
9
|
+
const host = req.headers.host ?? "localhost";
|
|
10
|
+
const proto = req.headers["x-forwarded-proto"] ?? "http";
|
|
11
|
+
const url = new URL(req.url ?? "/", `${proto}://${host}`);
|
|
12
|
+
const headers = new Headers();
|
|
13
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
14
|
+
if (Array.isArray(v))
|
|
15
|
+
v.forEach((x) => headers.append(k, x));
|
|
16
|
+
else if (typeof v === "string")
|
|
17
|
+
headers.set(k, v);
|
|
18
|
+
}
|
|
19
|
+
const method = req.method ?? "GET";
|
|
20
|
+
let body;
|
|
21
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
for await (const chunk of req)
|
|
24
|
+
chunks.push(Buffer.from(chunk));
|
|
25
|
+
body = new Uint8Array(Buffer.concat(chunks));
|
|
26
|
+
}
|
|
27
|
+
const response = await handle(new Request(url, { method, headers, body }));
|
|
28
|
+
res.statusCode = response.status;
|
|
29
|
+
response.headers.forEach((value, key) => {
|
|
30
|
+
if (key.toLowerCase() === "set-cookie")
|
|
31
|
+
return;
|
|
32
|
+
res.setHeader(key, value);
|
|
33
|
+
});
|
|
34
|
+
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
35
|
+
if (cookies.length)
|
|
36
|
+
res.setHeader("Set-Cookie", cookies);
|
|
37
|
+
if (response.body) {
|
|
38
|
+
Readable.fromWeb(response.body).pipe(res);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
res.end();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
next(e);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates the `virtual:vestry/formatter` module. When the project has
|
|
3
|
+
* Prettier installed, the control panel formats files with it before saving
|
|
4
|
+
* (using the project's own Prettier config), so commits made through Vestry
|
|
5
|
+
* match what `prettier --check` expects. Otherwise the module is a no-op.
|
|
6
|
+
*/
|
|
7
|
+
export declare function formatterModule(root: string): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
/**
|
|
6
|
+
* Generates the `virtual:vestry/formatter` module. When the project has
|
|
7
|
+
* Prettier installed, the control panel formats files with it before saving
|
|
8
|
+
* (using the project's own Prettier config), so commits made through Vestry
|
|
9
|
+
* match what `prettier --check` expects. Otherwise the module is a no-op.
|
|
10
|
+
*/
|
|
11
|
+
export async function formatterModule(root) {
|
|
12
|
+
const require = createRequire(path.join(root, "package.json"));
|
|
13
|
+
let standalone;
|
|
14
|
+
let plugins;
|
|
15
|
+
try {
|
|
16
|
+
// Resolve the ESM builds explicitly: `require.resolve` would pick the
|
|
17
|
+
// CommonJS entry, which doesn't load as a module in the browser.
|
|
18
|
+
const pkgDir = path.dirname(require.resolve("prettier/package.json"));
|
|
19
|
+
standalone = path.join(pkgDir, "standalone.mjs");
|
|
20
|
+
plugins = {
|
|
21
|
+
markdown: path.join(pkgDir, "plugins/markdown.mjs"),
|
|
22
|
+
yaml: path.join(pkgDir, "plugins/yaml.mjs"),
|
|
23
|
+
babel: path.join(pkgDir, "plugins/babel.mjs"),
|
|
24
|
+
estree: path.join(pkgDir, "plugins/estree.mjs"),
|
|
25
|
+
};
|
|
26
|
+
if (![standalone, ...Object.values(plugins)].every((p) => existsSync(p)))
|
|
27
|
+
throw new Error("prettier layout");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return `export const available = false;\nexport async function formatFile(text) { return text; }\n`;
|
|
31
|
+
}
|
|
32
|
+
let options = {};
|
|
33
|
+
try {
|
|
34
|
+
const prettier = await import(
|
|
35
|
+
/* @vite-ignore */ pathToFileURL(require.resolve("prettier")).href);
|
|
36
|
+
const resolved = await prettier.resolveConfig(path.join(root, "README.md"), { editorconfig: true });
|
|
37
|
+
if (resolved) {
|
|
38
|
+
options = { ...resolved };
|
|
39
|
+
// Plugins from the config are resolved in Node; the browser gets the fixed set above.
|
|
40
|
+
delete options.plugins;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* fall back to defaults */
|
|
45
|
+
}
|
|
46
|
+
return [
|
|
47
|
+
`import * as prettier from ${JSON.stringify(standalone)};`,
|
|
48
|
+
...Object.entries(plugins).map(([name, p]) => `import * as ${name} from ${JSON.stringify(p)};`),
|
|
49
|
+
`export const available = true;`,
|
|
50
|
+
`const options = ${JSON.stringify(options)};`,
|
|
51
|
+
`export async function formatFile(text, filepath) {`,
|
|
52
|
+
` return prettier.format(text, { ...options, filepath, plugins: [markdown, yaml, babel, estree] });`,
|
|
53
|
+
`}`,
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type * as ES from "estree";
|
|
2
|
+
/**
|
|
3
|
+
* Convert a parsed JSX attribute expression to a plain JSON value when it is
|
|
4
|
+
* made only of literals; otherwise return a RawExpression holding the source.
|
|
5
|
+
*/
|
|
6
|
+
export declare function estreeToValue(node: ES.Node | undefined, source: string): unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Serialize a JSON value (or RawExpression) to JS source for a JSX attribute,
|
|
9
|
+
* in the shape Prettier produces: multi-line objects and arrays with trailing
|
|
10
|
+
* commas, bare identifier keys, double-quoted strings.
|
|
11
|
+
*/
|
|
12
|
+
export declare function valueToSource(value: unknown, indent?: string): string;
|
|
13
|
+
/** Whether a value can be written as a plain string attribute (`a="b"`) rather than an expression. */
|
|
14
|
+
export declare function isPlainString(value: unknown): value is string;
|