@marvalt/digivalt-core 0.1.6 → 0.2.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 (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +32 -4
  3. package/bin/init.cjs +42 -7
  4. package/dist/config.cjs +520 -0
  5. package/dist/config.cjs.map +1 -0
  6. package/dist/config.d.ts +307 -0
  7. package/dist/config.esm.js +502 -0
  8. package/dist/config.esm.js.map +1 -0
  9. package/dist/generators.cjs +2481 -0
  10. package/dist/generators.cjs.map +1 -0
  11. package/dist/generators.d.ts +19 -0
  12. package/dist/generators.esm.js +2475 -0
  13. package/dist/generators.esm.js.map +1 -0
  14. package/dist/index.cjs +18 -7955
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +18 -1196
  17. package/dist/index.esm.js +13 -7929
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/runtime/env.d.ts +4 -0
  20. package/dist/runtime/lazy.d.ts +1 -0
  21. package/dist/services/cf-wp-webhook.d.ts +2 -0
  22. package/dist/services/gravityForms.d.ts +3 -0
  23. package/dist/services/mautic.d.ts +5 -1
  24. package/dist/services/suitecrm.d.ts +2 -0
  25. package/dist/services.cjs +1339 -0
  26. package/dist/services.cjs.map +1 -0
  27. package/dist/services.d.ts +432 -0
  28. package/dist/services.esm.js +1322 -0
  29. package/dist/services.esm.js.map +1 -0
  30. package/dist/static/index.d.ts +4 -0
  31. package/dist/static.cjs +997 -0
  32. package/dist/static.cjs.map +1 -0
  33. package/dist/static.d.ts +410 -0
  34. package/dist/static.esm.js +962 -0
  35. package/dist/static.esm.js.map +1 -0
  36. package/dist/types.cjs +3 -0
  37. package/dist/types.cjs.map +1 -0
  38. package/dist/types.d.ts +134 -0
  39. package/dist/types.esm.js +2 -0
  40. package/dist/types.esm.js.map +1 -0
  41. package/package.json +30 -2
  42. package/template/DIGIVALT_SETUP.md +62 -0
  43. package/template/scripts/deploy-secrets.js +55 -23
  44. package/template/scripts/generate.ts +19 -0
@@ -0,0 +1,2475 @@
1
+ import dotenv from 'dotenv';
2
+ import path, { join, dirname } from 'path';
3
+ import fs, { writeFileSync } from 'fs';
4
+ import { generateSuiteCRMData as generateSuiteCRMData$1 } from '@marvalt/sadapter';
5
+
6
+ /**
7
+ * @license GPL-3.0-or-later
8
+ *
9
+ * This file is part of the MarVAlt Open SDK.
10
+ * Copyright (c) 2025 Vibune Pty Ltd.
11
+ *
12
+ * This program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU General Public License as published by
14
+ * the Free Software Foundation, either version 3 of the License, or
15
+ * (at your option) any later version.
16
+ *
17
+ * This program is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
20
+ * See the GNU General Public License for more details.
21
+ */
22
+ class WordPressClient {
23
+ constructor(config) {
24
+ this.config = config;
25
+ }
26
+ async makeRequest(endpoint, params = {}, options = {}) {
27
+ const baseUrl = this.getBaseUrl();
28
+ const queryString = new URLSearchParams();
29
+ Object.entries(params).forEach(([key, value]) => {
30
+ if (value !== undefined && value !== null) {
31
+ if (Array.isArray(value)) {
32
+ value.forEach(v => queryString.append(key, v.toString()));
33
+ }
34
+ else {
35
+ queryString.append(key, value.toString());
36
+ }
37
+ }
38
+ });
39
+ // Construct URL based on auth mode
40
+ let url;
41
+ if (this.config.authMode === 'cloudflare_proxy') {
42
+ // For proxy modes, pass the full REST path including /wp-json as query parameter
43
+ const fullEndpoint = `/wp-json${endpoint}?${queryString.toString()}`;
44
+ url = `${baseUrl}?endpoint=${encodeURIComponent(fullEndpoint)}`;
45
+ }
46
+ else {
47
+ // For direct mode, construct the full WordPress API URL
48
+ url = `${baseUrl}/wp-json${endpoint}?${queryString.toString()}`;
49
+ }
50
+ const headers = {
51
+ 'Content-Type': 'application/json',
52
+ };
53
+ // Add authentication based on mode
54
+ if (this.config.authMode === 'cloudflare_proxy') ;
55
+ else {
56
+ // Direct mode - use basic auth
57
+ if (this.config.username && this.config.password) {
58
+ const credentials = btoa(`${this.config.username}:${this.config.password}`);
59
+ headers.Authorization = `Basic ${credentials}`;
60
+ }
61
+ // Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
62
+ if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
63
+ headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
64
+ headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
65
+ console.log('🔐 Added CF Access Service Token headers to WordPress request');
66
+ console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
67
+ console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
68
+ }
69
+ else {
70
+ console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
71
+ console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
72
+ console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
73
+ }
74
+ }
75
+ console.log('🔍 WordPress API Request Debug:', {
76
+ url,
77
+ method: options.method || 'GET',
78
+ authMode: this.config.authMode,
79
+ headers: Object.keys(headers),
80
+ hasBasicAuth: !!headers.Authorization,
81
+ hasCfAccess: !!(headers['CF-Access-Client-Id'] && headers['CF-Access-Client-Secret']),
82
+ });
83
+ const response = await fetch(url, {
84
+ method: options.method || 'GET',
85
+ headers,
86
+ body: options.body,
87
+ signal: AbortSignal.timeout(this.config.timeout || 30000),
88
+ ...options,
89
+ });
90
+ if (!response.ok) {
91
+ console.error('❌ WordPress API Error:', {
92
+ status: response.status,
93
+ statusText: response.statusText,
94
+ url,
95
+ headers: Object.fromEntries(response.headers.entries()),
96
+ });
97
+ throw new Error(`WordPress API request failed: ${response.status} ${response.statusText}`);
98
+ }
99
+ const responseText = await response.text();
100
+ console.log('🔍 WordPress API Response:', {
101
+ status: response.status,
102
+ responseLength: responseText.length,
103
+ responsePreview: responseText.substring(0, 200),
104
+ });
105
+ if (!responseText) {
106
+ // Return empty array for empty responses (common for endpoints with no data)
107
+ return [];
108
+ }
109
+ try {
110
+ return JSON.parse(responseText);
111
+ }
112
+ catch (error) {
113
+ const message = error instanceof Error ? error.message : String(error);
114
+ console.error('❌ JSON Parse Error:', {
115
+ error: message,
116
+ responseText: responseText.substring(0, 500),
117
+ });
118
+ throw new Error(`Failed to parse JSON response: ${message}`);
119
+ }
120
+ }
121
+ getBaseUrl() {
122
+ switch (this.config.authMode) {
123
+ case 'cloudflare_proxy':
124
+ return this.config.cloudflareWorkerUrl || '';
125
+ default:
126
+ return this.config.apiUrl || '';
127
+ }
128
+ }
129
+ /**
130
+ * Fetch posts with automatic pagination support
131
+ * @param params Query parameters including per_page for total items to fetch
132
+ * @returns All posts up to the limit, fetched across multiple pages if needed
133
+ */
134
+ async getPosts(params = {}) {
135
+ return this.fetchPaginated('/wp/v2/posts', params);
136
+ }
137
+ async getPost(id) {
138
+ return this.makeRequest(`/wp/v2/posts/${id}`);
139
+ }
140
+ /**
141
+ * Fetch pages with automatic pagination support
142
+ * @param params Query parameters including per_page for total items to fetch
143
+ * @returns All pages up to the limit, fetched across multiple pages if needed
144
+ */
145
+ async getPages(params = {}) {
146
+ return this.fetchPaginated('/wp/v2/pages', params);
147
+ }
148
+ async getPage(id) {
149
+ return this.makeRequest(`/wp/v2/pages/${id}`);
150
+ }
151
+ /**
152
+ * Fetch media with automatic pagination support
153
+ * @param params Query parameters including per_page for total items to fetch
154
+ * @returns All media items up to the limit, fetched across multiple pages if needed
155
+ */
156
+ async getMedia(params = {}) {
157
+ return this.fetchPaginated('/wp/v2/media', params);
158
+ }
159
+ async getMediaItem(id) {
160
+ return this.makeRequest(`/wp/v2/media/${id}`);
161
+ }
162
+ /**
163
+ * Fetch categories with automatic pagination support
164
+ * @param params Query parameters including per_page for total items to fetch
165
+ * @returns All categories up to the limit, fetched across multiple pages if needed
166
+ */
167
+ async getCategories(params = {}) {
168
+ return this.fetchPaginated('/wp/v2/categories', params);
169
+ }
170
+ /**
171
+ * Fetch tags with automatic pagination support
172
+ * @param params Query parameters including per_page for total items to fetch
173
+ * @returns All tags up to the limit, fetched across multiple pages if needed
174
+ */
175
+ async getTags(params = {}) {
176
+ return this.fetchPaginated('/wp/v2/tags', params);
177
+ }
178
+ /**
179
+ * Fetch data with automatic pagination
180
+ * Handles fetching all items up to the requested limit by making multiple API calls if needed
181
+ * @param endpoint The API endpoint to fetch from
182
+ * @param params Query parameters including per_page for total items to fetch
183
+ * @returns Array of all items fetched across all pages
184
+ */
185
+ async fetchPaginated(endpoint, params = {}) {
186
+ const requestedTotal = params.per_page || 100;
187
+ const itemsPerPage = Math.min(requestedTotal, 100); // WordPress max is typically 100
188
+ const maxPages = Math.ceil(requestedTotal / itemsPerPage);
189
+ let allItems = [];
190
+ let currentPage = 1;
191
+ let hasMorePages = true;
192
+ console.log(`🔄 Fetching ${endpoint} with pagination: ${requestedTotal} items requested (${maxPages} pages of ${itemsPerPage})`);
193
+ while (hasMorePages && currentPage <= maxPages && allItems.length < requestedTotal) {
194
+ const pageParams = {
195
+ ...params,
196
+ per_page: itemsPerPage,
197
+ page: currentPage,
198
+ };
199
+ console.log(`📄 Fetching page ${currentPage}/${maxPages} for ${endpoint}...`);
200
+ try {
201
+ const pageData = await this.makeRequest(endpoint, pageParams);
202
+ if (!pageData || pageData.length === 0) {
203
+ console.log(`✓ No more data on page ${currentPage}, stopping pagination`);
204
+ hasMorePages = false;
205
+ break;
206
+ }
207
+ allItems = allItems.concat(pageData);
208
+ console.log(`✓ Page ${currentPage}: fetched ${pageData.length} items (total: ${allItems.length})`);
209
+ // Stop if we got fewer items than requested per page (last page)
210
+ if (pageData.length < itemsPerPage) {
211
+ console.log(`✓ Reached last page (partial results)`);
212
+ hasMorePages = false;
213
+ }
214
+ currentPage++;
215
+ }
216
+ catch (error) {
217
+ console.error(`❌ Error fetching page ${currentPage}:`, error);
218
+ // Stop pagination on error but return what we have so far
219
+ hasMorePages = false;
220
+ }
221
+ }
222
+ console.log(`✅ Pagination complete for ${endpoint}: ${allItems.length} total items fetched`);
223
+ // Trim to exact requested amount if we fetched more
224
+ return allItems.slice(0, requestedTotal);
225
+ }
226
+ // Gravity Forms endpoints
227
+ async getGravityForms() {
228
+ return this.makeRequest('/gf-api/v1/forms');
229
+ }
230
+ async getGravityForm(formId) {
231
+ return this.makeRequest(`/gf-api/v1/forms/${formId}/config`);
232
+ }
233
+ async submitGravityForm(formId, entry) {
234
+ return this.makeRequest(`/gf-api/v1/forms/${formId}/submit`, {}, {
235
+ method: 'POST',
236
+ body: JSON.stringify(entry),
237
+ });
238
+ }
239
+ // Static data endpoints
240
+ async getStaticDataNonce() {
241
+ return this.makeRequest('/static-data/v1/nonce');
242
+ }
243
+ /**
244
+ * Fetch a custom post type with automatic pagination support
245
+ * @param postType The custom post type slug (e.g., 'chapter-member', 'events')
246
+ * @param params Query parameters including per_page for total items to fetch
247
+ * @returns All items of the custom post type up to the limit
248
+ */
249
+ async getCustomPostType(postType, params = {}) {
250
+ return this.fetchPaginated(`/wp/v2/${postType}`, params);
251
+ }
252
+ async getAllData(params = {}) {
253
+ const [posts, pages, media, categories, tags] = await Promise.all([
254
+ this.getPosts(params),
255
+ this.getPages(params),
256
+ this.getMedia(params),
257
+ this.getCategories(params),
258
+ this.getTags(params),
259
+ ]);
260
+ return { posts, pages, media, categories, tags };
261
+ }
262
+ /**
263
+ * Fetch blocks for a specific page from the custom blocks endpoint
264
+ * @param pageId Page ID
265
+ * @returns Blocks data from /wp-custom/v1/pages/{id}/blocks
266
+ */
267
+ async getPageBlocks(pageId) {
268
+ return this.makeRequest(`/wp-custom/v1/pages/${pageId}/blocks`);
269
+ }
270
+ /**
271
+ * Fetch WordPress Settings from custom endpoint
272
+ * @returns Settings including front page information and site branding
273
+ */
274
+ /**
275
+ * Fetch header template part blocks from custom endpoint
276
+ * @param slug Template part slug (default: 'header')
277
+ * @param area Template part area (default: 'header')
278
+ * @returns Header blocks data
279
+ */
280
+ async getHeader(slug = 'header', area = 'header') {
281
+ return this.makeRequest(`/wp-custom/v1/header`, { slug, area });
282
+ }
283
+ /**
284
+ * Fetch all navigation menus
285
+ * @returns Array of menu objects
286
+ */
287
+ async getMenus() {
288
+ return this.makeRequest('/wp-custom/v1/menus');
289
+ }
290
+ /**
291
+ * Fetch a specific navigation menu with its items
292
+ * Handles both menu term IDs and wp_navigation post IDs
293
+ * @param menuId Menu ID (can be menu term ID or wp_navigation post ID)
294
+ * @returns Menu data with hierarchical items
295
+ */
296
+ async getMenu(menuId) {
297
+ console.log(`🌐 [WordPressClient] Fetching menu ID ${menuId}...`);
298
+ try {
299
+ // First try the custom endpoint (handles both menu terms and wp_navigation posts)
300
+ console.log(` → Trying endpoint: /wp-custom/v1/menus/${menuId}`);
301
+ const result = await this.makeRequest(`/wp-custom/v1/menus/${menuId}`);
302
+ console.log(` ✅ Successfully fetched menu ID ${menuId} as menu term: ${result.name}`);
303
+ return result;
304
+ }
305
+ catch (error) {
306
+ const errorStatus = error.status || (error.response?.status) || 'unknown';
307
+ const errorMessage = error.message || 'Unknown error';
308
+ console.log(` ❌ Menu term fetch failed (status: ${errorStatus}): ${errorMessage}`);
309
+ // If 404, try fetching as wp_navigation post
310
+ if (errorStatus === 404 || errorMessage?.includes('404')) {
311
+ console.log(` → Menu ID ${menuId} not found as menu term, trying as wp_navigation post...`);
312
+ // Try custom endpoint first
313
+ try {
314
+ console.log(` → Trying navigation post endpoint: /wp-custom/v1/navigation/${menuId}`);
315
+ const navPost = await this.makeRequest(`/wp-custom/v1/navigation/${menuId}`);
316
+ console.log(` ✅ Navigation post fetched:`, {
317
+ id: navPost.id,
318
+ hasMenu: !!navPost.menu,
319
+ menuTermId: navPost.menu_term_id,
320
+ });
321
+ // If navigation post has menu data (either from menu_term_id or extracted from blocks)
322
+ if (navPost.menu) {
323
+ console.log(` ✅ Returning menu from navigation post: ${navPost.menu.name} (${navPost.menu.items?.length || 0} items)`);
324
+ return navPost.menu;
325
+ }
326
+ // If navigation post has a menu_term_id but no menu data, fetch that menu
327
+ if (navPost.menu_term_id) {
328
+ console.log(` → Navigation post has menu_term_id ${navPost.menu_term_id}, fetching menu...`);
329
+ const menu = await this.makeRequest(`/wp-custom/v1/menus/${navPost.menu_term_id}`);
330
+ console.log(` ✅ Fetched menu from menu_term_id ${navPost.menu_term_id}: ${menu.name}`);
331
+ return menu;
332
+ }
333
+ else {
334
+ console.warn(` ⚠️ Navigation post ${menuId} has no menu_term_id and no menu data extracted from blocks`);
335
+ throw new Error(`Navigation post ${menuId} has no menu_term_id and no menu data`);
336
+ }
337
+ }
338
+ catch (navError) {
339
+ const navErrorStatus = navError.status || (navError.response?.status) || 'unknown';
340
+ const navErrorMessage = navError.message || 'Unknown error';
341
+ console.log(` ❌ Navigation post fetch failed (status: ${navErrorStatus}): ${navErrorMessage}`);
342
+ // If custom endpoint fails, try WordPress native REST API (WordPress 6.3+)
343
+ if (navErrorStatus === 404 || navErrorMessage?.includes('404')) {
344
+ console.log(` → Custom navigation endpoint not found, trying WordPress native REST API...`);
345
+ try {
346
+ console.log(` → Trying WordPress native REST API: /wp/v2/navigation/${menuId}`);
347
+ // WordPress 6.3+ has native REST API for wp_navigation posts
348
+ const navPost = await this.makeRequest(`/wp/v2/navigation/${menuId}`);
349
+ console.log(` ✅ WordPress native navigation post fetched:`, {
350
+ id: navPost.id,
351
+ title: navPost.title?.rendered,
352
+ hasContent: !!navPost.content?.raw,
353
+ });
354
+ // Parse blocks from content to find menu reference
355
+ // The content.raw contains the block JSON
356
+ if (navPost.content?.raw) {
357
+ try {
358
+ const blocks = JSON.parse(navPost.content.raw);
359
+ console.log(` → Parsed ${Array.isArray(blocks) ? blocks.length : 'unknown'} blocks from navigation post`);
360
+ // Look for menu reference in blocks (this would need to be done server-side)
361
+ // For now, throw error to indicate menu extraction requires server-side processing
362
+ console.log(`ℹ️ Found wp_navigation post ${menuId}, but menu extraction requires server-side processing`);
363
+ throw new Error(`Menu extraction from wp_navigation post ${menuId} requires server-side processing`);
364
+ }
365
+ catch (parseError) {
366
+ console.log(`⚠️ Could not parse navigation post content`);
367
+ throw new Error(`Could not parse navigation post content for menu ID ${menuId}`);
368
+ }
369
+ }
370
+ else {
371
+ throw new Error(`wp_navigation post ${menuId} has no content to parse`);
372
+ }
373
+ }
374
+ catch (wpError) {
375
+ const wpErrorStatus = wpError.status || (wpError.response?.status) || 'unknown';
376
+ const wpErrorMessage = wpError.message || 'Unknown error';
377
+ console.error(` ❌ WordPress native REST API also failed (status: ${wpErrorStatus}): ${wpErrorMessage}`);
378
+ console.error(` Full WordPress API error:`, wpError);
379
+ // Re-throw to ensure error is propagated
380
+ throw new Error(`Could not fetch menu ID ${menuId} via any method: ${wpErrorMessage}`);
381
+ }
382
+ }
383
+ else {
384
+ console.error(` ❌ Navigation post fetch failed with non-404 error, re-throwing...`);
385
+ throw navError; // Re-throw navigation error
386
+ }
387
+ }
388
+ }
389
+ else {
390
+ console.error(` ❌ Menu term fetch failed with non-404 error (status: ${errorStatus}), re-throwing...`);
391
+ throw error; // Re-throw original error
392
+ }
393
+ }
394
+ }
395
+ async getFooter(slug = 'footer', area = 'footer') {
396
+ return this.makeRequest(`/wp-custom/v1/footer`, { slug, area });
397
+ }
398
+ async getSettings() {
399
+ return this.makeRequest('/wp-custom/v1/settings');
400
+ }
401
+ }
402
+
403
+ let document;
404
+ let offset;
405
+ let output;
406
+ let stack;
407
+ const tokenizer = /<!--\s+(\/)?wp:([a-z][a-z0-9_-]*\/)?([a-z][a-z0-9_-]*)\s+({(?:(?=([^}]+|}+(?=})|(?!}\s+\/?-->)[^])*)\5|[^]*?)}\s+)?(\/)?-->/g;
408
+ function Block(blockName, attrs, innerBlocks, innerHTML, innerContent) {
409
+ return {
410
+ blockName,
411
+ attrs,
412
+ innerBlocks,
413
+ innerHTML,
414
+ innerContent
415
+ };
416
+ }
417
+ function Freeform(innerHTML) {
418
+ return Block(null, {}, [], innerHTML, [innerHTML]);
419
+ }
420
+ function Frame(block, tokenStart, tokenLength, prevOffset, leadingHtmlStart) {
421
+ return {
422
+ block,
423
+ tokenStart,
424
+ tokenLength,
425
+ prevOffset: prevOffset || tokenStart + tokenLength,
426
+ leadingHtmlStart
427
+ };
428
+ }
429
+ const parse = (doc) => {
430
+ document = doc;
431
+ offset = 0;
432
+ output = [];
433
+ stack = [];
434
+ tokenizer.lastIndex = 0;
435
+ do {
436
+ } while (proceed());
437
+ return output;
438
+ };
439
+ function proceed() {
440
+ const stackDepth = stack.length;
441
+ const next = nextToken();
442
+ const [tokenType, blockName, attrs, startOffset, tokenLength] = next;
443
+ const leadingHtmlStart = startOffset > offset ? offset : null;
444
+ switch (tokenType) {
445
+ case "no-more-tokens":
446
+ if (0 === stackDepth) {
447
+ addFreeform();
448
+ return false;
449
+ }
450
+ if (1 === stackDepth) {
451
+ addBlockFromStack();
452
+ return false;
453
+ }
454
+ while (0 < stack.length) {
455
+ addBlockFromStack();
456
+ }
457
+ return false;
458
+ case "void-block":
459
+ if (0 === stackDepth) {
460
+ if (null !== leadingHtmlStart) {
461
+ output.push(
462
+ Freeform(
463
+ document.substr(
464
+ leadingHtmlStart,
465
+ startOffset - leadingHtmlStart
466
+ )
467
+ )
468
+ );
469
+ }
470
+ output.push(Block(blockName, attrs, [], "", []));
471
+ offset = startOffset + tokenLength;
472
+ return true;
473
+ }
474
+ addInnerBlock(
475
+ Block(blockName, attrs, [], "", []),
476
+ startOffset,
477
+ tokenLength
478
+ );
479
+ offset = startOffset + tokenLength;
480
+ return true;
481
+ case "block-opener":
482
+ stack.push(
483
+ Frame(
484
+ Block(blockName, attrs, [], "", []),
485
+ startOffset,
486
+ tokenLength,
487
+ startOffset + tokenLength,
488
+ leadingHtmlStart
489
+ )
490
+ );
491
+ offset = startOffset + tokenLength;
492
+ return true;
493
+ case "block-closer":
494
+ if (0 === stackDepth) {
495
+ addFreeform();
496
+ return false;
497
+ }
498
+ if (1 === stackDepth) {
499
+ addBlockFromStack(startOffset);
500
+ offset = startOffset + tokenLength;
501
+ return true;
502
+ }
503
+ const stackTop = stack.pop();
504
+ const html = document.substr(
505
+ stackTop.prevOffset,
506
+ startOffset - stackTop.prevOffset
507
+ );
508
+ stackTop.block.innerHTML += html;
509
+ stackTop.block.innerContent.push(html);
510
+ stackTop.prevOffset = startOffset + tokenLength;
511
+ addInnerBlock(
512
+ stackTop.block,
513
+ stackTop.tokenStart,
514
+ stackTop.tokenLength,
515
+ startOffset + tokenLength
516
+ );
517
+ offset = startOffset + tokenLength;
518
+ return true;
519
+ default:
520
+ addFreeform();
521
+ return false;
522
+ }
523
+ }
524
+ function parseJSON(input) {
525
+ try {
526
+ return JSON.parse(input);
527
+ } catch (e) {
528
+ return null;
529
+ }
530
+ }
531
+ function nextToken() {
532
+ const matches = tokenizer.exec(document);
533
+ if (null === matches) {
534
+ return ["no-more-tokens", "", null, 0, 0];
535
+ }
536
+ const startedAt = matches.index;
537
+ const [
538
+ match,
539
+ closerMatch,
540
+ namespaceMatch,
541
+ nameMatch,
542
+ attrsMatch,
543
+ ,
544
+ voidMatch
545
+ ] = matches;
546
+ const length = match.length;
547
+ const isCloser = !!closerMatch;
548
+ const isVoid = !!voidMatch;
549
+ const namespace = namespaceMatch || "core/";
550
+ const name = namespace + nameMatch;
551
+ const hasAttrs = !!attrsMatch;
552
+ const attrs = hasAttrs ? parseJSON(attrsMatch) : {};
553
+ if (isVoid) {
554
+ return ["void-block", name, attrs, startedAt, length];
555
+ }
556
+ if (isCloser) {
557
+ return ["block-closer", name, null, startedAt, length];
558
+ }
559
+ return ["block-opener", name, attrs, startedAt, length];
560
+ }
561
+ function addFreeform(rawLength) {
562
+ const length = document.length - offset;
563
+ if (0 === length) {
564
+ return;
565
+ }
566
+ output.push(Freeform(document.substr(offset, length)));
567
+ }
568
+ function addInnerBlock(block, tokenStart, tokenLength, lastOffset) {
569
+ const parent = stack[stack.length - 1];
570
+ parent.block.innerBlocks.push(block);
571
+ const html = document.substr(
572
+ parent.prevOffset,
573
+ tokenStart - parent.prevOffset
574
+ );
575
+ if (html) {
576
+ parent.block.innerHTML += html;
577
+ parent.block.innerContent.push(html);
578
+ }
579
+ parent.block.innerContent.push(null);
580
+ parent.prevOffset = lastOffset ? lastOffset : tokenStart + tokenLength;
581
+ }
582
+ function addBlockFromStack(endOffset) {
583
+ const { block, leadingHtmlStart, prevOffset, tokenStart } = stack.pop();
584
+ const html = endOffset ? document.substr(prevOffset, endOffset - prevOffset) : document.substr(prevOffset);
585
+ if (html) {
586
+ block.innerHTML += html;
587
+ block.innerContent.push(html);
588
+ }
589
+ if (null !== leadingHtmlStart) {
590
+ output.push(
591
+ Freeform(
592
+ document.substr(
593
+ leadingHtmlStart,
594
+ tokenStart - leadingHtmlStart
595
+ )
596
+ )
597
+ );
598
+ }
599
+ output.push(block);
600
+ }
601
+
602
+ /**
603
+ * @license GPL-3.0-or-later
604
+ *
605
+ * This file is part of the MarVAlt Open SDK.
606
+ * Copyright (c) 2025 Vibune Pty Ltd.
607
+ *
608
+ * This program is free software: you can redistribute it and/or modify
609
+ * it under the terms of the GNU General Public License as published by
610
+ * the Free Software Foundation, either version 3 of the License, or
611
+ * (at your option) any later version.
612
+ *
613
+ * This program is distributed in the hope that it will be useful,
614
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
615
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
616
+ * See the GNU General Public License for more details.
617
+ */
618
+ // WordPress Static Data Generator
619
+ /**
620
+ * Extract menu items from a navigation block's innerHTML (rendered HTML)
621
+ * WordPress renders navigation blocks, so menu items are in innerHTML as HTML
622
+ * This function properly handles submenus by parsing the nested <ul> structure
623
+ */
624
+ function extractMenuFromNavigationHTML(innerHTML) {
625
+ if (!innerHTML)
626
+ return null;
627
+ // Find the main navigation container <ul>
628
+ const mainNavMatch = innerHTML.match(/<ul[^>]*class="[^"]*wp-block-navigation__container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
629
+ if (!mainNavMatch) {
630
+ return null;
631
+ }
632
+ const mainNavContent = mainNavMatch[1];
633
+ const items = [];
634
+ let globalOrder = 0;
635
+ /**
636
+ * Parse a <li> element and extract menu item data
637
+ * Handles both regular links and submenus
638
+ */
639
+ function parseMenuItem(liElement, parentId = 0, order = 0) {
640
+ // Extract the link
641
+ const linkMatch = liElement.match(/<a[^>]+class="[^"]*wp-block-navigation-item__content[^"]*"[^>]+href=["']([^"']+)["'][^>]*>[\s\S]*?<span[^>]*class="[^"]*wp-block-navigation-item__label[^"]*"[^>]*>([^<]+)<\/span>[\s\S]*?<\/a>/i);
642
+ if (!linkMatch) {
643
+ return null;
644
+ }
645
+ const url = linkMatch[1].trim();
646
+ const title = linkMatch[2].trim();
647
+ if (!title) {
648
+ return null;
649
+ }
650
+ // Normalize URL
651
+ let finalUrl = url;
652
+ try {
653
+ const urlObj = new URL(url, 'http://dummy.com'); // Use dummy base for relative URLs
654
+ const isAbsoluteUrl = url.startsWith('http://') || url.startsWith('https://');
655
+ if (isAbsoluteUrl) {
656
+ const pathname = urlObj.pathname;
657
+ const isExternal = pathname === '' || pathname === '/' ||
658
+ !urlObj.hostname.includes('vibunedemos.com') &&
659
+ !urlObj.hostname.includes('localhost') &&
660
+ !urlObj.hostname.includes('127.0.0.1');
661
+ if (isExternal) {
662
+ finalUrl = url;
663
+ }
664
+ else {
665
+ finalUrl = pathname + urlObj.search + urlObj.hash;
666
+ if (!finalUrl.startsWith('/')) {
667
+ finalUrl = `/${finalUrl}`;
668
+ }
669
+ if (finalUrl !== '/' && finalUrl.endsWith('/')) {
670
+ finalUrl = finalUrl.slice(0, -1);
671
+ }
672
+ }
673
+ }
674
+ else {
675
+ if (!finalUrl.startsWith('/')) {
676
+ finalUrl = `/${finalUrl}`;
677
+ }
678
+ if (finalUrl !== '/' && finalUrl.endsWith('/')) {
679
+ finalUrl = finalUrl.slice(0, -1);
680
+ }
681
+ }
682
+ }
683
+ catch (e) {
684
+ if (!finalUrl.startsWith('http://') && !finalUrl.startsWith('https://') && !finalUrl.startsWith('/')) {
685
+ finalUrl = `/${finalUrl}`;
686
+ }
687
+ if (finalUrl !== '/' && finalUrl.endsWith('/')) {
688
+ finalUrl = finalUrl.slice(0, -1);
689
+ }
690
+ }
691
+ const itemId = globalOrder + 1;
692
+ globalOrder++;
693
+ const menuItem = {
694
+ id: itemId,
695
+ title: title,
696
+ url: finalUrl,
697
+ type: 'custom',
698
+ object_id: 0,
699
+ object: 'custom',
700
+ parent: parentId,
701
+ menu_order: order,
702
+ };
703
+ // Check if this <li> contains a submenu (<ul class="wp-block-navigation__submenu-container">)
704
+ const submenuMatch = liElement.match(/<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>([\s\S]*?)<\/ul>/i);
705
+ if (submenuMatch) {
706
+ const submenuContent = submenuMatch[1];
707
+ const children = [];
708
+ let childOrder = 0;
709
+ // Extract all <li> elements from the submenu
710
+ const submenuLiRegex = /<li[^>]*class="[^"]*wp-block-navigation-item[^"]*"[^>]*>([\s\S]*?)<\/li>/gi;
711
+ let submenuLiMatch;
712
+ while ((submenuLiMatch = submenuLiRegex.exec(submenuContent)) !== null) {
713
+ const childResult = parseMenuItem(submenuLiMatch[1], itemId, childOrder++);
714
+ if (childResult) {
715
+ children.push(childResult.item);
716
+ globalOrder = childResult.nextOrder;
717
+ }
718
+ }
719
+ if (children.length > 0) {
720
+ menuItem.children = children;
721
+ }
722
+ }
723
+ return { item: menuItem, nextOrder: globalOrder };
724
+ }
725
+ // Extract all top-level <li> elements from the main navigation
726
+ // Find all <li> tags and extract complete items, filtering by position
727
+ const liRegex = /<li[^>]*>/gi;
728
+ const allLiMatches = [];
729
+ // First pass: find all <li> tags and their matching </li>
730
+ let match;
731
+ while ((match = liRegex.exec(mainNavContent)) !== null) {
732
+ const liStart = match.index;
733
+ let depth = 0;
734
+ let j = liStart;
735
+ while (j < mainNavContent.length) {
736
+ if (mainNavContent.substr(j, 3) === '<li' && /[>\s\n\t]/.test(mainNavContent[j + 3] || '')) {
737
+ depth++;
738
+ }
739
+ else if (mainNavContent.substr(j, 5) === '</li>') {
740
+ depth--;
741
+ if (depth === 0) {
742
+ allLiMatches.push({ start: liStart, end: j + 5 });
743
+ break;
744
+ }
745
+ }
746
+ j++;
747
+ }
748
+ }
749
+ // Second pass: filter to only top-level items (not inside submenu containers)
750
+ // Build a map of submenu container ranges
751
+ const submenuRanges = [];
752
+ const submenuContainerRegex = /<ul[^>]*class="[^"]*wp-block-navigation__submenu-container[^"]*"[^>]*>/gi;
753
+ let submenuMatch;
754
+ // Find all submenu containers and their matching </ul> tags
755
+ while ((submenuMatch = submenuContainerRegex.exec(mainNavContent)) !== null) {
756
+ const submenuStart = submenuMatch.index;
757
+ const submenuUlEnd = mainNavContent.indexOf('>', submenuStart);
758
+ if (submenuUlEnd === -1)
759
+ continue;
760
+ // Find matching </ul> by tracking depth
761
+ let ulDepth = 0;
762
+ let checkPos = submenuUlEnd + 1;
763
+ let submenuEnd = -1;
764
+ while (checkPos < mainNavContent.length) {
765
+ const ulOpen = mainNavContent.indexOf('<ul', checkPos);
766
+ const ulClose = mainNavContent.indexOf('</ul>', checkPos);
767
+ if (ulOpen !== -1 && (ulClose === -1 || ulOpen < ulClose)) {
768
+ ulDepth++;
769
+ checkPos = ulOpen + 3;
770
+ }
771
+ else if (ulClose !== -1) {
772
+ ulDepth--;
773
+ if (ulDepth === 0) {
774
+ submenuEnd = ulClose + 5;
775
+ break;
776
+ }
777
+ checkPos = ulClose + 5;
778
+ }
779
+ else {
780
+ break;
781
+ }
782
+ }
783
+ if (submenuEnd !== -1) {
784
+ submenuRanges.push({ start: submenuStart, end: submenuEnd });
785
+ }
786
+ }
787
+ // Filter <li> elements: exclude only those that are children inside submenu containers
788
+ // An <li> is a child (inside submenu) if: submenu's <ul> starts before <li> AND submenu's </ul> ends after <li> ends
789
+ // An <li> is a parent (contains submenu) if: <li> starts before submenu's <ul> (we want to include these)
790
+ for (const { start, end } of allLiMatches) {
791
+ // Check if this <li> is a child inside any submenu container
792
+ let isChildItem = false;
793
+ for (const range of submenuRanges) {
794
+ // If submenu <ul> starts before <li> AND submenu </ul> ends after <li> ends, then <li> is a child
795
+ if (range.start < start && range.end > end) {
796
+ isChildItem = true;
797
+ break;
798
+ }
799
+ }
800
+ // Include all items except children (children will be extracted by parseMenuItem as part of their parent)
801
+ if (!isChildItem) {
802
+ const item = mainNavContent.substring(start, end);
803
+ if (item.includes('wp-block-navigation-item')) {
804
+ const result = parseMenuItem(item, 0, items.length);
805
+ if (result) {
806
+ items.push(result.item);
807
+ globalOrder = result.nextOrder;
808
+ }
809
+ }
810
+ }
811
+ }
812
+ if (items.length === 0) {
813
+ return null;
814
+ }
815
+ return {
816
+ id: 0, // No menu ID for inline menus
817
+ name: 'Navigation Menu',
818
+ slug: 'navigation-menu',
819
+ items: items,
820
+ };
821
+ }
822
+ /**
823
+ * Find a menu by name (checks both slug and name fields, case-insensitive)
824
+ * @param menus Array of menu objects
825
+ * @param menuNamePattern Name or slug pattern to match (case-insensitive)
826
+ * @returns Menu object if found, null otherwise
827
+ */
828
+ async function findMenuByName(client, menuNamePattern) {
829
+ try {
830
+ console.log(`🔍 Fetching all menus to search for "${menuNamePattern}"...`);
831
+ const allMenus = await client.getMenus();
832
+ console.log(`📋 Found ${allMenus.length} menu(s):`, allMenus.map((m) => `${m.name} (slug: ${m.slug})`).join(', '));
833
+ const patternLower = menuNamePattern.toLowerCase();
834
+ // Find menu by slug or name (case-insensitive)
835
+ const foundMenu = allMenus.find((menu) => {
836
+ const slugMatch = menu.slug?.toLowerCase() === patternLower;
837
+ const nameMatch = menu.name?.toLowerCase() === patternLower;
838
+ // Also check if slug or name contains the pattern (for variations like "main-menu" vs "mainmenu")
839
+ const slugNormalized = menu.slug?.toLowerCase().replace(/[^a-z0-9]/g, '');
840
+ const nameNormalized = menu.name?.toLowerCase().replace(/[^a-z0-9]/g, '');
841
+ const patternNormalized = patternLower.replace(/[^a-z0-9]/g, '');
842
+ const slugContains = slugNormalized === patternNormalized;
843
+ const nameContains = nameNormalized === patternNormalized;
844
+ return slugMatch || nameMatch || slugContains || nameContains;
845
+ });
846
+ if (foundMenu) {
847
+ console.log(`✅ Found matching menu: ${foundMenu.name} (ID: ${foundMenu.id}, slug: ${foundMenu.slug})`);
848
+ // Fetch the full menu with items
849
+ const fullMenu = await client.getMenu(foundMenu.id);
850
+ return fullMenu;
851
+ }
852
+ console.log(`ℹ️ No menu found matching pattern "${menuNamePattern}"`);
853
+ return null;
854
+ }
855
+ catch (error) {
856
+ console.warn(`⚠️ Error finding menu by name "${menuNamePattern}":`, error);
857
+ return null;
858
+ }
859
+ }
860
+ /**
861
+ * Extract menu items from a navigation block's innerBlocks
862
+ * Navigation blocks can have menu items stored as core/navigation-link blocks
863
+ */
864
+ function extractMenuFromNavigationBlock(navBlock) {
865
+ if (!navBlock) {
866
+ return null;
867
+ }
868
+ // First, try to extract from innerBlocks if available
869
+ if (navBlock.innerBlocks && navBlock.innerBlocks.length > 0) {
870
+ const extractMenuItems = (blocks, parentId = 0, order = 0) => {
871
+ const items = [];
872
+ let currentOrder = order;
873
+ for (const block of blocks) {
874
+ if (block.name === 'core/navigation-link' || block.name === 'core/page-list' || block.name === 'core/navigation-submenu') {
875
+ const attrs = block.attributes || {};
876
+ const label = attrs.label || attrs.text || '';
877
+ const url = attrs.url || attrs.href || '';
878
+ const kind = attrs.kind || attrs.type || 'custom';
879
+ const id = attrs.id || attrs.objectId || currentOrder;
880
+ const menuItem = {
881
+ id: id,
882
+ title: label,
883
+ url: url,
884
+ type: kind,
885
+ object_id: attrs.objectId || 0,
886
+ object: attrs.type || 'custom',
887
+ parent: parentId,
888
+ menu_order: currentOrder++,
889
+ };
890
+ // Check for submenu items (children)
891
+ if (block.innerBlocks && block.innerBlocks.length > 0) {
892
+ const children = extractMenuItems(block.innerBlocks, id, 0);
893
+ if (children.length > 0) {
894
+ menuItem.children = children;
895
+ }
896
+ }
897
+ items.push(menuItem);
898
+ }
899
+ else if (block.innerBlocks && block.innerBlocks.length > 0) {
900
+ // Recursively search in nested blocks
901
+ const nestedItems = extractMenuItems(block.innerBlocks, parentId, currentOrder);
902
+ items.push(...nestedItems);
903
+ currentOrder += nestedItems.length;
904
+ }
905
+ }
906
+ return items;
907
+ };
908
+ const items = extractMenuItems(navBlock.innerBlocks);
909
+ if (items.length > 0) {
910
+ return {
911
+ id: 0, // No menu ID for inline menus
912
+ name: 'Navigation Menu',
913
+ slug: 'navigation-menu',
914
+ items: items,
915
+ };
916
+ }
917
+ }
918
+ // Fallback: Try to extract from innerHTML if innerBlocks are empty
919
+ if (navBlock.innerHTML) {
920
+ return extractMenuFromNavigationHTML(navBlock.innerHTML);
921
+ }
922
+ return null;
923
+ }
924
+ class WordPressGenerator {
925
+ constructor(config) {
926
+ this.config = config;
927
+ }
928
+ async generateStaticData() {
929
+ const client = new WordPressClient(this.config);
930
+ console.log('🚀 Starting WordPress static data generation...');
931
+ const frontendId = this.config.frontendId ?? 'default-frontend';
932
+ const frontendName = this.config.frontendName || 'Default Frontend';
933
+ const postTypes = this.config.postTypes || ['posts', 'pages'];
934
+ // Use 'view' context - wp-custom-api-endpoints plugin adds blocks automatically.
935
+ // Frontends: when app provides frontendId, request Default + app frontend (OR); otherwise no filter (single-frontend backend).
936
+ // Use a custom slug-based param to avoid colliding with WordPress core taxonomy term ID filters.
937
+ const fetchParams = {
938
+ per_page: this.config.maxItems || 100,
939
+ _embed: true,
940
+ context: 'view', // Plugin adds blocks in 'view' context
941
+ };
942
+ if (this.config.frontendId && this.config.frontendId.trim() !== '') {
943
+ fetchParams.frontend_slugs = 'default,' + this.config.frontendId.trim();
944
+ }
945
+ console.log(`📋 Fetching post types: ${postTypes.join(', ')}`);
946
+ console.log(`⚙️ Settings: maxItems=${fetchParams.per_page}, includeEmbedded=${fetchParams._embed}`);
947
+ // Fetch WordPress Settings (Reading Settings + Site Branding + Theme Styles)
948
+ let frontPage;
949
+ let settingsPageId;
950
+ let siteSettings;
951
+ let themeStyles;
952
+ let header;
953
+ let footer;
954
+ let menuData;
955
+ let menus = {}; // Declare menus outside try block so it's accessible later
956
+ try {
957
+ console.log('🔍 Fetching WordPress Settings...');
958
+ const settings = await client.getSettings();
959
+ if (settings.front_page) {
960
+ // WordPress has a page set as front page in Reading Settings
961
+ frontPage = settings.front_page;
962
+ console.log(`✅ Front page determined from Reading Settings: ${frontPage.slug} (ID: ${frontPage.id})`);
963
+ }
964
+ else if (settings.show_on_front === 'page' && settings.page_on_front > 0) {
965
+ // Page is set but not returned in front_page (might be in pages list)
966
+ settingsPageId = settings.page_on_front;
967
+ console.log(`📋 Front page ID ${settingsPageId} set in Reading Settings, will check fetched pages`);
968
+ }
969
+ // Extract site branding settings
970
+ if (settings.logo || settings.site_icon || settings.site_name || settings.site_description) {
971
+ siteSettings = {
972
+ ...(settings.logo && { logo: settings.logo }),
973
+ ...(settings.site_icon && { site_icon: settings.site_icon }),
974
+ ...(settings.site_name && { site_name: settings.site_name }),
975
+ ...(settings.site_description && { site_description: settings.site_description }),
976
+ };
977
+ if (siteSettings.logo) {
978
+ console.log(`✅ Site logo found: ${siteSettings.logo.cloudflare_url || siteSettings.logo.url}`);
979
+ }
980
+ if (siteSettings.site_icon) {
981
+ console.log(`✅ Site icon found: ${siteSettings.site_icon.cloudflare_url || siteSettings.site_icon.url}`);
982
+ }
983
+ if (siteSettings.site_name) {
984
+ console.log(`✅ Site name: ${siteSettings.site_name}`);
985
+ }
986
+ }
987
+ // Extract theme styles
988
+ if (settings.theme_styles) {
989
+ themeStyles = settings.theme_styles;
990
+ console.log(`✅ Theme styles found:`, {
991
+ hasTypography: !!themeStyles.typography,
992
+ hasColors: !!themeStyles.colors,
993
+ hasLayout: !!themeStyles.layout,
994
+ });
995
+ }
996
+ else {
997
+ console.log('⚠️ No theme_styles found in settings response');
998
+ }
999
+ // Fetch header template part and extract menu
1000
+ console.log('🔍 Fetching Header template part...');
1001
+ try {
1002
+ header = await client.getHeader('header', 'header');
1003
+ if (header && header.blocks && header.blocks.length > 0) {
1004
+ console.log(`✅ Header found: ${header.blocks_count} blocks`);
1005
+ // Find navigation block and extract menu ID
1006
+ const findNavigationBlock = (blocks) => {
1007
+ for (const block of blocks) {
1008
+ if (block.name === 'core/navigation') {
1009
+ return block;
1010
+ }
1011
+ if (block.innerBlocks && block.innerBlocks.length > 0) {
1012
+ const found = findNavigationBlock(block.innerBlocks);
1013
+ if (found)
1014
+ return found;
1015
+ }
1016
+ }
1017
+ return null;
1018
+ };
1019
+ const navBlock = findNavigationBlock(header.blocks);
1020
+ if (navBlock && navBlock.attributes) {
1021
+ // Debug: Log all navigation block attributes
1022
+ console.log('🔍 Navigation block attributes:', JSON.stringify(navBlock.attributes, null, 2));
1023
+ console.log('🔍 Navigation block innerBlocks:', navBlock.innerBlocks ? `${navBlock.innerBlocks.length} blocks` : 'none');
1024
+ // WordPress stores menu ID in 'ref' attribute (term_id of the menu)
1025
+ // Also check for other possible attribute names
1026
+ const menuId = navBlock.attributes['ref'] ||
1027
+ navBlock.attributes['menuId'] ||
1028
+ navBlock.attributes['menu'] ||
1029
+ navBlock.attributes['navigationMenuId'] ||
1030
+ navBlock.attributes['menuRef'];
1031
+ if (menuId) {
1032
+ try {
1033
+ console.log(`🔍 Found navigation block with menu ID: ${menuId}, fetching menu data...`);
1034
+ menuData = await client.getMenu(parseInt(menuId));
1035
+ console.log(`✅ Menu data fetched: ${menuData.items?.length || 0} items`);
1036
+ console.log('📋 Menu data structure:', JSON.stringify({
1037
+ id: menuData.id,
1038
+ name: menuData.name,
1039
+ itemsCount: menuData.items?.length || 0,
1040
+ firstItem: menuData.items?.[0] ? {
1041
+ id: menuData.items[0].id,
1042
+ title: menuData.items[0].title,
1043
+ url: menuData.items[0].url
1044
+ } : null
1045
+ }, null, 2));
1046
+ }
1047
+ catch (error) {
1048
+ console.warn(`⚠️ Could not fetch menu data for menu ID ${menuId}:`, error);
1049
+ // Fallback: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
1050
+ console.log('🔍 Menu fetch failed, attempting to find menu by name "mainmenu"...');
1051
+ try {
1052
+ const namedMenu = await findMenuByName(client, 'mainmenu');
1053
+ if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
1054
+ menuData = namedMenu;
1055
+ console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
1056
+ }
1057
+ else {
1058
+ console.log('ℹ️ No menu found with name "mainmenu"');
1059
+ // Try extracting from Navigation block HTML as last resort
1060
+ console.log('🔍 Attempting to extract navigation from Navigation block innerHTML...');
1061
+ try {
1062
+ const extractedMenu = extractMenuFromNavigationBlock(navBlock);
1063
+ if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
1064
+ menuData = extractedMenu;
1065
+ console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
1066
+ }
1067
+ }
1068
+ catch (extractError) {
1069
+ console.warn('⚠️ Error extracting navigation from Navigation block:', extractError);
1070
+ }
1071
+ }
1072
+ }
1073
+ catch (nameError) {
1074
+ console.warn('⚠️ Error finding menu by name:', nameError);
1075
+ }
1076
+ }
1077
+ }
1078
+ else {
1079
+ console.log('ℹ️ Navigation block found but no menu ID attribute found');
1080
+ console.log('🔍 Available attributes:', Object.keys(navBlock.attributes));
1081
+ // Priority 2: Try to find menu by name (mainmenu, main-menu, Main Menu, etc.)
1082
+ console.log('🔍 Attempting to find menu by name "mainmenu"...');
1083
+ try {
1084
+ const namedMenu = await findMenuByName(client, 'mainmenu');
1085
+ if (namedMenu && namedMenu.items && namedMenu.items.length > 0) {
1086
+ menuData = namedMenu;
1087
+ console.log(`✅ Found menu by name "mainmenu": ${namedMenu.name} (${namedMenu.items.length} items)`);
1088
+ }
1089
+ else {
1090
+ console.log('ℹ️ No menu found with name "mainmenu"');
1091
+ // In block themes, navigation items may be stored directly in the Navigation block's innerBlocks
1092
+ // Try to extract from innerBlocks (this is how block themes work)
1093
+ console.log('🔍 Attempting to extract navigation from Navigation block innerBlocks...');
1094
+ try {
1095
+ const extractedMenu = extractMenuFromNavigationBlock(navBlock);
1096
+ if (extractedMenu && extractedMenu.items && extractedMenu.items.length > 0) {
1097
+ menuData = extractedMenu;
1098
+ console.log(`✅ Extracted navigation from Navigation block: ${extractedMenu.items.length} items`);
1099
+ console.log('📋 Navigation items:', extractedMenu.items.map((item) => `${item.title} (${item.url})`).join(', '));
1100
+ console.log('💡 TIP: For better control, create a WordPress menu (Appearance > Menus) named "mainmenu" and assign it to the Navigation block.');
1101
+ }
1102
+ else {
1103
+ console.log('⚠️ WARNING: No navigation found in Navigation block. The frontend will fall back to page hierarchy.');
1104
+ console.log('💡 TIP: Add navigation items to the Navigation block in the Site Editor, or create a menu named "mainmenu" in WordPress (Appearance > Menus).');
1105
+ menuData = null;
1106
+ }
1107
+ }
1108
+ catch (error) {
1109
+ console.warn('⚠️ Error extracting navigation from Navigation block:', error);
1110
+ console.log('⚠️ WARNING: The frontend will fall back to page hierarchy.');
1111
+ menuData = null;
1112
+ }
1113
+ }
1114
+ }
1115
+ catch (error) {
1116
+ console.warn('⚠️ Error finding menu by name:', error);
1117
+ console.log('ℹ️ Skipping HTML extraction - frontend will use page hierarchy fallback');
1118
+ menuData = null; // Don't use extracted menu - let frontend handle fallback
1119
+ }
1120
+ }
1121
+ }
1122
+ else {
1123
+ console.log('ℹ️ No navigation block found in header');
1124
+ }
1125
+ }
1126
+ else {
1127
+ console.log('ℹ️ No header template part found');
1128
+ }
1129
+ }
1130
+ catch (error) {
1131
+ console.warn('⚠️ Could not fetch header template part:', error);
1132
+ }
1133
+ // Fetch footer template part
1134
+ console.log('🔍 Fetching Footer template part...');
1135
+ try {
1136
+ footer = await client.getFooter('footer', 'footer');
1137
+ if (footer && footer.blocks && footer.blocks.length > 0) {
1138
+ console.log(`✅ Footer found: ${footer.blocks_count} blocks`);
1139
+ }
1140
+ else {
1141
+ console.log('⚠️ Footer template part is empty or not found');
1142
+ }
1143
+ }
1144
+ catch (error) {
1145
+ console.log('⚠️ Failed to fetch footer:', error instanceof Error ? error.message : 'Unknown error');
1146
+ }
1147
+ // Extract all menu IDs from footer blocks and fetch them for static data
1148
+ const allMenuIds = new Set();
1149
+ if (menuData && menuData.id) {
1150
+ allMenuIds.add(menuData.id); // Add header menu
1151
+ console.log(`📋 Added header menu ID ${menuData.id} (${menuData.name}) to menu list`);
1152
+ }
1153
+ if (footer && footer.blocks) {
1154
+ let navigationBlockCount = 0;
1155
+ const findNavigationBlocks = (blocks, depth = 0) => {
1156
+ for (const block of blocks) {
1157
+ if (block.name === 'core/navigation') {
1158
+ navigationBlockCount++;
1159
+ console.log(`🔍 Found navigation block at depth ${depth}:`, {
1160
+ blockName: block.name,
1161
+ hasAttributes: !!block.attributes,
1162
+ attributes: block.attributes ? Object.keys(block.attributes) : [],
1163
+ });
1164
+ if (block.attributes) {
1165
+ const menuId = block.attributes['ref'] ||
1166
+ block.attributes['menuId'] ||
1167
+ block.attributes['menu'] ||
1168
+ block.attributes['navigationMenuId'] ||
1169
+ block.attributes['menuRef'];
1170
+ if (menuId) {
1171
+ const menuIdNum = parseInt(String(menuId));
1172
+ allMenuIds.add(menuIdNum);
1173
+ console.log(` ✅ Extracted menu ID ${menuIdNum} from attribute:`, {
1174
+ ref: block.attributes['ref'],
1175
+ menuId: block.attributes['menuId'],
1176
+ menu: block.attributes['menu'],
1177
+ navigationMenuId: block.attributes['navigationMenuId'],
1178
+ menuRef: block.attributes['menuRef'],
1179
+ extracted: menuIdNum,
1180
+ });
1181
+ }
1182
+ else {
1183
+ console.warn(` ⚠️ Navigation block found but no menu ID attribute detected. Available attributes:`, Object.keys(block.attributes));
1184
+ }
1185
+ }
1186
+ else {
1187
+ console.warn(` ⚠️ Navigation block found but has no attributes`);
1188
+ }
1189
+ }
1190
+ if (block.innerBlocks && block.innerBlocks.length > 0) {
1191
+ findNavigationBlocks(block.innerBlocks, depth + 1);
1192
+ }
1193
+ }
1194
+ };
1195
+ findNavigationBlocks(footer.blocks);
1196
+ console.log(`📊 Navigation block extraction summary:`, {
1197
+ navigationBlocksFound: navigationBlockCount,
1198
+ uniqueMenuIds: allMenuIds.size,
1199
+ menuIds: Array.from(allMenuIds),
1200
+ });
1201
+ }
1202
+ else {
1203
+ console.log(`ℹ️ No footer blocks found or footer is empty`);
1204
+ }
1205
+ // Fetch all menus and store them in a menus object keyed by ID
1206
+ menus = {}; // Reset menus object (already declared above)
1207
+ if (menuData && menuData.id) {
1208
+ menus[menuData.id] = menuData;
1209
+ console.log(`✅ Added header menu to menus object: ID ${menuData.id} (${menuData.name})`);
1210
+ }
1211
+ let fetchAttempts = 0;
1212
+ let fetchSuccesses = 0;
1213
+ let fetchFailures = 0;
1214
+ for (const menuId of allMenuIds) {
1215
+ if (!menus[menuId]) {
1216
+ fetchAttempts++;
1217
+ console.log(`🔍 Attempting to fetch menu ID ${menuId}... (attempt ${fetchAttempts}/${allMenuIds.size})`);
1218
+ try {
1219
+ const fetchedMenu = await client.getMenu(menuId);
1220
+ // Validate menu structure
1221
+ if (!fetchedMenu) {
1222
+ console.error(`❌ Menu ID ${menuId}: Fetched menu is null or undefined`);
1223
+ fetchFailures++;
1224
+ continue;
1225
+ }
1226
+ if (!fetchedMenu.id) {
1227
+ console.error(`❌ Menu ID ${menuId}: Fetched menu missing 'id' property:`, fetchedMenu);
1228
+ fetchFailures++;
1229
+ continue;
1230
+ }
1231
+ if (!fetchedMenu.name) {
1232
+ console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'name' property:`, fetchedMenu);
1233
+ }
1234
+ if (!fetchedMenu.items) {
1235
+ console.warn(`⚠️ Menu ID ${menuId}: Fetched menu missing 'items' property:`, fetchedMenu);
1236
+ }
1237
+ else if (!Array.isArray(fetchedMenu.items)) {
1238
+ console.error(`❌ Menu ID ${menuId}: Fetched menu 'items' is not an array:`, typeof fetchedMenu.items);
1239
+ fetchFailures++;
1240
+ continue;
1241
+ }
1242
+ // Store menu even if items array is empty (for debugging)
1243
+ menus[menuId] = fetchedMenu;
1244
+ if (fetchedMenu.items && fetchedMenu.items.length > 0) {
1245
+ fetchSuccesses++;
1246
+ console.log(`✅ Fetched menu ID ${menuId}: ${fetchedMenu.name} (${fetchedMenu.items.length} items)`);
1247
+ }
1248
+ else {
1249
+ fetchSuccesses++; // Still count as success, just empty
1250
+ console.warn(`⚠️ Menu ID ${menuId}: ${fetchedMenu.name} fetched but has no items`);
1251
+ }
1252
+ }
1253
+ catch (error) {
1254
+ fetchFailures++;
1255
+ const errorDetails = {
1256
+ message: error.message || 'Unknown error',
1257
+ status: error.status || 'unknown',
1258
+ };
1259
+ // Try to extract more error details
1260
+ if (error.response) {
1261
+ errorDetails.responseStatus = error.response.status;
1262
+ errorDetails.responseStatusText = error.response.statusText;
1263
+ }
1264
+ if (error.body) {
1265
+ try {
1266
+ errorDetails.body = typeof error.body === 'string' ? error.body : JSON.stringify(error.body);
1267
+ }
1268
+ catch (e) {
1269
+ errorDetails.body = 'Could not stringify error body';
1270
+ }
1271
+ }
1272
+ console.error(`❌ Could not fetch menu ID ${menuId}:`, errorDetails);
1273
+ console.error(` Full error object:`, error);
1274
+ }
1275
+ }
1276
+ else {
1277
+ console.log(`ℹ️ Menu ID ${menuId} already in menus object, skipping fetch`);
1278
+ }
1279
+ }
1280
+ // Log menu fetching summary
1281
+ console.log(`📊 Menu fetching summary:`, {
1282
+ totalMenuIds: allMenuIds.size,
1283
+ fetchAttempts,
1284
+ fetchSuccesses,
1285
+ fetchFailures,
1286
+ menusInObject: Object.keys(menus).length,
1287
+ menuIdsInObject: Object.keys(menus).map(id => parseInt(id)),
1288
+ menuNames: Object.values(menus).map((m) => m.name || 'unnamed').join(', '),
1289
+ });
1290
+ // Debug: Dump all WordPress theme data in development
1291
+ if (process.env.NODE_ENV === 'development') {
1292
+ const debugData = {
1293
+ timestamp: new Date().toISOString(),
1294
+ settings_response: settings,
1295
+ theme_styles_extracted: themeStyles,
1296
+ theme_styles_typography: themeStyles?.typography,
1297
+ theme_styles_colors: themeStyles?.colors,
1298
+ theme_styles_layout: themeStyles?.layout,
1299
+ theme_styles_palette: themeStyles?.theme_palette,
1300
+ };
1301
+ // Use same directory as output file (public/)
1302
+ const outputFullPath = join(process.cwd(), this.config.outputPath);
1303
+ const outputDir = dirname(outputFullPath);
1304
+ const debugPath = join(outputDir, 'wp-theme-data.json');
1305
+ try {
1306
+ writeFileSync(debugPath, JSON.stringify(debugData, null, 2), 'utf-8');
1307
+ console.log(`📊 Debug: WordPress theme data dumped to ${debugPath}`);
1308
+ }
1309
+ catch (error) {
1310
+ console.warn('⚠️ Could not write debug file:', error);
1311
+ }
1312
+ }
1313
+ }
1314
+ catch (error) {
1315
+ console.warn('⚠️ Could not fetch WordPress settings, will use fallback logic:', error);
1316
+ }
1317
+ // Fetch standard post types
1318
+ const data = await client.getAllData(fetchParams);
1319
+ // Determine front page if not already set from settings
1320
+ if (!frontPage) {
1321
+ if (settingsPageId) {
1322
+ // Try to find the page by ID from Reading Settings
1323
+ const settingsPage = (data.pages || []).find((p) => p.id === settingsPageId);
1324
+ if (settingsPage) {
1325
+ const title = typeof settingsPage.title === 'string'
1326
+ ? settingsPage.title
1327
+ : (settingsPage.title?.rendered || 'Frontpage');
1328
+ frontPage = {
1329
+ id: settingsPage.id,
1330
+ slug: settingsPage.slug,
1331
+ title,
1332
+ };
1333
+ if (frontPage) {
1334
+ console.log(`✅ Front page determined from Reading Settings (ID match): ${frontPage.slug} (ID: ${frontPage.id})`);
1335
+ }
1336
+ }
1337
+ }
1338
+ // Fallback: look for page with slug "frontpage"
1339
+ if (!frontPage) {
1340
+ const frontpagePage = (data.pages || []).find((p) => p.slug === 'frontpage');
1341
+ if (frontpagePage) {
1342
+ const title = typeof frontpagePage.title === 'string'
1343
+ ? frontpagePage.title
1344
+ : (frontpagePage.title?.rendered || 'Frontpage');
1345
+ frontPage = {
1346
+ id: frontpagePage.id,
1347
+ slug: frontpagePage.slug,
1348
+ title,
1349
+ };
1350
+ if (frontPage) {
1351
+ console.log(`✅ Front page determined from slug "frontpage": ${frontPage.slug} (ID: ${frontPage.id})`);
1352
+ }
1353
+ }
1354
+ else {
1355
+ console.log('⚠️ No front page found (neither in Reading Settings nor slug "frontpage")');
1356
+ }
1357
+ }
1358
+ }
1359
+ // Create the static data structure
1360
+ const staticData = {
1361
+ generated_at: new Date().toISOString(),
1362
+ frontend_id: frontendId,
1363
+ frontend_name: frontendName,
1364
+ ...(frontPage && { front_page: frontPage }),
1365
+ ...(siteSettings && { site_settings: siteSettings }),
1366
+ ...(header && { header: header }),
1367
+ ...(menuData && { navigation_menu: menuData }),
1368
+ menus: menus, // Always include menus object, even if empty, for debugging
1369
+ ...(footer && { footer: footer }),
1370
+ ...(themeStyles && { theme_styles: themeStyles }),
1371
+ config: {
1372
+ frontend_id: frontendId,
1373
+ frontend_name: frontendName,
1374
+ post_types: postTypes.reduce((acc, postType) => {
1375
+ acc[postType] = {
1376
+ max_items: this.config.maxItems || 100,
1377
+ include_embedded: this.config.includeEmbedded || true,
1378
+ orderby: 'date',
1379
+ order: 'desc',
1380
+ };
1381
+ return acc;
1382
+ }, {}),
1383
+ },
1384
+ // Map standard post types
1385
+ posts: data.posts,
1386
+ pages: await Promise.all((data.pages || []).map(async (p) => {
1387
+ // Always use custom blocks endpoint to ensure we get the latest blocks with complete Cloudflare URLs
1388
+ // The custom endpoint (/wp-custom/v1/pages/{id}/blocks) is more reliable and always has the latest data
1389
+ let blocks = [];
1390
+ try {
1391
+ console.log(`🌐 Fetching blocks from custom endpoint for page "${p.slug}" (ID: ${p.id})...`);
1392
+ const blocksResponse = await client.getPageBlocks(p.id);
1393
+ console.log(`📦 Blocks response for "${p.slug}":`, blocksResponse ? `has blocks=${Array.isArray(blocksResponse.blocks) ? blocksResponse.blocks.length : 'N/A'}` : 'null');
1394
+ if (blocksResponse && Array.isArray(blocksResponse.blocks) && blocksResponse.blocks.length > 0) {
1395
+ blocks = blocksResponse.blocks;
1396
+ console.log(`✅ Fetched ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from custom endpoint`);
1397
+ }
1398
+ else {
1399
+ // Fallback: try blocks from API response if custom endpoint is empty
1400
+ const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
1401
+ if (blocksFromResponse.length > 0) {
1402
+ blocks = blocksFromResponse;
1403
+ console.log(`⚠️ Custom endpoint returned no blocks, using ${blocks.length} blocks from API response for page "${p.slug}" (ID: ${p.id})`);
1404
+ }
1405
+ else {
1406
+ console.warn(`⚠️ Custom endpoint returned no blocks for page "${p.slug}" (ID: ${p.id})`);
1407
+ // Last resort: try parsing from content.raw (but this won't work in 'view' context)
1408
+ const raw = p?.content?.raw || '';
1409
+ if (raw) {
1410
+ try {
1411
+ blocks = parse(raw);
1412
+ if (blocks.length > 0) {
1413
+ console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw`);
1414
+ }
1415
+ }
1416
+ catch (parseError) {
1417
+ console.warn(`⚠️ Failed to parse blocks from content.raw for page "${p.slug}" (ID: ${p.id}):`, parseError);
1418
+ }
1419
+ }
1420
+ else {
1421
+ console.warn(`⚠️ Page "${p.slug}" (ID: ${p.id}) has no blocks and no content.raw. Plugin may not be active or working.`);
1422
+ }
1423
+ }
1424
+ }
1425
+ }
1426
+ catch (e) {
1427
+ console.warn(`⚠️ Failed to fetch blocks from custom endpoint for page "${p.slug}" (ID: ${p.id}):`, e.message);
1428
+ // Fallback: try blocks from API response
1429
+ const blocksFromResponse = Array.isArray(p.blocks) ? p.blocks : [];
1430
+ if (blocksFromResponse.length > 0) {
1431
+ blocks = blocksFromResponse;
1432
+ console.log(`⚠️ Using ${blocks.length} blocks from API response as fallback for page "${p.slug}" (ID: ${p.id})`);
1433
+ }
1434
+ else {
1435
+ // Last resort: try parsing from content.raw
1436
+ try {
1437
+ const raw = p?.content?.raw || '';
1438
+ if (raw) {
1439
+ blocks = parse(raw);
1440
+ if (blocks.length > 0) {
1441
+ console.log(`✅ Parsed ${blocks.length} blocks for page "${p.slug}" (ID: ${p.id}) from content.raw (fallback)`);
1442
+ }
1443
+ }
1444
+ }
1445
+ catch (parseError) {
1446
+ // Silent fail - we've already logged the warning
1447
+ }
1448
+ }
1449
+ }
1450
+ return { ...p, blocks };
1451
+ })),
1452
+ media: data.media,
1453
+ categories: data.categories,
1454
+ tags: data.tags,
1455
+ };
1456
+ // Fetch custom post types (anything beyond the standard posts, pages, media, categories, tags)
1457
+ const standardTypes = ['posts', 'pages', 'media', 'categories', 'tags'];
1458
+ const customPostTypes = postTypes.filter(pt => !standardTypes.includes(pt));
1459
+ if (customPostTypes.length > 0) {
1460
+ console.log(`📦 Fetching custom post types: ${customPostTypes.join(', ')}`);
1461
+ for (const postType of customPostTypes) {
1462
+ try {
1463
+ console.log(`\n🔍 Fetching custom post type: ${postType}`);
1464
+ const customData = await client.getCustomPostType(postType, fetchParams);
1465
+ staticData[postType] = customData;
1466
+ console.log(`✅ ${postType}: ${customData.length} items fetched`);
1467
+ }
1468
+ catch (error) {
1469
+ const msg = error?.message || String(error);
1470
+ const status = error?.response?.status ?? error?.status;
1471
+ console.warn(`⚠️ Failed to fetch custom post type '${postType}':`, msg);
1472
+ if (status)
1473
+ console.warn(` HTTP status: ${status}`);
1474
+ if (error?.response?.statusText)
1475
+ console.warn(` Status text: ${error.response.statusText}`);
1476
+ staticData[postType] = [];
1477
+ }
1478
+ }
1479
+ // Enrich CPT items that have featured_media with Cloudflare URL from media list
1480
+ // (API may return WordPress URLs in featured_image_urls; media has cloudflare_image_standardized)
1481
+ const mediaById = new Map();
1482
+ (data.media || []).forEach((m) => {
1483
+ const url = m.cloudflare_image_standardized || m.cloudflare_url;
1484
+ if (url)
1485
+ mediaById.set(m.id, url);
1486
+ });
1487
+ if (mediaById.size > 0) {
1488
+ for (const postType of customPostTypes) {
1489
+ const items = staticData[postType];
1490
+ if (!Array.isArray(items))
1491
+ continue;
1492
+ let enriched = 0;
1493
+ items.forEach((item) => {
1494
+ const fid = item.featured_media ? Number(item.featured_media) : 0;
1495
+ if (fid && mediaById.has(fid)) {
1496
+ item.featured_media_cloudflare_url = mediaById.get(fid);
1497
+ enriched++;
1498
+ }
1499
+ });
1500
+ if (enriched > 0) {
1501
+ console.log(`✅ Enriched ${enriched} ${postType} with featured_media_cloudflare_url`);
1502
+ }
1503
+ }
1504
+ }
1505
+ }
1506
+ // Write to file
1507
+ this.writeStaticData(staticData);
1508
+ console.log('\n✅ WordPress static data generation completed');
1509
+ console.log(`📊 Standard data: ${data.posts.length} posts, ${data.pages.length} pages, ${data.media.length} media`);
1510
+ if (customPostTypes.length > 0) {
1511
+ const customCounts = customPostTypes.map(pt => `${pt}: ${staticData[pt]?.length || 0}`).join(', ');
1512
+ console.log(`📊 Custom data: ${customCounts}`);
1513
+ }
1514
+ return staticData;
1515
+ }
1516
+ writeStaticData(data) {
1517
+ try {
1518
+ const outputPath = join(process.cwd(), this.config.outputPath);
1519
+ writeFileSync(outputPath, JSON.stringify(data, null, 2));
1520
+ console.log(`📁 Static data written to: ${outputPath}`);
1521
+ }
1522
+ catch (error) {
1523
+ console.error('❌ Error writing static data:', error);
1524
+ throw error;
1525
+ }
1526
+ }
1527
+ async generatePostsOnly() {
1528
+ const client = new WordPressClient(this.config);
1529
+ return client.getPosts({ per_page: 100 });
1530
+ }
1531
+ async generatePagesOnly() {
1532
+ const client = new WordPressClient(this.config);
1533
+ return client.getPages({ per_page: 100 });
1534
+ }
1535
+ async generateMediaOnly() {
1536
+ const client = new WordPressClient(this.config);
1537
+ return client.getMedia({ per_page: 100 });
1538
+ }
1539
+ }
1540
+ // Convenience function for easy usage
1541
+ async function generateWordPressData$1(config) {
1542
+ const generator = new WordPressGenerator(config);
1543
+ return generator.generateStaticData();
1544
+ }
1545
+
1546
+ /**
1547
+ * @license GPL-3.0-or-later
1548
+ *
1549
+ * This file is part of the MarVAlt Open SDK.
1550
+ * Copyright (c) 2025 Vibune Pty Ltd.
1551
+ *
1552
+ * This program is free software: you can redistribute it and/or modify
1553
+ * it under the terms of the GNU General Public License as published by
1554
+ * the Free Software Foundation, either version 3 of the License, or
1555
+ * (at your option) any later version.
1556
+ *
1557
+ * This program is distributed in the hope that it will be useful,
1558
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1559
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1560
+ * See the GNU General Public License for more details.
1561
+ */
1562
+ class GravityFormsClient {
1563
+ constructor(config) {
1564
+ this.config = config;
1565
+ // Determine mode: direct for generators (Node.js), proxy for browser
1566
+ const hasCredentials = !!(config.username && config.password && config.username.length > 0 && config.password.length > 0);
1567
+ this.useDirectMode = config.authMode === 'direct' && hasCredentials;
1568
+ // Debug logging for Node.js environments (generators)
1569
+ if (typeof process !== 'undefined' && process.env) {
1570
+ console.log('🔍 GravityFormsClient Configuration:', {
1571
+ authMode: config.authMode,
1572
+ hasUsername: !!config.username,
1573
+ hasPassword: !!config.password,
1574
+ hasApiUrl: !!config.apiUrl,
1575
+ useDirectMode: this.useDirectMode,
1576
+ });
1577
+ }
1578
+ // Convention-based: Always use /api/gravity-forms-submit unless explicitly overridden
1579
+ this.proxyEndpoint = config.proxyEndpoint || '/api/gravity-forms-submit';
1580
+ }
1581
+ async makeRequest(endpoint, options = {}) {
1582
+ let url;
1583
+ const headers = {
1584
+ 'Content-Type': 'application/json',
1585
+ ...options.headers,
1586
+ };
1587
+ if (this.useDirectMode) {
1588
+ // Direct mode: Call WordPress API directly (for generators/Node.js)
1589
+ url = `${this.config.apiUrl}${endpoint}`;
1590
+ // Add Basic Auth
1591
+ if (this.config.username && this.config.password) {
1592
+ const credentials = btoa(`${this.config.username}:${this.config.password}`);
1593
+ headers['Authorization'] = `Basic ${credentials}`;
1594
+ }
1595
+ // Add Cloudflare Access Service Token headers if provided (for CF Access protected APIs)
1596
+ if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
1597
+ headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
1598
+ headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
1599
+ console.log('🔐 Added CF Access Service Token headers to Gravity Forms request');
1600
+ console.log('🔐 CF-Access-Client-Id:', this.config.cfAccessClientId.substring(0, 8) + '...');
1601
+ console.log('🔐 CF-Access-Client-Secret:', this.config.cfAccessClientSecret.substring(0, 8) + '... (hidden)');
1602
+ }
1603
+ else {
1604
+ console.log('⚠️ CF Access Service Token NOT provided - requests may be blocked by Zero Trust');
1605
+ console.log(' cfAccessClientId present:', !!this.config.cfAccessClientId);
1606
+ console.log(' cfAccessClientSecret present:', !!this.config.cfAccessClientSecret);
1607
+ }
1608
+ }
1609
+ else {
1610
+ // Proxy mode: Use Pages Function (for browser)
1611
+ const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
1612
+ url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
1613
+ }
1614
+ const response = await fetch(url, {
1615
+ ...options,
1616
+ headers,
1617
+ signal: AbortSignal.timeout(this.config.timeout || 30000),
1618
+ });
1619
+ const responseText = await response.text();
1620
+ if (!response.ok) {
1621
+ // Try to parse error response for validation details
1622
+ let errorDetails = null;
1623
+ try {
1624
+ if (responseText) {
1625
+ errorDetails = JSON.parse(responseText);
1626
+ }
1627
+ }
1628
+ catch (e) {
1629
+ // Ignore parse errors, use generic error
1630
+ }
1631
+ // Provide helpful error message for 404 (plugin not installed/activated)
1632
+ if (response.status === 404) {
1633
+ const errorMsg = `Gravity Forms API request failed: ${response.status} ${response.statusText}\n` +
1634
+ `The endpoint ${url} was not found.\n` +
1635
+ `This usually means the 'gravity-forms-api-endpoint' plugin is not installed or activated on your WordPress site.\n` +
1636
+ `Please verify:\n` +
1637
+ ` 1. The plugin is installed in /wp-content/plugins/gravity-forms-api-endpoint/\n` +
1638
+ ` 2. The plugin is activated in WordPress admin\n` +
1639
+ ` 3. The endpoint is accessible: ${this.config.apiUrl}/health`;
1640
+ throw new Error(errorMsg);
1641
+ }
1642
+ // For 400 errors, include validation details if available
1643
+ // WordPress REST API formats WP_Error as: { code, message, data: { status, validation_errors? } }
1644
+ if (response.status === 400 && errorDetails) {
1645
+ // Debug logging to see the actual error structure
1646
+ if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production') {
1647
+ console.log('🔍 GravityFormsClient 400 error details:', JSON.stringify(errorDetails, null, 2));
1648
+ }
1649
+ const error = new Error(errorDetails.message || `Gravity Forms API request failed: ${response.status} ${response.statusText}`);
1650
+ error.data = errorDetails;
1651
+ error.code = errorDetails.code;
1652
+ // Extract validation_errors from WordPress REST API format
1653
+ if (errorDetails.data?.validation_errors) {
1654
+ error.validation_errors = errorDetails.data.validation_errors;
1655
+ }
1656
+ throw error;
1657
+ }
1658
+ throw new Error(`Gravity Forms API request failed: ${response.status} ${response.statusText}`);
1659
+ }
1660
+ if (!responseText) {
1661
+ return [];
1662
+ }
1663
+ try {
1664
+ return JSON.parse(responseText);
1665
+ }
1666
+ catch (error) {
1667
+ console.error('❌ Gravity Forms JSON Parse Error:', {
1668
+ error: error.message,
1669
+ responseText: responseText.substring(0, 500),
1670
+ });
1671
+ throw new Error(`Failed to parse JSON response: ${error.message}`);
1672
+ }
1673
+ }
1674
+ async getForm(id) {
1675
+ // Always use custom gf-api/v1 endpoint (from custom plugin)
1676
+ const endpoint = `/forms/${id}`;
1677
+ return this.makeRequest(endpoint);
1678
+ }
1679
+ async getForms() {
1680
+ // Always use custom gf-api/v1 endpoint (from custom plugin)
1681
+ const endpoint = '/forms';
1682
+ return this.makeRequest(endpoint);
1683
+ }
1684
+ async getFormConfig(id) {
1685
+ // Always use custom gf-api/v1 endpoint (from custom plugin)
1686
+ const endpoint = `/forms/${id}/config`;
1687
+ return this.makeRequest(endpoint);
1688
+ }
1689
+ async submitForm(formId, submission) {
1690
+ // Always use custom gf-api/v1 submit endpoint (from custom plugin)
1691
+ const endpoint = `/forms/${formId}/submit`;
1692
+ // Debug logging
1693
+ console.log('🔍 GravityFormsClient.submitForm called:', {
1694
+ formId,
1695
+ field_values: submission.field_values,
1696
+ field_values_stringified: JSON.stringify(submission.field_values),
1697
+ has_headers: !!submission.headers,
1698
+ });
1699
+ // Merge custom headers (e.g., Turnstile token) with default headers
1700
+ const headers = {
1701
+ 'Content-Type': 'application/json',
1702
+ ...(submission.headers || {}),
1703
+ };
1704
+ return this.makeRequest(endpoint, {
1705
+ method: 'POST',
1706
+ headers,
1707
+ body: JSON.stringify(submission.field_values),
1708
+ });
1709
+ }
1710
+ async getHealth() {
1711
+ const endpoint = '/health';
1712
+ return this.makeRequest(endpoint);
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * @license GPL-3.0-or-later
1718
+ *
1719
+ * This file is part of the MarVAlt Open SDK.
1720
+ * Copyright (c) 2025 Vibune Pty Ltd.
1721
+ *
1722
+ * This program is free software: you can redistribute it and/or modify
1723
+ * it under the terms of the GNU General Public License as published by
1724
+ * the Free Software Foundation, either version 3 of the License, or
1725
+ * (at your option) any later version.
1726
+ *
1727
+ * This program is distributed in the hope that it will be useful,
1728
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1729
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1730
+ * See the GNU General Public License for more details.
1731
+ */
1732
+ // Gravity Forms Static Data Generator
1733
+ class GravityFormsGenerator {
1734
+ constructor(config) {
1735
+ this.config = config;
1736
+ }
1737
+ async generateStaticData() {
1738
+ const client = new GravityFormsClient(this.config);
1739
+ console.log('🚀 Starting Gravity Forms static data generation...');
1740
+ let forms = [];
1741
+ if (this.config.formIds && this.config.formIds.length > 0) {
1742
+ // Generate specific forms
1743
+ for (const formId of this.config.formIds) {
1744
+ try {
1745
+ const form = await client.getFormConfig(formId);
1746
+ if (this.config.includeInactive || form.is_active) {
1747
+ forms.push(form);
1748
+ }
1749
+ }
1750
+ catch (error) {
1751
+ console.warn(`⚠️ Could not fetch form ${formId}:`, error);
1752
+ }
1753
+ }
1754
+ }
1755
+ else {
1756
+ // Generate all forms
1757
+ const formsList = await client.getForms();
1758
+ console.log('🔍 Gravity Forms Response:', {
1759
+ formsType: typeof formsList,
1760
+ isArray: Array.isArray(formsList),
1761
+ formsLength: formsList?.length,
1762
+ formsPreview: formsList ? JSON.stringify(formsList).substring(0, 200) : 'null',
1763
+ });
1764
+ let formsMetadata = [];
1765
+ if (Array.isArray(formsList)) {
1766
+ formsMetadata = formsList;
1767
+ }
1768
+ else if (formsList && typeof formsList === 'object' && Array.isArray(formsList.forms)) {
1769
+ formsMetadata = formsList.forms;
1770
+ }
1771
+ else if (formsList) {
1772
+ formsMetadata = [formsList];
1773
+ }
1774
+ if (!this.config.includeInactive) {
1775
+ formsMetadata = formsMetadata.filter(form => form.is_active === '1' || form.is_active === true);
1776
+ }
1777
+ // Fetch full form configuration for each form (schema lives under /config)
1778
+ console.log(`🔄 Fetching full data for ${formsMetadata.length} forms...`);
1779
+ forms = [];
1780
+ for (const formMetadata of formsMetadata) {
1781
+ try {
1782
+ const formId = Number(formMetadata.id);
1783
+ const fullForm = await client.getFormConfig(formId);
1784
+ forms.push(fullForm);
1785
+ console.log(`✅ Fetched form: ${fullForm.title} (${fullForm.fields?.length || 0} fields)`);
1786
+ }
1787
+ catch (error) {
1788
+ console.error(`❌ Failed to fetch form ${formMetadata.id} config:`, error);
1789
+ throw error;
1790
+ }
1791
+ }
1792
+ }
1793
+ const staticData = {
1794
+ generated_at: new Date().toISOString(),
1795
+ total_forms: forms.length,
1796
+ forms,
1797
+ };
1798
+ // Write to file
1799
+ this.writeStaticData(staticData);
1800
+ console.log('✅ Gravity Forms static data generation completed');
1801
+ console.log(`📊 Generated data: ${forms.length} forms`);
1802
+ return staticData;
1803
+ }
1804
+ writeStaticData(data) {
1805
+ try {
1806
+ const outputPath = join(process.cwd(), this.config.outputPath);
1807
+ writeFileSync(outputPath, JSON.stringify(data, null, 2));
1808
+ console.log(`📁 Static data written to: ${outputPath}`);
1809
+ }
1810
+ catch (error) {
1811
+ console.error('❌ Error writing static data:', error);
1812
+ throw error;
1813
+ }
1814
+ }
1815
+ async generateFormConfig(formId) {
1816
+ const client = new GravityFormsClient(this.config);
1817
+ return client.getFormConfig(formId);
1818
+ }
1819
+ }
1820
+ // Convenience function for easy usage
1821
+ async function generateGravityFormsData$1(config) {
1822
+ const generator = new GravityFormsGenerator(config);
1823
+ return generator.generateStaticData();
1824
+ }
1825
+
1826
+ async function generateWordPressData(options) {
1827
+ const outputPath = options?.outputPath || './public/wordpress-data.json';
1828
+ const apiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
1829
+ const username = process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME;
1830
+ const password = process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD;
1831
+ const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
1832
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
1833
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
1834
+ if (!apiUrl) {
1835
+ console.error('❌ Missing WordPress API URL. Set WORDPRESS_API_URL or VITE_WORDPRESS_API_URL');
1836
+ return false;
1837
+ }
1838
+ const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
1839
+ const requestedAuth = process.env.VITE_AUTH_MODE;
1840
+ const hasProxy = !!cloudflareWorkerUrl;
1841
+ const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
1842
+ const config = {
1843
+ authMode,
1844
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
1845
+ apiUrl,
1846
+ username,
1847
+ password,
1848
+ cfAccessClientId,
1849
+ cfAccessClientSecret,
1850
+ };
1851
+ const enabledPostTypes = process.env.VITE_ENABLED_POST_TYPES || process.env.ENABLED_POST_TYPES || 'posts,pages';
1852
+ const rawPostTypes = enabledPostTypes.split(',').map(pt => pt.trim()).filter(Boolean);
1853
+ const postTypeMapping = {
1854
+ 'members': 'member',
1855
+ 'testimonials': 'testimonial',
1856
+ 'faqs': 'faq',
1857
+ 'eventbrite_events': 'eventbrite_event',
1858
+ };
1859
+ const postTypes = rawPostTypes.map(pt => postTypeMapping[pt] || pt);
1860
+ const frontendId = process.env.VITE_FRONTEND_ID || process.env.FRONTEND_ID || undefined;
1861
+ const frontendName = process.env.VITE_FRONTEND_NAME || process.env.FRONTEND_NAME || 'Default Frontend';
1862
+ try {
1863
+ const result = await generateWordPressData$1({
1864
+ ...config,
1865
+ outputPath,
1866
+ postTypes,
1867
+ ...(frontendId && { frontendId }),
1868
+ frontendName,
1869
+ includeEmbedded: true,
1870
+ maxItems: parseInt(process.env.VITE_DEFAULT_MAX_ITEMS || '200', 10),
1871
+ });
1872
+ console.log(`📁 WordPress data written to: ${path.join(process.cwd(), outputPath)}`);
1873
+ return true;
1874
+ }
1875
+ catch (e) {
1876
+ console.error('❌ WordPress generation failed:', e.message);
1877
+ return false;
1878
+ }
1879
+ }
1880
+
1881
+ async function generateGravityFormsData(options) {
1882
+ const outputPath = options?.outputPath || './public/gravity-forms-data.json';
1883
+ const wpApiUrl = process.env.WORDPRESS_API_URL || process.env.VITE_WORDPRESS_API_URL;
1884
+ const gfApiUrl = process.env.GRAVITY_FORMS_API_URL || process.env.VITE_GRAVITY_FORMS_API_URL;
1885
+ const username = process.env.GRAVITY_FORMS_USERNAME || process.env.VITE_GRAVITY_FORMS_USERNAME ||
1886
+ process.env.WP_API_USERNAME || process.env.VITE_WP_API_USERNAME || '';
1887
+ const password = process.env.GRAVITY_FORMS_PASSWORD || process.env.VITE_GRAVITY_FORMS_PASSWORD ||
1888
+ process.env.WP_APP_PASSWORD || process.env.VITE_WP_APP_PASSWORD || '';
1889
+ const cloudflareWorkerUrl = process.env.CLOUDFLARE_WORKER_URL || process.env.VITE_CLOUDFLARE_WORKER_URL;
1890
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
1891
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
1892
+ const apiUrl = gfApiUrl || (wpApiUrl ? `${wpApiUrl}/wp-json/gf-api/v1` : undefined);
1893
+ if (!apiUrl) {
1894
+ console.error('❌ Missing WordPress/Gravity Forms API URL.');
1895
+ return false;
1896
+ }
1897
+ const localDev = process.env.VITE_LOCAL_DEVELOPMENT === 'true';
1898
+ const requestedAuth = process.env.VITE_AUTH_MODE;
1899
+ const hasProxy = !!cloudflareWorkerUrl;
1900
+ const authMode = localDev ? 'direct' : (requestedAuth === 'cloudflare_proxy' && hasProxy ? 'cloudflare_proxy' : 'direct');
1901
+ const config = {
1902
+ authMode,
1903
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? cloudflareWorkerUrl : undefined,
1904
+ apiUrl,
1905
+ username,
1906
+ password,
1907
+ cfAccessClientId,
1908
+ cfAccessClientSecret,
1909
+ useCustomEndpoint: true,
1910
+ };
1911
+ try {
1912
+ const result = await generateGravityFormsData$1({
1913
+ ...config,
1914
+ outputPath,
1915
+ includeInactive: false,
1916
+ });
1917
+ console.log(`📁 Gravity Forms data written to: ${path.join(process.cwd(), outputPath)}`);
1918
+ return true;
1919
+ }
1920
+ catch (e) {
1921
+ console.error('❌ Gravity Forms generation failed:', e.message);
1922
+ return false;
1923
+ }
1924
+ }
1925
+
1926
+ /**
1927
+ * @license GPL-3.0-or-later
1928
+ *
1929
+ * This file is part of the MarVAlt Open SDK.
1930
+ * Copyright (c) 2025 Vibune Pty Ltd.
1931
+ *
1932
+ * This program is free software: you can redistribute it and/or modify
1933
+ * it under the terms of the GNU General Public License as published by
1934
+ * the Free Software Foundation, either version 3 of the License, or
1935
+ * (at your option) any later version.
1936
+ *
1937
+ * This program is distributed in the hope that it will be useful,
1938
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1939
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1940
+ * See the GNU General Public License for more details.
1941
+ */
1942
+ class MauticClient {
1943
+ constructor(config) {
1944
+ this.config = config;
1945
+ // Convention-based: Always use /api/mautic-submit unless explicitly overridden
1946
+ this.proxyEndpoint = config.proxyEndpoint || '/api/mautic-submit';
1947
+ }
1948
+ async makeRequest(endpoint, options = {}) {
1949
+ // Always use proxy endpoint (convention-based)
1950
+ const proxyEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
1951
+ const url = `${this.proxyEndpoint}?endpoint=${encodeURIComponent(proxyEndpoint)}`;
1952
+ const isFormSubmission = endpoint.startsWith('/form/submit') ||
1953
+ (endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
1954
+ const headers = {
1955
+ ...options.headers,
1956
+ };
1957
+ // Default JSON only for non-form submissions when a body exists and no explicit content-type provided
1958
+ const hasBody = options.body != null && options.body !== undefined;
1959
+ if (!isFormSubmission && hasBody && !('Content-Type' in headers)) {
1960
+ headers['Content-Type'] = 'application/json';
1961
+ }
1962
+ try {
1963
+ const response = await fetch(url, {
1964
+ ...options,
1965
+ headers,
1966
+ });
1967
+ // Determine if this was a form submission based on the original endpoint
1968
+ const wasFormSubmission = endpoint.startsWith('/form/submit') ||
1969
+ (endpoint.startsWith('/forms/') && endpoint.includes('/submit'));
1970
+ if (!response.ok) {
1971
+ // For form submissions, some Mautic setups respond with an HTML 404 after a successful submit.
1972
+ // Do not treat this as an error; let the frontend handle success/redirect messaging.
1973
+ const isRedirect = response.status >= 300 && response.status < 400;
1974
+ const isHtmlLike = (response.headers.get('content-type') || '').includes('text/html');
1975
+ if (!(wasFormSubmission && (isRedirect || response.status === 404 || isHtmlLike))) {
1976
+ throw new Error(`Mautic API request failed: ${response.status} ${response.statusText}`);
1977
+ }
1978
+ }
1979
+ const responseText = await response.text();
1980
+ // Handle empty responses
1981
+ if (!responseText) {
1982
+ return [];
1983
+ }
1984
+ // For Mautic form submissions, the response may be HTML. Try JSON first, fallback to text.
1985
+ try {
1986
+ return JSON.parse(responseText);
1987
+ }
1988
+ catch {
1989
+ return responseText;
1990
+ }
1991
+ }
1992
+ catch (error) {
1993
+ const message = error instanceof Error ? error.message : String(error);
1994
+ console.error('Mautic API request error:', message);
1995
+ throw new Error(message);
1996
+ }
1997
+ }
1998
+ /**
1999
+ * Submit a form to Mautic
2000
+ */
2001
+ async submitForm(formId, submission) {
2002
+ // Endpoint should be just the path, without query string (formId is in the body)
2003
+ const endpoint = `/form/submit`;
2004
+ // Encode as application/x-www-form-urlencoded to match Mautic expectations
2005
+ const formParams = new URLSearchParams();
2006
+ // Add fields
2007
+ Object.entries(submission.fields || {}).forEach(([alias, value]) => {
2008
+ if (Array.isArray(value)) {
2009
+ value.forEach((v) => formParams.append(`mauticform[${alias}]`, String(v)));
2010
+ }
2011
+ else if (typeof value === 'boolean') {
2012
+ formParams.append(`mauticform[${alias}]`, value ? '1' : '0');
2013
+ }
2014
+ else if (value != null) {
2015
+ formParams.append(`mauticform[${alias}]`, String(value));
2016
+ }
2017
+ });
2018
+ // Required hidden fields
2019
+ formParams.append('mauticform[formId]', String(formId));
2020
+ // Prevent Mautic from attempting a redirect; frontend will handle any redirect
2021
+ formParams.append('mauticform[return]', '');
2022
+ // Mautic submit flag similar to native form button name/value
2023
+ formParams.append('mauticform[submit]', '1');
2024
+ // Include formName only if provided (derived from cachedHtml)
2025
+ if (submission.formName) {
2026
+ formParams.append('mauticform[formName]', submission.formName);
2027
+ }
2028
+ // Merge custom headers (e.g., Turnstile token) with default headers
2029
+ const headers = {
2030
+ 'Content-Type': 'application/x-www-form-urlencoded',
2031
+ ...(submission.headers || {}),
2032
+ };
2033
+ return this.makeRequest(endpoint, {
2034
+ method: 'POST',
2035
+ headers,
2036
+ body: formParams.toString(),
2037
+ });
2038
+ }
2039
+ /**
2040
+ * Create a new contact in Mautic
2041
+ */
2042
+ async createContact(contact) {
2043
+ return this.makeRequest('/contacts/new', {
2044
+ method: 'POST',
2045
+ body: JSON.stringify(contact),
2046
+ });
2047
+ }
2048
+ /**
2049
+ * Update an existing contact in Mautic
2050
+ */
2051
+ async updateContact(contactId, contact) {
2052
+ return this.makeRequest(`/contacts/${contactId}/edit`, {
2053
+ method: 'PATCH',
2054
+ body: JSON.stringify(contact),
2055
+ });
2056
+ }
2057
+ /**
2058
+ * Get a contact by email
2059
+ */
2060
+ async getContactByEmail(email) {
2061
+ return this.makeRequest(`/contacts?search=email:${encodeURIComponent(email)}`);
2062
+ }
2063
+ /**
2064
+ * Get all forms from Mautic
2065
+ */
2066
+ async getForms() {
2067
+ const response = await this.makeRequest('/forms');
2068
+ return response.forms || [];
2069
+ }
2070
+ /**
2071
+ * Get a specific form by ID
2072
+ */
2073
+ async getForm(formId) {
2074
+ return this.makeRequest(`/forms/${formId}`);
2075
+ }
2076
+ /**
2077
+ * Track an event in Mautic
2078
+ */
2079
+ async trackEvent(eventName, eventData = {}) {
2080
+ return this.makeRequest('/events', {
2081
+ method: 'POST',
2082
+ body: JSON.stringify({
2083
+ eventName,
2084
+ eventData,
2085
+ }),
2086
+ });
2087
+ }
2088
+ /**
2089
+ * Add tags to a contact
2090
+ */
2091
+ async addTagToContact(contactId, tags) {
2092
+ return this.makeRequest(`/contacts/${contactId}/tags`, {
2093
+ method: 'POST',
2094
+ body: JSON.stringify({ tags }),
2095
+ });
2096
+ }
2097
+ /**
2098
+ * Remove tags from a contact
2099
+ */
2100
+ async removeTagFromContact(contactId, tags) {
2101
+ return this.makeRequest(`/contacts/${contactId}/tags`, {
2102
+ method: 'DELETE',
2103
+ body: JSON.stringify({ tags }),
2104
+ });
2105
+ }
2106
+ /**
2107
+ * Get contact tags
2108
+ */
2109
+ async getContactTags(contactId) {
2110
+ return this.makeRequest(`/contacts/${contactId}/tags`);
2111
+ }
2112
+ /**
2113
+ * Get all tags
2114
+ */
2115
+ async getTags() {
2116
+ return this.makeRequest('/tags');
2117
+ }
2118
+ /**
2119
+ * Get segments
2120
+ */
2121
+ async getSegments() {
2122
+ return this.makeRequest('/segments');
2123
+ }
2124
+ /**
2125
+ * Add contact to segment
2126
+ */
2127
+ async addContactToSegment(contactId, segmentId) {
2128
+ return this.makeRequest(`/contacts/${contactId}/segments`, {
2129
+ method: 'POST',
2130
+ body: JSON.stringify({ segmentId }),
2131
+ });
2132
+ }
2133
+ /**
2134
+ * Remove contact from segment
2135
+ */
2136
+ async removeContactFromSegment(contactId, segmentId) {
2137
+ return this.makeRequest(`/contacts/${contactId}/segments`, {
2138
+ method: 'DELETE',
2139
+ body: JSON.stringify({ segmentId }),
2140
+ });
2141
+ }
2142
+ }
2143
+
2144
+ /**
2145
+ * @license GPL-3.0-or-later
2146
+ *
2147
+ * This file is part of the MarVAlt Open SDK.
2148
+ * Copyright (c) 2025 Vibune Pty Ltd.
2149
+ *
2150
+ * This program is free software: you can redistribute it and/or modify
2151
+ * it under the terms of the GNU General Public License as published by
2152
+ * the Free Software Foundation, either version 3 of the License, or
2153
+ * (at your option) any later version.
2154
+ *
2155
+ * This program is distributed in the hope that it will be useful,
2156
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2157
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2158
+ * See the GNU General Public License for more details.
2159
+ */
2160
+ class MauticGenerator {
2161
+ constructor(config) {
2162
+ this.cachedToken = null;
2163
+ this.config = config;
2164
+ // Generator uses direct OAuth2 calls, not the client
2165
+ // Client is only used for proxy mode (cloudflare_proxy)
2166
+ this.client = new MauticClient({
2167
+ apiUrl: config.apiUrl,
2168
+ proxyEndpoint: config.cloudflareWorkerUrl,
2169
+ timeout: config.timeout,
2170
+ retries: config.retries,
2171
+ });
2172
+ }
2173
+ /**
2174
+ * Get OAuth2 token for direct mode API calls
2175
+ */
2176
+ async getOAuth2Token() {
2177
+ if (this.cachedToken && this.cachedToken.expires_at > Date.now() + 300000) {
2178
+ console.log('🔑 Using cached OAuth2 token');
2179
+ return this.cachedToken.access_token;
2180
+ }
2181
+ if (!this.config.clientId || !this.config.clientSecret) {
2182
+ throw new Error('OAuth2 credentials (clientId, clientSecret) required for direct mode');
2183
+ }
2184
+ console.log('🔑 Fetching new OAuth2 token...');
2185
+ const tokenUrl = `${this.config.apiUrl}/oauth/v2/token`;
2186
+ const body = new URLSearchParams({
2187
+ grant_type: 'client_credentials',
2188
+ client_id: this.config.clientId,
2189
+ client_secret: this.config.clientSecret,
2190
+ });
2191
+ const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
2192
+ if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2193
+ headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2194
+ headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2195
+ console.log('🔐 Added CF Access headers to OAuth2 token request');
2196
+ }
2197
+ const resp = await fetch(tokenUrl, { method: 'POST', headers, body: body.toString() });
2198
+ if (!resp.ok) {
2199
+ const errText = await resp.text();
2200
+ throw new Error(`OAuth2 token failed: ${resp.status} ${errText}`);
2201
+ }
2202
+ const data = await resp.json();
2203
+ this.cachedToken = {
2204
+ access_token: data.access_token,
2205
+ expires_at: Date.now() + (data.expires_in * 1000),
2206
+ };
2207
+ console.log('✅ OAuth2 token cached');
2208
+ return this.cachedToken.access_token;
2209
+ }
2210
+ /**
2211
+ * Generate static data for Mautic forms
2212
+ */
2213
+ async generateStaticData() {
2214
+ console.log('🔄 Generating Mautic forms JSON data...');
2215
+ try {
2216
+ const allForms = await this.fetchMauticFormsList();
2217
+ console.log(`📝 Found ${allForms.length} forms`);
2218
+ const forms = [];
2219
+ for (const formSummary of allForms) {
2220
+ const formId = formSummary.id?.toString();
2221
+ if (!formId) {
2222
+ console.warn(`⚠️ Form summary missing ID:`, formSummary);
2223
+ continue;
2224
+ }
2225
+ try {
2226
+ const formDetails = await this.fetchMauticForm(formId);
2227
+ if (!formDetails) {
2228
+ console.warn(`⚠️ No form details returned for form ${formId}`);
2229
+ continue;
2230
+ }
2231
+ // Extract the form data with proper structure
2232
+ const formData = {
2233
+ id: formId,
2234
+ name: formDetails.name || formSummary.name || `Form ${formId}`,
2235
+ description: formDetails.description,
2236
+ isPublished: formDetails.isPublished || formSummary.isPublished || false,
2237
+ // Include post-submission behavior configuration
2238
+ postAction: formDetails.postAction || 'message',
2239
+ postActionProperty: formDetails.postActionProperty || 'Thank you for your submission!',
2240
+ // Include other important form properties
2241
+ formType: formDetails.formType || 'standalone',
2242
+ // Extract fields with validation and properties
2243
+ fields: (formDetails.fields || []).map((field) => ({
2244
+ id: field.id,
2245
+ label: field.label,
2246
+ alias: field.alias,
2247
+ type: field.type,
2248
+ isRequired: field.isRequired || false,
2249
+ validationMessage: field.validationMessage,
2250
+ defaultValue: field.defaultValue,
2251
+ properties: {
2252
+ placeholder: field.properties?.placeholder,
2253
+ cssClass: field.properties?.cssClass,
2254
+ validation: field.properties?.validation,
2255
+ options: field.properties?.options,
2256
+ helpText: field.properties?.helpText,
2257
+ size: field.properties?.size,
2258
+ ...field.properties
2259
+ }
2260
+ })),
2261
+ // Extract actions
2262
+ actions: (formDetails.actions || []).map((action) => ({
2263
+ id: action.id,
2264
+ name: action.name,
2265
+ type: action.type,
2266
+ properties: action.properties || {}
2267
+ })),
2268
+ // Include styling and behavior
2269
+ cssClass: formDetails.cssClass,
2270
+ submitAction: formDetails.submitAction,
2271
+ // Keep the details for reference
2272
+ ...formSummary
2273
+ };
2274
+ forms.push(formData);
2275
+ console.log(`✅ Processed form: ${formData.name} (ID: ${formId})`);
2276
+ }
2277
+ catch (error) {
2278
+ const message = error instanceof Error ? error.message : String(error);
2279
+ console.error(`❌ Error processing form ${formId}:`, message);
2280
+ // Continue with other forms
2281
+ }
2282
+ }
2283
+ const staticData = {
2284
+ generated_at: new Date().toISOString(),
2285
+ total_forms: forms.length,
2286
+ forms: forms.map((form) => ({
2287
+ id: parseInt(form.id),
2288
+ name: form.name,
2289
+ alias: String(form.alias ?? (form.name || '').toLowerCase().replace(/\s+/g, '-')),
2290
+ description: form.description,
2291
+ isPublished: form.isPublished,
2292
+ fields: form.fields,
2293
+ actions: form.actions,
2294
+ cssClass: form.cssClass,
2295
+ submitAction: form.submitAction,
2296
+ postAction: form.postAction,
2297
+ postActionProperty: form.postActionProperty,
2298
+ formType: form.formType,
2299
+ }))
2300
+ };
2301
+ console.log(`✅ Generated static data for ${forms.length} forms`);
2302
+ return staticData;
2303
+ }
2304
+ catch (error) {
2305
+ const message = error instanceof Error ? error.message : String(error);
2306
+ console.error('❌ Error generating Mautic static data:', message);
2307
+ throw new Error(message);
2308
+ }
2309
+ }
2310
+ /**
2311
+ * Write static data to file
2312
+ */
2313
+ async writeStaticData(staticData) {
2314
+ const outputPath = path.resolve(this.config.outputPath);
2315
+ const outputDir = path.dirname(outputPath);
2316
+ // Ensure output directory exists
2317
+ if (!fs.existsSync(outputDir)) {
2318
+ fs.mkdirSync(outputDir, { recursive: true });
2319
+ }
2320
+ // Write the static data
2321
+ fs.writeFileSync(outputPath, JSON.stringify(staticData, null, 2));
2322
+ console.log(`📁 Static data written to: ${outputPath}`);
2323
+ }
2324
+ /**
2325
+ * Generate and write static data
2326
+ */
2327
+ async generateAndWrite() {
2328
+ const staticData = await this.generateStaticData();
2329
+ await this.writeStaticData(staticData);
2330
+ return staticData;
2331
+ }
2332
+ /**
2333
+ * Fetch list of forms from Mautic
2334
+ */
2335
+ async fetchMauticFormsList() {
2336
+ if (this.config.authMode === 'cloudflare_proxy') {
2337
+ const forms = await this.client.getForms();
2338
+ return Array.isArray(forms) ? forms : [];
2339
+ }
2340
+ // Direct mode with OAuth2
2341
+ const token = await this.getOAuth2Token();
2342
+ const url = `${this.config.apiUrl}/api/forms`;
2343
+ const headers = { 'Authorization': `Bearer ${token}` };
2344
+ if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2345
+ headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2346
+ headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2347
+ }
2348
+ const resp = await fetch(url, { headers });
2349
+ if (!resp.ok)
2350
+ throw new Error(`Failed to fetch forms: ${resp.status}`);
2351
+ const data = await resp.json();
2352
+ return Array.isArray(data.forms) ? data.forms : [];
2353
+ }
2354
+ /**
2355
+ * Fetch individual form details from Mautic
2356
+ */
2357
+ async fetchMauticForm(formId) {
2358
+ if (this.config.authMode === 'cloudflare_proxy') {
2359
+ return this.client.getForm(parseInt(formId, 10));
2360
+ }
2361
+ // Direct mode with OAuth2
2362
+ const token = await this.getOAuth2Token();
2363
+ const url = `${this.config.apiUrl}/api/forms/${formId}`;
2364
+ const headers = { 'Authorization': `Bearer ${token}` };
2365
+ if (this.config.cfAccessClientId && this.config.cfAccessClientSecret) {
2366
+ headers['CF-Access-Client-Id'] = this.config.cfAccessClientId;
2367
+ headers['CF-Access-Client-Secret'] = this.config.cfAccessClientSecret;
2368
+ }
2369
+ const resp = await fetch(url, { headers });
2370
+ if (!resp.ok)
2371
+ throw new Error(`Failed to fetch form ${formId}: ${resp.status}`);
2372
+ const data = await resp.json();
2373
+ return data.form || data;
2374
+ }
2375
+ }
2376
+ /**
2377
+ * Generate Mautic static data with configuration
2378
+ */
2379
+ async function generateMauticData$1(config) {
2380
+ const generator = new MauticGenerator(config);
2381
+ return generator.generateAndWrite();
2382
+ }
2383
+
2384
+ async function generateMauticData(options) {
2385
+ const outputPath = options?.outputPath || './src/data/mautic-data.json';
2386
+ const mauticUrl = process.env.MAUTIC_URL || process.env.VITE_MAUTIC_URL;
2387
+ const mauticProxyUrl = process.env.MAUTIC_PROXY_URL || process.env.VITE_MAUTIC_PROXY_URL;
2388
+ const clientId = process.env.MAUTIC_API_PUBLIC_KEY || process.env.VITE_MAUTIC_API_PUBLIC_KEY;
2389
+ const clientSecret = process.env.MAUTIC_API_SECRET_KEY || process.env.VITE_MAUTIC_API_SECRET_KEY;
2390
+ const cfAccessClientId = process.env.CF_ACCESS_CLIENT_ID || process.env.VITE_CF_ACCESS_CLIENT_ID;
2391
+ const cfAccessClientSecret = process.env.CF_ACCESS_CLIENT_SECRET || process.env.VITE_CF_ACCESS_CLIENT_SECRET;
2392
+ if (!mauticUrl) {
2393
+ console.log('⚠️ Mautic URL not configured. Skipping Mautic data generation.');
2394
+ return true;
2395
+ }
2396
+ try {
2397
+ const hasProxy = !!mauticProxyUrl;
2398
+ const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
2399
+ const result = await generateMauticData$1({
2400
+ authMode,
2401
+ apiUrl: mauticUrl,
2402
+ cloudflareWorkerUrl: authMode === 'cloudflare_proxy' ? mauticProxyUrl : undefined,
2403
+ clientId: clientId,
2404
+ clientSecret: clientSecret,
2405
+ cfAccessClientId,
2406
+ cfAccessClientSecret,
2407
+ outputPath,
2408
+ includeInactive: false,
2409
+ });
2410
+ console.log(`📁 Mautic data written to: ${path.join(process.cwd(), outputPath)}`);
2411
+ return true;
2412
+ }
2413
+ catch (e) {
2414
+ console.error('❌ Mautic generation failed:', e.message);
2415
+ return false;
2416
+ }
2417
+ }
2418
+
2419
+ async function generateSuiteCRMData(options) {
2420
+ const outputPath = options?.outputPath || './src/data/suitecrm-data.json';
2421
+ const apiUrl = process.env.VITE_SUITECRM_URL;
2422
+ if (!apiUrl) {
2423
+ console.log('⚠️ SuiteCRM URL not configured. Skipping SuiteCRM data generation.');
2424
+ return true;
2425
+ }
2426
+ try {
2427
+ const hasProxy = !!process.env.VITE_SUITECRM_PROXY_URL;
2428
+ const authMode = hasProxy ? 'cloudflare_proxy' : 'direct';
2429
+ const result = await generateSuiteCRMData$1({
2430
+ authMode,
2431
+ apiUrl,
2432
+ proxyUrl: authMode === 'cloudflare_proxy' ? process.env.VITE_SUITECRM_PROXY_URL : undefined,
2433
+ oauth2: {
2434
+ clientId: process.env.VITE_SUITECRM_CLIENT_ID,
2435
+ clientSecret: process.env.VITE_SUITECRM_CLIENT_SECRET,
2436
+ username: process.env.VITE_SUITECRM_USERNAME,
2437
+ password: process.env.VITE_SUITECRM_PASSWORD,
2438
+ },
2439
+ accessFunctionPath: '/api/fetch-with-access',
2440
+ outputPath,
2441
+ modules: ['Leads', 'Contacts', 'Accounts'],
2442
+ includeFields: true,
2443
+ includeRelationships: false,
2444
+ });
2445
+ console.log(`📁 SuiteCRM data written to: ${path.join(process.cwd(), outputPath)}`);
2446
+ return true;
2447
+ }
2448
+ catch (e) {
2449
+ console.error('❌ SuiteCRM generation failed:', e.message);
2450
+ return false;
2451
+ }
2452
+ }
2453
+
2454
+ async function generateAllData() {
2455
+ // Load .env files if they exist (for local development)
2456
+ dotenv.config();
2457
+ dotenv.config({ path: '.env.local' });
2458
+ console.log('🚀 Starting DigiVAlt static data generation...');
2459
+ if (process.env.VITE_IS_LOVABLE === 'true') {
2460
+ console.log('⚡ Lovable Environment Detected (VITE_IS_LOVABLE). Skipping static data generation to use GitHub synced static files.');
2461
+ return;
2462
+ }
2463
+ const wpSuccess = await generateWordPressData();
2464
+ const gfSuccess = await generateGravityFormsData();
2465
+ const mauticSuccess = await generateMauticData();
2466
+ const suitecrmSuccess = await generateSuiteCRMData();
2467
+ if (!wpSuccess || !gfSuccess || !mauticSuccess || !suitecrmSuccess) {
2468
+ console.error('❌ Some generation tasks failed. See above for details.');
2469
+ process.exit(1);
2470
+ }
2471
+ console.log('✅ Generation complete!');
2472
+ }
2473
+
2474
+ export { generateAllData, generateGravityFormsData, generateMauticData, generateSuiteCRMData, generateWordPressData };
2475
+ //# sourceMappingURL=generators.esm.js.map