ayezee-astro-cms 1.3.0 → 1.5.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.
@@ -50,6 +50,31 @@ export interface AyezeeCmsOptions {
50
50
  * @default true
51
51
  */
52
52
  enableAnalytics?: boolean;
53
+ /**
54
+ * Report site version to CMS dashboard for tracking
55
+ * @default true
56
+ */
57
+ reportVersion?: boolean;
58
+ /**
59
+ * Site URL for version reporting (auto-detected if not provided)
60
+ * @default process.env.SITE_URL or process.env.URL
61
+ */
62
+ siteUrl?: string;
63
+ /**
64
+ * Environment name for version reporting
65
+ * @default process.env.NODE_ENV or 'production'
66
+ */
67
+ environment?: string;
68
+ /**
69
+ * Generate TypeScript types for module data
70
+ * @default true
71
+ */
72
+ generateTypes?: boolean;
73
+ /**
74
+ * Generate a wrapper file that auto-initializes the CMS helper
75
+ * @default true
76
+ */
77
+ generateWrapper?: boolean;
53
78
  }
54
79
  /**
55
80
  * AyeZee CMS Astro Integration
@@ -15,6 +15,8 @@
15
15
  */
16
16
  import fs from 'fs';
17
17
  import path from 'path';
18
+ // Package version - automatically updated during build
19
+ const PACKAGE_VERSION = '1.5.0';
18
20
  /**
19
21
  * Load environment variables from .env file
20
22
  */
@@ -48,6 +50,240 @@ function slugify(text) {
48
50
  .replace(/[\s_-]+/g, '-')
49
51
  .replace(/^-+|-+$/g, '');
50
52
  }
