@gxp-dev/tools 2.0.6 → 2.0.7

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 (99) hide show
  1. package/browser-extensions/README.md +1 -0
  2. package/browser-extensions/chrome/background.js +857 -0
  3. package/browser-extensions/chrome/content.js +51 -0
  4. package/browser-extensions/chrome/devtools.html +9 -0
  5. package/browser-extensions/chrome/devtools.js +23 -0
  6. package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
  7. package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
  8. package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
  9. package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
  10. package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
  11. package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
  12. package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
  13. package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
  14. package/browser-extensions/chrome/inspector.js +1087 -0
  15. package/browser-extensions/chrome/manifest.json +70 -0
  16. package/browser-extensions/chrome/panel.html +638 -0
  17. package/browser-extensions/chrome/panel.js +862 -0
  18. package/browser-extensions/chrome/popup.html +399 -0
  19. package/browser-extensions/chrome/popup.js +515 -0
  20. package/browser-extensions/chrome/rules.json +1 -0
  21. package/browser-extensions/chrome/test-chrome.html +145 -0
  22. package/browser-extensions/chrome/test-mixed-content.html +190 -0
  23. package/browser-extensions/chrome/test-uri-pattern.html +199 -0
  24. package/browser-extensions/firefox/README.md +134 -0
  25. package/browser-extensions/firefox/background.js +804 -0
  26. package/browser-extensions/firefox/content.js +120 -0
  27. package/browser-extensions/firefox/debug-errors.html +229 -0
  28. package/browser-extensions/firefox/debug-https.html +113 -0
  29. package/browser-extensions/firefox/devtools.html +9 -0
  30. package/browser-extensions/firefox/devtools.js +24 -0
  31. package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
  32. package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
  33. package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
  34. package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
  35. package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
  36. package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
  37. package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
  38. package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
  39. package/browser-extensions/firefox/inspector.js +1087 -0
  40. package/browser-extensions/firefox/manifest.json +67 -0
  41. package/browser-extensions/firefox/panel.html +638 -0
  42. package/browser-extensions/firefox/panel.js +862 -0
  43. package/browser-extensions/firefox/popup.html +525 -0
  44. package/browser-extensions/firefox/popup.js +536 -0
  45. package/browser-extensions/firefox/test-gramercy.html +126 -0
  46. package/browser-extensions/firefox/test-imports.html +58 -0
  47. package/browser-extensions/firefox/test-masking.html +147 -0
  48. package/browser-extensions/firefox/test-uri-pattern.html +199 -0
  49. package/package.json +7 -2
  50. package/runtime/PortalContainer.vue +326 -0
  51. package/runtime/dev-tools/DevToolsModal.vue +217 -0
  52. package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
  53. package/runtime/dev-tools/MockDataEditor.vue +621 -0
  54. package/runtime/dev-tools/SocketSimulator.vue +562 -0
  55. package/runtime/dev-tools/StoreInspector.vue +644 -0
  56. package/runtime/dev-tools/index.js +6 -0
  57. package/runtime/gxpStringsPlugin.js +428 -0
  58. package/runtime/index.html +22 -0
  59. package/runtime/main.js +32 -0
  60. package/runtime/mock-api/auth-middleware.js +97 -0
  61. package/runtime/mock-api/image-generator.js +221 -0
  62. package/runtime/mock-api/index.js +197 -0
  63. package/runtime/mock-api/response-generator.js +394 -0
  64. package/runtime/mock-api/route-generator.js +323 -0
  65. package/runtime/mock-api/socket-triggers.js +371 -0
  66. package/runtime/mock-api/spec-loader.js +300 -0
  67. package/runtime/server.js +180 -0
  68. package/runtime/stores/gxpPortalConfigStore.js +554 -0
  69. package/runtime/stores/index.js +6 -0
  70. package/runtime/vite-inspector-plugin.js +749 -0
  71. package/runtime/vite-source-tracker-plugin.js +232 -0
  72. package/runtime/vite.config.js +402 -0
  73. package/scripts/launch-chrome.js +90 -0
  74. package/scripts/pack-chrome.js +91 -0
  75. package/socket-events/AiSessionMessageCreated.json +18 -0
  76. package/socket-events/SocialStreamPostCreated.json +24 -0
  77. package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
  78. package/template/README.md +332 -0
  79. package/template/app-manifest.json +32 -0
  80. package/template/dev-assets/images/avatar-placeholder.png +0 -0
  81. package/template/dev-assets/images/background-placeholder.jpg +0 -0
  82. package/template/dev-assets/images/banner-placeholder.jpg +0 -0
  83. package/template/dev-assets/images/icon-placeholder.png +0 -0
  84. package/template/dev-assets/images/logo-placeholder.png +0 -0
  85. package/template/dev-assets/images/product-placeholder.jpg +0 -0
  86. package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
  87. package/template/env.example +51 -0
  88. package/template/gitignore +53 -0
  89. package/template/index.html +22 -0
  90. package/template/main.js +28 -0
  91. package/template/src/DemoPage.vue +459 -0
  92. package/template/src/Plugin.vue +38 -0
  93. package/template/src/stores/index.js +9 -0
  94. package/template/src/stores/test-data.json +173 -0
  95. package/template/theme-layouts/AdditionalStyling.css +0 -0
  96. package/template/theme-layouts/PrivateLayout.vue +39 -0
  97. package/template/theme-layouts/PublicLayout.vue +39 -0
  98. package/template/theme-layouts/SystemLayout.vue +39 -0
  99. package/template/vite.config.js +333 -0
