@createcms/core 0.1.1

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 (83) hide show
  1. package/README.md +169 -0
  2. package/dist/ab-edge/index.cjs +214 -0
  3. package/dist/ab-edge/index.d.cts +121 -0
  4. package/dist/ab-edge/index.d.ts +121 -0
  5. package/dist/ab-edge/index.js +205 -0
  6. package/dist/bin/createcms.js +3082 -0
  7. package/dist/db.cjs +496 -0
  8. package/dist/db.d.cts +128 -0
  9. package/dist/db.d.ts +128 -0
  10. package/dist/db.js +488 -0
  11. package/dist/index.cjs +13789 -0
  12. package/dist/index.d.cts +10277 -0
  13. package/dist/index.d.ts +10277 -0
  14. package/dist/index.js +13737 -0
  15. package/dist/nanoid.cjs +50 -0
  16. package/dist/nanoid.d.cts +29 -0
  17. package/dist/nanoid.d.ts +29 -0
  18. package/dist/nanoid.js +47 -0
  19. package/dist/next/index.cjs +60 -0
  20. package/dist/next/index.d.cts +141 -0
  21. package/dist/next/index.d.ts +141 -0
  22. package/dist/next/index.js +58 -0
  23. package/dist/next/middleware.cjs +113 -0
  24. package/dist/next/middleware.d.cts +77 -0
  25. package/dist/next/middleware.d.ts +77 -0
  26. package/dist/next/middleware.js +111 -0
  27. package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
  28. package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
  29. package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
  30. package/dist/plugins/ab-test/analytics/upstash.js +343 -0
  31. package/dist/plugins/ab-test/client.cjs +686 -0
  32. package/dist/plugins/ab-test/client.d.cts +233 -0
  33. package/dist/plugins/ab-test/client.d.ts +233 -0
  34. package/dist/plugins/ab-test/client.js +684 -0
  35. package/dist/plugins/ab-test/index.cjs +3400 -0
  36. package/dist/plugins/ab-test/index.d.cts +1131 -0
  37. package/dist/plugins/ab-test/index.d.ts +1131 -0
  38. package/dist/plugins/ab-test/index.js +3367 -0
  39. package/dist/plugins/client.cjs +20 -0
  40. package/dist/plugins/client.d.cts +3 -0
  41. package/dist/plugins/client.d.ts +3 -0
  42. package/dist/plugins/client.js +3 -0
  43. package/dist/plugins/consent/client.cjs +315 -0
  44. package/dist/plugins/consent/client.d.cts +145 -0
  45. package/dist/plugins/consent/client.d.ts +145 -0
  46. package/dist/plugins/consent/client.js +313 -0
  47. package/dist/plugins/consent/index.cjs +267 -0
  48. package/dist/plugins/consent/index.d.cts +618 -0
  49. package/dist/plugins/consent/index.d.ts +618 -0
  50. package/dist/plugins/consent/index.js +258 -0
  51. package/dist/plugins/i18n/index.cjs +2177 -0
  52. package/dist/plugins/i18n/index.d.cts +562 -0
  53. package/dist/plugins/i18n/index.d.ts +562 -0
  54. package/dist/plugins/i18n/index.js +2150 -0
  55. package/dist/plugins/media-optimize/index.cjs +315 -0
  56. package/dist/plugins/media-optimize/index.d.cts +144 -0
  57. package/dist/plugins/media-optimize/index.d.ts +144 -0
  58. package/dist/plugins/media-optimize/index.js +311 -0
  59. package/dist/plugins/multi-tenant/index.cjs +210 -0
  60. package/dist/plugins/multi-tenant/index.d.cts +431 -0
  61. package/dist/plugins/multi-tenant/index.d.ts +431 -0
  62. package/dist/plugins/multi-tenant/index.js +207 -0
  63. package/dist/plugins/server.cjs +24 -0
  64. package/dist/plugins/server.d.cts +3 -0
  65. package/dist/plugins/server.d.ts +3 -0
  66. package/dist/plugins/server.js +3 -0
  67. package/dist/react/blocks.cjs +233 -0
  68. package/dist/react/blocks.d.cts +320 -0
  69. package/dist/react/blocks.d.ts +320 -0
  70. package/dist/react/blocks.js +226 -0
  71. package/dist/react/index.cjs +901 -0
  72. package/dist/react/index.d.cts +992 -0
  73. package/dist/react/index.d.ts +992 -0
  74. package/dist/react/index.js +872 -0
  75. package/dist/react/tracking.cjs +243 -0
  76. package/dist/react/tracking.d.cts +364 -0
  77. package/dist/react/tracking.d.ts +364 -0
  78. package/dist/react/tracking.js +216 -0
  79. package/dist/react/variant.cjs +59 -0
  80. package/dist/react/variant.d.cts +26 -0
  81. package/dist/react/variant.d.ts +26 -0
  82. package/dist/react/variant.js +57 -0
  83. package/package.json +303 -0
