@o2vend/theme-cli 1.0.32

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 (116) hide show
  1. package/README.md +425 -0
  2. package/assets/Logo_o2vend.png +0 -0
  3. package/assets/favicon.png +0 -0
  4. package/assets/logo-white.png +0 -0
  5. package/bin/o2vend +42 -0
  6. package/config/widget-map.json +50 -0
  7. package/lib/commands/check.js +201 -0
  8. package/lib/commands/generate.js +33 -0
  9. package/lib/commands/init.js +214 -0
  10. package/lib/commands/optimize.js +216 -0
  11. package/lib/commands/package.js +208 -0
  12. package/lib/commands/serve.js +105 -0
  13. package/lib/commands/validate.js +191 -0
  14. package/lib/lib/api-client.js +357 -0
  15. package/lib/lib/dev-server.js +2618 -0
  16. package/lib/lib/file-watcher.js +80 -0
  17. package/lib/lib/hot-reload.js +106 -0
  18. package/lib/lib/liquid-engine.js +822 -0
  19. package/lib/lib/liquid-filters.js +671 -0
  20. package/lib/lib/mock-api-server.js +989 -0
  21. package/lib/lib/mock-data.js +1468 -0
  22. package/lib/lib/widget-service.js +321 -0
  23. package/package.json +70 -0
  24. package/test-theme/README.md +27 -0
  25. package/test-theme/assets/async-sections.js +446 -0
  26. package/test-theme/assets/cart-drawer.js +463 -0
  27. package/test-theme/assets/cart-manager.js +223 -0
  28. package/test-theme/assets/checkout-price-handler.js +368 -0
  29. package/test-theme/assets/components.css +4629 -0
  30. package/test-theme/assets/delivery-zone.css +299 -0
  31. package/test-theme/assets/delivery-zone.js +396 -0
  32. package/test-theme/assets/logo.png +0 -0
  33. package/test-theme/assets/sections.css +48 -0
  34. package/test-theme/assets/theme.css +3500 -0
  35. package/test-theme/assets/theme.js +3745 -0
  36. package/test-theme/config/settings_data.json +292 -0
  37. package/test-theme/config/settings_schema.json +1050 -0
  38. package/test-theme/layout/theme.liquid +195 -0
  39. package/test-theme/locales/en.default.json +260 -0
  40. package/test-theme/sections/content-fallback.liquid +53 -0
  41. package/test-theme/sections/content.liquid +57 -0
  42. package/test-theme/sections/footer-fallback.liquid +328 -0
  43. package/test-theme/sections/footer.liquid +278 -0
  44. package/test-theme/sections/header-fallback.liquid +1805 -0
  45. package/test-theme/sections/header.liquid +1145 -0
  46. package/test-theme/sections/hero-fallback.liquid +212 -0
  47. package/test-theme/sections/hero.liquid +136 -0
  48. package/test-theme/snippets/account-sidebar.liquid +200 -0
  49. package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
  50. package/test-theme/snippets/breadcrumbs.liquid +134 -0
  51. package/test-theme/snippets/cart-drawer.liquid +467 -0
  52. package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
  53. package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
  54. package/test-theme/snippets/delivery-zone-search.liquid +78 -0
  55. package/test-theme/snippets/icon.liquid +105 -0
  56. package/test-theme/snippets/login-modal.liquid +346 -0
  57. package/test-theme/snippets/mega-menu.liquid +812 -0
  58. package/test-theme/snippets/news-thumbnail.liquid +187 -0
  59. package/test-theme/snippets/pagination.liquid +120 -0
  60. package/test-theme/snippets/price.liquid +92 -0
  61. package/test-theme/snippets/product-card-related.liquid +78 -0
  62. package/test-theme/snippets/product-card-simple.liquid +41 -0
  63. package/test-theme/snippets/product-card.liquid +697 -0
  64. package/test-theme/snippets/rating.liquid +85 -0
  65. package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
  66. package/test-theme/snippets/skeleton-product-card.liquid +124 -0
  67. package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
  68. package/test-theme/snippets/social-sharing.liquid +185 -0
  69. package/test-theme/templates/account/dashboard.liquid +401 -0
  70. package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
  71. package/test-theme/templates/account/loyalty.liquid +588 -0
  72. package/test-theme/templates/account/order-detail.liquid +230 -0
  73. package/test-theme/templates/account/orders.liquid +349 -0
  74. package/test-theme/templates/account/profile.liquid +758 -0
  75. package/test-theme/templates/account/register.liquid +232 -0
  76. package/test-theme/templates/account/return-orders.liquid +348 -0
  77. package/test-theme/templates/account/store-credit.liquid +464 -0
  78. package/test-theme/templates/account/subscriptions.liquid +601 -0
  79. package/test-theme/templates/account/wishlist.liquid +419 -0
  80. package/test-theme/templates/address-book.liquid +1092 -0
  81. package/test-theme/templates/categories.liquid +452 -0
  82. package/test-theme/templates/checkout.liquid +4511 -0
  83. package/test-theme/templates/error.liquid +384 -0
  84. package/test-theme/templates/index.liquid +11 -0
  85. package/test-theme/templates/login.liquid +185 -0
  86. package/test-theme/templates/order-confirmation.liquid +720 -0
  87. package/test-theme/templates/page.liquid +297 -0
  88. package/test-theme/templates/product-detail.liquid +4363 -0
  89. package/test-theme/templates/products.liquid +518 -0
  90. package/test-theme/templates/search.liquid +922 -0
  91. package/test-theme/theme.json.example +19 -0
  92. package/test-theme/widgets/brand-carousel.liquid +676 -0
  93. package/test-theme/widgets/brand.liquid +245 -0
  94. package/test-theme/widgets/carousel.liquid +843 -0
  95. package/test-theme/widgets/category-list-carousel.liquid +656 -0
  96. package/test-theme/widgets/category-list.liquid +340 -0
  97. package/test-theme/widgets/category.liquid +475 -0
  98. package/test-theme/widgets/discount-time.liquid +176 -0
  99. package/test-theme/widgets/footer-menu.liquid +695 -0
  100. package/test-theme/widgets/footer.liquid +179 -0
  101. package/test-theme/widgets/gallery.liquid +271 -0
  102. package/test-theme/widgets/header-menu.liquid +932 -0
  103. package/test-theme/widgets/header.liquid +159 -0
  104. package/test-theme/widgets/html.liquid +214 -0
  105. package/test-theme/widgets/news.liquid +217 -0
  106. package/test-theme/widgets/product-canvas.liquid +235 -0
  107. package/test-theme/widgets/product-carousel.liquid +502 -0
  108. package/test-theme/widgets/product.liquid +45 -0
  109. package/test-theme/widgets/recently-viewed.liquid +26 -0
  110. package/test-theme/widgets/shared/product-grid.liquid +339 -0
  111. package/test-theme/widgets/simple-product.liquid +42 -0
  112. package/test-theme/widgets/single-product.liquid +610 -0
  113. package/test-theme/widgets/spacebar-carousel.liquid +663 -0
  114. package/test-theme/widgets/spacebar.liquid +279 -0
  115. package/test-theme/widgets/splash.liquid +378 -0
  116. package/test-theme/widgets/testimonial-carousel.liquid +709 -0
