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