@o2vend/theme-cli 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.
- package/README.md +211 -0
- package/bin/o2vend +34 -0
- package/config/widget-map.json +45 -0
- package/package.json +64 -0
- package/src/commands/check.js +201 -0
- package/src/commands/generate.js +33 -0
- package/src/commands/init.js +302 -0
- package/src/commands/optimize.js +216 -0
- package/src/commands/package.js +208 -0
- package/src/commands/serve.js +105 -0
- package/src/commands/validate.js +191 -0
- package/src/lib/api-client.js +339 -0
- package/src/lib/dev-server.js +482 -0
- package/src/lib/file-watcher.js +80 -0
- package/src/lib/hot-reload.js +106 -0
- package/src/lib/liquid-engine.js +169 -0
- package/src/lib/liquid-filters.js +589 -0
- package/src/lib/mock-api-server.js +225 -0
- package/src/lib/mock-data.js +290 -0
- package/src/lib/widget-service.js +293 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O2VEND Theme CLI - Init Command
|
|
3
|
+
* Initialize new theme with best-practice structure
|
|
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 theme')
|
|
16
|
+
.argument('[theme-name]', 'Theme name')
|
|
17
|
+
.option('-t, --template <template>', 'Starter template (minimal|full-featured)', 'minimal')
|
|
18
|
+
.option('--cwd <path>', 'Working directory', process.cwd())
|
|
19
|
+
.action(async (themeName, options) => {
|
|
20
|
+
try {
|
|
21
|
+
const cwd = path.resolve(options.cwd);
|
|
22
|
+
|
|
23
|
+
// If theme name not provided, prompt for it
|
|
24
|
+
if (!themeName) {
|
|
25
|
+
const answers = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: 'input',
|
|
28
|
+
name: 'themeName',
|
|
29
|
+
message: 'Theme name:',
|
|
30
|
+
validate: (input) => {
|
|
31
|
+
if (!input.trim()) {
|
|
32
|
+
return 'Theme name is required';
|
|
33
|
+
}
|
|
34
|
+
if (!/^[a-z0-9-]+$/.test(input.trim())) {
|
|
35
|
+
return 'Theme name should contain only lowercase letters, numbers, and hyphens';
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
themeName = answers.themeName.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const themeDir = path.join(cwd, themeName);
|
|
45
|
+
|
|
46
|
+
// Check if directory already exists
|
|
47
|
+
if (fs.existsSync(themeDir)) {
|
|
48
|
+
const { overwrite } = await inquirer.prompt([
|
|
49
|
+
{
|
|
50
|
+
type: 'confirm',
|
|
51
|
+
name: 'overwrite',
|
|
52
|
+
message: `Directory "${themeName}" already exists. Overwrite?`,
|
|
53
|
+
default: false
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
if (!overwrite) {
|
|
58
|
+
console.log(chalk.yellow('❌ Cancelled'));
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fs.removeSync(themeDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.cyan(`✨ Creating theme "${themeName}"...\n`));
|
|
66
|
+
|
|
67
|
+
// Create directory structure
|
|
68
|
+
const dirs = [
|
|
69
|
+
'layout',
|
|
70
|
+
'templates',
|
|
71
|
+
'sections',
|
|
72
|
+
'snippets',
|
|
73
|
+
'assets',
|
|
74
|
+
'config',
|
|
75
|
+
'widgets',
|
|
76
|
+
'migrations'
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
dirs.forEach(dir => {
|
|
80
|
+
fs.ensureDirSync(path.join(themeDir, dir));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Create layout/theme.liquid
|
|
84
|
+
const layoutContent = `<!DOCTYPE html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="UTF-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
89
|
+
<title>{{ shop.name }}{% if page.title %} - {{ page.title }}{% endif %}</title>
|
|
90
|
+
|
|
91
|
+
{% if settings.favicon %}
|
|
92
|
+
<link rel="icon" href="{{ settings.favicon | asset_url }}">
|
|
93
|
+
{% endif %}
|
|
94
|
+
|
|
95
|
+
{{ 'theme.css' | asset_url | stylesheet_tag }}
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
{% section 'header' %}
|
|
99
|
+
|
|
100
|
+
<main class="main-content">
|
|
101
|
+
{{ content }}
|
|
102
|
+
</main>
|
|
103
|
+
|
|
104
|
+
{% section 'footer' %}
|
|
105
|
+
|
|
106
|
+
{{ 'theme.js' | asset_url | script_tag }}
|
|
107
|
+
</body>
|
|
108
|
+
</html>`;
|
|
109
|
+
fs.writeFileSync(path.join(themeDir, 'layout', 'theme.liquid'), layoutContent);
|
|
110
|
+
|
|
111
|
+
// Create templates/index.liquid
|
|
112
|
+
const indexTemplate = `{% layout 'theme' %}
|
|
113
|
+
|
|
114
|
+
<div class="homepage">
|
|
115
|
+
<h1>Welcome to {{ shop.name }}</h1>
|
|
116
|
+
|
|
117
|
+
{% for widget in widgets.hero %}
|
|
118
|
+
{{ widget | render_widget }}
|
|
119
|
+
{% endfor %}
|
|
120
|
+
|
|
121
|
+
{% unless widgets.hero %}
|
|
122
|
+
<p>Configure widgets in the admin to add content here.</p>
|
|
123
|
+
{% endunless %}
|
|
124
|
+
</div>`;
|
|
125
|
+
fs.writeFileSync(path.join(themeDir, 'templates', 'index.liquid'), indexTemplate);
|
|
126
|
+
|
|
127
|
+
// Create basic sections
|
|
128
|
+
const headerSection = `<header class="site-header">
|
|
129
|
+
<div class="container">
|
|
130
|
+
<h1>{{ shop.name }}</h1>
|
|
131
|
+
</div>
|
|
132
|
+
</header>
|
|
133
|
+
|
|
134
|
+
{% schema %}
|
|
135
|
+
{
|
|
136
|
+
"name": "Header",
|
|
137
|
+
"settings": []
|
|
138
|
+
}
|
|
139
|
+
{% endschema %}`;
|
|
140
|
+
fs.writeFileSync(path.join(themeDir, 'sections', 'header.liquid'), headerSection);
|
|
141
|
+
|
|
142
|
+
const footerSection = `<footer class="site-footer">
|
|
143
|
+
<div class="container">
|
|
144
|
+
<p>© {{ 'now' | date: '%Y' }} {{ shop.name }}. All rights reserved.</p>
|
|
145
|
+
</div>
|
|
146
|
+
</footer>
|
|
147
|
+
|
|
148
|
+
{% schema %}
|
|
149
|
+
{
|
|
150
|
+
"name": "Footer",
|
|
151
|
+
"settings": []
|
|
152
|
+
}
|
|
153
|
+
{% endschema %}`;
|
|
154
|
+
fs.writeFileSync(path.join(themeDir, 'sections', 'footer.liquid'), footerSection);
|
|
155
|
+
|
|
156
|
+
// Create basic assets
|
|
157
|
+
const themeCss = `/* Theme Styles */
|
|
158
|
+
|
|
159
|
+
:root {
|
|
160
|
+
--primary-color: #000;
|
|
161
|
+
--text-color: #333;
|
|
162
|
+
--bg-color: #fff;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
* {
|
|
166
|
+
box-sizing: border-box;
|
|
167
|
+
margin: 0;
|
|
168
|
+
padding: 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
body {
|
|
172
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
173
|
+
color: var(--text-color);
|
|
174
|
+
background-color: var(--bg-color);
|
|
175
|
+
line-height: 1.6;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.container {
|
|
179
|
+
max-width: 1200px;
|
|
180
|
+
margin: 0 auto;
|
|
181
|
+
padding: 0 20px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.site-header {
|
|
185
|
+
background-color: var(--primary-color);
|
|
186
|
+
color: #fff;
|
|
187
|
+
padding: 1rem 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.site-header h1 {
|
|
191
|
+
margin: 0;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.main-content {
|
|
195
|
+
min-height: 60vh;
|
|
196
|
+
padding: 2rem 0;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.site-footer {
|
|
200
|
+
background-color: #f5f5f5;
|
|
201
|
+
padding: 2rem 0;
|
|
202
|
+
margin-top: 4rem;
|
|
203
|
+
text-align: center;
|
|
204
|
+
}`;
|
|
205
|
+
fs.writeFileSync(path.join(themeDir, 'assets', 'theme.css'), themeCss);
|
|
206
|
+
|
|
207
|
+
const themeJs = `// Theme JavaScript
|
|
208
|
+
|
|
209
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
210
|
+
console.log('Theme loaded');
|
|
211
|
+
});`;
|
|
212
|
+
fs.writeFileSync(path.join(themeDir, 'assets', 'theme.js'), themeJs);
|
|
213
|
+
|
|
214
|
+
// Create config files
|
|
215
|
+
const settingsSchema = {
|
|
216
|
+
name: 'Theme Settings',
|
|
217
|
+
settings: [
|
|
218
|
+
{
|
|
219
|
+
type: 'text',
|
|
220
|
+
id: 'favicon',
|
|
221
|
+
label: 'Favicon URL',
|
|
222
|
+
default: ''
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
};
|
|
226
|
+
fs.writeJsonSync(path.join(themeDir, 'config', 'settings_schema.json'), settingsSchema, { spaces: 2 });
|
|
227
|
+
|
|
228
|
+
const settingsData = {
|
|
229
|
+
current: {
|
|
230
|
+
favicon: ''
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
fs.writeJsonSync(path.join(themeDir, 'config', 'settings_data.json'), settingsData, { spaces: 2 });
|
|
234
|
+
|
|
235
|
+
// Create theme.json.example
|
|
236
|
+
const themeJsonExample = {
|
|
237
|
+
id: themeName,
|
|
238
|
+
name: themeName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
239
|
+
version: '1.0.0',
|
|
240
|
+
author: 'Theme Developer',
|
|
241
|
+
description: `A beautiful O2VEND theme: ${themeName}`,
|
|
242
|
+
migration: {
|
|
243
|
+
files: {
|
|
244
|
+
modified: [],
|
|
245
|
+
added: [],
|
|
246
|
+
deleted: []
|
|
247
|
+
},
|
|
248
|
+
script: null
|
|
249
|
+
},
|
|
250
|
+
compatibility: {
|
|
251
|
+
minO2VENDVersion: '1.0.0',
|
|
252
|
+
dependencies: []
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
fs.writeJsonSync(path.join(themeDir, 'theme.json.example'), themeJsonExample, { spaces: 2 });
|
|
256
|
+
|
|
257
|
+
// Create README
|
|
258
|
+
const readme = `# ${themeName}
|
|
259
|
+
|
|
260
|
+
O2VEND Theme
|
|
261
|
+
|
|
262
|
+
## Getting Started
|
|
263
|
+
|
|
264
|
+
1. Start development server:
|
|
265
|
+
\`\`\`bash
|
|
266
|
+
o2vend serve
|
|
267
|
+
\`\`\`
|
|
268
|
+
|
|
269
|
+
2. Edit theme files in this directory
|
|
270
|
+
|
|
271
|
+
3. Package for marketplace:
|
|
272
|
+
\`\`\`bash
|
|
273
|
+
o2vend package
|
|
274
|
+
\`\`\`
|
|
275
|
+
|
|
276
|
+
## Structure
|
|
277
|
+
|
|
278
|
+
- \`layout/\` - Layout templates
|
|
279
|
+
- \`templates/\` - Page templates
|
|
280
|
+
- \`sections/\` - Section components
|
|
281
|
+
- \`widgets/\` - Widget templates
|
|
282
|
+
- \`snippets/\` - Reusable snippets
|
|
283
|
+
- \`assets/\` - CSS, JavaScript, images
|
|
284
|
+
- \`config/\` - Theme configuration
|
|
285
|
+
`;
|
|
286
|
+
fs.writeFileSync(path.join(themeDir, 'README.md'), readme);
|
|
287
|
+
|
|
288
|
+
console.log(chalk.green('✅ Theme initialized successfully!'));
|
|
289
|
+
console.log(chalk.cyan(`\n📁 Theme created at: ${themeDir}`));
|
|
290
|
+
console.log(chalk.yellow('\n💡 Next steps:'));
|
|
291
|
+
console.log(chalk.gray(` cd ${themeName}`));
|
|
292
|
+
console.log(chalk.gray(' o2vend serve'));
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error(chalk.red('❌ Error initializing theme:'), error.message);
|
|
295
|
+
if (error.stack && process.env.DEBUG) {
|
|
296
|
+
console.error(error.stack);
|
|
297
|
+
}
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
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;
|