@kidecms/core 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/README.md +28 -0
- package/admin/components/AdminCard.astro +25 -0
- package/admin/components/AiGenerateButton.tsx +102 -0
- package/admin/components/AssetsGrid.tsx +711 -0
- package/admin/components/BlockEditor.tsx +996 -0
- package/admin/components/CheckboxField.tsx +31 -0
- package/admin/components/DocumentActions.tsx +317 -0
- package/admin/components/DocumentLock.tsx +54 -0
- package/admin/components/DocumentsDataTable.tsx +804 -0
- package/admin/components/FieldControl.astro +397 -0
- package/admin/components/FocalPointSelector.tsx +100 -0
- package/admin/components/ImageBrowseDialog.tsx +176 -0
- package/admin/components/ImagePicker.tsx +149 -0
- package/admin/components/InternalLinkPicker.tsx +80 -0
- package/admin/components/LiveHeading.tsx +17 -0
- package/admin/components/MobileSidebar.tsx +29 -0
- package/admin/components/RelationField.tsx +204 -0
- package/admin/components/RichTextEditor.tsx +685 -0
- package/admin/components/SelectField.tsx +65 -0
- package/admin/components/SidebarUserMenu.tsx +99 -0
- package/admin/components/SlugField.tsx +77 -0
- package/admin/components/TaxonomySelect.tsx +52 -0
- package/admin/components/Toast.astro +40 -0
- package/admin/components/TreeItemsEditor.tsx +790 -0
- package/admin/components/TreeSelect.tsx +166 -0
- package/admin/components/UnsavedGuard.tsx +181 -0
- package/admin/components/tree-utils.ts +86 -0
- package/admin/components/ui/alert-dialog.tsx +92 -0
- package/admin/components/ui/badge.tsx +83 -0
- package/admin/components/ui/button.tsx +53 -0
- package/admin/components/ui/card.tsx +70 -0
- package/admin/components/ui/checkbox.tsx +28 -0
- package/admin/components/ui/collapsible.tsx +26 -0
- package/admin/components/ui/command.tsx +88 -0
- package/admin/components/ui/dialog.tsx +92 -0
- package/admin/components/ui/dropdown-menu.tsx +259 -0
- package/admin/components/ui/input.tsx +20 -0
- package/admin/components/ui/label.tsx +20 -0
- package/admin/components/ui/popover.tsx +42 -0
- package/admin/components/ui/select.tsx +165 -0
- package/admin/components/ui/separator.tsx +21 -0
- package/admin/components/ui/sheet.tsx +104 -0
- package/admin/components/ui/skeleton.tsx +7 -0
- package/admin/components/ui/table.tsx +74 -0
- package/admin/components/ui/textarea.tsx +18 -0
- package/admin/components/ui/tooltip.tsx +52 -0
- package/admin/layouts/AdminLayout.astro +340 -0
- package/admin/lib/utils.ts +19 -0
- package/dist/admin.js +92 -0
- package/dist/ai.js +67 -0
- package/dist/api.js +827 -0
- package/dist/assets.js +163 -0
- package/dist/auth.js +132 -0
- package/dist/blocks.js +110 -0
- package/dist/content.js +29 -0
- package/dist/create-admin.js +23 -0
- package/dist/define.js +36 -0
- package/dist/generator.js +370 -0
- package/dist/image.js +69 -0
- package/dist/index.js +16 -0
- package/dist/integration.js +256 -0
- package/dist/locks.js +37 -0
- package/dist/richtext.js +1 -0
- package/dist/runtime.js +26 -0
- package/dist/schema.js +13 -0
- package/dist/seed.js +84 -0
- package/dist/values.js +102 -0
- package/middleware/auth.ts +100 -0
- package/package.json +102 -0
- package/routes/api/cms/[collection]/[...path].ts +366 -0
- package/routes/api/cms/ai/alt-text.ts +25 -0
- package/routes/api/cms/ai/seo.ts +25 -0
- package/routes/api/cms/ai/translate.ts +31 -0
- package/routes/api/cms/assets/[id].ts +82 -0
- package/routes/api/cms/assets/folders.ts +81 -0
- package/routes/api/cms/assets/index.ts +23 -0
- package/routes/api/cms/assets/upload.ts +112 -0
- package/routes/api/cms/auth/invite.ts +166 -0
- package/routes/api/cms/auth/login.ts +124 -0
- package/routes/api/cms/auth/logout.ts +33 -0
- package/routes/api/cms/auth/setup.ts +77 -0
- package/routes/api/cms/cron/publish.ts +33 -0
- package/routes/api/cms/img/[...path].ts +24 -0
- package/routes/api/cms/locks/[...path].ts +37 -0
- package/routes/api/cms/preview/render.ts +36 -0
- package/routes/api/cms/references/[collection]/[id].ts +60 -0
- package/routes/pages/admin/[...path].astro +1104 -0
- package/routes/pages/admin/assets/[id].astro +183 -0
- package/routes/pages/admin/assets/index.astro +58 -0
- package/routes/pages/admin/invite.astro +116 -0
- package/routes/pages/admin/login.astro +57 -0
- package/routes/pages/admin/setup.astro +91 -0
- package/virtual.d.ts +61 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getTranslatableFieldNames } from "./define";
|
|
4
|
+
const pascalCase = (value) => value
|
|
5
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
8
|
+
.join("");
|
|
9
|
+
const snakeCase = (value) => value
|
|
10
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
11
|
+
.replace(/[^a-zA-Z0-9]+/g, "_")
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
const isJsonField = (field) => field.type === "richText" ||
|
|
14
|
+
field.type === "array" ||
|
|
15
|
+
field.type === "json" ||
|
|
16
|
+
field.type === "blocks" ||
|
|
17
|
+
(field.type === "relation" && field.hasMany);
|
|
18
|
+
const generateColumnDef = (fieldName, field) => {
|
|
19
|
+
const colName = snakeCase(fieldName);
|
|
20
|
+
let col;
|
|
21
|
+
if (field.type === "number") {
|
|
22
|
+
col = `integer("${colName}")`;
|
|
23
|
+
}
|
|
24
|
+
else if (field.type === "boolean") {
|
|
25
|
+
col = `integer("${colName}", { mode: "boolean" })`;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
col = `text("${colName}")`;
|
|
29
|
+
}
|
|
30
|
+
if (field.required && !field.condition)
|
|
31
|
+
col += ".notNull()";
|
|
32
|
+
if (field.unique)
|
|
33
|
+
col += ".unique()";
|
|
34
|
+
if (field.defaultValue !== undefined && !isJsonField(field)) {
|
|
35
|
+
if (field.type === "number" || field.type === "boolean") {
|
|
36
|
+
col += `.default(${field.defaultValue})`;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
col += `.default(${JSON.stringify(field.defaultValue)})`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return ` ${fieldName}: ${col},`;
|
|
43
|
+
};
|
|
44
|
+
const generateMainTable = (collection) => {
|
|
45
|
+
const tableName = `cms_${snakeCase(collection.slug)}`;
|
|
46
|
+
const varName = `cms${pascalCase(collection.slug)}`;
|
|
47
|
+
const columns = [` _id: text("_id").primaryKey(),`];
|
|
48
|
+
for (const [fieldName, field] of Object.entries(collection.fields)) {
|
|
49
|
+
columns.push(generateColumnDef(fieldName, field));
|
|
50
|
+
}
|
|
51
|
+
if (collection.drafts) {
|
|
52
|
+
columns.push(` _status: text("_status").notNull().default("draft"),`);
|
|
53
|
+
columns.push(` _publishedAt: text("_published_at"),`);
|
|
54
|
+
columns.push(` _publishAt: text("_publish_at"),`);
|
|
55
|
+
columns.push(` _unpublishAt: text("_unpublish_at"),`);
|
|
56
|
+
columns.push(` _published: text("_published"),`);
|
|
57
|
+
}
|
|
58
|
+
if (collection.timestamps !== false) {
|
|
59
|
+
columns.push(` _createdAt: text("_created_at").notNull(),`);
|
|
60
|
+
columns.push(` _updatedAt: text("_updated_at").notNull(),`);
|
|
61
|
+
}
|
|
62
|
+
return `export const ${varName} = sqliteTable("${tableName}", {\n${columns.join("\n")}\n});`;
|
|
63
|
+
};
|
|
64
|
+
const generateTranslationsTable = (config, collection) => {
|
|
65
|
+
if (!config.locales)
|
|
66
|
+
return null;
|
|
67
|
+
const translatableFields = getTranslatableFieldNames(collection);
|
|
68
|
+
if (translatableFields.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const tableName = `cms_${snakeCase(collection.slug)}_translations`;
|
|
71
|
+
const varName = `cms${pascalCase(collection.slug)}Translations`;
|
|
72
|
+
const mainVar = `cms${pascalCase(collection.slug)}`;
|
|
73
|
+
const columns = [
|
|
74
|
+
` _id: text("_id").primaryKey(),`,
|
|
75
|
+
` _entityId: text("_entity_id").notNull().references(() => ${mainVar}._id, { onDelete: "cascade" }),`,
|
|
76
|
+
` _languageCode: text("_language_code").notNull(),`,
|
|
77
|
+
];
|
|
78
|
+
for (const fieldName of translatableFields) {
|
|
79
|
+
columns.push(generateColumnDef(fieldName, collection.fields[fieldName]));
|
|
80
|
+
}
|
|
81
|
+
return `export const ${varName} = sqliteTable("${tableName}", {\n${columns.join("\n")}\n}, (table) => ({\n uniqueLocale: unique().on(table._entityId, table._languageCode),\n}));`;
|
|
82
|
+
};
|
|
83
|
+
const generateVersionsTable = (collection) => {
|
|
84
|
+
if (!collection.versions)
|
|
85
|
+
return null;
|
|
86
|
+
const tableName = `cms_${snakeCase(collection.slug)}_versions`;
|
|
87
|
+
const varName = `cms${pascalCase(collection.slug)}Versions`;
|
|
88
|
+
const mainVar = `cms${pascalCase(collection.slug)}`;
|
|
89
|
+
return `export const ${varName} = sqliteTable("${tableName}", {
|
|
90
|
+
_id: text("_id").primaryKey(),
|
|
91
|
+
_docId: text("_doc_id").notNull().references(() => ${mainVar}._id, { onDelete: "cascade" }),
|
|
92
|
+
_version: integer("_version").notNull(),
|
|
93
|
+
_snapshot: text("_snapshot").notNull(),
|
|
94
|
+
_createdAt: text("_created_at").notNull(),
|
|
95
|
+
});`;
|
|
96
|
+
};
|
|
97
|
+
const generateSchemaFile = (config) => {
|
|
98
|
+
const parts = [
|
|
99
|
+
`// auto-generated — do not edit`,
|
|
100
|
+
`import { sqliteTable, text, integer, real, unique } from "drizzle-orm/sqlite-core";`,
|
|
101
|
+
``,
|
|
102
|
+
];
|
|
103
|
+
for (const collection of config.collections) {
|
|
104
|
+
parts.push(generateMainTable(collection));
|
|
105
|
+
const translationsTable = generateTranslationsTable(config, collection);
|
|
106
|
+
if (translationsTable)
|
|
107
|
+
parts.push("", translationsTable);
|
|
108
|
+
const versionsTable = generateVersionsTable(collection);
|
|
109
|
+
if (versionsTable)
|
|
110
|
+
parts.push("", versionsTable);
|
|
111
|
+
parts.push("");
|
|
112
|
+
}
|
|
113
|
+
parts.push(`export const cmsAssets = sqliteTable("cms_assets", {
|
|
114
|
+
_id: text("_id").primaryKey(),
|
|
115
|
+
filename: text("filename").notNull(),
|
|
116
|
+
mimeType: text("mime_type").notNull(),
|
|
117
|
+
size: integer("size").notNull(),
|
|
118
|
+
width: integer("width"),
|
|
119
|
+
height: integer("height"),
|
|
120
|
+
focalX: real("focal_x"),
|
|
121
|
+
focalY: real("focal_y"),
|
|
122
|
+
alt: text("alt"),
|
|
123
|
+
folder: text("folder"),
|
|
124
|
+
storagePath: text("storage_path").notNull(),
|
|
125
|
+
_createdAt: text("_created_at").notNull(),
|
|
126
|
+
});`);
|
|
127
|
+
parts.push("");
|
|
128
|
+
parts.push(`export const cmsAssetFolders = sqliteTable("cms_asset_folders", {
|
|
129
|
+
_id: text("_id").primaryKey(),
|
|
130
|
+
name: text("name").notNull(),
|
|
131
|
+
parent: text("parent"),
|
|
132
|
+
_createdAt: text("_created_at").notNull(),
|
|
133
|
+
});`);
|
|
134
|
+
parts.push("");
|
|
135
|
+
parts.push(`export const cmsSessions = sqliteTable("cms_sessions", {
|
|
136
|
+
_id: text("_id").primaryKey(),
|
|
137
|
+
userId: text("user_id").notNull(),
|
|
138
|
+
expiresAt: text("expires_at").notNull(),
|
|
139
|
+
});`);
|
|
140
|
+
parts.push("");
|
|
141
|
+
parts.push(`export const cmsLocks = sqliteTable("cms_locks", {
|
|
142
|
+
_id: text("_id").primaryKey(),
|
|
143
|
+
collection: text("collection").notNull(),
|
|
144
|
+
documentId: text("document_id").notNull(),
|
|
145
|
+
userId: text("user_id").notNull(),
|
|
146
|
+
userEmail: text("user_email").notNull(),
|
|
147
|
+
lockedAt: text("locked_at").notNull(),
|
|
148
|
+
});`);
|
|
149
|
+
parts.push("");
|
|
150
|
+
parts.push(`export const cmsInvites = sqliteTable("cms_invites", {
|
|
151
|
+
_id: text("_id").primaryKey(),
|
|
152
|
+
userId: text("user_id").notNull(),
|
|
153
|
+
token: text("token").notNull().unique(),
|
|
154
|
+
expiresAt: text("expires_at").notNull(),
|
|
155
|
+
usedAt: text("used_at"),
|
|
156
|
+
});`);
|
|
157
|
+
parts.push("");
|
|
158
|
+
const tableExports = [];
|
|
159
|
+
for (const collection of config.collections) {
|
|
160
|
+
const varName = `cms${pascalCase(collection.slug)}`;
|
|
161
|
+
const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(collection.slug) ? collection.slug : `"${collection.slug}"`;
|
|
162
|
+
tableExports.push(` ${safeKey}: { main: ${varName}`);
|
|
163
|
+
const translatableFields = getTranslatableFieldNames(collection);
|
|
164
|
+
if (translatableFields.length > 0 && config.locales) {
|
|
165
|
+
tableExports[tableExports.length - 1] += `, translations: ${varName}Translations`;
|
|
166
|
+
}
|
|
167
|
+
if (collection.versions) {
|
|
168
|
+
tableExports[tableExports.length - 1] += `, versions: ${varName}Versions`;
|
|
169
|
+
}
|
|
170
|
+
tableExports[tableExports.length - 1] += ` },`;
|
|
171
|
+
}
|
|
172
|
+
parts.push(`export const cmsTables = {\n${tableExports.join("\n")}\n};`);
|
|
173
|
+
return parts.join("\n");
|
|
174
|
+
};
|
|
175
|
+
const zodTypeForField = (field) => {
|
|
176
|
+
if (["text", "slug", "email", "image", "date"].includes(field.type)) {
|
|
177
|
+
let zodType = "z.string()";
|
|
178
|
+
if (field.type === "email")
|
|
179
|
+
zodType = "z.string().email()";
|
|
180
|
+
if (field.type === "text" && field.maxLength)
|
|
181
|
+
zodType = `z.string().max(${field.maxLength})`;
|
|
182
|
+
return zodType;
|
|
183
|
+
}
|
|
184
|
+
if (field.type === "number")
|
|
185
|
+
return "z.number()";
|
|
186
|
+
if (field.type === "boolean")
|
|
187
|
+
return "z.boolean()";
|
|
188
|
+
if (field.type === "select")
|
|
189
|
+
return `z.enum([${field.options.map((option) => JSON.stringify(option)).join(", ")}])`;
|
|
190
|
+
if (field.type === "relation")
|
|
191
|
+
return field.hasMany ? "z.array(z.string())" : "z.string()";
|
|
192
|
+
if (field.type === "array")
|
|
193
|
+
return `z.array(${zodTypeForField(field.of)})`;
|
|
194
|
+
if (field.type === "richText")
|
|
195
|
+
return "z.object({ type: z.literal('root'), children: z.array(z.any()) })";
|
|
196
|
+
if (field.type === "json")
|
|
197
|
+
return "z.record(z.unknown())";
|
|
198
|
+
if (field.type === "blocks") {
|
|
199
|
+
const variants = Object.entries(field.types).map(([blockType, fields]) => {
|
|
200
|
+
const members = Object.entries(fields)
|
|
201
|
+
.map(([fieldName, nestedField]) => `${fieldName}: ${zodTypeForField(nestedField)}${nestedField.required ? "" : ".optional()"}`)
|
|
202
|
+
.join(", ");
|
|
203
|
+
return `z.object({ type: z.literal(${JSON.stringify(blockType)}), ${members} })`;
|
|
204
|
+
});
|
|
205
|
+
if (variants.length === 0)
|
|
206
|
+
return "z.array(z.record(z.unknown()))";
|
|
207
|
+
if (variants.length === 1)
|
|
208
|
+
return `z.array(${variants[0]})`;
|
|
209
|
+
return `z.array(z.discriminatedUnion("type", [\n ${variants.join(",\n ")},\n ]))`;
|
|
210
|
+
}
|
|
211
|
+
return "z.unknown()";
|
|
212
|
+
};
|
|
213
|
+
const generateValidatorsFile = (config) => {
|
|
214
|
+
const parts = [`// auto-generated — do not edit`, `import { z } from "zod";`, ``];
|
|
215
|
+
for (const collection of config.collections) {
|
|
216
|
+
const name = pascalCase(collection.slug);
|
|
217
|
+
const fieldEntries = Object.entries(collection.fields).map(([fieldName, field]) => {
|
|
218
|
+
const zodType = zodTypeForField(field);
|
|
219
|
+
const isOptional = !field.required || !!field.condition;
|
|
220
|
+
return ` ${fieldName}: ${zodType}${isOptional ? ".optional()" : ""},`;
|
|
221
|
+
});
|
|
222
|
+
parts.push(`export const ${name}CreateSchema = z.object({\n${fieldEntries.join("\n")}\n});`);
|
|
223
|
+
parts.push("");
|
|
224
|
+
const partialEntries = Object.entries(collection.fields).map(([fieldName, field]) => {
|
|
225
|
+
const zodType = zodTypeForField(field);
|
|
226
|
+
return ` ${fieldName}: ${zodType}.optional(),`;
|
|
227
|
+
});
|
|
228
|
+
parts.push(`export const ${name}UpdateSchema = z.object({\n${partialEntries.join("\n")}\n});`);
|
|
229
|
+
parts.push("");
|
|
230
|
+
}
|
|
231
|
+
const mapEntries = config.collections.map((collection) => {
|
|
232
|
+
const name = pascalCase(collection.slug);
|
|
233
|
+
return ` ${collection.slug}: { create: ${name}CreateSchema, update: ${name}UpdateSchema },`;
|
|
234
|
+
});
|
|
235
|
+
parts.push(`export const validators = {\n${mapEntries.join("\n")}\n};`);
|
|
236
|
+
return parts.join("\n");
|
|
237
|
+
};
|
|
238
|
+
const typeForField = (field) => {
|
|
239
|
+
if (["text", "slug", "email", "image", "date"].includes(field.type))
|
|
240
|
+
return "string";
|
|
241
|
+
if (field.type === "number")
|
|
242
|
+
return "number";
|
|
243
|
+
if (field.type === "boolean")
|
|
244
|
+
return "boolean";
|
|
245
|
+
if (field.type === "select")
|
|
246
|
+
return field.options.map((option) => JSON.stringify(option)).join(" | ");
|
|
247
|
+
if (field.type === "relation")
|
|
248
|
+
return field.hasMany ? "string[]" : "string";
|
|
249
|
+
if (field.type === "array")
|
|
250
|
+
return `${typeForField(field.of)}[]`;
|
|
251
|
+
if (field.type === "richText")
|
|
252
|
+
return "RichTextDocument";
|
|
253
|
+
if (field.type === "json")
|
|
254
|
+
return "Record<string, unknown>";
|
|
255
|
+
if (field.type === "blocks") {
|
|
256
|
+
const variants = Object.entries(field.types).map(([blockType, fields]) => {
|
|
257
|
+
const members = Object.entries(fields)
|
|
258
|
+
.map(([fieldName, nestedField]) => `${fieldName}${nestedField.required ? "" : "?"}: ${typeForField(nestedField)};`)
|
|
259
|
+
.join(" ");
|
|
260
|
+
return `{ type: ${JSON.stringify(blockType)}; ${members} }`;
|
|
261
|
+
});
|
|
262
|
+
return `Array<${variants.join(" | ")}>`;
|
|
263
|
+
}
|
|
264
|
+
return "unknown";
|
|
265
|
+
};
|
|
266
|
+
const generateTypesFile = (config, coreImportPath) => {
|
|
267
|
+
const parts = [
|
|
268
|
+
`// auto-generated — do not edit`,
|
|
269
|
+
`import type { RichTextDocument } from "${coreImportPath}";`,
|
|
270
|
+
``,
|
|
271
|
+
`export type CMSCollectionSlug = ${config.collections.map((collection) => JSON.stringify(collection.slug)).join(" | ")};`,
|
|
272
|
+
``,
|
|
273
|
+
];
|
|
274
|
+
for (const collection of config.collections) {
|
|
275
|
+
const docName = `${pascalCase(collection.slug)}Document`;
|
|
276
|
+
const inputName = `${pascalCase(collection.slug)}Input`;
|
|
277
|
+
const translationName = `${pascalCase(collection.slug)}TranslationInput`;
|
|
278
|
+
const translatableFields = getTranslatableFieldNames(collection);
|
|
279
|
+
const fieldEntries = Object.entries(collection.fields)
|
|
280
|
+
.map(([fieldName, field]) => {
|
|
281
|
+
const isOptional = !field.required || !!field.condition;
|
|
282
|
+
return ` ${fieldName}${isOptional ? "?" : ""}: ${typeForField(field)};`;
|
|
283
|
+
})
|
|
284
|
+
.join("\n");
|
|
285
|
+
const translationEntries = translatableFields.length
|
|
286
|
+
? translatableFields
|
|
287
|
+
.map((fieldName) => ` ${fieldName}?: ${typeForField(collection.fields[fieldName])};`)
|
|
288
|
+
.join("\n")
|
|
289
|
+
: " [key: string]: never;";
|
|
290
|
+
parts.push(`export type ${inputName} = {\n${fieldEntries}\n};`);
|
|
291
|
+
parts.push("");
|
|
292
|
+
parts.push(`export type ${translationName} = {\n${translationEntries}\n};`);
|
|
293
|
+
parts.push("");
|
|
294
|
+
parts.push(`export type ${docName} = ${inputName} & {
|
|
295
|
+
_id: string;
|
|
296
|
+
_status: "draft" | "published" | "scheduled";
|
|
297
|
+
_publishedAt?: string | null;
|
|
298
|
+
_publishAt?: string | null;
|
|
299
|
+
_unpublishAt?: string | null;
|
|
300
|
+
_createdAt: string;
|
|
301
|
+
_updatedAt: string;
|
|
302
|
+
_locale?: string | null;
|
|
303
|
+
_availableLocales?: string[];
|
|
304
|
+
};`);
|
|
305
|
+
parts.push("");
|
|
306
|
+
}
|
|
307
|
+
parts.push(`export type StoredVersion = {
|
|
308
|
+
version: number;
|
|
309
|
+
createdAt: string;
|
|
310
|
+
snapshot: Record<string, unknown>;
|
|
311
|
+
};`);
|
|
312
|
+
return parts.join("\n");
|
|
313
|
+
};
|
|
314
|
+
const generateApiFile = (config, coreImportPath, runtimeImportPath, configImportPath) => {
|
|
315
|
+
const imports = config.collections
|
|
316
|
+
.map((collection) => {
|
|
317
|
+
const name = pascalCase(collection.slug);
|
|
318
|
+
return [`${name}Document`, `${name}Input`, `${name}TranslationInput`];
|
|
319
|
+
})
|
|
320
|
+
.flat();
|
|
321
|
+
const apiTypes = config.collections
|
|
322
|
+
.map((collection) => {
|
|
323
|
+
const baseName = pascalCase(collection.slug);
|
|
324
|
+
const ctx = `context?: { user?: { id: string; role?: string; email?: string } | null }`;
|
|
325
|
+
const apiKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(collection.slug) ? collection.slug : `"${collection.slug}"`;
|
|
326
|
+
return ` ${apiKey}: {
|
|
327
|
+
find(options?: import("${coreImportPath}").FindOptions, ${ctx}): Promise<${baseName}Document[]>;
|
|
328
|
+
findOne(filter: Record<string, unknown> & { locale?: string; status?: "draft" | "published" | "scheduled" | "any" }, ${ctx}): Promise<${baseName}Document | null>;
|
|
329
|
+
findById(id: string, options?: { locale?: string; status?: "draft" | "published" | "scheduled" | "any" }, ${ctx}): Promise<${baseName}Document | null>;
|
|
330
|
+
create(data: ${baseName}Input, ${ctx}): Promise<${baseName}Document>;
|
|
331
|
+
update(id: string, data: Partial<${baseName}Input>, ${ctx}): Promise<${baseName}Document>;
|
|
332
|
+
delete(id: string, ${ctx}): Promise<void>;
|
|
333
|
+
count(filter?: Omit<import("${coreImportPath}").FindOptions, "limit" | "offset" | "sort">, ${ctx}): Promise<number>;
|
|
334
|
+
versions(id: string): Promise<import("./types").StoredVersion[]>;
|
|
335
|
+
restore(id: string, versionNumber: number, ${ctx}): Promise<${baseName}Document>;
|
|
336
|
+
publish(id: string, ${ctx}): Promise<${baseName}Document>;
|
|
337
|
+
unpublish(id: string, ${ctx}): Promise<${baseName}Document>;
|
|
338
|
+
schedule(id: string, publishAt: string, unpublishAt?: string | null, ${ctx}): Promise<${baseName}Document>;
|
|
339
|
+
getTranslations(id: string): Promise<Record<string, ${baseName}TranslationInput>>;
|
|
340
|
+
upsertTranslation(id: string, locale: string, data: ${baseName}TranslationInput, ${ctx}): Promise<${baseName}Document>;
|
|
341
|
+
};`;
|
|
342
|
+
})
|
|
343
|
+
.join("\n");
|
|
344
|
+
return `// auto-generated — do not edit
|
|
345
|
+
import config from "${configImportPath}";
|
|
346
|
+
import { createCms } from "${runtimeImportPath}";
|
|
347
|
+
import type {
|
|
348
|
+
${imports.map((entry) => `${entry},`).join("\n ")}
|
|
349
|
+
} from "./types";
|
|
350
|
+
|
|
351
|
+
export const cms = createCms(config) as {
|
|
352
|
+
${apiTypes}
|
|
353
|
+
meta: ReturnType<typeof createCms>["meta"];
|
|
354
|
+
scheduled: ReturnType<typeof createCms>["scheduled"];
|
|
355
|
+
};
|
|
356
|
+
`;
|
|
357
|
+
};
|
|
358
|
+
export const generate = async (config, options) => {
|
|
359
|
+
const outputDir = options.outputDir;
|
|
360
|
+
const coreImportPath = options.coreImportPath ?? "@kidecms/core";
|
|
361
|
+
const runtimeImportPath = options.runtimeImportPath ?? "../runtime";
|
|
362
|
+
const configImportPath = options.configImportPath ?? "../cms.config";
|
|
363
|
+
await mkdir(outputDir, { recursive: true });
|
|
364
|
+
await Promise.all([
|
|
365
|
+
writeFile(path.join(outputDir, "schema.ts"), generateSchemaFile(config), "utf-8"),
|
|
366
|
+
writeFile(path.join(outputDir, "types.ts"), generateTypesFile(config, coreImportPath), "utf-8"),
|
|
367
|
+
writeFile(path.join(outputDir, "validators.ts"), generateValidatorsFile(config), "utf-8"),
|
|
368
|
+
writeFile(path.join(outputDir, "api.ts"), generateApiFile(config, coreImportPath, runtimeImportPath, configImportPath), "utf-8"),
|
|
369
|
+
]);
|
|
370
|
+
};
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const ALLOWED_WIDTHS = [320, 480, 640, 768, 960, 1024, 1280, 1536, 1920];
|
|
2
|
+
const ALLOWED_FORMATS = ["webp", "avif", "jpeg", "png"];
|
|
3
|
+
function clampWidth(width) {
|
|
4
|
+
return ALLOWED_WIDTHS.reduce((prev, curr) => (Math.abs(curr - width) < Math.abs(prev - width) ? curr : prev));
|
|
5
|
+
}
|
|
6
|
+
function isCloudflare() {
|
|
7
|
+
return typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers";
|
|
8
|
+
}
|
|
9
|
+
export function cmsImage(src, width, format = "webp") {
|
|
10
|
+
if (!src)
|
|
11
|
+
return "";
|
|
12
|
+
if (isCloudflare()) {
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (width)
|
|
15
|
+
parts.push(`width=${clampWidth(width)}`);
|
|
16
|
+
parts.push(`format=${format}`);
|
|
17
|
+
parts.push("quality=80");
|
|
18
|
+
return `/cdn-cgi/image/${parts.join(",")}${src}`;
|
|
19
|
+
}
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (width)
|
|
22
|
+
params.set("w", String(clampWidth(width)));
|
|
23
|
+
if (format !== "webp")
|
|
24
|
+
params.set("f", format);
|
|
25
|
+
const qs = params.toString();
|
|
26
|
+
return `/api/cms/img${src}${qs ? `?${qs}` : ""}`;
|
|
27
|
+
}
|
|
28
|
+
export function cmsSrcset(src, widths = [480, 768, 1024, 1280], format = "webp") {
|
|
29
|
+
if (!src)
|
|
30
|
+
return "";
|
|
31
|
+
return widths.map((width) => `${cmsImage(src, width, format)} ${width}w`).join(", ");
|
|
32
|
+
}
|
|
33
|
+
export async function transformImage(src, width, format, quality) {
|
|
34
|
+
const { existsSync, mkdirSync, readFileSync } = await import("node:fs");
|
|
35
|
+
const { writeFile } = await import("node:fs/promises");
|
|
36
|
+
const path = await import("node:path");
|
|
37
|
+
const sharpModule = "sharp";
|
|
38
|
+
const sharp = (await import(/* @vite-ignore */ sharpModule)).default;
|
|
39
|
+
const publicDir = path.join(process.cwd(), "public");
|
|
40
|
+
const cacheDir = path.join(process.cwd(), ".cms-cache", "img");
|
|
41
|
+
const filePath = path.join(publicDir, src);
|
|
42
|
+
if (!existsSync(filePath))
|
|
43
|
+
return null;
|
|
44
|
+
const resolvedFormat = ALLOWED_FORMATS.includes(format) ? format : "webp";
|
|
45
|
+
const resolvedWidth = width ? clampWidth(width) : undefined;
|
|
46
|
+
const resolvedQuality = quality ?? 80;
|
|
47
|
+
if (!existsSync(cacheDir))
|
|
48
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
49
|
+
const safeName = src.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
50
|
+
const cacheKey = `${safeName}_${resolvedWidth ?? 0}.${resolvedFormat}`;
|
|
51
|
+
const cachePath = path.join(cacheDir, cacheKey);
|
|
52
|
+
if (existsSync(cachePath)) {
|
|
53
|
+
return {
|
|
54
|
+
buffer: readFileSync(cachePath),
|
|
55
|
+
contentType: `image/${resolvedFormat === "jpeg" ? "jpeg" : resolvedFormat}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let pipeline = sharp(readFileSync(filePath));
|
|
59
|
+
if (resolvedWidth) {
|
|
60
|
+
pipeline = pipeline.resize(resolvedWidth, undefined, { withoutEnlargement: true });
|
|
61
|
+
}
|
|
62
|
+
pipeline = pipeline.toFormat(resolvedFormat, { quality: resolvedQuality });
|
|
63
|
+
const buffer = await pipeline.toBuffer();
|
|
64
|
+
writeFile(cachePath, buffer).catch(() => { });
|
|
65
|
+
return {
|
|
66
|
+
buffer,
|
|
67
|
+
contentType: `image/${resolvedFormat === "jpeg" ? "jpeg" : resolvedFormat}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { configureCmsRuntime, resetCmsRuntime, getCmsRuntime, getDb, closeDb, getStorage, getEmail, readEnv, } from "./runtime";
|
|
2
|
+
export { initSchema, getSchema, resetSchema } from "./schema";
|
|
3
|
+
export { fields, defineCollection, defineConfig, getCollectionMap, getDefaultLocale, getTranslatableFieldNames, isStructuralField, getCollectionLabel, getLabelField, hasRole, } from "./define";
|
|
4
|
+
export { createCms } from "./api";
|
|
5
|
+
export { hashPassword, verifyPassword, createSession, validateSession, destroySession, getSessionUser, createInvite, validateInvite, consumeInvite, SESSION_COOKIE_NAME, setSessionCookie, clearSessionCookie, } from "./auth";
|
|
6
|
+
export { assets, folders } from "./assets";
|
|
7
|
+
export { parseBlocks, parseList, cacheTags } from "./content";
|
|
8
|
+
export { renderRichText, createRichTextFromPlainText, richTextToPlainText } from "./richtext";
|
|
9
|
+
export { cloneValue, slugify, escapeHtml, serializeFieldValue } from "./values";
|
|
10
|
+
export { cmsImage, cmsSrcset, transformImage } from "./image";
|
|
11
|
+
export { initDateFormat, formatDate, resolveAdminRoute, humanize, formatFieldValue, getListColumns, getFieldSets, } from "./admin";
|
|
12
|
+
export { acquireLock, releaseLock } from "./locks";
|
|
13
|
+
export { isAiEnabled, getAiModel, streamAltText, streamSeoDescription, streamTranslation } from "./ai";
|
|
14
|
+
export { generate } from "./generator";
|
|
15
|
+
export { seedDatabase } from "./seed";
|
|
16
|
+
export { createAdminUser } from "./create-admin";
|