@@ -0,0 +1,201 @@
1
+ /**
2
+ * O2VEND Theme CLI - Check Command
3
+ * Lint and check best practices
4
+ */
5
+
6
+ const { Command } = require('commander');
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const chalk = require('chalk');
10
+
11
+ const checkCommand = new Command('check');
12
+
13
+ checkCommand
14
+ .description('Check theme for issues and best practices')
15
+ .option('--strict', 'Enable strict mode')
16
+ .option('--format <format>', 'Output format (table|json|compact)', 'table')
17
+ .option('--cwd <path>', 'Working directory (theme path)', process.cwd())
18
+ .action(async (options) => {
19
+ try {
20
+ console.log(chalk.cyan('šŸ” Checking theme...\n'));
21
+
22
+ const themePath = path.resolve(options.cwd);
23
+ const issues = [];
24
+
25
+ if (!fs.existsSync(themePath)) {
26
+ console.error(chalk.red(`Theme directory does not exist: ${themePath}`));
27
+ process.exit(1);
28
+ }
29
+
30
+ // Performance checks
31
+ const assetFiles = findFiles(themePath, ['css', 'js']);
32
+ assetFiles.forEach(file => {
33
+ const size = fs.statSync(file).size;
34
+ const sizeKB = (size / 1024).toFixed(2);
35
+
36
+ if (size > 500 * 1024) { // > 500KB
37
+ issues.push({
38
+ type: 'performance',
39
+ severity: 'warning',
40
+ file: path.relative(themePath, file),
41
+ message: `Large file: ${sizeKB}KB (consider optimization)`
42
+ });
43
+ }
44
+ });
45
+
46
+ // Accessibility checks (basic)
47
+ const liquidFiles = findFiles(themePath, 'liquid');
48
+ liquidFiles.forEach(file => {
49
+ const content = fs.readFileSync(file, 'utf8');
50
+
51
+ // Check for images without alt text
52
+ const imgTags = content.match(/<img[^>]*>/g) || [];
53
+ imgTags.forEach(img => {
54
+ if (!img.includes('alt=')) {
55
+ issues.push({
56
+ type: 'accessibility',
57
+ severity: 'warning',
58
+ file: path.relative(themePath, file),
59
+ message: 'Image missing alt attribute'
60
+ });
61
+ }
62
+ });
63
+
64
+ // Check for form inputs without labels
65
+ const inputTags = content.match(/<input[^>]*>/g) || [];
66
+ inputTags.forEach(input => {
67
+ const id = input.match(/id=["']([^"']+)["']/);
68
+ if (id && !content.includes(`for="${id[1]}"`)) {
69
+ issues.push({
70
+ type: 'accessibility',
71
+ severity: 'warning',
72
+ file: path.relative(themePath, file),
73
+ message: 'Input field should have associated label'
74
+ });
75
+ }
76
+ });
77
+ });
78
+
79
+ // SEO checks
80
+ const layoutPath = path.join(themePath, 'layout', 'theme.liquid');
81
+ if (fs.existsSync(layoutPath)) {
82
+ const layoutContent = fs.readFileSync(layoutPath, 'utf8');
83
+
84
+ if (!layoutContent.includes('<title>')) {
85
+ issues.push({
86
+ type: 'seo',
87
+ severity: 'error',
88
+ file: 'layout/theme.liquid',
89
+ message: 'Layout should include <title> tag'
90
+ });
91
+ }
92
+
93
+ if (!layoutContent.includes('meta name="description"')) {
94
+ issues.push({
95
+ type: 'seo',
96
+ severity: 'warning',
97
+ file: 'layout/theme.liquid',
98
+ message: 'Layout should include meta description'
99
+ });
100
+ }
101
+ }
102
+
103
+ // Best practices
104
+ const settingsSchemaPath = path.join(themePath, 'config', 'settings_schema.json');
105
+ if (!fs.existsSync(settingsSchemaPath)) {
106
+ issues.push({
107
+ type: 'best-practice',
108
+ severity: 'info',
109
+ file: 'config/settings_schema.json',
110
+ message: 'Consider adding settings_schema.json for theme customization'
111
+ });
112
+ }
113
+
114
+ // Output results
115
+ outputResults(issues, options);
116
+ } catch (error) {
117
+ console.error(chalk.red('āŒ Check failed:'), error.message);
118
+ if (error.stack && process.env.DEBUG) {
119
+ console.error(error.stack);
120
+ }
121
+ process.exit(1);
122
+ }
123
+ });
124
+
125
+ /**
126
+ * Find files by extension(s)
127
+ */
128
+ function findFiles(dir, ext) {
129
+ const extensions = Array.isArray(ext) ? ext : [ext];
130
+ const files = [];
131
+
132
+ if (!fs.existsSync(dir)) return files;
133
+
134
+ const items = fs.readdirSync(dir);
135
+ items.forEach(item => {
136
+ const itemPath = path.join(dir, item);
137
+ const stat = fs.statSync(itemPath);
138
+ if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
139
+ files.push(...findFiles(itemPath, ext));
140
+ } else {
141
+ const fileExt = item.split('.').pop();
142
+ if (extensions.includes(fileExt)) {
143
+ files.push(itemPath);
144
+ }
145
+ }
146
+ });
147
+
148
+ return files;
149
+ }
150
+
151
+ /**
152
+ * Output check results
153
+ */
154
+ function outputResults(issues, options) {
155
+ if (options.format === 'json') {
156
+ console.log(JSON.stringify(issues, null, 2));
157
+ return;
158
+ }
159
+
160
+ if (issues.length === 0) {
161
+ console.log(chalk.green('āœ… All checks passed!'));
162
+ return;
163
+ }
164
+
165
+ // Group by type
166
+ const byType = {};
167
+ issues.forEach(issue => {
168
+ if (!byType[issue.type]) {
169
+ byType[issue.type] = [];
170
+ }
171
+ byType[issue.type].push(issue);
172
+ });
173
+
174
+ // Output by type
175
+ Object.keys(byType).forEach(type => {
176
+ const typeIssues = byType[type];
177
+ const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
178
+
179
+ console.log(chalk.cyan(`\n${typeLabel} (${typeIssues.length}):`));
180
+
181
+ typeIssues.forEach(issue => {
182
+ const icon = issue.severity === 'error' ? 'āŒ' : issue.severity === 'warning' ? 'āš ļø' : 'ā„¹ļø';
183
+ const color = issue.severity === 'error' ? chalk.red : issue.severity === 'warning' ? chalk.yellow : chalk.blue;
184
+
185
+ if (options.format === 'compact') {
186
+ console.log(color(` ${icon} ${issue.file}: ${issue.message}`));
187
+ } else {
188
+ console.log(color(` ${icon} ${issue.file}`));
189
+ console.log(chalk.gray(` ${issue.message}`));
190
+ }
191
+ });
192
+ });
193
+
194
+ const errors = issues.filter(i => i.severity === 'error').length;
195
+ const warnings = issues.filter(i => i.severity === 'warning').length;
196
+ const info = issues.filter(i => i.severity === 'info').length;
197
+
198
+ console.log(chalk.cyan(`\nšŸ“Š Summary: ${errors} errors, ${warnings} warnings, ${info} info`));
199
+ }
200
+
201
+ module.exports = checkCommand;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * O2VEND Theme CLI - Generate Command
3
+ * AI-powered theme generation from natural language
4
+ */
5
+
6
+ const { Command } = require('commander');
7
+ const chalk = require('chalk');
8
+
9
+ const generateCommand = new Command('generate');
10
+
11
+ generateCommand
12
+ .description('Generate theme from natural language description')
13
+ .argument('<description>', 'Theme description')
14
+ .option('--cwd <path>', 'Output directory', process.cwd())
15
+ .action(async (description, options) => {
16
+ try {
17
+ console.log(chalk.cyan('šŸ¤– Generating theme with AI...\n'));
18
+
19
+ // TODO: Implement AI-powered theme generation
20
+ // 1. Parse description
21
+ // 2. Generate theme structure
22
+ // 3. Generate templates
23
+ // 4. Generate settings schema
24
+ // 5. Generate mock data
25
+
26
+ console.log(chalk.green('āœ… Theme generated successfully!'));
27
+ } catch (error) {
28
+ console.error(chalk.red('āŒ Error generating theme:'), error.message);
29
+ process.exit(1);
30
+ }
31
+ });
32
+
33
+ module.exports = generateCommand;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * O2VEND Theme CLI - Init Command
3
+ * Initialize new theme from test-theme template
4
+ */
5
+
6
+ const { Command } = require('commander');
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const chalk = require('chalk');
10
+ const inquirer = require('inquirer');
11
+
12
+ const initCommand = new Command('init');
13
+
14
+ initCommand
15
+ .description('Initialize a new O2VEND theme')
16
+ .argument('[theme-name]', 'Theme name')
17
+ .option('--cwd <path>', 'Working directory', process.cwd())
18
+ .action(async (themeName, options) => {
19
+ try {
20
+ const cwd = path.resolve(options.cwd);
21
+
22
+ console.log(chalk.cyan('\nšŸŽØ O2VEND Theme Initializer\n'));
23
+
24
+ // If theme name not provided, prompt for it
25
+ if (!themeName) {
26
+ const answers = await inquirer.prompt([
27
+ {
28
+ type: 'input',
29
+ name: 'themeName',
30
+ message: 'Theme name:',
31
+ default: 'my-theme',
32
+ validate: (input) => {
33
+ if (!input.trim()) {
34
+ return 'Theme name is required';
35
+ }
36
+ if (!/^[a-zA-Z0-9-_]+$/.test(input.trim())) {
37
+ return 'Theme name should contain only letters, numbers, hyphens, and underscores';
38
+ }
39
+ return true;
40
+ }
41
+ }
42
+ ]);
43
+ themeName = answers.themeName.trim();
44
+ }
45
+
46
+ const themeDir = path.join(cwd, themeName);
47
+
48
+ // Check if directory already exists
49
+ if (fs.existsSync(themeDir)) {
50
+ const { overwrite } = await inquirer.prompt([
51
+ {
52
+ type: 'confirm',
53
+ name: 'overwrite',
54
+ message: `Directory "${themeName}" already exists. Overwrite?`,
55
+ default: false
56
+ }
57
+ ]);
58
+
59
+ if (!overwrite) {
60
+ console.log(chalk.yellow('\nāŒ Cancelled'));
61
+ process.exit(0);
62
+ }
63
+
64
+ fs.removeSync(themeDir);
65
+ }
66
+
67
+ console.log(chalk.cyan(`\n✨ Creating theme "${themeName}"...\n`));
68
+
69
+ // Find the test-theme template directory
70
+ // Check multiple possible locations (installed package vs development)
71
+ const possibleTemplatePaths = [
72
+ path.join(__dirname, '..', '..', 'test-theme'), // Built: lib/commands -> lib -> test-theme
73
+ path.join(__dirname, '..', 'test-theme'), // Built alternate: lib/commands -> test-theme
74
+ path.join(__dirname, '..', '..', '..', 'test-theme'), // Source: src/commands -> src -> cli -> test-theme
75
+ ];
76
+
77
+ let templatePath = null;
78
+ for (const p of possibleTemplatePaths) {
79
+ if (fs.existsSync(p) && fs.existsSync(path.join(p, 'layout', 'theme.liquid'))) {
80
+ templatePath = p;
81
+ break;
82
+ }
83
+ }
84
+
85
+ if (!templatePath) {
86
+ console.error(chalk.red('āŒ Template not found. The test-theme directory is missing.'));
87
+ console.log(chalk.yellow('Searched in:'));
88
+ possibleTemplatePaths.forEach(p => console.log(chalk.gray(` - ${p}`)));
89
+ process.exit(1);
90
+ }
91
+
92
+ console.log(chalk.gray(`šŸ“¦ Using template from: ${templatePath}\n`));
93
+
94
+ // Copy the entire test-theme directory
95
+ fs.copySync(templatePath, themeDir, {
96
+ filter: (src) => {
97
+ // Skip node_modules, .git, and other unwanted files
98
+ const basename = path.basename(src);
99
+ return !['node_modules', '.git', '.DS_Store', 'Thumbs.db'].includes(basename);
100
+ }
101
+ });
102
+
103
+ // Update theme.json.example with new theme name
104
+ const themeJsonPath = path.join(themeDir, 'theme.json.example');
105
+ if (fs.existsSync(themeJsonPath)) {
106
+ const themeJson = fs.readJsonSync(themeJsonPath);
107
+ themeJson.id = themeName;
108
+ themeJson.name = themeName.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
109
+ themeJson.description = `O2VEND theme: ${themeJson.name}`;
110
+ fs.writeJsonSync(themeJsonPath, themeJson, { spaces: 2 });
111
+ }
112
+
113
+ // Update README.md with new theme name
114
+ const readmePath = path.join(themeDir, 'README.md');
115
+ if (fs.existsSync(readmePath)) {
116
+ let readme = fs.readFileSync(readmePath, 'utf8');
117
+ readme = readme.replace(/test-theme/g, themeName);
118
+ readme = readme.replace(/Test Theme/g, themeName.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '));
119
+ fs.writeFileSync(readmePath, readme);
120
+ } else {
121
+ // Create README if it doesn't exist
122
+ const displayName = themeName.split(/[-_]/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
123
+ const readme = `# ${displayName}
124
+
125
+ O2VEND Theme
126
+
127
+ ## Getting Started
128
+
129
+ 1. Navigate to the theme directory:
130
+ \`\`\`bash
131
+ cd ${themeName}
132
+ \`\`\`
133
+
134
+ 2. Start the development server:
135
+ \`\`\`bash
136
+ o2vend serve --cwd .
137
+ \`\`\`
138
+
139
+ 3. Open your browser to http://localhost:3000
140
+
141
+ 4. Edit theme files and see changes live!
142
+
143
+ ## Theme Structure
144
+
145
+ - \`layout/\` - Main layout template (theme.liquid)
146
+ - \`templates/\` - Page templates (index, product-detail, categories, etc.)
147
+ - \`sections/\` - Section components (header, footer, hero, content)
148
+ - \`widgets/\` - Widget templates for dynamic content
149
+ - \`snippets/\` - Reusable code snippets
150
+ - \`assets/\` - CSS, JavaScript, and images
151
+ - \`config/\` - Theme settings and configuration
152
+ - \`locales/\` - Translation files
153
+
154
+ ## Development Commands
155
+
156
+ \`\`\`bash
157
+ # Start dev server with mock data
158
+ o2vend serve --cwd .
159
+
160
+ # Start dev server connected to live API
161
+ o2vend serve --cwd . --api-url https://your-store.o2vend.com
162
+
163
+ # Validate theme structure
164
+ o2vend check --cwd .
165
+
166
+ # Package theme for upload
167
+ o2vend package --cwd .
168
+ \`\`\`
169
+
170
+ ## Customization
171
+
172
+ 1. **Colors & Typography**: Edit \`config/settings_data.json\`
173
+ 2. **Layout**: Modify \`layout/theme.liquid\`
174
+ 3. **Styles**: Update \`assets/theme.css\` and \`assets/components.css\`
175
+ 4. **Widgets**: Customize templates in \`widgets/\` directory
176
+
177
+ ## Documentation
178
+
179
+ Visit https://docs.o2vend.com for full theme development documentation.
180
+ `;
181
+ fs.writeFileSync(readmePath, readme);
182
+ }
183
+
184
+ // Print success message
185
+ console.log(chalk.green('āœ… Theme initialized successfully!\n'));
186
+
187
+ console.log(chalk.cyan('šŸ“ Theme structure created:'));
188
+ const dirs = ['layout', 'templates', 'sections', 'widgets', 'snippets', 'assets', 'config', 'locales'];
189
+ dirs.forEach(dir => {
190
+ const dirPath = path.join(themeDir, dir);
191
+ if (fs.existsSync(dirPath)) {
192
+ const files = fs.readdirSync(dirPath);
193
+ console.log(chalk.gray(` ${dir}/`) + chalk.white(` (${files.length} files)`));
194
+ }
195
+ });
196
+
197
+ console.log(chalk.cyan(`\nšŸ“ Theme location: ${themeDir}`));
198
+
199
+ console.log(chalk.yellow('\nšŸš€ Next steps:'));
200
+ console.log(chalk.white(` 1. cd ${themeName}`));
201
+ console.log(chalk.white(' 2. o2vend serve --cwd .'));
202
+ console.log(chalk.white(' 3. Open http://localhost:3000 in your browser'));
203
+ console.log(chalk.white(' 4. Start editing your theme!\n'));
204
+
205
+ } catch (error) {
206
+ console.error(chalk.red('\nāŒ Error initializing theme:'), error.message);
207
+ if (error.stack && process.env.DEBUG) {
208
+ console.error(error.stack);
209
+ }
210
+ process.exit(1);
211
+ }
212
+ });
213
+
214
+ module.exports = initCommand;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * O2VEND Theme CLI - Optimize Command
3
+ * Optimize theme performance
4
+ */
5
+
6
+ const { Command } = require('commander');
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const chalk = require('chalk');
10
+
11
+ const optimizeCommand = new Command('optimize');
12
+
13
+ optimizeCommand
14
+ .description('Optimize theme performance')
15
+ .option('--analyze', 'Only analyze, don\'t make changes')
16
+ .option('--apply', 'Apply optimizations automatically')
17
+ .option('--backup', 'Create backup before changes')
18
+ .option('--cwd <path>', 'Working directory (theme path)', process.cwd())
19
+ .action(async (options) => {
20
+ try {
21
+ console.log(chalk.cyan('⚔ Analyzing theme performance...\n'));
22
+
23
+ const themePath = path.resolve(options.cwd);
24
+
25
+ if (!fs.existsSync(themePath)) {
26
+ console.error(chalk.red(`Theme directory does not exist: ${themePath}`));
27
+ process.exit(1);
28
+ }
29
+
30
+ const analysis = {
31
+ cssFiles: [],
32
+ jsFiles: [],
33
+ images: [],
34
+ recommendations: []
35
+ };
36
+
37
+ // Analyze CSS files
38
+ const cssFiles = findFiles(themePath, 'css');
39
+ cssFiles.forEach(file => {
40
+ const size = fs.statSync(file).size;
41
+ const content = fs.readFileSync(file, 'utf8');
42
+ const minifiedSize = estimateMinifiedSize(content);
43
+
44
+ analysis.cssFiles.push({
45
+ file: path.relative(themePath, file),
46
+ size: size,
47
+ sizeKB: (size / 1024).toFixed(2),
48
+ minifiedSize: minifiedSize,
49
+ savings: size - minifiedSize,
50
+ savingsPercent: ((1 - minifiedSize / size) * 100).toFixed(1)
51
+ });
52
+
53
+ if (size > 100 * 1024) { // > 100KB
54
+ analysis.recommendations.push({
55
+ type: 'optimization',
56
+ file: path.relative(themePath, file),
57
+ message: `Large CSS file (${(size / 1024).toFixed(2)}KB) - consider minification`,
58
+ savings: `${((1 - minifiedSize / size) * 100).toFixed(1)}% savings possible`
59
+ });
60
+ }
61
+ });
62
+
63
+ // Analyze JS files
64
+ const jsFiles = findFiles(themePath, 'js');
65
+ jsFiles.forEach(file => {
66
+ const size = fs.statSync(file).size;
67
+
68
+ analysis.jsFiles.push({
69
+ file: path.relative(themePath, file),
70
+ size: size,
71
+ sizeKB: (size / 1024).toFixed(2)
72
+ });
73
+
74
+ if (size > 100 * 1024) { // > 100KB
75
+ analysis.recommendations.push({
76
+ type: 'optimization',
77
+ file: path.relative(themePath, file),
78
+ message: `Large JS file (${(size / 1024).toFixed(2)}KB) - consider minification`,
79
+ savings: '~30-50% savings possible'
80
+ });
81
+ }
82
+ });
83
+
84
+ // Analyze images
85
+ const imageFiles = findFiles(themePath, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp']);
86
+ imageFiles.forEach(file => {
87
+ const size = fs.statSync(file).size;
88
+
89
+ analysis.images.push({
90
+ file: path.relative(themePath, file),
91
+ size: size,
92
+ sizeKB: (size / 1024).toFixed(2)
93
+ });
94
+
95
+ if (size > 500 * 1024) { // > 500KB
96
+ analysis.recommendations.push({
97
+ type: 'optimization',
98
+ file: path.relative(themePath, file),
99
+ message: `Large image (${(size / 1024).toFixed(2)}KB) - consider compression`,
100
+ savings: '~50-70% savings possible'
101
+ });
102
+ }
103
+ });
104
+
105
+ // Output analysis
106
+ outputAnalysis(analysis, options);
107
+
108
+ if (!options.analyze && options.apply) {
109
+ console.log(chalk.yellow('\nāš ļø Automatic optimization not yet implemented'));
110
+ console.log(chalk.gray(' Use external tools for minification and compression'));
111
+ console.log(chalk.gray(' - CSS: csso, clean-css'));
112
+ console.log(chalk.gray(' - JS: terser, uglify-js'));
113
+ console.log(chalk.gray(' - Images: imagemin, sharp'));
114
+ }
115
+ } catch (error) {
116
+ console.error(chalk.red('āŒ Error optimizing theme:'), error.message);
117
+ if (error.stack && process.env.DEBUG) {
118
+ console.error(error.stack);
119
+ }
120
+ process.exit(1);
121
+ }
122
+ });
123
+
124
+ /**
125
+ * Find files by extension(s)
126
+ */
127
+ function findFiles(dir, ext) {
128
+ const extensions = Array.isArray(ext) ? ext : [ext];
129
+ const files = [];
130
+
131
+ if (!fs.existsSync(dir)) return files;
132
+
133
+ const items = fs.readdirSync(dir);
134
+ items.forEach(item => {
135
+ const itemPath = path.join(dir, item);
136
+ const stat = fs.statSync(itemPath);
137
+ if (stat.isDirectory() && !item.startsWith('.') && item !== 'node_modules') {
138
+ files.push(...findFiles(itemPath, ext));
139
+ } else {
140
+ const fileExt = item.split('.').pop().toLowerCase();
141
+ if (extensions.includes(fileExt)) {
142
+ files.push(itemPath);
143
+ }
144
+ }
145
+ });
146
+
147
+ return files;
148
+ }
149
+
150
+ /**
151
+ * Estimate minified CSS size (rough approximation)
152
+ */
153
+ function estimateMinifiedSize(css) {
154
+ return css
155
+ .replace(/\/\*[^*]*\*+(?:[^/*][^*]*\*+)*\//g, '') // Remove comments
156
+ .replace(/\s+/g, ' ') // Collapse whitespace
157
+ .replace(/;\s*}/g, '}') // Remove semicolons before closing braces
158
+ .replace(/\s*{\s*/g, '{') // Remove spaces around braces
159
+ .replace(/\s*}\s*/g, '}')
160
+ .replace(/\s*:\s*/g, ':') // Remove spaces around colons
161
+ .replace(/\s*;\s*/g, ';') // Remove spaces around semicolons
162
+ .trim().length;
163
+ }
164
+
165
+ /**
166
+ * Output analysis results
167
+ */
168
+ function outputAnalysis(analysis, options) {
169
+ const totalCssSize = analysis.cssFiles.reduce((sum, f) => sum + f.size, 0);
170
+ const totalJsSize = analysis.jsFiles.reduce((sum, f) => sum + f.size, 0);
171
+ const totalImageSize = analysis.images.reduce((sum, f) => sum + f.size, 0);
172
+
173
+ console.log(chalk.cyan('šŸ“Š Analysis Results:\n'));
174
+
175
+ if (analysis.cssFiles.length > 0) {
176
+ console.log(chalk.yellow('CSS Files:'));
177
+ analysis.cssFiles.forEach(file => {
178
+ console.log(chalk.gray(` ${file.file}: ${file.sizeKB}KB`));
179
+ if (file.savings > 0) {
180
+ console.log(chalk.green(` → Minified: ~${(file.minifiedSize / 1024).toFixed(2)}KB (${file.savingsPercent}% savings)`));
181
+ }
182
+ });
183
+ console.log(chalk.cyan(` Total: ${(totalCssSize / 1024).toFixed(2)}KB\n`));
184
+ }
185
+
186
+ if (analysis.jsFiles.length > 0) {
187
+ console.log(chalk.yellow('JavaScript Files:'));
188
+ analysis.jsFiles.forEach(file => {
189
+ console.log(chalk.gray(` ${file.file}: ${file.sizeKB}KB`));
190
+ });
191
+ console.log(chalk.cyan(` Total: ${(totalJsSize / 1024).toFixed(2)}KB\n`));
192
+ }
193
+
194
+ if (analysis.images.length > 0) {
195
+ console.log(chalk.yellow('Images:'));
196
+ analysis.images.forEach(image => {
197
+ console.log(chalk.gray(` ${image.file}: ${image.sizeKB}KB`));
198
+ });
199
+ console.log(chalk.cyan(` Total: ${(totalImageSize / 1024).toFixed(2)}KB\n`));
200
+ }
201
+
202
+ if (analysis.recommendations.length > 0) {
203
+ console.log(chalk.yellow('šŸ’” Recommendations:\n'));
204
+ analysis.recommendations.forEach(rec => {
205
+ console.log(chalk.gray(` ${rec.file}:`));
206
+ console.log(chalk.white(` ${rec.message}`));
207
+ if (rec.savings) {
208
+ console.log(chalk.green(` → ${rec.savings}`));
209
+ }
210
+ });
211
+ } else {
212
+ console.log(chalk.green('āœ… No optimization recommendations'));
213
+ }
214
+ }
215
+
216
+ module.exports = optimizeCommand;