aztomiq 1.0.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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/aztomiq.js +336 -0
  4. package/bin/create-aztomiq.js +77 -0
  5. package/package.json +58 -0
  6. package/scripts/analyze-screenshots.js +217 -0
  7. package/scripts/build.js +39 -0
  8. package/scripts/builds/admin.js +17 -0
  9. package/scripts/builds/assets.js +167 -0
  10. package/scripts/builds/cache.js +48 -0
  11. package/scripts/builds/config.js +31 -0
  12. package/scripts/builds/data.js +210 -0
  13. package/scripts/builds/pages.js +288 -0
  14. package/scripts/builds/playground-examples.js +50 -0
  15. package/scripts/builds/templates.js +118 -0
  16. package/scripts/builds/utils.js +37 -0
  17. package/scripts/create-bug-tracker.js +277 -0
  18. package/scripts/deploy.js +135 -0
  19. package/scripts/feedback-generator.js +102 -0
  20. package/scripts/ui-test.js +624 -0
  21. package/scripts/utils/extract-examples.js +44 -0
  22. package/scripts/utils/migrate-icons.js +67 -0
  23. package/src/includes/breadcrumbs.ejs +100 -0
  24. package/src/includes/cloud-tags.ejs +120 -0
  25. package/src/includes/footer.ejs +37 -0
  26. package/src/includes/generator.ejs +226 -0
  27. package/src/includes/head.ejs +73 -0
  28. package/src/includes/header-data-only.ejs +43 -0
  29. package/src/includes/header.ejs +71 -0
  30. package/src/includes/layout.ejs +68 -0
  31. package/src/includes/legacy-banner.ejs +19 -0
  32. package/src/includes/mega-menu.ejs +80 -0
  33. package/src/includes/schema.ejs +20 -0
  34. package/src/templates/manifest.json.ejs +30 -0
  35. package/src/templates/readme-dist.md.ejs +58 -0
  36. package/src/templates/robots.txt.ejs +4 -0
  37. package/src/templates/sitemap.xml.ejs +69 -0
  38. package/src/templates/sw.js.ejs +78 -0
