@gxp-dev/tools 2.0.63 → 2.0.64

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