@@ -0,0 +1,749 @@
1
+ /**
2
+ * GxP Component Inspector Vite Plugin
3
+ *
4
+ * Provides API endpoints for the browser extension to:
5
+ * - Get component information
6
+ * - Extract strings to the stringsList
7
+ * - Update Vue source files
8
+ */
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+
13
+ /**
14
+ * Generate a key from text content
15
+ * @param {string} text - The text to convert to a key
16
+ * @returns {string} - A valid key for stringsList
17
+ */
18
+ function textToKey(text) {
19
+ return text
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\s]/g, '')
22
+ .replace(/\s+/g, '_')
23
+ .substring(0, 40)
24
+ .replace(/_+$/, ''); // Remove trailing underscores
25
+ }
26
+
27
+ /**
28
+ * Parse JSON body from request
29
+ */
30
+ async function parseBody(req) {
31
+ return new Promise((resolve, reject) => {
32
+ let body = '';
33
+ req.on('data', chunk => body += chunk);
34
+ req.on('end', () => {
35
+ try {
36
+ resolve(JSON.parse(body));
37
+ } catch (e) {
38
+ reject(new Error('Invalid JSON'));
39
+ }
40
+ });
41
+ req.on('error', reject);
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Send JSON response
47
+ */
48
+ function sendJson(res, data, status = 200) {
49
+ res.statusCode = status;
50
+ res.setHeader('Content-Type', 'application/json');
51
+ res.end(JSON.stringify(data));
52
+ }
53
+
54
+ /**
55
+ * Create the inspector plugin
56
+ */
57
+ export function gxpInspectorPlugin() {
58
+ return {
59
+ name: 'gxp-inspector',
60
+
61
+ configureServer(server) {
62
+ // API endpoint prefix
63
+ const API_PREFIX = '/__gxp-inspector';
64
+
65
+ // Watch for app-manifest.json changes and trigger HMR
66
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
67
+ let manifestWatcher = null;
68
+
69
+ // Setup manifest file watcher
70
+ try {
71
+ // Use chokidar if available (Vite uses it internally)
72
+ if (server.watcher) {
73
+ server.watcher.add(manifestPath);
74
+ server.watcher.on('change', (changedPath) => {
75
+ if (changedPath === manifestPath || changedPath.endsWith('app-manifest.json')) {
76
+ console.log('[GxP Inspector] app-manifest.json changed, sending HMR update');
77
+ try {
78
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
79
+ // Send custom HMR event to all connected clients
80
+ server.ws.send({
81
+ type: 'custom',
82
+ event: 'gxp:manifest-update',
83
+ data: manifest
84
+ });
85
+ } catch (e) {
86
+ console.warn('[GxP Inspector] Could not parse app-manifest.json:', e.message);
87
+ // Send reload signal if parse failed
88
+ server.ws.send({
89
+ type: 'custom',
90
+ event: 'gxp:manifest-reload',
91
+ data: {}
92
+ });
93
+ }
94
+ }
95
+ });
96
+ console.log('[GxP Inspector] Watching app-manifest.json for changes');
97
+ }
98
+ } catch (e) {
99
+ console.warn('[GxP Inspector] Could not setup manifest watcher:', e.message);
100
+ }
101
+
102
+ server.middlewares.use(async (req, res, next) => {
103
+ // Only handle our API endpoints
104
+ if (!req.url?.startsWith(API_PREFIX)) {
105
+ return next();
106
+ }
107
+
108
+ const endpoint = req.url.replace(API_PREFIX, '').split('?')[0];
109
+
110
+ try {
111
+ // GET /ping - Health check
112
+ if (req.method === 'GET' && endpoint === '/ping') {
113
+ return sendJson(res, {
114
+ success: true,
115
+ status: 'ok',
116
+ version: '1.0.0',
117
+ projectRoot: process.cwd()
118
+ });
119
+ }
120
+
121
+ // GET /strings - Get current strings from app-manifest.json
122
+ if (req.method === 'GET' && endpoint === '/strings') {
123
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
124
+
125
+ if (!fs.existsSync(manifestPath)) {
126
+ // Return empty strings if manifest doesn't exist yet
127
+ return sendJson(res, {
128
+ success: true,
129
+ stringsList: {}
130
+ });
131
+ }
132
+
133
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
134
+ return sendJson(res, {
135
+ success: true,
136
+ stringsList: manifest.strings?.default || {}
137
+ });
138
+ }
139
+
140
+ // POST /lookup-string - Check if a text value exists in manifest and return its key
141
+ if (req.method === 'POST' && endpoint === '/lookup-string') {
142
+ const body = await parseBody(req);
143
+ const { text, filePath } = body;
144
+
145
+ if (!text) {
146
+ return sendJson(res, {
147
+ success: false,
148
+ error: 'text is required'
149
+ }, 400);
150
+ }
151
+
152
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
153
+ let foundKey = null;
154
+ let isFromGetString = false;
155
+
156
+ // Check if text exists as a value in the manifest
157
+ if (fs.existsSync(manifestPath)) {
158
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
159
+ const strings = manifest.strings?.default || {};
160
+
161
+ // Find key by value
162
+ for (const [key, value] of Object.entries(strings)) {
163
+ if (value === text) {
164
+ foundKey = key;
165
+ break;
166
+ }
167
+ }
168
+ }
169
+
170
+ // If we found a key, check if the source file uses getString with that key
171
+ if (foundKey && filePath) {
172
+ const fullPath = path.resolve(process.cwd(), filePath);
173
+ if (fs.existsSync(fullPath)) {
174
+ const fileContent = fs.readFileSync(fullPath, 'utf-8');
175
+ // Check for getString('key' or getString("key"
176
+ const getStringRegex = new RegExp(`getString\\s*\\(\\s*['"]${foundKey}['"]`, 'g');
177
+ if (getStringRegex.test(fileContent)) {
178
+ isFromGetString = true;
179
+ }
180
+ }
181
+ }
182
+
183
+ return sendJson(res, {
184
+ success: true,
185
+ found: foundKey !== null,
186
+ key: foundKey,
187
+ isFromGetString: isFromGetString,
188
+ text: text
189
+ });
190
+ }
191
+
192
+ // POST /update-string - Update an existing gxp-string attribute key/value in manifest and source
193
+ if (req.method === 'POST' && endpoint === '/update-string') {
194
+ const body = await parseBody(req);
195
+ const {
196
+ oldKey, // The current key
197
+ newKey, // The new key (can be same as oldKey)
198
+ newValue, // The new default value (text content)
199
+ filePath // The Vue file path
200
+ } = body;
201
+
202
+ if (!oldKey || !newKey || !filePath) {
203
+ return sendJson(res, {
204
+ success: false,
205
+ error: 'oldKey, newKey, and filePath are required'
206
+ }, 400);
207
+ }
208
+
209
+ const fullPath = path.resolve(process.cwd(), filePath);
210
+
211
+ if (!fs.existsSync(fullPath)) {
212
+ return sendJson(res, {
213
+ success: false,
214
+ error: `File not found: ${filePath}`
215
+ }, 404);
216
+ }
217
+
218
+ // Read the Vue file
219
+ let fileContent = fs.readFileSync(fullPath, 'utf-8');
220
+
221
+ // Find and replace the gxp-string attribute
222
+ // Match: gxp-string="oldKey"
223
+ const oldAttrRegex = new RegExp(`gxp-string="${oldKey}"`, 'g');
224
+
225
+ let replaced = false;
226
+ if (oldAttrRegex.test(fileContent)) {
227
+ oldAttrRegex.lastIndex = 0;
228
+ fileContent = fileContent.replace(oldAttrRegex, `gxp-string="${newKey}"`);
229
+ replaced = true;
230
+ }
231
+
232
+ if (!replaced) {
233
+ return sendJson(res, {
234
+ success: false,
235
+ error: `Could not find gxp-string="${oldKey}" in ${filePath}`
236
+ }, 400);
237
+ }
238
+
239
+ // Write the updated file
240
+ fs.writeFileSync(fullPath, fileContent, 'utf-8');
241
+
242
+ // Update app-manifest.json
243
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
244
+ let manifest = {};
245
+
246
+ if (fs.existsSync(manifestPath)) {
247
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
248
+ } else {
249
+ manifest = {
250
+ name: "GxToolkit",
251
+ version: "1.0.0",
252
+ description: "GxToolkit",
253
+ manifest_version: 3,
254
+ settings: {},
255
+ strings: { default: {} },
256
+ assets: {}
257
+ };
258
+ }
259
+
260
+ manifest.strings = manifest.strings || { default: {} };
261
+ manifest.strings.default = manifest.strings.default || {};
262
+
263
+ // Remove old key if different from new key
264
+ if (oldKey !== newKey && manifest.strings.default[oldKey] !== undefined) {
265
+ delete manifest.strings.default[oldKey];
266
+ }
267
+
268
+ // Set new key with new value
269
+ if (newValue) {
270
+ manifest.strings.default[newKey] = newValue;
271
+ }
272
+
273
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
274
+
275
+ return sendJson(res, {
276
+ success: true,
277
+ oldKey,
278
+ newKey,
279
+ newValue,
280
+ file: filePath
281
+ });
282
+ }
283
+
284
+ // POST /extract-string - Extract a string by adding gxp-string attribute
285
+ if (req.method === 'POST' && endpoint === '/extract-string') {
286
+ const body = await parseBody(req);
287
+ const {
288
+ text, // The original text to extract
289
+ key, // Optional: custom key (otherwise generated from text)
290
+ filePath, // The Vue file path (relative to project root)
291
+ } = body;
292
+
293
+ if (!text || !filePath) {
294
+ return sendJson(res, {
295
+ success: false,
296
+ error: 'text and filePath are required'
297
+ }, 400);
298
+ }
299
+
300
+ const stringKey = key || textToKey(text);
301
+ const fullPath = path.resolve(process.cwd(), filePath);
302
+
303
+ // Validate file exists
304
+ if (!fs.existsSync(fullPath)) {
305
+ return sendJson(res, {
306
+ success: false,
307
+ error: `File not found: ${filePath}`
308
+ }, 404);
309
+ }
310
+
311
+ // Read the Vue file
312
+ let fileContent = fs.readFileSync(fullPath, 'utf-8');
313
+
314
+ // Find and replace the text in the template section
315
+ const templateMatch = fileContent.match(/<template>([\s\S]*?)<\/template>/);
316
+ if (!templateMatch) {
317
+ return sendJson(res, {
318
+ success: false,
319
+ error: 'No template section found in file'
320
+ }, 400);
321
+ }
322
+
323
+ let template = templateMatch[1];
324
+ const originalTemplate = template;
325
+
326
+ let replaced = false;
327
+
328
+ // Pattern: Find element containing the text and add gxp-string attribute
329
+ // Match: <tag ...>text</tag> or <tag ...>text< (self-closing or nested)
330
+ // We need to find the opening tag that contains this text
331
+
332
+ // First try: Look for >text< pattern and work backwards to find the opening tag
333
+ const textPattern = new RegExp(`(<([a-zA-Z][a-zA-Z0-9-]*)([^>]*)>\\s*)${escapeRegex(text)}(\\s*<)`, 'g');
334
+
335
+ if (textPattern.test(template)) {
336
+ textPattern.lastIndex = 0; // Reset regex
337
+ template = template.replace(textPattern, (match, openTag, tagName, attrs, closeStart) => {
338
+ // Check if gxp-string attribute already exists
339
+ if (attrs.includes('gxp-string=')) {
340
+ return match; // Already has the attribute
341
+ }
342
+ // Add gxp-string attribute to the opening tag
343
+ const newOpenTag = `<${tagName}${attrs} gxp-string="${stringKey}">`;
344
+ return `${newOpenTag}${text}${closeStart}`;
345
+ });
346
+ replaced = true;
347
+ }
348
+
349
+ // If pattern 1 didn't work, try a simpler approach for standalone text
350
+ if (!replaced) {
351
+ // Look for the text and find its parent tag
352
+ const simpleTextPattern = new RegExp(`(>\\s*)${escapeRegex(text)}(\\s*</)`, 'g');
353
+ if (simpleTextPattern.test(template)) {
354
+ // We found the text, now we need to add attribute to parent
355
+ // This is trickier - let's find the text position and work backwards
356
+ const textIndex = template.indexOf(`>${text}<`);
357
+ if (textIndex !== -1) {
358
+ // Find the opening tag before this text
359
+ let tagStart = textIndex;
360
+ while (tagStart > 0 && template[tagStart] !== '<') {
361
+ tagStart--;
362
+ }
363
+
364
+ // Extract the tag
365
+ const tagEnd = template.indexOf('>', tagStart);
366
+ if (tagEnd !== -1 && tagEnd <= textIndex) {
367
+ const openTag = template.substring(tagStart, tagEnd + 1);
368
+ // Check if it already has gxp-string
369
+ if (!openTag.includes('gxp-string=')) {
370
+ // Add the attribute before the closing >
371
+ const newOpenTag = openTag.replace(/>$/, ` gxp-string="${stringKey}">`);
372
+ template = template.substring(0, tagStart) + newOpenTag + template.substring(tagEnd + 1);
373
+ replaced = true;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ if (!replaced) {
381
+ return sendJson(res, {
382
+ success: false,
383
+ error: `Could not find "${text}" in template section`,
384
+ suggestion: 'The text might be part of a more complex expression or already extracted'
385
+ }, 400);
386
+ }
387
+
388
+ // Update the file content with new template
389
+ fileContent = fileContent.replace(originalTemplate, template);
390
+
391
+ // Write the updated file
392
+ fs.writeFileSync(fullPath, fileContent, 'utf-8');
393
+
394
+ // Now update app-manifest.json with the new string
395
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
396
+ let manifest = {};
397
+
398
+ // Create default manifest structure if file doesn't exist
399
+ if (fs.existsSync(manifestPath)) {
400
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
401
+ } else {
402
+ // Create default manifest
403
+ manifest = {
404
+ name: "GxToolkit",
405
+ version: "1.0.0",
406
+ description: "GxToolkit",
407
+ manifest_version: 3,
408
+ settings: {},
409
+ strings: {
410
+ default: {}
411
+ },
412
+ assets: {}
413
+ };
414
+ }
415
+
416
+ // Ensure strings object exists
417
+ manifest.strings = manifest.strings || { default: {} };
418
+ manifest.strings.default = manifest.strings.default || {};
419
+
420
+ // Only add if not already exists
421
+ if (!manifest.strings.default[stringKey]) {
422
+ manifest.strings.default[stringKey] = text;
423
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
424
+ }
425
+
426
+ return sendJson(res, {
427
+ success: true,
428
+ key: stringKey,
429
+ text: text,
430
+ file: filePath,
431
+ attributeAdded: true
432
+ });
433
+ }
434
+
435
+ // POST /add-string - Just add a string to app-manifest.json without modifying source
436
+ if (req.method === 'POST' && endpoint === '/add-string') {
437
+ const body = await parseBody(req);
438
+ const { key, value } = body;
439
+
440
+ if (!key || !value) {
441
+ return sendJson(res, {
442
+ success: false,
443
+ error: 'key and value are required'
444
+ }, 400);
445
+ }
446
+
447
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
448
+ let manifest = {};
449
+
450
+ // Create default manifest structure if file doesn't exist
451
+ if (fs.existsSync(manifestPath)) {
452
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
453
+ } else {
454
+ manifest = {
455
+ name: "GxToolkit",
456
+ version: "1.0.0",
457
+ description: "GxToolkit",
458
+ manifest_version: 3,
459
+ settings: {},
460
+ strings: {
461
+ default: {}
462
+ },
463
+ assets: {}
464
+ };
465
+ }
466
+
467
+ // Ensure strings object exists
468
+ manifest.strings = manifest.strings || { default: {} };
469
+ manifest.strings.default = manifest.strings.default || {};
470
+ manifest.strings.default[key] = value;
471
+
472
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
473
+
474
+ return sendJson(res, {
475
+ success: true,
476
+ key,
477
+ value
478
+ });
479
+ }
480
+
481
+ // GET /file - Get file content
482
+ if (req.method === 'GET' && endpoint === '/file') {
483
+ const url = new URL(req.url, 'http://localhost');
484
+ const filePath = url.searchParams.get('path');
485
+
486
+ if (!filePath) {
487
+ return sendJson(res, {
488
+ success: false,
489
+ error: 'path parameter required'
490
+ }, 400);
491
+ }
492
+
493
+ const fullPath = path.resolve(process.cwd(), filePath);
494
+
495
+ if (!fs.existsSync(fullPath)) {
496
+ return sendJson(res, {
497
+ success: false,
498
+ error: 'File not found'
499
+ }, 404);
500
+ }
501
+
502
+ const content = fs.readFileSync(fullPath, 'utf-8');
503
+
504
+ return sendJson(res, {
505
+ success: true,
506
+ path: filePath,
507
+ content
508
+ });
509
+ }
510
+
511
+ // POST /update-file - Direct file update
512
+ if (req.method === 'POST' && endpoint === '/update-file') {
513
+ const body = await parseBody(req);
514
+ const { filePath, content, backup } = body;
515
+
516
+ if (!filePath || content === undefined) {
517
+ return sendJson(res, {
518
+ success: false,
519
+ error: 'filePath and content are required'
520
+ }, 400);
521
+ }
522
+
523
+ const fullPath = path.resolve(process.cwd(), filePath);
524
+
525
+ // Create backup if requested
526
+ if (backup && fs.existsSync(fullPath)) {
527
+ const backupPath = fullPath + '.backup';
528
+ fs.copyFileSync(fullPath, backupPath);
529
+ }
530
+
531
+ fs.writeFileSync(fullPath, content, 'utf-8');
532
+
533
+ return sendJson(res, {
534
+ success: true,
535
+ path: filePath
536
+ });
537
+ }
538
+
539
+ // POST /analyze-text - Analyze if text content comes from a dynamic expression
540
+ if (req.method === 'POST' && endpoint === '/analyze-text') {
541
+ const body = await parseBody(req);
542
+ const { text, filePath } = body;
543
+
544
+ if (!text || !filePath) {
545
+ return sendJson(res, {
546
+ success: false,
547
+ error: 'text and filePath are required'
548
+ }, 400);
549
+ }
550
+
551
+ const fullPath = path.resolve(process.cwd(), filePath);
552
+
553
+ if (!fs.existsSync(fullPath)) {
554
+ return sendJson(res, {
555
+ success: false,
556
+ error: `File not found: ${filePath}`
557
+ }, 404);
558
+ }
559
+
560
+ const fileContent = fs.readFileSync(fullPath, 'utf-8');
561
+
562
+ // Extract template section
563
+ const templateMatch = fileContent.match(/<template>([\s\S]*?)<\/template>/);
564
+ if (!templateMatch) {
565
+ return sendJson(res, {
566
+ success: true,
567
+ isDynamic: false,
568
+ reason: 'No template section found'
569
+ });
570
+ }
571
+
572
+ const template = templateMatch[1];
573
+ const result = {
574
+ isDynamic: false,
575
+ expressionType: null,
576
+ expression: null,
577
+ sourceKey: null
578
+ };
579
+
580
+ // Check if the exact text appears as static content (not in an expression)
581
+ // Static text would appear as >text< without {{ }}
582
+ const staticTextPattern = new RegExp(`>\\s*${escapeRegex(text)}\\s*<`, 'g');
583
+ const hasStaticText = staticTextPattern.test(template);
584
+
585
+ // Check for template expressions {{ ... }} that might produce this text
586
+ // We look for expressions in the template and check if the text could come from them
587
+ const expressionPattern = /\{\{\s*([^}]+)\s*\}\}/g;
588
+ const expressions = [];
589
+ let match;
590
+
591
+ while ((match = expressionPattern.exec(template)) !== null) {
592
+ expressions.push({
593
+ full: match[0],
594
+ expression: match[1].trim(),
595
+ index: match.index
596
+ });
597
+ }
598
+
599
+ // Extract script setup section to find variable definitions
600
+ const scriptSetupMatch = fileContent.match(/<script\s+setup[^>]*>([\s\S]*?)<\/script>/);
601
+ const scriptMatch = fileContent.match(/<script(?!\s+setup)[^>]*>([\s\S]*?)<\/script>/);
602
+ const scriptContent = scriptSetupMatch?.[1] || scriptMatch?.[1] || '';
603
+
604
+ // Check for getString calls that might produce this text
605
+ // Pattern: gxpStore.getString('key') or getString('key')
606
+ const getStringPattern = /(?:gxpStore\.)?getString\s*\(\s*['"]([^'"]+)['"]/g;
607
+ const getStringCalls = [];
608
+
609
+ while ((match = getStringPattern.exec(fileContent)) !== null) {
610
+ getStringCalls.push({
611
+ key: match[1],
612
+ full: match[0]
613
+ });
614
+ }
615
+
616
+ // Check manifest for getString keys that match this text
617
+ const manifestPath = path.join(process.cwd(), 'app-manifest.json');
618
+ let manifestStrings = {};
619
+ if (fs.existsSync(manifestPath)) {
620
+ try {
621
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
622
+ manifestStrings = manifest.strings?.default || {};
623
+ } catch (e) {
624
+ // Ignore parse errors
625
+ }
626
+ }
627
+
628
+ // Check if any getString call's value matches the text
629
+ for (const call of getStringCalls) {
630
+ if (manifestStrings[call.key] === text) {
631
+ result.isDynamic = true;
632
+ result.expressionType = 'getString';
633
+ result.expression = `getString('${call.key}')`;
634
+ result.sourceKey = call.key;
635
+ break;
636
+ }
637
+ }
638
+
639
+ // If not a getString match, check if text appears inside a template expression context
640
+ // This is a heuristic: if the text does NOT appear as static content but we have expressions,
641
+ // it's likely dynamic
642
+ if (!result.isDynamic && !hasStaticText && expressions.length > 0) {
643
+ // Check each expression to see if it could produce this text
644
+ for (const expr of expressions) {
645
+ // Check if expression references a variable that might contain this text
646
+ // Look for the variable in script content
647
+ const varName = expr.expression.split('.')[0].split('(')[0].trim();
648
+
649
+ // Check for ref/reactive definitions
650
+ const refPattern = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*(?:ref|reactive)\\s*\\(\\s*['"]${escapeRegex(text)}['"]`, 'g');
651
+ const constPattern = new RegExp(`(?:const|let|var)\\s+${varName}\\s*=\\s*['"]${escapeRegex(text)}['"]`, 'g');
652
+
653
+ if (refPattern.test(scriptContent) || constPattern.test(scriptContent)) {
654
+ result.isDynamic = true;
655
+ result.expressionType = 'variable';
656
+ result.expression = expr.expression;
657
+ break;
658
+ }
659
+
660
+ // Check for computed properties or store getters
661
+ if (expr.expression.includes('Store') || expr.expression.includes('store')) {
662
+ result.isDynamic = true;
663
+ result.expressionType = 'store';
664
+ result.expression = expr.expression;
665
+ break;
666
+ }
667
+ }
668
+ }
669
+
670
+ // Check for gxp-string attribute with this text - if found, it's managed by directive
671
+ const gxpStringPattern = new RegExp(`gxp-string=["'][^"']+["'][^>]*>\\s*${escapeRegex(text)}\\s*<`, 'g');
672
+ if (gxpStringPattern.test(template)) {
673
+ result.isDynamic = true;
674
+ result.expressionType = 'gxp-directive';
675
+ result.expression = 'gxp-string directive';
676
+ }
677
+
678
+ // Additional check: look for the text value in props/settings patterns
679
+ const settingsPattern = /(?:pluginVars|settings|props)\s*\.\s*(\w+)/g;
680
+ while ((match = settingsPattern.exec(template)) !== null) {
681
+ // Check if this setting might produce the text
682
+ result.possibleSettings = result.possibleSettings || [];
683
+ result.possibleSettings.push(match[1]);
684
+ }
685
+
686
+ return sendJson(res, {
687
+ success: true,
688
+ ...result,
689
+ hasStaticText,
690
+ expressionCount: expressions.length,
691
+ getStringCallsCount: getStringCalls.length
692
+ });
693
+ }
694
+
695
+ // GET /component-files - List Vue component files
696
+ if (req.method === 'GET' && endpoint === '/component-files') {
697
+ const srcDir = path.join(process.cwd(), 'src');
698
+ const files = [];
699
+
700
+ function scanDir(dir, relativePath = '') {
701
+ if (!fs.existsSync(dir)) return;
702
+
703
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
704
+ for (const entry of entries) {
705
+ const entryPath = path.join(relativePath, entry.name);
706
+ if (entry.isDirectory()) {
707
+ scanDir(path.join(dir, entry.name), entryPath);
708
+ } else if (entry.name.endsWith('.vue')) {
709
+ files.push('src/' + entryPath);
710
+ }
711
+ }
712
+ }
713
+
714
+ scanDir(srcDir);
715
+
716
+ return sendJson(res, {
717
+ success: true,
718
+ files
719
+ });
720
+ }
721
+
722
+ // Unknown endpoint
723
+ return sendJson(res, {
724
+ success: false,
725
+ error: 'Unknown endpoint'
726
+ }, 404);
727
+
728
+ } catch (error) {
729
+ console.error('[GxP Inspector] Error:', error);
730
+ return sendJson(res, {
731
+ success: false,
732
+ error: error.message
733
+ }, 500);
734
+ }
735
+ });
736
+
737
+ console.log('[GxP Inspector] API endpoints available at /__gxp-inspector/*');
738
+ }
739
+ };
740
+ }
741
+
742
+ /**
743
+ * Escape special regex characters
744
+ */
745
+ function escapeRegex(string) {
746
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
747
+ }
748
+
749
+ export default gxpInspectorPlugin;