@@ -0,0 +1,624 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * UI Testing Tool - Automated Visual & i18n Testing
5
+ *
6
+ * Features:
7
+ * - Crawls all pages from sitemap
8
+ * - Takes screenshots for visual review
9
+ * - Detects missing translations (i18n keys)
10
+ * - Checks for CSS layout issues
11
+ * - Generates HTML report
12
+ */
13
+
14
+ const puppeteer = require('puppeteer');
15
+ const fs = require('fs').promises;
16
+ const path = require('path');
17
+ const { parseStringPromise } = require('xml2js');
18
+
19
+ // Configuration
20
+ const CONFIG = {
21
+ baseUrl: process.env.BASE_URL || 'http://localhost:3000',
22
+ sitemapPath: './dist-dev/sitemap.xml',
23
+ outputDir: './ui-test-results',
24
+ screenshotDir: './ui-test-results/screenshots',
25
+ viewport: {
26
+ width: 1400,
27
+ height: 900
28
+ },
29
+ mobileViewport: {
30
+ width: 375,
31
+ height: 667
32
+ },
33
+ // Language filter: 'vi', 'en', or null for all
34
+ langFilter: process.argv.includes('--lang=vi') ? 'vi' :
35
+ process.argv.includes('--lang=en') ? 'en' : null,
36
+ // Theme filter: 'light' or 'dark'
37
+ theme: process.argv.includes('--theme=dark') ? 'dark' : 'light'
38
+ };
39
+
40
+ // Results storage
41
+ const results = {
42
+ timestamp: new Date().toISOString(),
43
+ summary: {
44
+ total: 0,
45
+ passed: 0,
46
+ failed: 0,
47
+ warnings: 0
48
+ },
49
+ pages: []
50
+ };
51
+
52
+ /**
53
+ * Read and parse sitemap
54
+ */
55
+ async function readSitemap() {
56
+ console.log('šŸ“„ Reading sitemap...');
57
+ const sitemapContent = await fs.readFile(CONFIG.sitemapPath, 'utf-8');
58
+ const sitemap = await parseStringPromise(sitemapContent);
59
+
60
+ // Map production URLs to localhost
61
+ let urls = sitemap.urlset.url
62
+ .map(entry => entry.loc[0].trim())
63
+ .map(url => url.replace('https://aztomiq.site', CONFIG.baseUrl));
64
+
65
+ // Filter by language if specified
66
+ if (CONFIG.langFilter) {
67
+ urls = urls.filter(url => url.includes(`/${CONFIG.langFilter}/`));
68
+ console.log(`šŸ” Filtering for language: ${CONFIG.langFilter}`);
69
+ }
70
+
71
+ console.log(`āœ… Found ${urls.length} URLs in sitemap`);
72
+ console.log(`šŸ“ Testing against: ${CONFIG.baseUrl}`);
73
+
74
+ return urls;
75
+ }
76
+
77
+ /**
78
+ * Check for i18n issues (missing translation keys)
79
+ */
80
+ async function checkI18nIssues(page, url) {
81
+ const issues = await page.evaluate(() => {
82
+ const found = [];
83
+
84
+ // Whitelist of known non-translation patterns
85
+ const whitelist = [
86
+ 'AZtomiq.site',
87
+ 'aztomiq.site',
88
+ 'localhost.3000',
89
+ 'github.com',
90
+ 'npmjs.com'
91
+ ];
92
+
93
+ const bodyText = document.body.innerText;
94
+
95
+ // Look for untranslated keys (camelCase.camelCase pattern)
96
+ const possibleKeys = bodyText.match(/[a-z][a-zA-Z]+\.[a-z][a-zA-Z]+/g) || [];
97
+
98
+ possibleKeys.forEach(key => {
99
+ // Skip if in whitelist
100
+ if (whitelist.some(w => key.toLowerCase().includes(w.toLowerCase()))) {
101
+ return;
102
+ }
103
+
104
+ // Skip if it looks like a URL or domain
105
+ if (key.includes('http') || key.includes('www') || key.includes('.com') || key.includes('.site')) {
106
+ return;
107
+ }
108
+
109
+ // Skip version numbers (e.g., v1.2.0)
110
+ if (/v?\d+\.\d+/.test(key)) {
111
+ return;
112
+ }
113
+
114
+ // Find visible elements with this exact text
115
+ const elements = Array.from(document.querySelectorAll('*')).filter(el =>
116
+ el.textContent.trim() === key &&
117
+ el.offsetParent !== null && // visible
118
+ el.children.length === 0 // leaf node (not parent container)
119
+ );
120
+
121
+ if (elements.length > 0) {
122
+ found.push({
123
+ key: key,
124
+ count: elements.length,
125
+ selector: elements[0].tagName.toLowerCase()
126
+ });
127
+ }
128
+ });
129
+
130
+ return found;
131
+ });
132
+
133
+ return issues;
134
+ }
135
+
136
+ /**
137
+ * Check for CSS layout issues
138
+ */
139
+ async function checkCSSIssues(page) {
140
+ const issues = await page.evaluate(() => {
141
+ const problems = [];
142
+
143
+ // Check for horizontal overflow
144
+ if (document.body.scrollWidth > window.innerWidth) {
145
+ problems.push({
146
+ type: 'horizontal-overflow',
147
+ message: `Page width (${document.body.scrollWidth}px) exceeds viewport (${window.innerWidth}px)`,
148
+ severity: 'warning'
149
+ });
150
+ }
151
+
152
+ // Check for elements outside viewport
153
+ const allElements = document.querySelectorAll('*');
154
+ let elementsOutside = 0;
155
+
156
+ allElements.forEach(el => {
157
+ const rect = el.getBoundingClientRect();
158
+ if (rect.right > window.innerWidth + 50) { // 50px tolerance
159
+ elementsOutside++;
160
+ }
161
+ });
162
+
163
+ if (elementsOutside > 0) {
164
+ problems.push({
165
+ type: 'elements-overflow',
166
+ message: `${elementsOutside} elements extend beyond viewport`,
167
+ severity: 'warning'
168
+ });
169
+ }
170
+
171
+ // Check for invisible text (color too similar to background)
172
+ const textElements = Array.from(document.querySelectorAll('p, span, a, button, h1, h2, h3, h4, h5, h6, label'));
173
+ let lowContrastCount = 0;
174
+
175
+ textElements.forEach(el => {
176
+ const style = window.getComputedStyle(el);
177
+ const color = style.color;
178
+ const bgColor = style.backgroundColor;
179
+
180
+ // Simple check: if both are very similar (basic heuristic)
181
+ if (color && bgColor && color === bgColor) {
182
+ lowContrastCount++;
183
+ }
184
+ });
185
+
186
+ if (lowContrastCount > 0) {
187
+ problems.push({
188
+ type: 'low-contrast',
189
+ message: `${lowContrastCount} elements may have low contrast`,
190
+ severity: 'info'
191
+ });
192
+ }
193
+
194
+ return problems;
195
+ });
196
+
197
+ return issues;
198
+ }
199
+
200
+ /**
201
+ * Check for console errors
202
+ */
203
+ async function captureConsoleErrors(page) {
204
+ const errors = [];
205
+
206
+ page.on('console', msg => {
207
+ if (msg.type() === 'error') {
208
+ errors.push(msg.text());
209
+ }
210
+ });
211
+
212
+ page.on('pageerror', error => {
213
+ errors.push(error.message);
214
+ });
215
+
216
+ return errors;
217
+ }
218
+
219
+ /**
220
+ * Test a single page
221
+ */
222
+ async function testPage(browser, url, index, total) {
223
+ const pageName = url.replace(CONFIG.baseUrl, '').replace(/\//g, '_') || 'home';
224
+ console.log(`\n[${index + 1}/${total}] Testing: ${url}`);
225
+
226
+ const page = await browser.newPage();
227
+ const errors = [];
228
+
229
+ // Capture console errors
230
+ page.on('console', msg => {
231
+ if (msg.type() === 'error') {
232
+ errors.push(msg.text());
233
+ }
234
+ });
235
+
236
+ page.on('pageerror', error => {
237
+ errors.push(error.message);
238
+ });
239
+
240
+ const pageResult = {
241
+ url,
242
+ pageName,
243
+ status: 'passed',
244
+ issues: [],
245
+ screenshots: {}
246
+ };
247
+
248
+ try {
249
+ // Desktop test
250
+ await page.setViewport(CONFIG.viewport);
251
+
252
+ // Force theme
253
+ await page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: CONFIG.theme }]);
254
+
255
+ await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
256
+
257
+ // Ensure theme is set in localStorage to avoid flashes or overrides
258
+ await page.evaluate((theme) => {
259
+ localStorage.setItem('theme', theme);
260
+ document.documentElement.setAttribute('data-theme', theme);
261
+ }, CONFIG.theme);
262
+
263
+ // Wait a bit for any animations
264
+ await new Promise(resolve => setTimeout(resolve, 1000));
265
+
266
+ // Take desktop screenshot
267
+ const desktopScreenshot = path.join(CONFIG.screenshotDir, `${pageName}_desktop.png`);
268
+ await page.screenshot({
269
+ path: desktopScreenshot,
270
+ fullPage: true
271
+ });
272
+ pageResult.screenshots.desktop = desktopScreenshot;
273
+ console.log(` šŸ“ø Desktop screenshot saved`);
274
+
275
+ // Check i18n issues
276
+ const i18nIssues = await checkI18nIssues(page, url);
277
+ if (i18nIssues.length > 0) {
278
+ pageResult.issues.push({
279
+ type: 'i18n',
280
+ severity: 'error',
281
+ count: i18nIssues.length,
282
+ details: i18nIssues
283
+ });
284
+ console.log(` āš ļø Found ${i18nIssues.length} i18n issues`);
285
+ }
286
+
287
+ // Check CSS issues
288
+ const cssIssues = await checkCSSIssues(page);
289
+ if (cssIssues.length > 0) {
290
+ pageResult.issues.push({
291
+ type: 'css',
292
+ severity: 'warning',
293
+ count: cssIssues.length,
294
+ details: cssIssues
295
+ });
296
+ console.log(` āš ļø Found ${cssIssues.length} CSS issues`);
297
+ }
298
+
299
+ // Check console errors
300
+ if (errors.length > 0) {
301
+ pageResult.issues.push({
302
+ type: 'console-errors',
303
+ severity: 'error',
304
+ count: errors.length,
305
+ details: errors
306
+ });
307
+ console.log(` āŒ Found ${errors.length} console errors`);
308
+ }
309
+
310
+ // Mobile test
311
+ await page.setViewport(CONFIG.mobileViewport);
312
+ await page.reload({ waitUntil: 'networkidle0' });
313
+ await new Promise(resolve => setTimeout(resolve, 1000));
314
+
315
+ const mobileScreenshot = path.join(CONFIG.screenshotDir, `${pageName}_mobile.png`);
316
+ await page.screenshot({
317
+ path: mobileScreenshot,
318
+ fullPage: true
319
+ });
320
+ pageResult.screenshots.mobile = mobileScreenshot;
321
+ console.log(` šŸ“± Mobile screenshot saved`);
322
+
323
+ // Determine status
324
+ const hasErrors = pageResult.issues.some(i => i.severity === 'error');
325
+ const hasWarnings = pageResult.issues.some(i => i.severity === 'warning');
326
+
327
+ if (hasErrors) {
328
+ pageResult.status = 'failed';
329
+ results.summary.failed++;
330
+ } else if (hasWarnings) {
331
+ pageResult.status = 'warning';
332
+ results.summary.warnings++;
333
+ } else {
334
+ pageResult.status = 'passed';
335
+ results.summary.passed++;
336
+ }
337
+
338
+ console.log(` āœ… Status: ${pageResult.status.toUpperCase()}`);
339
+
340
+ } catch (error) {
341
+ pageResult.status = 'error';
342
+ pageResult.error = error.message;
343
+ results.summary.failed++;
344
+ console.log(` āŒ Error: ${error.message}`);
345
+ } finally {
346
+ await page.close();
347
+ }
348
+
349
+ return pageResult;
350
+ }
351
+
352
+ /**
353
+ * Generate HTML report
354
+ */
355
+ async function generateReport() {
356
+ console.log('\nšŸ“Š Generating HTML report...');
357
+
358
+ const html = `
359
+ <!DOCTYPE html>
360
+ <html lang="en">
361
+ <head>
362
+ <meta charset="UTF-8">
363
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
364
+ <title>UI Test Report - ${new Date(results.timestamp).toLocaleString()}</title>
365
+ <style>
366
+ * { box-sizing: border-box; margin: 0; padding: 0; }
367
+ body {
368
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
369
+ background: #f5f5f5;
370
+ padding: 2rem;
371
+ line-height: 1.6;
372
+ }
373
+ .container { max-width: 1400px; margin: 0 auto; }
374
+ .header {
375
+ background: white;
376
+ padding: 2rem;
377
+ border-radius: 12px;
378
+ margin-bottom: 2rem;
379
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
380
+ }
381
+ h1 { color: #333; margin-bottom: 0.5rem; }
382
+ .timestamp { color: #666; font-size: 0.9rem; }
383
+ .summary {
384
+ display: grid;
385
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
386
+ gap: 1rem;
387
+ margin-bottom: 2rem;
388
+ }
389
+ .summary-card {
390
+ background: white;
391
+ padding: 1.5rem;
392
+ border-radius: 12px;
393
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
394
+ }
395
+ .summary-card h3 { font-size: 2rem; margin-bottom: 0.5rem; }
396
+ .summary-card p { color: #666; font-size: 0.9rem; }
397
+ .passed { color: #16a34a; }
398
+ .failed { color: #dc2626; }
399
+ .warning { color: #ea580c; }
400
+ .page-card {
401
+ background: white;
402
+ padding: 1.5rem;
403
+ border-radius: 12px;
404
+ margin-bottom: 1rem;
405
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
406
+ }
407
+ .page-header {
408
+ display: flex;
409
+ justify-content: space-between;
410
+ align-items: center;
411
+ margin-bottom: 1rem;
412
+ padding-bottom: 1rem;
413
+ border-bottom: 1px solid #e5e5e5;
414
+ }
415
+ .page-url { font-weight: 600; color: #333; }
416
+ .status-badge {
417
+ padding: 0.25rem 0.75rem;
418
+ border-radius: 6px;
419
+ font-size: 0.85rem;
420
+ font-weight: 600;
421
+ text-transform: uppercase;
422
+ }
423
+ .status-passed { background: #dcfce7; color: #16a34a; }
424
+ .status-failed { background: #fee2e2; color: #dc2626; }
425
+ .status-warning { background: #ffedd5; color: #ea580c; }
426
+ .screenshots {
427
+ display: grid;
428
+ grid-template-columns: 1fr 1fr;
429
+ gap: 1rem;
430
+ margin-top: 1rem;
431
+ }
432
+ .screenshot-box {
433
+ border: 1px solid #e5e5e5;
434
+ border-radius: 8px;
435
+ overflow: hidden;
436
+ }
437
+ .screenshot-box img {
438
+ width: 100%;
439
+ display: block;
440
+ }
441
+ .screenshot-label {
442
+ padding: 0.5rem;
443
+ background: #f5f5f5;
444
+ font-size: 0.85rem;
445
+ font-weight: 600;
446
+ text-align: center;
447
+ }
448
+ .issues {
449
+ margin-top: 1rem;
450
+ }
451
+ .issue {
452
+ background: #fef3c7;
453
+ border-left: 4px solid #f59e0b;
454
+ padding: 1rem;
455
+ margin-bottom: 0.5rem;
456
+ border-radius: 4px;
457
+ }
458
+ .issue.error {
459
+ background: #fee2e2;
460
+ border-left-color: #dc2626;
461
+ }
462
+ .issue-title {
463
+ font-weight: 600;
464
+ margin-bottom: 0.5rem;
465
+ }
466
+ .issue-details {
467
+ font-size: 0.9rem;
468
+ color: #666;
469
+ font-family: monospace;
470
+ background: white;
471
+ padding: 0.5rem;
472
+ border-radius: 4px;
473
+ margin-top: 0.5rem;
474
+ overflow-x: auto;
475
+ }
476
+ </style>
477
+ </head>
478
+ <body>
479
+ <div class="container">
480
+ <div class="header">
481
+ <h1>🧪 UI Test Report</h1>
482
+ <p class="timestamp">Generated: ${new Date(results.timestamp).toLocaleString()}</p>
483
+ </div>
484
+
485
+ <div class="summary">
486
+ <div class="summary-card">
487
+ <h3>${results.summary.total}</h3>
488
+ <p>Total Pages</p>
489
+ </div>
490
+ <div class="summary-card">
491
+ <h3 class="passed">${results.summary.passed}</h3>
492
+ <p>Passed</p>
493
+ </div>
494
+ <div class="summary-card">
495
+ <h3 class="warning">${results.summary.warnings}</h3>
496
+ <p>Warnings</p>
497
+ </div>
498
+ <div class="summary-card">
499
+ <h3 class="failed">${results.summary.failed}</h3>
500
+ <p>Failed</p>
501
+ </div>
502
+ </div>
503
+
504
+ ${results.pages.map(page => `
505
+ <div class="page-card">
506
+ <div class="page-header">
507
+ <div class="page-url">${page.url}</div>
508
+ <span class="status-badge status-${page.status}">${page.status}</span>
509
+ </div>
510
+
511
+ ${page.issues.length > 0 ? `
512
+ <div class="issues">
513
+ <strong>Issues Found:</strong>
514
+ ${page.issues.map(issue => `
515
+ <div class="issue ${issue.severity === 'error' ? 'error' : ''}">
516
+ <div class="issue-title">
517
+ ${issue.type.toUpperCase()} (${issue.count} ${issue.count === 1 ? 'issue' : 'issues'})
518
+ </div>
519
+ <div class="issue-details">
520
+ ${JSON.stringify(issue.details, null, 2)}
521
+ </div>
522
+ </div>
523
+ `).join('')}
524
+ </div>
525
+ ` : '<p style="color: #16a34a;">āœ… No issues found</p>'}
526
+
527
+ <div class="screenshots">
528
+ <div class="screenshot-box">
529
+ <div class="screenshot-label">šŸ–„ļø Desktop (1400x900)</div>
530
+ <img src="${path.relative(CONFIG.outputDir, page.screenshots.desktop)}" alt="Desktop screenshot">
531
+ </div>
532
+ <div class="screenshot-box">
533
+ <div class="screenshot-label">šŸ“± Mobile (375x667)</div>
534
+ <img src="${path.relative(CONFIG.outputDir, page.screenshots.mobile)}" alt="Mobile screenshot">
535
+ </div>
536
+ </div>
537
+ </div>
538
+ `).join('')}
539
+ </div>
540
+ </body>
541
+ </html>
542
+ `;
543
+
544
+ const reportPath = path.join(CONFIG.outputDir, 'report.html');
545
+ await fs.writeFile(reportPath, html);
546
+ console.log(`āœ… Report saved to: ${reportPath}`);
547
+
548
+ return reportPath;
549
+ }
550
+
551
+ /**
552
+ * Main execution
553
+ */
554
+ async function main() {
555
+ console.log('šŸš€ Starting UI Testing Tool\n');
556
+ console.log(`Base URL: ${CONFIG.baseUrl}`);
557
+ console.log(`Output: ${CONFIG.outputDir}\n`);
558
+
559
+ // Clean up old screenshots
560
+ try {
561
+ const files = await fs.readdir(CONFIG.screenshotDir);
562
+ console.log(`šŸ—‘ļø Cleaning up ${files.length} old screenshots...`);
563
+ for (const file of files) {
564
+ if (file.endsWith('.png')) {
565
+ await fs.unlink(path.join(CONFIG.screenshotDir, file));
566
+ }
567
+ }
568
+ console.log('āœ… Cleanup complete\n');
569
+ } catch (err) {
570
+ // Directory doesn't exist yet, that's fine
571
+ }
572
+
573
+ // Create output directories
574
+ await fs.mkdir(CONFIG.outputDir, { recursive: true });
575
+ await fs.mkdir(CONFIG.screenshotDir, { recursive: true });
576
+
577
+ // Read sitemap
578
+ const urls = await readSitemap();
579
+ results.summary.total = urls.length;
580
+
581
+ // Launch browser
582
+ console.log('\n🌐 Launching browser...');
583
+ const browser = await puppeteer.launch({
584
+ headless: 'new',
585
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
586
+ });
587
+
588
+ // Test each page
589
+ for (let i = 0; i < urls.length; i++) {
590
+ const pageResult = await testPage(browser, urls[i], i, urls.length);
591
+ results.pages.push(pageResult);
592
+ }
593
+
594
+ await browser.close();
595
+
596
+ // Generate report
597
+ const reportPath = await generateReport();
598
+
599
+ // Save JSON results
600
+ const jsonPath = path.join(CONFIG.outputDir, 'results.json');
601
+ await fs.writeFile(jsonPath, JSON.stringify(results, null, 2));
602
+ console.log(`āœ… JSON results saved to: ${jsonPath}`);
603
+
604
+ // Print summary
605
+ console.log('\n' + '='.repeat(60));
606
+ console.log('šŸ“Š TEST SUMMARY');
607
+ console.log('='.repeat(60));
608
+ console.log(`Total Pages: ${results.summary.total}`);
609
+ console.log(`āœ… Passed: ${results.summary.passed}`);
610
+ console.log(`āš ļø Warnings: ${results.summary.warnings}`);
611
+ console.log(`āŒ Failed: ${results.summary.failed}`);
612
+ console.log('='.repeat(60));
613
+ console.log(`\nšŸ“„ Full report: ${reportPath}`);
614
+ console.log(`\nšŸ’” Open report: open ${reportPath}\n`);
615
+
616
+ // Exit with error code if there are failures
617
+ process.exit(results.summary.failed > 0 ? 1 : 0);
618
+ }
619
+
620
+ // Run
621
+ main().catch(error => {
622
+ console.error('āŒ Fatal error:', error);
623
+ process.exit(1);
624
+ });
@@ -0,0 +1,44 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ const EXAMPLES_FILE = path.join(__dirname, '../../src/features/web-playground/examples.js');
5
+ const EXAMPLES_DIR = path.join(__dirname, '../../src/features/web-playground/examples');
6
+
7
+ async function extract() {
8
+ const content = await fs.readFile(EXAMPLES_FILE, 'utf8');
9
+ // Basic extraction using regex (not perfect but should work for this specific file structure)
10
+ // Actually, it's easier to just REQUIRE the file if it was a module, but it's a window property.
11
+ // Let's use a safer way: eval or just parse the JS.
12
+ // Since I know the structure, I'll just use a small hack to get the array.
13
+
14
+ const arrayStart = content.indexOf('window.PLAYGROUND_EXAMPLES = [');
15
+ const arrayEnd = content.lastIndexOf('];');
16
+ const arrayStr = content.substring(arrayStart + 'window.PLAYGROUND_EXAMPLES ='.length, arrayEnd + 1);
17
+
18
+ let examples = [];
19
+ try {
20
+ examples = eval(arrayStr);
21
+ } catch (e) {
22
+ console.error("Failed to eval examples array", e);
23
+ return;
24
+ }
25
+
26
+ await fs.ensureDir(EXAMPLES_DIR);
27
+
28
+ for (const ex of examples) {
29
+ const dir = path.join(EXAMPLES_DIR, ex.id);
30
+ await fs.ensureDir(dir);
31
+
32
+ await fs.writeFile(path.join(dir, 'index.html'), ex.html || '');
33
+ await fs.writeFile(path.join(dir, 'style.css'), ex.css || '');
34
+ await fs.writeFile(path.join(dir, 'script.js'), ex.js || '');
35
+ await fs.writeJson(path.join(dir, 'meta.json'), {
36
+ id: ex.id,
37
+ title: ex.title
38
+ }, { spaces: 2 });
39
+
40
+ console.log(`āœ… Extracted: ${ex.id}`);
41
+ }
42
+ }
43
+
44
+ extract();