53
+ /**
54
+ * Get Astro version from package.json
55
+ */
56
+ function getAstroVersion() {
57
+ try {
58
+ const pkgPath = path.join(process.cwd(), 'node_modules', 'astro', 'package.json');
59
+ if (fs.existsSync(pkgPath)) {
60
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
61
+ return pkg.version;
62
+ }
63
+ }
64
+ catch {
65
+ // Ignore errors
66
+ }
67
+ return undefined;
68
+ }
69
+ /**
70
+ * Convert a field type to a TypeScript type
71
+ */
72
+ function fieldTypeToTsType(fieldType) {
73
+ switch (fieldType) {
74
+ case 'number':
75
+ case 'rating':
76
+ return 'number';
77
+ case 'checkbox':
78
+ case 'toggle':
79
+ return 'boolean';
80
+ case 'select':
81
+ case 'radio':
82
+ return 'string';
83
+ case 'multiselect':
84
+ case 'tags':
85
+ return 'string[]';
86
+ case 'json':
87
+ case 'object':
88
+ return 'Record<string, unknown>';
89
+ case 'date':
90
+ case 'datetime':
91
+ return 'string'; // ISO date string
92
+ case 'image':
93
+ case 'file':
94
+ case 'url':
95
+ case 'text':
96
+ case 'textarea':
97
+ case 'richtext':
98
+ case 'email':
99
+ case 'phone':
100
+ default:
101
+ return 'string';
102
+ }
103
+ }
104
+ /**
105
+ * Convert a string to PascalCase for type names
106
+ */
107
+ function toPascalCase(str) {
108
+ return str
109
+ .replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
110
+ .replace(/^(.)/, (c) => c.toUpperCase());
111
+ }
112
+ /**
113
+ * Generate TypeScript types for all modules
114
+ */
115
+ function generateTypeDefinitions(modules) {
116
+ const lines = [
117
+ '/**',
118
+ ' * Auto-generated TypeScript types for CMS modules',
119
+ ' * Generated by ayezee-astro-cms integration',
120
+ ' * DO NOT EDIT - This file is regenerated on each build',
121
+ ' */',
122
+ '',
123
+ '// Base type for all module data items',
124
+ 'export interface ModuleDataItem<T = Record<string, unknown>> {',
125
+ ' id: string;',
126
+ ' data: T;',
127
+ ' sortOrder?: number;',
128
+ ' status?: string;',
129
+ ' createdAt: string;',
130
+ ' updatedAt?: string;',
131
+ '}',
132
+ '',
133
+ ];
134
+ for (const module of modules) {
135
+ const typeName = toPascalCase(module.instanceKey);
136
+ const dataTypeName = `${typeName}Data`;
137
+ const itemTypeName = `${typeName}Item`;
138
+ // Generate the data shape interface
139
+ lines.push(`// Module: ${module.label} (${module.instanceKey})`);
140
+ lines.push(`export interface ${dataTypeName} {`);
141
+ for (const field of module.fields) {
142
+ const tsType = fieldTypeToTsType(field.fieldType);
143
+ const optional = field.required ? '' : '?';
144
+ const comment = field.label !== field.key ? ` // ${field.label}` : '';
145
+ lines.push(` ${field.key}${optional}: ${tsType};${comment}`);
146
+ }
147
+ lines.push('}');
148
+ lines.push('');
149
+ // Generate the full item type (with id, createdAt, etc.)
150
+ lines.push(`export type ${itemTypeName} = ModuleDataItem<${dataTypeName}>;`);
151
+ lines.push('');
152
+ }
153
+ // Generate a union type of all module keys for type-safe lookups
154
+ lines.push('// All available module instance keys');
155
+ lines.push('export type ModuleInstanceKey =');
156
+ for (let i = 0; i < modules.length; i++) {
157
+ const comma = i < modules.length - 1 ? '' : ';';
158
+ lines.push(` | '${modules[i].instanceKey}'${comma}`);
159
+ }
160
+ lines.push('');
161
+ // Generate a mapping type for getModuleData
162
+ lines.push('// Type mapping for getModuleData helper');
163
+ lines.push('export interface ModuleDataMap {');
164
+ for (const module of modules) {
165
+ const itemTypeName = `${toPascalCase(module.instanceKey)}Item`;
166
+ lines.push(` '${module.instanceKey}': ${itemTypeName}[];`);
167
+ }
168
+ lines.push('}');
169
+ lines.push('');
170
+ return lines.join('\n');
171
+ }
172
+ /**
173
+ * Generate wrapper file that auto-initializes the CMS helper
174
+ */
175
+ function generateWrapperFile() {
176
+ return `/**
177
+ * Auto-generated CMS wrapper with pre-initialized helper
178
+ * Generated by ayezee-astro-cms integration
179
+ * DO NOT EDIT - This file is regenerated on each build
180
+ */
181
+
182
+ import cmsData from './cms-cache.json';
183
+ import {
184
+ initCmsHelper,
185
+ getProject,
186
+ getModules,
187
+ getModule,
188
+ getModuleByLabel,
189
+ getModuleBySlug,
190
+ getModulesByCategory,
191
+ getModulesByDataType,
192
+ getForms,
193
+ getCollections,
194
+ getSingletons,
195
+ getModuleData,
196
+ getModuleDataByLabel,
197
+ getModuleDataBySlug,
198
+ getTurnstileConfig,
199
+ isTurnstileEnabled,
200
+ getFormConfig,
201
+ getCacheInfo,
202
+ getAnalyticsConfig,
203
+ isAnalyticsEnabled,
204
+ getAnalyticsWebsiteId,
205
+ type CMSCache,
206
+ type CachedModule,
207
+ type ModuleDataItem,
208
+ type ModuleField,
209
+ type AnalyticsConfig,
210
+ } from 'ayezee-astro-cms';
211
+
212
+ // Auto-initialize on import
213
+ initCmsHelper(cmsData as unknown as CMSCache);
214
+
215
+ // Re-export everything for convenience
216
+ export {
217
+ getProject,
218
+ getModules,
219
+ getModule,
220
+ getModuleByLabel,
221
+ getModuleBySlug,
222
+ getModulesByCategory,
223
+ getModulesByDataType,
224
+ getForms,
225
+ getCollections,
226
+ getSingletons,
227
+ getModuleData,
228
+ getModuleDataByLabel,
229
+ getModuleDataBySlug,
230
+ getTurnstileConfig,
231
+ isTurnstileEnabled,
232
+ getFormConfig,
233
+ getCacheInfo,
234
+ getAnalyticsConfig,
235
+ isAnalyticsEnabled,
236
+ getAnalyticsWebsiteId,
237
+ };
238
+
239
+ // Re-export types
240
+ export type {
241
+ CMSCache,
242
+ CachedModule,
243
+ ModuleDataItem,
244
+ ModuleField,
245
+ AnalyticsConfig,
246
+ };
247
+
248
+ // Export the raw cache data for advanced use cases
249
+ export { cmsData };
250
+ `;
251
+ }
252
+ /**
253
+ * Report site version to CMS dashboard
254
+ */
255
+ async function reportSiteVersion(baseUrl, apiKey, siteUrl, environment, astroVersion, logger) {
256
+ try {
257
+ const response = await fetch(`${baseUrl}/sites`, {
258
+ method: 'POST',
259
+ headers: {
260
+ 'Content-Type': 'application/json',
261
+ ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
262
+ },
263
+ body: JSON.stringify({
264
+ siteUrl,
265
+ environment,
266
+ packageVersion: PACKAGE_VERSION,
267
+ astroVersion,
268
+ metadata: {
269
+ nodeVersion: process.version,
270
+ platform: process.platform,
271
+ buildTime: new Date().toISOString(),
272
+ },
273
+ }),
274
+ });
275
+ if (response.ok) {
276
+ logger.info(`📡 Reported site version to dashboard`);
277
+ }
278
+ else {
279
+ logger.warn(`⚠️ Failed to report site version: ${response.status}`);
280
+ }
281
+ }
282
+ catch (error) {
283
+ // Don't fail the build if version reporting fails
284
+ logger.warn(`⚠️ Could not report site version (non-critical)`);
285
+ }
286
+ }
51
287
  /**
52
288
  * AyeZee CMS Astro Integration
53
289
  */