@@ -0,0 +1,311 @@
1
+ import { useState, useRef, useMemo, useEffect } from 'react';
2
+
3
+ function isImageFile(file) {
4
+ return file.type.startsWith('image/');
5
+ }
6
+ function loadImage(file) {
7
+ return new Promise((resolve, reject)=>{
8
+ const img = new Image();
9
+ const url = URL.createObjectURL(file);
10
+ img.onload = ()=>{
11
+ URL.revokeObjectURL(url);
12
+ resolve(img);
13
+ };
14
+ img.onerror = ()=>{
15
+ URL.revokeObjectURL(url);
16
+ reject(new Error(`Failed to load image: ${file.name}`));
17
+ };
18
+ img.src = url;
19
+ });
20
+ }
21
+ function calculateDimensions(width, height, maxSize) {
22
+ if (width <= maxSize && height <= maxSize) {
23
+ return {
24
+ width,
25
+ height
26
+ };
27
+ }
28
+ const ratio = width / height;
29
+ if (width > height) {
30
+ return {
31
+ width: maxSize,
32
+ height: Math.round(maxSize / ratio)
33
+ };
34
+ }
35
+ return {
36
+ width: Math.round(maxSize * ratio),
37
+ height: maxSize
38
+ };
39
+ }
40
+ function drawToCanvas(img, width, height) {
41
+ const canvas = document.createElement('canvas');
42
+ canvas.width = width;
43
+ canvas.height = height;
44
+ const ctx = canvas.getContext('2d');
45
+ if (!ctx) throw new Error('Failed to get canvas 2d context');
46
+ ctx.imageSmoothingEnabled = true;
47
+ ctx.imageSmoothingQuality = 'high';
48
+ ctx.drawImage(img, 0, 0, width, height);
49
+ return canvas;
50
+ }
51
+ function canvasToBlobAsync(canvas, mimeType, quality) {
52
+ return new Promise((resolve, reject)=>{
53
+ canvas.toBlob((blob)=>blob ? resolve(blob) : reject(new Error(`canvas.toBlob failed for ${mimeType}`)), mimeType, quality);
54
+ });
55
+ }
56
+ let _nativeWebpSupported = null;
57
+ async function supportsNativeWebp() {
58
+ if (_nativeWebpSupported !== null) return _nativeWebpSupported;
59
+ const c = document.createElement('canvas');
60
+ c.width = 1;
61
+ c.height = 1;
62
+ const blob = await new Promise((r)=>c.toBlob((b)=>r(b), 'image/webp', 0.5));
63
+ _nativeWebpSupported = blob?.type === 'image/webp';
64
+ return _nativeWebpSupported;
65
+ }
66
+ async function encodeWebp(canvas, quality) {
67
+ if (await supportsNativeWebp()) {
68
+ return canvasToBlobAsync(canvas, 'image/webp', quality);
69
+ }
70
+ try {
71
+ const { encode } = await import(/* webpackIgnore: true */ '@jsquash/webp');
72
+ const ctx = canvas.getContext('2d');
73
+ if (!ctx) throw new Error('Failed to get canvas 2d context');
74
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
75
+ const buffer = await encode(imageData, {
76
+ quality: quality * 100
77
+ });
78
+ return new Blob([
79
+ buffer
80
+ ], {
81
+ type: 'image/webp'
82
+ });
83
+ } catch {
84
+ throw new Error('WebP encoding not supported natively and @jsquash/webp is not installed. ' + 'Install it as a dependency: npm install @jsquash/webp');
85
+ }
86
+ }
87
+ function inferMimeFromName(name) {
88
+ const ext = name.split('.').pop()?.toLowerCase();
89
+ const map = {
90
+ png: 'image/png',
91
+ jpg: 'image/jpeg',
92
+ jpeg: 'image/jpeg',
93
+ gif: 'image/gif',
94
+ webp: 'image/webp',
95
+ svg: 'image/svg+xml',
96
+ bmp: 'image/bmp',
97
+ avif: 'image/avif'
98
+ };
99
+ return ext && map[ext] || 'image/jpeg';
100
+ }
101
+ function replaceExtension(filename, newExt) {
102
+ const dot = filename.lastIndexOf('.');
103
+ const base = dot > 0 ? filename.slice(0, dot) : filename;
104
+ return `${base}.${newExt}`;
105
+ }
106
+ /**
107
+ * Optimize an image file on the client before uploading.
108
+ *
109
+ * - **Resize**: Downscale if either dimension exceeds `config.resize.maxSize`
110
+ * - **Compress**: Reduce quality via `config.compress.quality` (1-100)
111
+ * - **Convert**: Convert to WebP when `config.convert.format` is set
112
+ * - **storeOriginal**: When `convert.storeOriginal` is true, also returns an
113
+ * `originalVariant` -- same resize + compress but kept in the original format
114
+ *
115
+ * Non-image files are returned unchanged with `optimized: false`.
116
+ */ async function optimizeImage(file, config) {
117
+ if (!isImageFile(file)) {
118
+ return {
119
+ file,
120
+ optimized: false
121
+ };
122
+ }
123
+ const hasResize = !!config.resize?.maxSize;
124
+ const hasCompress = config.compress?.quality != null;
125
+ const hasConvert = config.convert?.format === 'webp';
126
+ if (!hasResize && !hasCompress && !hasConvert) {
127
+ return {
128
+ file,
129
+ optimized: false
130
+ };
131
+ }
132
+ const img = await loadImage(file);
133
+ const maxSize = config.resize?.maxSize ?? Math.max(img.naturalWidth, img.naturalHeight);
134
+ const quality = config.compress?.quality != null ? config.compress.quality / 100 : 0.8;
135
+ const { width, height } = calculateDimensions(img.naturalWidth, img.naturalHeight, maxSize);
136
+ const canvas = drawToCanvas(img, width, height);
137
+ let originalVariant;
138
+ const originalMime = file.type || inferMimeFromName(file.name);
139
+ if (hasConvert && config.convert.storeOriginal) {
140
+ const originalBlob = await canvasToBlobAsync(canvas, originalMime, quality);
141
+ originalVariant = new File([
142
+ originalBlob
143
+ ], file.name, {
144
+ type: originalMime,
145
+ lastModified: Date.now()
146
+ });
147
+ }
148
+ let primaryBlob;
149
+ let primaryName;
150
+ let primaryType;
151
+ if (hasConvert) {
152
+ primaryBlob = await encodeWebp(canvas, quality);
153
+ primaryName = replaceExtension(file.name, 'webp');
154
+ primaryType = 'image/webp';
155
+ } else {
156
+ primaryBlob = await canvasToBlobAsync(canvas, originalMime, quality);
157
+ primaryName = file.name;
158
+ primaryType = originalMime;
159
+ }
160
+ const primaryFile = new File([
161
+ primaryBlob
162
+ ], primaryName, {
163
+ type: primaryType,
164
+ lastModified: Date.now()
165
+ });
166
+ return {
167
+ file: primaryFile,
168
+ originalVariant,
169
+ optimized: true
170
+ };
171
+ }
172
+
173
+ const INITIAL_STATE = {
174
+ results: null,
175
+ isOptimizing: false,
176
+ error: null
177
+ };
178
+ /**
179
+ * React hook that optimizes image files on the client.
180
+ *
181
+ * Accepts a single `File` or `File[]` and an `OptimizationConfig`.
182
+ * Runs `optimizeImage` automatically whenever the input reference changes.
183
+ * Non-image files pass through with `optimized: false`.
184
+ *
185
+ * ```tsx
186
+ * const { results, isOptimizing, error } = cmsClient.optimize.useOptimize(file, {
187
+ * compress: { quality: 80 },
188
+ * resize: { maxSize: 1200 },
189
+ * });
190
+ * ```
191
+ */ function useOptimize(input, config) {
192
+ const [state, setState] = useState(INITIAL_STATE);
193
+ const abortRef = useRef(false);
194
+ const runIdRef = useRef(0);
195
+ const files = useMemo(()=>Array.isArray(input) ? input : [
196
+ input
197
+ ], // Stabilize: same File instances → same array
198
+ // eslint-disable-next-line react-hooks/exhaustive-deps
199
+ [
200
+ Array.isArray(input) ? input.length : input
201
+ ]);
202
+ const configKey = JSON.stringify(config);
203
+ const configRef = useRef(config);
204
+ configRef.current = config;
205
+ useEffect(()=>{
206
+ if (files.length === 0) {
207
+ setState((prev)=>prev.results?.length === 0 && !prev.isOptimizing ? prev : {
208
+ results: [],
209
+ isOptimizing: false,
210
+ error: null
211
+ });
212
+ return;
213
+ }
214
+ abortRef.current = false;
215
+ const currentRun = ++runIdRef.current;
216
+ setState({
217
+ results: null,
218
+ isOptimizing: true,
219
+ error: null
220
+ });
221
+ Promise.all(files.map((file)=>optimizeImage(file, configRef.current))).then((results)=>{
222
+ if (abortRef.current || runIdRef.current !== currentRun) return;
223
+ setState({
224
+ results,
225
+ isOptimizing: false,
226
+ error: null
227
+ });
228
+ }).catch((err)=>{
229
+ if (abortRef.current || runIdRef.current !== currentRun) return;
230
+ setState({
231
+ results: null,
232
+ isOptimizing: false,
233
+ error: err instanceof Error ? err.message : 'Optimization failed'
234
+ });
235
+ });
236
+ return ()=>{
237
+ abortRef.current = true;
238
+ };
239
+ // eslint-disable-next-line react-hooks/exhaustive-deps
240
+ }, [
241
+ files,
242
+ configKey
243
+ ]);
244
+ return state;
245
+ }
246
+
247
+ const PLUGIN_ID = 'media-optimize';
248
+ const $ERROR_CODES = {
249
+ OPTIMIZATION_FAILED: {
250
+ status: 422,
251
+ message: 'Image optimization failed'
252
+ },
253
+ WEBP_NOT_SUPPORTED: {
254
+ status: 422,
255
+ message: 'WebP encoding is not supported in this browser and @jsquash/webp is not installed'
256
+ },
257
+ CANVAS_CONTEXT_FAILED: {
258
+ status: 500,
259
+ message: 'Failed to acquire canvas 2D context for image processing'
260
+ }
261
+ };
262
+ /**
263
+ * Client plugin that adds image optimization under its own namespace.
264
+ *
265
+ * Exposes `cmsClient.optimize.useOptimize(file, config)` for client-side
266
+ * image optimization. Optimized files can then be passed to
267
+ * `cmsClient.media.useUploadAssets().upload(files)`.
268
+ *
269
+ * ```ts
270
+ * import { mediaOptimizeClient } from '@createcms/core/plugins/media-optimize';
271
+ *
272
+ * const client = createCMSClient<typeof cms>({
273
+ * baseURL: '/api/cms',
274
+ * plugins: [
275
+ * mediaOptimizeClient({
276
+ * compress: { quality: 90 },
277
+ * resize: { maxSize: 2000 },
278
+ * convert: { format: 'webp', storeOriginal: true },
279
+ * }),
280
+ * ],
281
+ * });
282
+ *
283
+ * // In a component:
284
+ * const { results, isOptimizing } = client.optimize.useOptimize(file, config);
285
+ * ```
286
+ */ function mediaOptimizeClient(config) {
287
+ return {
288
+ id: PLUGIN_ID,
289
+ $ERROR_CODES,
290
+ async init (_$fetch, _$store) {
291
+ return {
292
+ context: {
293
+ [`${PLUGIN_ID}:config`]: config
294
+ }
295
+ };
296
+ },
297
+ getActions: ()=>({
298
+ optimize: {
299
+ useOptimize: (input, overrideConfig)=>useOptimize(input, overrideConfig ?? config)
300
+ }
301
+ }),
302
+ atomListeners: [
303
+ {
304
+ matcher: (path)=>path.startsWith('/media/createSignedUpload'),
305
+ signal: '$mediaSignal'
306
+ }
307
+ ]
308
+ };
309
+ }
310
+
311
+ export { mediaOptimizeClient, optimizeImage, useOptimize };
@@ -0,0 +1,210 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var betterCall = require('better-call');
4
+ var drizzleOrm = require('drizzle-orm');
5
+
6
+ function definePluginSchema(schema) {
7
+ return {
8
+ ...schema
9
+ };
10
+ }
11
+
12
+ /**
13
+ * Plugin schema that adds the `tenantSlug` column and tenant-scoped indexes
14
+ * to the core tables. The column does not exist in the core schema — it is
15
+ * entirely owned by this plugin.
16
+ */ const multiTenantSchema = definePluginSchema({
17
+ extend: {
18
+ roots: {
19
+ columns: {
20
+ tenantSlug: {
21
+ type: 'text',
22
+ notNull: true
23
+ }
24
+ },
25
+ indexes: {
26
+ tenantCollectionIdx: {
27
+ columns: [
28
+ 'tenantSlug',
29
+ 'collection'
30
+ ]
31
+ },
32
+ // Per-tenant slug uniqueness — the DB backstop for the (now per-tenant)
33
+ // app-level validateSlugUniqueness (the core slug index was demoted to
34
+ // non-unique). NESTED-only in practice (a NULL parentRootId is distinct in
35
+ // Postgres, so top-level relies on the app-level check); composes with the
36
+ // i18n plugin's (language,…) unique because each tenant+language has its
37
+ // own parent tree, so neither over-constrains the other.
38
+ tenantRootSlugUnique: {
39
+ columns: [
40
+ 'tenantSlug',
41
+ 'collection',
42
+ 'parentRootId',
43
+ 'slug'
44
+ ],
45
+ unique: true
46
+ }
47
+ }
48
+ },
49
+ assetFolders: {
50
+ columns: {
51
+ tenantSlug: {
52
+ type: 'text',
53
+ notNull: true
54
+ }
55
+ },
56
+ indexes: {
57
+ tenantNameUnique: {
58
+ columns: [
59
+ 'tenantSlug',
60
+ 'parentId',
61
+ 'name'
62
+ ],
63
+ unique: true
64
+ },
65
+ tenantIdx: {
66
+ columns: [
67
+ 'tenantSlug'
68
+ ]
69
+ }
70
+ }
71
+ },
72
+ assets: {
73
+ columns: {
74
+ tenantSlug: {
75
+ type: 'text',
76
+ notNull: true
77
+ }
78
+ },
79
+ indexes: {
80
+ tenantIdx: {
81
+ columns: [
82
+ 'tenantSlug'
83
+ ]
84
+ },
85
+ tenantSlugUnique: {
86
+ columns: [
87
+ 'tenantSlug',
88
+ 'slug'
89
+ ],
90
+ unique: true
91
+ }
92
+ }
93
+ },
94
+ // Redirects have NO core unique index (uniqueness is app-level), so the
95
+ // plugin owns the real per-tenant DB guarantee. PARTIAL (active rows only)
96
+ // mirrors the app-level checks: archiving a redirect frees its source.
97
+ redirects: {
98
+ columns: {
99
+ tenantSlug: {
100
+ type: 'text',
101
+ notNull: true
102
+ }
103
+ },
104
+ indexes: {
105
+ // NOTE: no per-tenant PATH-source unique. A path can legitimately have a
106
+ // different redirect per language (the i18n plugin adds `language`), and
107
+ // the correct compound key (tenant_slug, language, collection, sourcePath)
108
+ // can't be expressed by either plugin alone — so path-source uniqueness is
109
+ // the app-level authority (assertSourceUnique + the auto-create pre-check,
110
+ // both scope.redirects-aware). Lookup is still indexed below.
111
+ //
112
+ // PAGE-source IS safely per-tenant-unique: sourceRootId is a specific root
113
+ // (a single language), so this never over-constrains under i18n.
114
+ tenantSourceRootUnique: {
115
+ columns: [
116
+ 'tenantSlug',
117
+ 'sourceRootId'
118
+ ],
119
+ unique: true,
120
+ where: 'archived_at IS NULL'
121
+ },
122
+ // Per-tenant lookup/listing (+ the path-source lookup, since the unique
123
+ // that used to cover it is gone).
124
+ tenantCollectionIdx: {
125
+ columns: [
126
+ 'tenantSlug',
127
+ 'collection'
128
+ ]
129
+ },
130
+ tenantSourcePathIdx: {
131
+ columns: [
132
+ 'tenantSlug',
133
+ 'collection',
134
+ 'sourcePath'
135
+ ]
136
+ }
137
+ }
138
+ }
139
+ }
140
+ });
141
+
142
+ /**
143
+ * Resolves the tenant slug from the incoming request context.
144
+ * Priority: body.tenantSlug -> query.tenantSlug -> fallback.
145
+ *
146
+ * Use this inside your `authMiddleware` to support per-request tenant
147
+ * overrides (e.g. for admin cross-tenant access) while keeping a
148
+ * sensible default from the user's session.
149
+ *
150
+ * @param ctx - The middleware context (must have `request`)
151
+ * @param fallback - Default tenant slug (e.g. `session.organizationSlug`)
152
+ */ function resolveTenantSlug(ctx, fallback) {
153
+ return ctx.request?.body?.tenantSlug ?? ctx.request?.query?.tenantSlug ?? fallback;
154
+ }
155
+ const PLUGIN_ID = 'multi-tenant';
156
+ const $ERROR_CODES = {
157
+ TENANT_SLUG_REQUIRED: {
158
+ status: 400,
159
+ message: 'tenantSlug is required -- authMiddleware must return { tenantSlug } when multiTenant plugin is active'
160
+ }
161
+ };
162
+ function multiTenant() {
163
+ return {
164
+ id: PLUGIN_ID,
165
+ schema: multiTenantSchema,
166
+ $ERROR_CODES,
167
+ init (_ctx) {
168
+ const factory = (mwResult)=>{
169
+ const tenantSlug = mwResult.tenantSlug;
170
+ if (typeof tenantSlug !== 'string' || tenantSlug.length === 0) {
171
+ throw new betterCall.APIError(400, {
172
+ message: $ERROR_CODES.TENANT_SLUG_REQUIRED.message,
173
+ code: 'TENANT_SLUG_REQUIRED'
174
+ });
175
+ }
176
+ const insertColumns = {
177
+ tenant_slug: tenantSlug
178
+ };
179
+ return {
180
+ roots: {
181
+ where: drizzleOrm.sql`"cms"."roots"."tenant_slug" = ${tenantSlug}`,
182
+ insertColumns
183
+ },
184
+ assets: {
185
+ where: drizzleOrm.sql`"cms"."assets"."tenant_slug" = ${tenantSlug}`,
186
+ insertColumns
187
+ },
188
+ assetFolders: {
189
+ where: drizzleOrm.sql`"cms"."asset_folders"."tenant_slug" = ${tenantSlug}`,
190
+ insertColumns
191
+ },
192
+ redirects: {
193
+ where: drizzleOrm.sql`"cms"."redirects"."tenant_slug" = ${tenantSlug}`,
194
+ insertColumns
195
+ }
196
+ };
197
+ };
198
+ return {
199
+ context: {
200
+ scopeConditions: [
201
+ factory
202
+ ]
203
+ }
204
+ };
205
+ }
206
+ };
207
+ }
208
+
209
+ exports.multiTenant = multiTenant;
210
+ exports.resolveTenantSlug = resolveTenantSlug;