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/DEV_DOC/LoginRegisterPagesDev.md +658 -0
- package/DEV_DOC/PaymentPageDev.md +576 -0
- package/package.json +3 -2
- package/src/api.js +52 -0
- package/src/cache.js +20 -4
- package/src/files.js +155 -2
- package/src/index.js +174 -25
- package/src/vue-utils.js +149 -0
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
|
|
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
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
for (const [spaKey, endpoint] of Object.entries(spaEndpoints)) {
|
|
418
|
+
console.log(chalk.blue(`\nUploading ${endpoint.name}...`));
|
|
327
419
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
let localContent = '';
|
|
420
|
+
try {
|
|
421
|
+
// Get local files
|
|
422
|
+
const localFiles = getLocalSpaFiles(spaKey, cwd);
|
|
332
423
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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();
|
package/src/vue-utils.js
ADDED
|
@@ -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 };
|