@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.
Files changed (93) hide show
  1. package/README.md +28 -0
  2. package/admin/components/AdminCard.astro +25 -0
  3. package/admin/components/AiGenerateButton.tsx +102 -0
  4. package/admin/components/AssetsGrid.tsx +711 -0
  5. package/admin/components/BlockEditor.tsx +996 -0
  6. package/admin/components/CheckboxField.tsx +31 -0
  7. package/admin/components/DocumentActions.tsx +317 -0
  8. package/admin/components/DocumentLock.tsx +54 -0
  9. package/admin/components/DocumentsDataTable.tsx +804 -0
  10. package/admin/components/FieldControl.astro +397 -0
  11. package/admin/components/FocalPointSelector.tsx +100 -0
  12. package/admin/components/ImageBrowseDialog.tsx +176 -0
  13. package/admin/components/ImagePicker.tsx +149 -0
  14. package/admin/components/InternalLinkPicker.tsx +80 -0
  15. package/admin/components/LiveHeading.tsx +17 -0
  16. package/admin/components/MobileSidebar.tsx +29 -0
  17. package/admin/components/RelationField.tsx +204 -0
  18. package/admin/components/RichTextEditor.tsx +685 -0
  19. package/admin/components/SelectField.tsx +65 -0
  20. package/admin/components/SidebarUserMenu.tsx +99 -0
  21. package/admin/components/SlugField.tsx +77 -0
  22. package/admin/components/TaxonomySelect.tsx +52 -0
  23. package/admin/components/Toast.astro +40 -0
  24. package/admin/components/TreeItemsEditor.tsx +790 -0
  25. package/admin/components/TreeSelect.tsx +166 -0
  26. package/admin/components/UnsavedGuard.tsx +181 -0
  27. package/admin/components/tree-utils.ts +86 -0
  28. package/admin/components/ui/alert-dialog.tsx +92 -0
  29. package/admin/components/ui/badge.tsx +83 -0
  30. package/admin/components/ui/button.tsx +53 -0
  31. package/admin/components/ui/card.tsx +70 -0
  32. package/admin/components/ui/checkbox.tsx +28 -0
  33. package/admin/components/ui/collapsible.tsx +26 -0
  34. package/admin/components/ui/command.tsx +88 -0
  35. package/admin/components/ui/dialog.tsx +92 -0
  36. package/admin/components/ui/dropdown-menu.tsx +259 -0
  37. package/admin/components/ui/input.tsx +20 -0
  38. package/admin/components/ui/label.tsx +20 -0
  39. package/admin/components/ui/popover.tsx +42 -0
  40. package/admin/components/ui/select.tsx +165 -0
  41. package/admin/components/ui/separator.tsx +21 -0
  42. package/admin/components/ui/sheet.tsx +104 -0
  43. package/admin/components/ui/skeleton.tsx +7 -0
  44. package/admin/components/ui/table.tsx +74 -0
  45. package/admin/components/ui/textarea.tsx +18 -0
  46. package/admin/components/ui/tooltip.tsx +52 -0
  47. package/admin/layouts/AdminLayout.astro +340 -0
  48. package/admin/lib/utils.ts +19 -0
  49. package/dist/admin.js +92 -0
  50. package/dist/ai.js +67 -0
  51. package/dist/api.js +827 -0
  52. package/dist/assets.js +163 -0
  53. package/dist/auth.js +132 -0
  54. package/dist/blocks.js +110 -0
  55. package/dist/content.js +29 -0
  56. package/dist/create-admin.js +23 -0
  57. package/dist/define.js +36 -0
  58. package/dist/generator.js +370 -0
  59. package/dist/image.js +69 -0
  60. package/dist/index.js +16 -0
  61. package/dist/integration.js +256 -0
  62. package/dist/locks.js +37 -0
  63. package/dist/richtext.js +1 -0
  64. package/dist/runtime.js +26 -0
  65. package/dist/schema.js +13 -0
  66. package/dist/seed.js +84 -0
  67. package/dist/values.js +102 -0
  68. package/middleware/auth.ts +100 -0
  69. package/package.json +102 -0
  70. package/routes/api/cms/[collection]/[...path].ts +366 -0
  71. package/routes/api/cms/ai/alt-text.ts +25 -0
  72. package/routes/api/cms/ai/seo.ts +25 -0
  73. package/routes/api/cms/ai/translate.ts +31 -0
  74. package/routes/api/cms/assets/[id].ts +82 -0
  75. package/routes/api/cms/assets/folders.ts +81 -0
  76. package/routes/api/cms/assets/index.ts +23 -0
  77. package/routes/api/cms/assets/upload.ts +112 -0
  78. package/routes/api/cms/auth/invite.ts +166 -0
  79. package/routes/api/cms/auth/login.ts +124 -0
  80. package/routes/api/cms/auth/logout.ts +33 -0
  81. package/routes/api/cms/auth/setup.ts +77 -0
  82. package/routes/api/cms/cron/publish.ts +33 -0
  83. package/routes/api/cms/img/[...path].ts +24 -0
  84. package/routes/api/cms/locks/[...path].ts +37 -0
  85. package/routes/api/cms/preview/render.ts +36 -0
  86. package/routes/api/cms/references/[collection]/[id].ts +60 -0
  87. package/routes/pages/admin/[...path].astro +1104 -0
  88. package/routes/pages/admin/assets/[id].astro +183 -0
  89. package/routes/pages/admin/assets/index.astro +58 -0
  90. package/routes/pages/admin/invite.astro +116 -0
  91. package/routes/pages/admin/login.astro +57 -0
  92. package/routes/pages/admin/setup.astro +91 -0
  93. 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";