cyber-elx 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/files.js CHANGED
@@ -19,7 +19,12 @@ function ensureDirectories(cwd = process.cwd()) {
19
19
  path.join(cwd, 'layouts'),
20
20
  path.join(cwd, 'defaults', 'sections'),
21
21
  path.join(cwd, 'defaults', 'templates'),
22
- path.join(cwd, 'defaults', 'layouts')
22
+ path.join(cwd, 'defaults', 'layouts'),
23
+ // SPA folders
24
+ path.join(cwd, 'SPA_general_pages'),
25
+ path.join(cwd, 'SPA_teacher_dashboard'),
26
+ path.join(cwd, 'SPA_student_dashboard'),
27
+ path.join(cwd, 'SPA_student_dashboard', 'pages')
23
28
  ];
24
29
  for (const dir of dirs) {
25
30
  if (!fs.existsSync(dir)) {
@@ -119,6 +124,145 @@ function getLocalPages(cwd = process.cwd()) {
119
124
  return pages;
120
125
  }
121
126
 
127
+ // SPA folder configurations
128
+ const SPA_CONFIGS = {
129
+ general_pages: {
130
+ folder: 'SPA_general_pages',
131
+ files: [
132
+ { name: 'login.js', type: 'vue-component' },
133
+ { name: 'register.js', type: 'vue-component' },
134
+ { name: 'payment.js', type: 'vue-component' }
135
+ ]
136
+ },
137
+ teacher_dashboard: {
138
+ folder: 'SPA_teacher_dashboard',
139
+ files: [
140
+ { name: 'teacher_custom_css.css', type: 'css' }
141
+ ]
142
+ },
143
+ student_dashboard: {
144
+ folder: 'SPA_student_dashboard',
145
+ files: [
146
+ { name: 'student_custom_css.css', type: 'css' },
147
+ { name: 'startup.js', type: 'js' },
148
+ { name: 'pages/my_courses.js', type: 'vue-component' },
149
+ { name: 'pages/course_player.js', type: 'vue-component' },
150
+ { name: 'pages/courses_list.js', type: 'vue-component' },
151
+ { name: 'pages/course_detail.js', type: 'vue-component' },
152
+ { name: 'pages/sessions.js', type: 'vue-component' },
153
+ { name: 'pages/profile.js', type: 'vue-component' }
154
+ ]
155
+ }
156
+ };
157
+
158
+ const EMPTY_FILE_MARKERS = {
159
+ 'vue-component': '/* EMPTY FILE */',
160
+ 'js': '/* EMPTY FILE */',
161
+ 'css': '/* EMPTY FILE */'
162
+ };
163
+
164
+ function getSpaFilePath(spaKey, fileName, cwd = process.cwd()) {
165
+ const config = SPA_CONFIGS[spaKey];
166
+ if (!config) return null;
167
+ return path.join(cwd, config.folder, fileName);
168
+ }
169
+
170
+ function readSpaFile(spaKey, fileName, cwd = process.cwd()) {
171
+ const filePath = getSpaFilePath(spaKey, fileName, cwd);
172
+ if (!filePath || !fs.existsSync(filePath)) {
173
+ return null;
174
+ }
175
+ const content = fs.readFileSync(filePath, 'utf-8');
176
+ if (content === '/* EMPTY FILE */') {
177
+ return '';
178
+ }
179
+ return content;
180
+ }
181
+
182
+ function writeSpaFile(spaKey, fileName, content, cwd = process.cwd()) {
183
+ const filePath = getSpaFilePath(spaKey, fileName, cwd);
184
+ if (!filePath) return;
185
+
186
+ const dir = path.dirname(filePath);
187
+ if (!fs.existsSync(dir)) {
188
+ fs.mkdirSync(dir, { recursive: true });
189
+ }
190
+
191
+ // Determine file type for empty marker
192
+ const ext = path.extname(fileName).slice(1).toLowerCase();
193
+ const isVueComponent = fileName.startsWith('pages/') ||
194
+ (spaKey === 'general_pages' && ext === 'js');
195
+ const fileType = isVueComponent ? 'vue-component' : ext;
196
+
197
+ if (!content || content.trim() === '') {
198
+ fs.writeFileSync(filePath, EMPTY_FILE_MARKERS[fileType] || '/* EMPTY FILE */', 'utf-8');
199
+ } else {
200
+ fs.writeFileSync(filePath, content, 'utf-8');
201
+ }
202
+ }
203
+
204
+ function spaFileExists(spaKey, fileName, cwd = process.cwd()) {
205
+ const filePath = getSpaFilePath(spaKey, fileName, cwd);
206
+ return filePath && fs.existsSync(filePath);
207
+ }
208
+
209
+ function getLocalSpaFiles(spaKey, cwd = process.cwd()) {
210
+ const config = SPA_CONFIGS[spaKey];
211
+ if (!config) return [];
212
+
213
+ const items = [];
214
+ const folderPath = path.join(cwd, config.folder);
215
+
216
+ if (!fs.existsSync(folderPath)) return [];
217
+
218
+ // Read all expected files
219
+ for (const fileConfig of config.files) {
220
+ const filePath = path.join(folderPath, fileConfig.name);
221
+ if (fs.existsSync(filePath)) {
222
+ let content = fs.readFileSync(filePath, 'utf-8');
223
+ if (content === '/* EMPTY FILE */') {
224
+ content = '';
225
+ }
226
+ items.push({
227
+ name: fileConfig.name,
228
+ type: fileConfig.type,
229
+ content
230
+ });
231
+ }
232
+ }
233
+
234
+ return items;
235
+ }
236
+
237
+ async function updateDevDoc() {
238
+ const devDocDir = path.join(process.cwd(), 'DEV_DOC');
239
+ const configFileExists = fs.existsSync(path.join(process.cwd(), 'cyber-elx.jsonc'));
240
+
241
+ if (configFileExists) {
242
+ if (!fs.existsSync(devDocDir)) {
243
+ fs.mkdirSync(devDocDir, { recursive: true });
244
+ }
245
+
246
+ const files = ['ThemeDev.md', 'README.md', 'LoginRegisterPagesDev.md', 'PaymentPageDev.md'];
247
+
248
+ for (const file of files) {
249
+ const sourceContent = fs.readFileSync(path.join(__dirname, '..', 'DEV_DOC', file), 'utf-8');
250
+ const localPath = path.join(devDocDir, file);
251
+ let localContent = '';
252
+
253
+ try {
254
+ localContent = fs.readFileSync(localPath, 'utf-8');
255
+ } catch (err) {
256
+ }
257
+
258
+ if (sourceContent !== localContent) {
259
+ fs.writeFileSync(localPath, sourceContent);
260
+ console.log(chalk.green(`DEV_DOC/${file} was updated`));
261
+ }
262
+ }
263
+ }
264
+ }
265
+
122
266
  module.exports = {
123
267
  DEFAULT_TEMPLATE_KEYS,
124
268
  ensureDirectories,
@@ -127,5 +271,14 @@ module.exports = {
127
271
  readPageFile,
128
272
  writePageFile,
129
273
  fileExists,
130
- getLocalPages
274
+ getLocalPages,
275
+ updateDevDoc,
276
+ // SPA exports
277
+ SPA_CONFIGS,
278
+ EMPTY_FILE_MARKERS,
279
+ getSpaFilePath,
280
+ readSpaFile,
281
+ writeSpaFile,
282
+ spaFileExists,
283
+ getLocalSpaFiles
131
284
  };
package/src/index.js CHANGED
@@ -3,21 +3,21 @@ const chalk = require('chalk');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { readConfig, writeConfig, validateConfig, configExists } = require('./config');
6
- const { readCache, writeCache, getPageTimestamp, setPageTimestamp } = require('./cache');
6
+ const { readCache, writeCache, getPageTimestamp, setPageTimestamp, getSpaTimestamp, setSpaTimestamp } = require('./cache');
7
7
  const { createApiClient } = require('./api');
8
- const { ensureDirectories, writePageFile, getLocalPages, DEFAULT_TEMPLATE_KEYS, fileExists, readPageFile, getFilePath, getFolder } = require('./files');
8
+ const { ensureDirectories, writePageFile, getLocalPages, DEFAULT_TEMPLATE_KEYS, fileExists, readPageFile, getFilePath, getFolder, updateDevDoc, SPA_CONFIGS, writeSpaFile, getLocalSpaFiles } = require('./files');
9
9
  const { promptInitConfig, confirmOverwrite, confirmUpload } = require('./prompts');
10
+ const { compileComponentTemplates, componentObjectToJsCode, parseComponentJsCode } = require('./vue-utils');
10
11
 
11
12
  program
12
13
  .name('cyber-elx')
13
14
  .description('CLI tool to upload/download ELX custom pages')
14
- .version('1.0.7');
15
+ .version('1.1.0');
15
16
 
16
17
  program
17
18
  .command('init')
18
19
  .description('Initialize configuration and download pages')
19
20
  .action(async () => {
20
- await updateDevDoc();
21
21
  try {
22
22
  const cwd = process.cwd();
23
23
 
@@ -33,6 +33,8 @@ program
33
33
  console.log(chalk.blue('Testing connection...'));
34
34
  const api = createApiClient(config);
35
35
 
36
+ await updateDevDoc();
37
+
36
38
  try {
37
39
  await api.getPages();
38
40
  console.log(chalk.green('✓ Connection successful!'));
@@ -229,6 +231,78 @@ async function downloadPages(cwd, config, force = false) {
229
231
  writeCache(cache, cwd);
230
232
 
231
233
  console.log(chalk.blue(`\nDownload complete: ${downloadedCount} downloaded, ${skippedCount} skipped`));
234
+
235
+ // Download SPA folders
236
+ await downloadSpaFolders(cwd, api, cache, force);
237
+
238
+ writeCache(cache, cwd);
239
+ }
240
+
241
+ async function downloadSpaFolders(cwd, api, cache, force = false) {
242
+ console.log(chalk.blue('\n--- SPA Folders ---'));
243
+
244
+ const spaEndpoints = {
245
+ general_pages: { get: () => api.getGeneralPages(), name: 'SPA_general_pages' },
246
+ teacher_dashboard: { get: () => api.getTeacherDashboard(), name: 'SPA_teacher_dashboard' },
247
+ student_dashboard: { get: () => api.getStudentDashboard(), name: 'SPA_student_dashboard' }
248
+ };
249
+
250
+ for (const [spaKey, endpoint] of Object.entries(spaEndpoints)) {
251
+ console.log(chalk.blue(`\nDownloading ${endpoint.name}...`));
252
+
253
+ try {
254
+ const response = await endpoint.get();
255
+ const remoteItems = response.items || [];
256
+ const remoteUpdatedAt = response.updated_at || null;
257
+ const cachedTimestamp = getSpaTimestamp(cache, spaKey);
258
+
259
+ // Check for conflicts
260
+ if (remoteUpdatedAt && cachedTimestamp && remoteUpdatedAt > cachedTimestamp && !force) {
261
+ const shouldOverwrite = await confirmOverwrite(endpoint.name, 'has been modified on server');
262
+ if (!shouldOverwrite) {
263
+ console.log(chalk.yellow(` ⊘ ${endpoint.name} (skipped)`));
264
+ continue;
265
+ }
266
+ }
267
+
268
+ // Get expected files from config
269
+ const config = SPA_CONFIGS[spaKey];
270
+ const expectedFiles = config.files.map(f => f.name);
271
+
272
+ // Create a map of remote items by name
273
+ const remoteItemsMap = new Map();
274
+ for (const item of remoteItems) {
275
+ remoteItemsMap.set(item.name, item);
276
+ }
277
+
278
+ // Download/create each expected file
279
+ for (const fileConfig of config.files) {
280
+ const remoteItem = remoteItemsMap.get(fileConfig.name);
281
+ let content = '';
282
+
283
+ if (remoteItem && remoteItem.data) {
284
+ if (fileConfig.type === 'vue-component') {
285
+ // Convert component object back to JS code
286
+ content = componentObjectToJsCode(remoteItem.data);
287
+ } else {
288
+ // CSS or JS - raw content
289
+ content = remoteItem.data;
290
+ }
291
+ }
292
+
293
+ writeSpaFile(spaKey, fileConfig.name, content, cwd);
294
+ console.log(chalk.green(` ✓ ${endpoint.name}/${fileConfig.name}`));
295
+ }
296
+
297
+ // Update cache timestamp
298
+ if (remoteUpdatedAt) {
299
+ setSpaTimestamp(cache, spaKey, remoteUpdatedAt);
300
+ }
301
+
302
+ } catch (err) {
303
+ console.log(chalk.yellow(` ⚠ Could not download ${endpoint.name}: ${err.message}`));
304
+ }
305
+ }
232
306
  }
233
307
 
234
308
  async function uploadPages(cwd, config, force = false) {
@@ -312,37 +386,112 @@ async function uploadPages(cwd, config, force = false) {
312
386
  if(response.debug) {
313
387
  console.log(chalk.gray('Debug info:'), response.debug);
314
388
  }
389
+
390
+ // Upload SPA folders
391
+ await uploadSpaFolders(cwd, api, cache, force);
392
+
393
+ writeCache(cache, cwd);
315
394
  }
316
395
 
317
- async function updateDevDoc() {
318
- const devDocDir = path.join(process.cwd(), 'DEV_DOC');
319
- const configFileExists = fs.existsSync(path.join(process.cwd(), 'cyber-elx.jsonc'));
320
-
321
- if (configFileExists) {
322
- if (!fs.existsSync(devDocDir)) {
323
- fs.mkdirSync(devDocDir, { recursive: true });
396
+ async function uploadSpaFolders(cwd, api, cache, force = false) {
397
+ console.log(chalk.blue('\n--- SPA Folders ---'));
398
+
399
+ const spaEndpoints = {
400
+ general_pages: {
401
+ get: () => api.getGeneralPages(),
402
+ set: (items) => api.setGeneralPages(items),
403
+ name: 'SPA_general_pages'
404
+ },
405
+ teacher_dashboard: {
406
+ get: () => api.getTeacherDashboard(),
407
+ set: (items) => api.setTeacherDashboard(items),
408
+ name: 'SPA_teacher_dashboard'
409
+ },
410
+ student_dashboard: {
411
+ get: () => api.getStudentDashboard(),
412
+ set: (items) => api.setStudentDashboard(items),
413
+ name: 'SPA_student_dashboard'
324
414
  }
325
-
326
- const files = ['ThemeDev.md', 'README.md'];
415
+ };
416
+
417
+ for (const [spaKey, endpoint] of Object.entries(spaEndpoints)) {
418
+ console.log(chalk.blue(`\nUploading ${endpoint.name}...`));
327
419
 
328
- for (const file of files) {
329
- const sourceContent = fs.readFileSync(path.join(__dirname, '..', 'DEV_DOC', file), 'utf-8');
330
- const localPath = path.join(devDocDir, file);
331
- let localContent = '';
420
+ try {
421
+ // Get local files
422
+ const localFiles = getLocalSpaFiles(spaKey, cwd);
332
423
 
333
- try {
334
- localContent = fs.readFileSync(localPath, 'utf-8');
335
- } catch (err) {
424
+ if (localFiles.length === 0) {
425
+ console.log(chalk.yellow(` No files found in ${endpoint.name}`));
426
+ continue;
427
+ }
428
+
429
+ // Check for server conflicts
430
+ const remoteResponse = await endpoint.get();
431
+ const remoteUpdatedAt = remoteResponse.updated_at || null;
432
+ const cachedTimestamp = getSpaTimestamp(cache, spaKey);
433
+
434
+ if (remoteUpdatedAt && cachedTimestamp && remoteUpdatedAt > cachedTimestamp && !force) {
435
+ const shouldUpload = await confirmUpload(endpoint.name, 'has been modified on server since last download');
436
+ if (!shouldUpload) {
437
+ console.log(chalk.yellow(` ⊘ ${endpoint.name} (skipped)`));
438
+ continue;
439
+ }
440
+ }
441
+
442
+ // Prepare items for upload
443
+ const itemsToUpload = [];
444
+ let hasError = false;
445
+
446
+ for (const file of localFiles) {
447
+ const item = {
448
+ name: file.name,
449
+ type: file.type,
450
+ data: null
451
+ };
452
+
453
+ if (file.type === 'vue-component') {
454
+ // Parse and compile Vue component
455
+ if (!file.content || file.content.trim() === '') {
456
+ item.data = null;
457
+ } else {
458
+ try {
459
+ const componentObj = parseComponentJsCode(file.content);
460
+ const compiledComponent = compileComponentTemplates(componentObj);
461
+ item.data = compiledComponent;
462
+ } catch (err) {
463
+ console.log(chalk.red(` ✗ ${endpoint.name}/${file.name}: Vue compilation failed - ${err.message}`));
464
+ hasError = true;
465
+ break;
466
+ }
467
+ }
468
+ } else {
469
+ // CSS or JS - raw content
470
+ item.data = file.content || '';
471
+ }
472
+
473
+ itemsToUpload.push(item);
474
+ console.log(chalk.cyan(` → ${endpoint.name}/${file.name}`));
475
+ }
476
+
477
+ if (hasError) {
478
+ console.log(chalk.red(` Upload cancelled for ${endpoint.name} due to compilation errors`));
479
+ continue;
336
480
  }
337
481
 
338
- if (sourceContent !== localContent) {
339
- fs.writeFileSync(localPath, sourceContent);
340
- console.log(chalk.green(`DEV_DOC/${file} was updated`));
482
+ // Upload to server
483
+ const response = await endpoint.set(itemsToUpload);
484
+
485
+ if (response.updated_at) {
486
+ setSpaTimestamp(cache, spaKey, response.updated_at);
341
487
  }
488
+
489
+ console.log(chalk.green(` ✓ ${endpoint.name} uploaded successfully`));
490
+
491
+ } catch (err) {
492
+ console.log(chalk.red(` ✗ Could not upload ${endpoint.name}: ${err.message}`));
342
493
  }
343
494
  }
344
495
  }
345
496
 
346
-
347
-
348
497
  program.parse();
@@ -0,0 +1,149 @@
1
+ const { compile } = require('vue-template-compiler');
2
+
3
+ /**
4
+ * Parses a component JavaScript code string into an object
5
+ * @param {string} componentText - JavaScript code representing the component
6
+ * @returns {Object} - Parsed component object
7
+ */
8
+ function parseComponentJsCode(componentText) {
9
+ var codeToEval = componentText.replace("module.exports =", "");
10
+ if(codeToEval.endsWith(";")) {
11
+ codeToEval = codeToEval.slice(0, -1);
12
+ }
13
+ var parsedComponent = eval(`(${codeToEval})`);
14
+ for(var key of Object.keys(parsedComponent)) {
15
+ if(typeof parsedComponent[key] === "string") {
16
+ parsedComponent[key] = parsedComponent[key].trim();
17
+ }
18
+ }
19
+ return parsedComponent;
20
+ }
21
+
22
+ /**
23
+ * Converts a component object to JavaScript code (For file saving)
24
+ * @param {Object} component - Component definition with code.template
25
+ * @returns {string} - JavaScript code representing the component
26
+ */
27
+ function componentObjectToJsCode(component) {
28
+ var newComponent = Object.keys(component).reduce((final, key) => {
29
+ if(key == "compiledTemplate" || !component[key]) return final;
30
+ final[key] = "#" + key.toUpperCase();
31
+ return final;
32
+ }, {});
33
+ var jsCode = JSON.stringify(newComponent, null, 2);
34
+ jsCode = jsCode.replace(/"/g, "");
35
+ Object.keys(component).forEach(key => {
36
+ if(key == "compiledTemplate" || !component[key]) return;
37
+ var newContent = (typeof component[key] === "string") ? component[key] : JSON.stringify(component[key]);
38
+ newContent = "\n " + newContent.trim();
39
+ // // Add Tabs
40
+ // newContent = " " + newContent.replace(/\n/g, "\n ");
41
+ // Add comments
42
+ if(key == "template") {
43
+ newContent = "\/* html *\/`" + newContent + "\n `";
44
+ } else if(key == "style") {
45
+ newContent = "\/* css *\/`" + newContent + "\n `";
46
+ } else if(key == "name") {
47
+ newContent = "\"" + newContent.trim() + "\"";
48
+ } else if(key == "props" && typeof component[key] !== "string") {
49
+ // Add nothing
50
+ } else {
51
+ newContent = "\/* js *\/`" + newContent + "\n `";
52
+ }
53
+ // Replace
54
+ jsCode = jsCode.replace("#" + key.toUpperCase(), newContent);
55
+ });
56
+ return "module.exports = " + jsCode;
57
+ }
58
+
59
+ /**
60
+ * Compiles Vue template for a single component
61
+ * @param {Object} component - Component definition with code.template
62
+ * @returns {Object} - Same component with `compiledTemplate` added to code
63
+ */
64
+ function compileComponentTemplates(component) {
65
+ const compiled = compile(component.template);
66
+
67
+ if (compiled.errors.length > 0) {
68
+ console.warn(`Template compilation errors:`, compiled.errors);
69
+ }
70
+
71
+ return {
72
+ ...component,
73
+ compiledTemplate: {
74
+ render: compiled.render,
75
+ staticRenderFns: compiled.staticRenderFns,
76
+ errors: compiled.errors,
77
+ tips: compiled.tips
78
+ }
79
+ };
80
+ }
81
+
82
+ // function test() {
83
+ // console.log('test +++++++');
84
+ // var components = [
85
+ // {
86
+ // "name": "CardComponent",
87
+ // "template": "\n <div class=\"card-component\">\n <h2>🎴 {{ title }}</h2>\n <p>{{ description }}</p>\n \n <div style=\"margin: 20px 0;\">\n <v-text-field \n v-model=\"userInput\" \n label=\"Type something...\"\n variant=\"outlined\"\n density=\"compact\"\n style=\"max-width: 300px; display: inline-block;\"\n />\n <v-btn color=\"primary\" @click=\"incrementCounter\" class=\"ml-2\">\n Clicked {{ counter }} times\n </v-btn>\n </div>\n \n <p v-if=\"userInput\">You typed: <strong>{{ userInput }}</strong></p>\n \n <div style=\"margin: 20px 0;\">\n <h3>Default Slot:</h3>\n <slot>\n <p style=\"color: #999;\">No slot content provided</p>\n </slot>\n </div>\n \n <slot name=\"footer\"></slot>\n </div>\n ",
88
+ // "data": "function() {\n return {\n title: 'Card Component',\n description: 'This is a dynamically loaded CARD component with slots!',\n counter: 0,\n userInput: ''\n };\n }",
89
+ // "computed": "{\n doubleCounter() {\n return this.counter * 2;\n }\n }",
90
+ // "watch": "{\n counter(newVal) {\n console.log('Counter changed to:', newVal);\n }\n }",
91
+ // "methods": "{\n incrementCounter() {\n this.counter++;\n }\n }",
92
+ // "beforeCreate": null,
93
+ // "created": null,
94
+ // "beforeMount": null,
95
+ // "mounted": "function() {\n console.log('Card component mounted!');\n }",
96
+ // "beforeUpdate": null,
97
+ // "updated": null,
98
+ // "beforeDestroy": null,
99
+ // "destroyed": null,
100
+ // "compiledTemplate": {
101
+ // "render": "with(this){return _c('div',{staticClass:\"card-component\"},[_c('h2',[_v(\"🎴 \"+_s(title))]),_v(\" \"),_c('p',[_v(_s(description))]),_v(\" \"),_c('div',{staticStyle:{\"margin\":\"20px 0\"}},[_c('v-text-field',{staticStyle:{\"max-width\":\"300px\",\"display\":\"inline-block\"},attrs:{\"label\":\"Type something...\",\"variant\":\"outlined\",\"density\":\"compact\"},model:{value:(userInput),callback:function ($$v) {userInput=$$v},expression:\"userInput\"}}),_v(\" \"),_c('v-btn',{staticClass:\"ml-2\",attrs:{\"color\":\"primary\"},on:{\"click\":incrementCounter}},[_v(\"\\n Clicked \"+_s(counter)+\" times\\n \")])],1),_v(\" \"),(userInput)?_c('p',[_v(\"You typed: \"),_c('strong',[_v(_s(userInput))])]):_e(),_v(\" \"),_c('div',{staticStyle:{\"margin\":\"20px 0\"}},[_c('h3',[_v(\"Default Slot:\")]),_v(\" \"),_t(\"default\",function(){return [_c('p',{staticStyle:{\"color\":\"#999\"}},[_v(\"No slot content provided\")])]})],2),_v(\" \"),_t(\"footer\")],2)}",
102
+ // "staticRenderFns": [],
103
+ // "errors": [],
104
+ // "tips": []
105
+ // }
106
+ // },
107
+ // {
108
+ // "name": "ListComponent",
109
+ // "template": "<div class=\"list-component\">\n <h2>📋 {{ title }}</h2>\n <p>{{ description }}</p>\n \n <div style=\"margin: 20px 0;\">\n <v-text-field \n v-model=\"newItem\" \n @keyup.enter=\"addItem\"\n label=\"Add new item...\"\n variant=\"outlined\"\n density=\"compact\"\n style=\"max-width: 300px; display: inline-block;\"\n />\n <v-btn color=\"primary\" @click=\"addItem\" class=\"ml-2\">Add Item</v-btn>\n </div>\n \n <v-list>\n <v-list-item v-for=\"item in items\" :key=\"item.id\">\n <v-list-item-title>{{ item.name }}</v-list-item-title>\n <template v-slot:append>\n<v-btn color=\"error\" size=\"small\" @click=\"removeItem(item.id)\">Remove</v-btn>\n </template>\n </v-list-item>\n </v-list>\n \n <div style=\"margin: 20px 0;\">\n <h3>Default Slot:</h3>\n <slot>\n <p style=\"color: #999;\">No slot content provided</p>\n </slot>\n </div>\n \n <slot name=\"footer\"></slot>\n</div>\n ",
110
+ // "data": "function() {\nreturn {\n title: 'List Component',\n description: 'This is a dynamically loaded LIST component with slots!',\n newItem: '',\n items: [\n { id: 1, name: 'Learn Vue.js' },\n { id: 2, name: 'Build dynamic components' },\n { id: 3, name: 'Master slots' }\n ],\n nextId: 4\n};\n }",
111
+ // "computed": "{\nitemCount() {\n return this.items.length;\n}\n }",
112
+ // "watch": null,
113
+ // "methods": "{\naddItem() {\n if (this.newItem.trim()) {\n this.items.push({\n id: this.nextId++,\n name: this.newItem\n });\n this.newItem = '';\n }\n},\nremoveItem(id) {\n this.items = this.items.filter(item => item.id !== id);\n}\n }",
114
+ // "beforeCreate": null,
115
+ // "created": null,
116
+ // "beforeMount": null,
117
+ // "mounted": "function() {\nconsole.log('List component mounted!');\n }",
118
+ // "beforeUpdate": null,
119
+ // "updated": null,
120
+ // "beforeDestroy": null,
121
+ // "destroyed": null,
122
+ // "compiledTemplate": {
123
+ // "render": "with(this){return _c('div',{staticClass:\"list-component\"},[_c('h2',[_v(\"📋 \"+_s(title))]),_v(\" \"),_c('p',[_v(_s(description))]),_v(\" \"),_c('div',{staticStyle:{\"margin\":\"20px 0\"}},[_c('v-text-field',{staticStyle:{\"max-width\":\"300px\",\"display\":\"inline-block\"},attrs:{\"label\":\"Add new item...\",\"variant\":\"outlined\",\"density\":\"compact\"},on:{\"keyup\":function($event){if(!$event.type.indexOf('key')&&_k($event.keyCode,\"enter\",13,$event.key,\"Enter\"))return null;return addItem.apply(null, arguments)}},model:{value:(newItem),callback:function ($$v) {newItem=$$v},expression:\"newItem\"}}),_v(\" \"),_c('v-btn',{staticClass:\"ml-2\",attrs:{\"color\":\"primary\"},on:{\"click\":addItem}},[_v(\"Add Item\")])],1),_v(\" \"),_c('v-list',_l((items),function(item){return _c('v-list-item',{key:item.id,scopedSlots:_u([{key:\"append\",fn:function(){return [_c('v-btn',{attrs:{\"color\":\"error\",\"size\":\"small\"},on:{\"click\":function($event){return removeItem(item.id)}}},[_v(\"Remove\")])]},proxy:true}],null,true)},[_c('v-list-item-title',[_v(_s(item.name))])],1)}),1),_v(\" \"),_c('div',{staticStyle:{\"margin\":\"20px 0\"}},[_c('h3',[_v(\"Default Slot:\")]),_v(\" \"),_t(\"default\",function(){return [_c('p',{staticStyle:{\"color\":\"#999\"}},[_v(\"No slot content provided\")])]})],2),_v(\" \"),_t(\"footer\")],2)}",
124
+ // "staticRenderFns": [],
125
+ // "errors": [],
126
+ // "tips": []
127
+ // }
128
+ // }
129
+ // ];
130
+
131
+ // const fs = require('fs');
132
+
133
+ // // Create components directory if it doesn't exist
134
+ // if (!fs.existsSync('./components')) {
135
+ // fs.mkdirSync('./components');
136
+ // }
137
+
138
+ // // For each component
139
+ // for (const component of components) {
140
+ // fs.writeFileSync(`./components/${component.name}.js`, componentObjectToJsCode(component));
141
+ // fs.writeFileSync(`./components/${component.name}_clean.json`, JSON.stringify(parseComponentJsCode(componentObjectToJsCode(component)), null, 2));
142
+ // }
143
+
144
+ // }
145
+
146
+ // test();
147
+
148
+
149
+ module.exports = { compileComponentTemplates, componentObjectToJsCode, parseComponentJsCode };