@@ -57,7 +293,7 @@ export function ayezeeCms(options = {}) {
57
293
  name: 'ayezee-cms',
58
294
  hooks: {
59
295
  'astro:config:setup': async ({ config, logger, injectScript }) => {
60
- logger.info('🚀 AyeZee CMS: Fetching data...');
296
+ logger.info(`🚀 AyeZee CMS v${PACKAGE_VERSION}: Fetching data...`);
61
297
  // Load environment variables from .env
62
298
  loadEnvFile();
63
299
  // Get configuration
@@ -68,21 +304,104 @@ export function ayezeeCms(options = {}) {
68
304
  const cacheFileName = options.cacheFileName || 'cms-cache.json';
69
305
  const skipOnError = options.skipOnError || false;
70
306
  const enableAnalytics = options.enableAnalytics !== false;
71
- // Validate config
307
+ const reportVersion = options.reportVersion !== false;
308
+ const siteUrl = options.siteUrl || process.env.SITE_URL || process.env.URL || process.env.VERCEL_URL;
309
+ const environment = options.environment || process.env.NODE_ENV || 'production';
310
+ // Validate config with helpful error messages
72
311
  if (!cmsDomain || !projectSlug) {
73
- const errorMsg = 'Missing environment variables:';
74
- if (!cmsDomain)
75
- logger.error(` - PUBLIC_CMS_DOMAIN`);
76
- if (!projectSlug)
77
- logger.error(` - PUBLIC_PROJECT_SLUG`);
312
+ logger.error('');
313
+ logger.error('❌ AyeZee CMS Configuration Error');
314
+ logger.error('═══════════════════════════════════════════════════════════');
315
+ logger.error('');
316
+ logger.error('Missing required environment variables:');
317
+ if (!cmsDomain) {
318
+ logger.error(' ✗ PUBLIC_CMS_DOMAIN - The URL of your CMS dashboard');
319
+ logger.error(' Example: PUBLIC_CMS_DOMAIN=https://cms.yourdomain.com');
320
+ }
321
+ if (!projectSlug) {
322
+ logger.error(' ✗ PUBLIC_PROJECT_SLUG - Your project identifier');
323
+ logger.error(' Example: PUBLIC_PROJECT_SLUG=my-project');
324
+ }
325
+ logger.error('');
326
+ logger.error('Add these to your .env file:');
327
+ logger.error('');
328
+ logger.error(' PUBLIC_CMS_DOMAIN=https://your-cms-domain.com');
329
+ logger.error(' PUBLIC_PROJECT_SLUG=your-project-slug');
330
+ logger.error(' PUBLIC_AYEZEE_API_KEY=AZ_your_api_key (optional)');
331
+ logger.error('');
332
+ logger.error('═══════════════════════════════════════════════════════════');
333
+ logger.error('');
78
334
  if (skipOnError) {
79
335
  logger.warn('⚠️ Skipping CMS data fetch due to missing config');
336
+ logger.warn(' Build will continue with cached data (if available)');
80
337
  return;
81
338
  }
82
- throw new Error(`AyeZee CMS integration requires PUBLIC_CMS_DOMAIN and PUBLIC_PROJECT_SLUG`);
339
+ throw new Error(`AyeZee CMS integration requires PUBLIC_CMS_DOMAIN and PUBLIC_PROJECT_SLUG. ` +
340
+ `See the error messages above for details.`);
83
341
  }
342
+ const generateTypes = options.generateTypes !== false;
343
+ const generateWrapper = options.generateWrapper !== false;
84
344
  logger.info(` Domain: ${cmsDomain}`);
85
345
  logger.info(` Project: ${projectSlug}`);
346
+ logger.info(` Environment: ${environment}`);
347
+ // Get the data directory path
348
+ const dataDir = path.join(process.cwd(), outputDir);
349
+ const cachePath = path.join(dataDir, cacheFileName);
350
+ // Check if we can skip fetching in development mode
351
+ const isDev = environment === 'development' || process.env.NODE_ENV === 'development';
352
+ const forceFresh = process.env.CMS_FRESH === 'true';
353
+ const cacheExists = fs.existsSync(cachePath);
354
+ if (isDev && cacheExists && !forceFresh) {
355
+ logger.info('');
356
+ logger.info('📦 Using cached CMS data (dev mode)');
357
+ logger.info(' Run with CMS_FRESH=true to fetch fresh data');
358
+ logger.info('');
359
+ // Still generate types and wrapper from existing cache
360
+ if (generateTypes || generateWrapper) {
361
+ try {
362
+ const existingCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
363
+ if (generateTypes && existingCache.modules) {
364
+ const typesContent = generateTypeDefinitions(existingCache.modules);
365
+ const typesPath = path.join(dataDir, 'cms-types.ts');
366
+ fs.writeFileSync(typesPath, typesContent, 'utf-8');
367
+ logger.info(`✅ Generated types: ${path.relative(process.cwd(), typesPath)}`);
368
+ }
369
+ if (generateWrapper) {
370
+ const wrapperContent = generateWrapperFile();
371
+ const wrapperPath = path.join(dataDir, 'cms.ts');
372
+ fs.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
373
+ logger.info(`✅ Generated wrapper: ${path.relative(process.cwd(), wrapperPath)}`);
374
+ }
375
+ }
376
+ catch {
377
+ logger.warn('⚠️ Could not generate types/wrapper from cached data');
378
+ }
379
+ }
380
+ // Handle analytics injection from cached data
381
+ if (enableAnalytics) {
382
+ try {
383
+ const existingCache = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
384
+ if (existingCache.analytics) {
385
+ analyticsConfig = existingCache.analytics;
386
+ injectScript('head-inline', `
387
+ (function() {
388
+ var script = document.createElement('script');
389
+ script.defer = true;
390
+ script.src = '${analyticsConfig.baseUrl}/script.js';
391
+ script.setAttribute('data-website-id', '${analyticsConfig.websiteId}');
392
+ document.head.appendChild(script);
393
+ })();
394
+ `);
395
+ logger.info('📊 Umami analytics script injected from cache');
396
+ }
397
+ }
398
+ catch {
399
+ // Ignore errors reading cache for analytics
400
+ }
401
+ }
402
+ logger.info('');
403
+ return;
404
+ }
86
405
  try {
87
406
  // Build API URL
88
407
  let domain = cmsDomain;
@@ -91,9 +410,9 @@ export function ayezeeCms(options = {}) {
91
410
  domain = `${protocol}://${domain}`;
92
411
  }
93
412
  const baseUrl = `${domain}/api/v1/projects/${projectSlug}`;
94
- // Fetch modules
95
- logger.info('📡 Fetching modules...');
96
- const modulesResponse = await fetch(`${baseUrl}/modules`, {
413
+ // Fetch all modules with data in a single API call
414
+ logger.info('📡 Fetching modules with data...');
415
+ const modulesResponse = await fetch(`${baseUrl}/modules?include=data`, {
97
416
  headers: {
98
417
  ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
99
418
  },
@@ -112,44 +431,21 @@ export function ayezeeCms(options = {}) {
112
431
  analyticsConfig = modulesResult.data.analytics;
113
432
  logger.info(`📊 Analytics configured: ${analyticsConfig.websiteId}`);
114
433
  }
115
- // Fetch data for each module
116
- const modulesWithData = [];
117
- for (const module of modules) {
118
- try {
119
- const dataResponse = await fetch(`${baseUrl}/modules/${module.instanceKey}/data`, {
120
- headers: {
121
- ...(apiKey && { 'Authorization': `Bearer ${apiKey}` }),
122
- },
123
- });
124
- if (!dataResponse.ok) {
125
- throw new Error(`HTTP ${dataResponse.status}: ${dataResponse.statusText}`);
126
- }
127
- const dataResult = await dataResponse.json();
128
- const itemCount = dataResult.data?.pagination?.total || 0;
129
- logger.info(` 📦 ${module.label}: ${itemCount} items`);
130
- modulesWithData.push({
131
- ...module,
132
- slug: slugify(module.label),
133
- data: dataResult.data?.data || [],
134
- pagination: dataResult.data?.pagination || {
135
- total: 0,
136
- count: 0,
137
- },
138
- parameters: dataResult.data?.module?.parameters || {},
139
- });
140
- }
141
- catch (error) {
142
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
143
- logger.warn(` ⚠️ Failed to fetch ${module.label}: ${errorMessage}`);
144
- modulesWithData.push({
145
- ...module,
146
- slug: slugify(module.label),
147
- data: [],
148
- pagination: { total: 0, count: 0 },
149
- parameters: {},
150
- });
151
- }
152
- }
434
+ // Transform modules to include slug and ensure consistent shape
435
+ const modulesWithData = modules.map((module) => {
436
+ const itemCount = module.data?.length || 0;
437
+ logger.info(` 📦 ${module.label}: ${itemCount} items`);
438
+ return {
439
+ ...module,
440
+ slug: slugify(module.label),
441
+ data: module.data || [],
442
+ pagination: module.pagination || {
443
+ total: itemCount,
444
+ count: itemCount,
445
+ },
446
+ parameters: module.parameters || {},
447
+ };
448
+ });
153
449
  // Create cache data
154
450
  const cacheData = {
155
451
  project: modulesResult.data.project,
@@ -158,17 +454,28 @@ export function ayezeeCms(options = {}) {
158
454
  fetchedAt: new Date().toISOString(),
159
455
  version: '1.0',
160
456
  };
161
- // Get the data directory path
162
- const dataDir = path.join(process.cwd(), outputDir);
163
457
  // Ensure directory exists
164
458
  if (!fs.existsSync(dataDir)) {
165
459
  fs.mkdirSync(dataDir, { recursive: true });
166
460
  }
167
461
  // Write cache file
168
- const cachePath = path.join(dataDir, cacheFileName);
169
462
  fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2), 'utf-8');
170
463
  logger.info(`✅ Cached data to: ${path.relative(process.cwd(), cachePath)}`);
171
464
  logger.info(`📅 Fetched at: ${cacheData.fetchedAt}`);
465
+ // Generate TypeScript types
466
+ if (generateTypes) {
467
+ const typesContent = generateTypeDefinitions(modulesWithData);
468
+ const typesPath = path.join(dataDir, 'cms-types.ts');
469
+ fs.writeFileSync(typesPath, typesContent, 'utf-8');
470
+ logger.info(`✅ Generated types: ${path.relative(process.cwd(), typesPath)}`);
471
+ }
472
+ // Generate wrapper file
473
+ if (generateWrapper) {
474
+ const wrapperContent = generateWrapperFile();
475
+ const wrapperPath = path.join(dataDir, 'cms.ts');
476
+ fs.writeFileSync(wrapperPath, wrapperContent, 'utf-8');
477
+ logger.info(`✅ Generated wrapper: ${path.relative(process.cwd(), wrapperPath)}`);
478
+ }
172
479
  // Inject Umami analytics script if configured
173
480
  if (enableAnalytics && analyticsConfig) {
174
481
  logger.info('📊 Injecting Umami analytics script...');
@@ -183,13 +490,49 @@ export function ayezeeCms(options = {}) {
183
490
  `);
184
491
  logger.info('✅ Umami analytics script injected');
185
492
  }
493
+ // Report site version to dashboard (non-blocking)
494
+ if (reportVersion && siteUrl) {
495
+ const astroVersion = getAstroVersion();
496
+ reportSiteVersion(baseUrl, apiKey, siteUrl, environment, astroVersion, logger);
497
+ }
498
+ logger.info('');
499
+ logger.info('🎉 AyeZee CMS data fetched successfully!');
500
+ logger.info('');
186
501
  }
187
502
  catch (error) {
188
503
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
189
- logger.error('❌ Failed to fetch CMS data:');
190
- logger.error(` ${errorMessage}`);
504
+ logger.error('');
505
+ logger.error('❌ Failed to fetch CMS data');
506
+ logger.error('═══════════════════════════════════════════════════════════');
507
+ logger.error('');
508
+ logger.error(`Error: ${errorMessage}`);
509
+ logger.error('');
510
+ // Provide helpful troubleshooting tips
511
+ if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('fetch failed')) {
512
+ logger.error('Troubleshooting tips:');
513
+ logger.error(' 1. Is the CMS dashboard running?');
514
+ logger.error(` 2. Can you access ${cmsDomain} in your browser?`);
515
+ logger.error(' 3. Check your network connection');
516
+ logger.error(' 4. If using localhost, ensure the dashboard is started');
517
+ }
518
+ else if (errorMessage.includes('401') || errorMessage.includes('403')) {
519
+ logger.error('Troubleshooting tips:');
520
+ logger.error(' 1. Check that PUBLIC_AYEZEE_API_KEY is correct');
521
+ logger.error(' 2. Verify the API key has read permissions');
522
+ logger.error(' 3. Ensure the project slug matches your dashboard project');
523
+ }
524
+ else if (errorMessage.includes('404')) {
525
+ logger.error('Troubleshooting tips:');
526
+ logger.error(' 1. Verify PUBLIC_PROJECT_SLUG is correct');
527
+ logger.error(' 2. Check that the project exists in the dashboard');
528
+ logger.error(` 3. Project slug should match: ${projectSlug}`);
529
+ }
530
+ logger.error('');
531
+ logger.error('═══════════════════════════════════════════════════════════');
532
+ logger.error('');
191
533
  if (skipOnError) {
192
- logger.warn('⚠️ Continuing build without CMS data');
534
+ logger.warn('⚠️ Continuing build with cached data (skipOnError: true)');
535
+ logger.warn(' The site will use previously cached CMS data if available.');
193
536
  return;
194
537
  }
195
538
  throw error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ayezee-astro-cms",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "AyeZee CMS integration for Astro with automatic data fetching, form handling, validation, and analytics",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",