@gxp-dev/tools 2.0.11 → 2.0.12

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.
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Extract Config Utility
3
+ *
4
+ * Parses Vue/JS files in src/ directory to extract GxP store usage and directives,
5
+ * then generates/updates app-manifest.json with the extracted configuration.
6
+ *
7
+ * Extracts:
8
+ * - getString(key, default) -> strings.default
9
+ * - getSetting(key, default) -> settings
10
+ * - getAsset(key, default) -> assets
11
+ * - getState(key, default) -> triggerState
12
+ * - callApi(path, identifier) -> dependencies
13
+ * - listenSocket(socketName, event, callback) -> (tracked for reference)
14
+ * - gxp-string/v-gxp-string directives -> strings.default
15
+ * - gxp-setting/v-gxp-setting + gxp-string -> settings
16
+ * - gxp-asset/v-gxp-asset + gxp-string -> assets
17
+ * - gxp-state/v-gxp-state + gxp-string -> triggerState
18
+ * - gxp-src/v-gxp-src -> assets
19
+ */
20
+
21
+ const fs = require("fs");
22
+ const path = require("path");
23
+
24
+ /**
25
+ * Extract configuration from all Vue/JS files in the src directory
26
+ * @param {string} srcDir - Path to the src directory
27
+ * @returns {Object} Extracted configuration
28
+ */
29
+ function extractConfigFromSource(srcDir) {
30
+ const config = {
31
+ strings: {},
32
+ settings: {},
33
+ assets: {},
34
+ triggerState: {},
35
+ dependencies: [],
36
+ };
37
+
38
+ if (!fs.existsSync(srcDir)) {
39
+ console.error(`❌ Source directory not found: ${srcDir}`);
40
+ return config;
41
+ }
42
+
43
+ // Find all .vue and .js files recursively
44
+ const files = findFilesRecursive(srcDir, [".vue", ".js", ".ts", ".jsx", ".tsx"]);
45
+
46
+ for (const file of files) {
47
+ try {
48
+ const content = fs.readFileSync(file, "utf-8");
49
+ const relativePath = path.relative(srcDir, file);
50
+
51
+ // Extract from JavaScript/TypeScript code
52
+ extractFromScript(content, config, relativePath);
53
+
54
+ // Extract from Vue templates
55
+ if (file.endsWith(".vue")) {
56
+ extractFromTemplate(content, config, relativePath);
57
+ }
58
+ } catch (error) {
59
+ console.warn(`⚠ Could not parse ${file}: ${error.message}`);
60
+ }
61
+ }
62
+
63
+ return config;
64
+ }
65
+
66
+ /**
67
+ * Find files recursively with given extensions
68
+ * @param {string} dir - Directory to search
69
+ * @param {string[]} extensions - File extensions to match
70
+ * @returns {string[]} Array of file paths
71
+ */
72
+ function findFilesRecursive(dir, extensions) {
73
+ const files = [];
74
+
75
+ function walk(currentDir) {
76
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
77
+
78
+ for (const entry of entries) {
79
+ const fullPath = path.join(currentDir, entry.name);
80
+
81
+ if (entry.isDirectory()) {
82
+ // Skip node_modules and hidden directories
83
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
84
+ walk(fullPath);
85
+ }
86
+ } else if (entry.isFile()) {
87
+ const ext = path.extname(entry.name).toLowerCase();
88
+ if (extensions.includes(ext)) {
89
+ files.push(fullPath);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ walk(dir);
96
+ return files;
97
+ }
98
+
99
+ /**
100
+ * Extract store method calls from JavaScript/TypeScript code
101
+ * @param {string} content - File content
102
+ * @param {Object} config - Configuration object to populate
103
+ * @param {string} sourcePath - Source file path for logging
104
+ */
105
+ function extractFromScript(content, config, sourcePath) {
106
+ // Extract getString calls: getString('key', 'default') or getString("key", "default")
107
+ const getStringRegex = /\.getString\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]([^'"`]*)['"`])?\s*\)/g;
108
+ let match;
109
+ while ((match = getStringRegex.exec(content)) !== null) {
110
+ const key = match[1];
111
+ const defaultValue = match[2] || "";
112
+ if (!config.strings[key]) {
113
+ config.strings[key] = defaultValue;
114
+ }
115
+ }
116
+
117
+ // Extract getSetting calls: getSetting('key', 'default') or getSetting('key', value)
118
+ const getSettingRegex = /\.getSetting\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]?([^'"`),]*)['"`]?)?\s*\)/g;
119
+ while ((match = getSettingRegex.exec(content)) !== null) {
120
+ const key = match[1];
121
+ let defaultValue = match[2] || "";
122
+ // Clean up the default value
123
+ defaultValue = defaultValue.trim();
124
+ // Try to parse as JSON for non-string values
125
+ if (defaultValue && !config.settings[key]) {
126
+ try {
127
+ config.settings[key] = JSON.parse(defaultValue);
128
+ } catch {
129
+ config.settings[key] = defaultValue;
130
+ }
131
+ } else if (!config.settings[key]) {
132
+ config.settings[key] = "";
133
+ }
134
+ }
135
+
136
+ // Extract getAsset calls: getAsset('key', 'default')
137
+ const getAssetRegex = /\.getAsset\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]([^'"`]*)['"`])?\s*\)/g;
138
+ while ((match = getAssetRegex.exec(content)) !== null) {
139
+ const key = match[1];
140
+ const defaultValue = match[2] || "";
141
+ if (!config.assets[key]) {
142
+ config.assets[key] = defaultValue;
143
+ }
144
+ }
145
+
146
+ // Extract getState calls: getState('key', default)
147
+ const getStateRegex = /\.getState\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]?([^'"`),]*)['"`]?)?\s*\)/g;
148
+ while ((match = getStateRegex.exec(content)) !== null) {
149
+ const key = match[1];
150
+ let defaultValue = match[2] || "";
151
+ defaultValue = defaultValue.trim();
152
+ if (!config.triggerState[key]) {
153
+ try {
154
+ config.triggerState[key] = JSON.parse(defaultValue);
155
+ } catch {
156
+ config.triggerState[key] = defaultValue || null;
157
+ }
158
+ }
159
+ }
160
+
161
+ // Extract callApi calls: callApi('path', 'identifier') or apiGet/apiPost with identifier patterns
162
+ // Pattern: callApi('/path', 'identifier') or any api method with path
163
+ const callApiRegex = /\.(?:callApi|apiGet|apiPost|apiPut|apiPatch|apiDelete)\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`]([^'"`]+)['"`])?\s*\)/g;
164
+ while ((match = callApiRegex.exec(content)) !== null) {
165
+ const apiPath = match[1];
166
+ const identifier = match[2];
167
+ if (identifier) {
168
+ // Check if this dependency already exists
169
+ const exists = config.dependencies.some(
170
+ (dep) => dep.identifier === identifier && dep.path === apiPath
171
+ );
172
+ if (!exists) {
173
+ config.dependencies.push({
174
+ identifier: identifier,
175
+ path: apiPath,
176
+ });
177
+ }
178
+ }
179
+ }
180
+
181
+ // Extract listenSocket calls for reference: listenSocket('socketName', 'event', callback)
182
+ // These help identify what socket events the app expects
183
+ const listenSocketRegex = /\.(?:listenSocket|useSocketListener)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/g;
184
+ while ((match = listenSocketRegex.exec(content)) !== null) {
185
+ const socketName = match[1];
186
+ const eventName = match[2];
187
+ // Add as a dependency reference if not 'primary'
188
+ if (socketName !== "primary") {
189
+ const exists = config.dependencies.some(
190
+ (dep) => dep.identifier === socketName
191
+ );
192
+ if (!exists) {
193
+ config.dependencies.push({
194
+ identifier: socketName,
195
+ path: "",
196
+ events: { [eventName]: eventName },
197
+ });
198
+ } else {
199
+ // Add the event to existing dependency
200
+ const dep = config.dependencies.find((d) => d.identifier === socketName);
201
+ if (dep) {
202
+ dep.events = dep.events || {};
203
+ dep.events[eventName] = eventName;
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Extract directive usage from Vue templates
212
+ * @param {string} content - File content
213
+ * @param {Object} config - Configuration object to populate
214
+ * @param {string} sourcePath - Source file path for logging
215
+ */
216
+ function extractFromTemplate(content, config, sourcePath) {
217
+ // Extract template section
218
+ const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/i);
219
+ if (!templateMatch) {
220
+ return;
221
+ }
222
+
223
+ const template = templateMatch[1];
224
+
225
+ // Extract gxp-string/v-gxp-string without other gxp attributes (pure strings)
226
+ // Pattern: <tag gxp-string="key">default</tag> or <tag v-gxp-string="'key'">default</tag>
227
+ // But NOT when gxp-setting, gxp-asset, or gxp-state is present
228
+ const pureStringRegex = /<([a-z][a-z0-9-]*)\s+[^>]*(?:v-gxp-string|gxp-string)=["']([^"']+)["'][^>]*>([^<]*)</gi;
229
+ let match;
230
+ while ((match = pureStringRegex.exec(template)) !== null) {
231
+ const fullMatch = match[0];
232
+ const key = match[2].replace(/['"]/g, ""); // Remove quotes for v-gxp-string="'key'"
233
+ const defaultValue = match[3].trim();
234
+
235
+ // Check if this element has gxp-setting, gxp-asset, or gxp-state
236
+ const hasGxpSetting = /gxp-setting|v-gxp-setting/i.test(fullMatch);
237
+ const hasGxpAsset = /gxp-asset|v-gxp-asset/i.test(fullMatch);
238
+ const hasGxpState = /gxp-state|v-gxp-state/i.test(fullMatch);
239
+
240
+ if (hasGxpSetting) {
241
+ // gxp-string key with gxp-setting -> settings
242
+ if (!config.settings[key]) {
243
+ config.settings[key] = defaultValue;
244
+ }
245
+ } else if (hasGxpAsset) {
246
+ // gxp-string key with gxp-asset -> assets
247
+ if (!config.assets[key]) {
248
+ config.assets[key] = defaultValue;
249
+ }
250
+ } else if (hasGxpState) {
251
+ // gxp-string key with gxp-state -> triggerState
252
+ if (!config.triggerState[key]) {
253
+ config.triggerState[key] = defaultValue || null;
254
+ }
255
+ } else {
256
+ // Pure gxp-string -> strings
257
+ if (!config.strings[key]) {
258
+ config.strings[key] = defaultValue;
259
+ }
260
+ }
261
+ }
262
+
263
+ // Extract standalone gxp-setting/v-gxp-setting with value (not using gxp-string for key)
264
+ // Pattern: <tag gxp-setting="key">default</tag>
265
+ const settingRegex = /<([a-z][a-z0-9-]*)\s+[^>]*(?:v-gxp-setting|gxp-setting)=["']([^"']+)["'][^>]*>([^<]*)</gi;
266
+ while ((match = settingRegex.exec(template)) !== null) {
267
+ const key = match[2].replace(/['"]/g, "");
268
+ const defaultValue = match[3].trim();
269
+ // Only add if key looks like an actual key (not empty)
270
+ if (key && !config.settings[key]) {
271
+ config.settings[key] = defaultValue;
272
+ }
273
+ }
274
+
275
+ // Extract standalone gxp-asset/v-gxp-asset with value
276
+ const assetRegex = /<([a-z][a-z0-9-]*)\s+[^>]*(?:v-gxp-asset|gxp-asset)=["']([^"']+)["'][^>]*>([^<]*)</gi;
277
+ while ((match = assetRegex.exec(template)) !== null) {
278
+ const key = match[2].replace(/['"]/g, "");
279
+ const defaultValue = match[3].trim();
280
+ if (key && !config.assets[key]) {
281
+ config.assets[key] = defaultValue;
282
+ }
283
+ }
284
+
285
+ // Extract gxp-src/v-gxp-src: <img gxp-src="key" src="default" />
286
+ // Pattern matches self-closing and regular tags with gxp-src and src attributes
287
+ const gxpSrcRegex = /<([a-z][a-z0-9-]*)\s+[^>]*(?:v-gxp-src|gxp-src)=["']([^"']+)["'][^>]*src=["']([^"']+)["'][^>]*\/?>/gi;
288
+ while ((match = gxpSrcRegex.exec(template)) !== null) {
289
+ const key = match[2].replace(/['"]/g, "");
290
+ const defaultSrc = match[3];
291
+
292
+ // Check if gxp-state is present
293
+ const fullMatch = match[0];
294
+ const hasGxpState = /gxp-state|v-gxp-state/i.test(fullMatch);
295
+
296
+ if (hasGxpState) {
297
+ if (!config.triggerState[key]) {
298
+ config.triggerState[key] = defaultSrc;
299
+ }
300
+ } else {
301
+ if (!config.assets[key]) {
302
+ config.assets[key] = defaultSrc;
303
+ }
304
+ }
305
+ }
306
+
307
+ // Also try reverse order: src before gxp-src
308
+ const gxpSrcReverseRegex = /<([a-z][a-z0-9-]*)\s+[^>]*src=["']([^"']+)["'][^>]*(?:v-gxp-src|gxp-src)=["']([^"']+)["'][^>]*\/?>/gi;
309
+ while ((match = gxpSrcReverseRegex.exec(template)) !== null) {
310
+ const defaultSrc = match[2];
311
+ const key = match[3].replace(/['"]/g, "");
312
+
313
+ const fullMatch = match[0];
314
+ const hasGxpState = /gxp-state|v-gxp-state/i.test(fullMatch);
315
+
316
+ if (hasGxpState) {
317
+ if (!config.triggerState[key]) {
318
+ config.triggerState[key] = defaultSrc;
319
+ }
320
+ } else {
321
+ if (!config.assets[key]) {
322
+ config.assets[key] = defaultSrc;
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Merge extracted config into existing manifest
330
+ * @param {Object} existingManifest - Current app-manifest.json content
331
+ * @param {Object} extractedConfig - Newly extracted configuration
332
+ * @param {Object} options - Merge options
333
+ * @returns {Object} Merged manifest
334
+ */
335
+ function mergeConfig(existingManifest, extractedConfig, options = {}) {
336
+ const { overwrite = false } = options;
337
+
338
+ const merged = { ...existingManifest };
339
+
340
+ // Ensure nested structures exist
341
+ if (!merged.strings) merged.strings = {};
342
+ if (!merged.strings.default) merged.strings.default = {};
343
+ if (!merged.settings) merged.settings = {};
344
+ if (!merged.assets) merged.assets = {};
345
+ if (!merged.triggerState) merged.triggerState = {};
346
+ if (!merged.dependencies) merged.dependencies = [];
347
+
348
+ // Merge strings
349
+ for (const [key, value] of Object.entries(extractedConfig.strings)) {
350
+ if (overwrite || !merged.strings.default[key]) {
351
+ merged.strings.default[key] = value;
352
+ }
353
+ }
354
+
355
+ // Merge settings
356
+ for (const [key, value] of Object.entries(extractedConfig.settings)) {
357
+ if (overwrite || merged.settings[key] === undefined) {
358
+ merged.settings[key] = value;
359
+ }
360
+ }
361
+
362
+ // Merge assets
363
+ for (const [key, value] of Object.entries(extractedConfig.assets)) {
364
+ if (overwrite || !merged.assets[key]) {
365
+ merged.assets[key] = value;
366
+ }
367
+ }
368
+
369
+ // Merge triggerState
370
+ for (const [key, value] of Object.entries(extractedConfig.triggerState)) {
371
+ if (overwrite || merged.triggerState[key] === undefined) {
372
+ merged.triggerState[key] = value;
373
+ }
374
+ }
375
+
376
+ // Merge dependencies (by identifier)
377
+ for (const dep of extractedConfig.dependencies) {
378
+ const existingIndex = merged.dependencies.findIndex(
379
+ (d) => d.identifier === dep.identifier
380
+ );
381
+ if (existingIndex === -1) {
382
+ merged.dependencies.push(dep);
383
+ } else if (overwrite) {
384
+ merged.dependencies[existingIndex] = {
385
+ ...merged.dependencies[existingIndex],
386
+ ...dep,
387
+ };
388
+ }
389
+ }
390
+
391
+ return merged;
392
+ }
393
+
394
+ /**
395
+ * Generate a summary of extracted configuration
396
+ * @param {Object} config - Extracted configuration
397
+ * @returns {string} Summary text
398
+ */
399
+ function generateSummary(config) {
400
+ const lines = [];
401
+
402
+ const stringCount = Object.keys(config.strings).length;
403
+ const settingCount = Object.keys(config.settings).length;
404
+ const assetCount = Object.keys(config.assets).length;
405
+ const stateCount = Object.keys(config.triggerState).length;
406
+ const depCount = config.dependencies.length;
407
+
408
+ lines.push("📊 Extraction Summary:");
409
+ lines.push("");
410
+
411
+ if (stringCount > 0) {
412
+ lines.push(`📝 Strings (${stringCount}):`);
413
+ for (const [key, value] of Object.entries(config.strings)) {
414
+ const displayValue = value ? `"${value}"` : "(empty)";
415
+ lines.push(` ${key}: ${displayValue}`);
416
+ }
417
+ lines.push("");
418
+ }
419
+
420
+ if (settingCount > 0) {
421
+ lines.push(`⚙️ Settings (${settingCount}):`);
422
+ for (const [key, value] of Object.entries(config.settings)) {
423
+ lines.push(` ${key}: ${JSON.stringify(value)}`);
424
+ }
425
+ lines.push("");
426
+ }
427
+
428
+ if (assetCount > 0) {
429
+ lines.push(`🖼️ Assets (${assetCount}):`);
430
+ for (const [key, value] of Object.entries(config.assets)) {
431
+ lines.push(` ${key}: ${value || "(empty)"}`);
432
+ }
433
+ lines.push("");
434
+ }
435
+
436
+ if (stateCount > 0) {
437
+ lines.push(`🔄 Trigger State (${stateCount}):`);
438
+ for (const [key, value] of Object.entries(config.triggerState)) {
439
+ lines.push(` ${key}: ${JSON.stringify(value)}`);
440
+ }
441
+ lines.push("");
442
+ }
443
+
444
+ if (depCount > 0) {
445
+ lines.push(`🔗 Dependencies (${depCount}):`);
446
+ for (const dep of config.dependencies) {
447
+ lines.push(` ${dep.identifier}: ${dep.path || "(no path)"}`);
448
+ if (dep.events) {
449
+ lines.push(` Events: ${Object.keys(dep.events).join(", ")}`);
450
+ }
451
+ }
452
+ lines.push("");
453
+ }
454
+
455
+ if (stringCount + settingCount + assetCount + stateCount + depCount === 0) {
456
+ lines.push(" No configuration found in source files.");
457
+ lines.push("");
458
+ }
459
+
460
+ return lines.join("\n");
461
+ }
462
+
463
+ module.exports = {
464
+ extractConfigFromSource,
465
+ mergeConfig,
466
+ generateSummary,
467
+ findFilesRecursive,
468
+ };
@@ -30,15 +30,18 @@ function safeCopyFile(src, dest, description) {
30
30
 
31
31
  /**
32
32
  * Creates package.json for new projects
33
+ * @param {string} projectPath - Path to project directory
34
+ * @param {string} projectName - Name of the project
35
+ * @param {string} description - Optional project description
33
36
  */
34
- function createPackageJson(projectPath, projectName) {
37
+ function createPackageJson(projectPath, projectName, description = "") {
35
38
  const packageJsonPath = path.join(projectPath, "package.json");
36
39
  const globalConfig = loadGlobalConfig();
37
40
 
38
41
  const packageJson = {
39
42
  name: projectName,
40
43
  version: "1.0.0",
41
- description: `GxP Plugin: ${projectName}`,
44
+ description: description || `GxP Plugin: ${projectName}`,
42
45
  main: "main.js",
43
46
  scripts: {
44
47
  ...DEFAULT_SCRIPTS,
@@ -55,6 +58,43 @@ function createPackageJson(projectPath, projectName) {
55
58
  console.log("✓ Created package.json");
56
59
  }
57
60
 
61
+ /**
62
+ * Updates app-manifest.json with project name and description
63
+ * @param {string} projectPath - Path to project directory
64
+ * @param {string} projectName - Name of the project
65
+ * @param {string} description - Optional project description
66
+ */
67
+ function updateAppManifest(projectPath, projectName, description = "") {
68
+ const manifestPath = path.join(projectPath, "app-manifest.json");
69
+
70
+ if (!fs.existsSync(manifestPath)) {
71
+ console.warn("⚠ app-manifest.json not found, skipping update");
72
+ return;
73
+ }
74
+
75
+ try {
76
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
77
+
78
+ // Update name and description
79
+ manifest.name = projectName;
80
+ if (description) {
81
+ manifest.description = description;
82
+ } else {
83
+ manifest.description = `GxP Plugin: ${projectName}`;
84
+ }
85
+
86
+ // Update strings with project name
87
+ if (manifest.strings && manifest.strings.default) {
88
+ manifest.strings.default.welcome_text = `Welcome to ${projectName}`;
89
+ }
90
+
91
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, "\t"));
92
+ console.log("✓ Updated app-manifest.json with project details");
93
+ } catch (error) {
94
+ console.warn("⚠ Could not update app-manifest.json:", error.message);
95
+ }
96
+ }
97
+
58
98
  /**
59
99
  * Installs npm dependencies
60
100
  */
@@ -173,6 +213,7 @@ function ensureImageMagickInstalled() {
173
213
  module.exports = {
174
214
  safeCopyFile,
175
215
  createPackageJson,
216
+ updateAppManifest,
176
217
  installDependencies,
177
218
  updateExistingProject,
178
219
  isImageMagickInstalled,
@@ -8,10 +8,14 @@ const paths = require("./paths");
8
8
  const ssl = require("./ssl");
9
9
  const files = require("./files");
10
10
  const prompts = require("./prompts");
11
+ const aiScaffold = require("./ai-scaffold");
12
+ const extractConfig = require("./extract-config");
11
13
 
12
14
  module.exports = {
13
15
  ...paths,
14
16
  ...ssl,
15
17
  ...files,
16
18
  ...prompts,
19
+ ...aiScaffold,
20
+ ...extractConfig,
17
21
  };