@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.
- package/README.md +425 -0
- package/assets/Logo_o2vend.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/logo-white.png +0 -0
- package/bin/o2vend +42 -0
- package/config/widget-map.json +50 -0
- package/lib/commands/check.js +201 -0
- package/lib/commands/generate.js +33 -0
- package/lib/commands/init.js +214 -0
- package/lib/commands/optimize.js +216 -0
- package/lib/commands/package.js +208 -0
- package/lib/commands/serve.js +105 -0
- package/lib/commands/validate.js +191 -0
- package/lib/lib/api-client.js +357 -0
- package/lib/lib/dev-server.js +2618 -0
- package/lib/lib/file-watcher.js +80 -0
- package/lib/lib/hot-reload.js +106 -0
- package/lib/lib/liquid-engine.js +822 -0
- package/lib/lib/liquid-filters.js +671 -0
- package/lib/lib/mock-api-server.js +989 -0
- package/lib/lib/mock-data.js +1468 -0
- package/lib/lib/widget-service.js +321 -0
- package/package.json +70 -0
- package/test-theme/README.md +27 -0
- package/test-theme/assets/async-sections.js +446 -0
- package/test-theme/assets/cart-drawer.js +463 -0
- package/test-theme/assets/cart-manager.js +223 -0
- package/test-theme/assets/checkout-price-handler.js +368 -0
- package/test-theme/assets/components.css +4629 -0
- package/test-theme/assets/delivery-zone.css +299 -0
- package/test-theme/assets/delivery-zone.js +396 -0
- package/test-theme/assets/logo.png +0 -0
- package/test-theme/assets/sections.css +48 -0
- package/test-theme/assets/theme.css +3500 -0
- package/test-theme/assets/theme.js +3745 -0
- package/test-theme/config/settings_data.json +292 -0
- package/test-theme/config/settings_schema.json +1050 -0
- package/test-theme/layout/theme.liquid +195 -0
- package/test-theme/locales/en.default.json +260 -0
- package/test-theme/sections/content-fallback.liquid +53 -0
- package/test-theme/sections/content.liquid +57 -0
- package/test-theme/sections/footer-fallback.liquid +328 -0
- package/test-theme/sections/footer.liquid +278 -0
- package/test-theme/sections/header-fallback.liquid +1805 -0
- package/test-theme/sections/header.liquid +1145 -0
- package/test-theme/sections/hero-fallback.liquid +212 -0
- package/test-theme/sections/hero.liquid +136 -0
- package/test-theme/snippets/account-sidebar.liquid +200 -0
- package/test-theme/snippets/add-to-cart-modal.liquid +484 -0
- package/test-theme/snippets/breadcrumbs.liquid +134 -0
- package/test-theme/snippets/cart-drawer.liquid +467 -0
- package/test-theme/snippets/delivery-zone-city-selector.liquid +79 -0
- package/test-theme/snippets/delivery-zone-modal.liquid +337 -0
- package/test-theme/snippets/delivery-zone-search.liquid +78 -0
- package/test-theme/snippets/icon.liquid +105 -0
- package/test-theme/snippets/login-modal.liquid +346 -0
- package/test-theme/snippets/mega-menu.liquid +812 -0
- package/test-theme/snippets/news-thumbnail.liquid +187 -0
- package/test-theme/snippets/pagination.liquid +120 -0
- package/test-theme/snippets/price.liquid +92 -0
- package/test-theme/snippets/product-card-related.liquid +78 -0
- package/test-theme/snippets/product-card-simple.liquid +41 -0
- package/test-theme/snippets/product-card.liquid +697 -0
- package/test-theme/snippets/rating.liquid +85 -0
- package/test-theme/snippets/skeleton-collection-grid.liquid +114 -0
- package/test-theme/snippets/skeleton-product-card.liquid +124 -0
- package/test-theme/snippets/skeleton-product-grid.liquid +34 -0
- package/test-theme/snippets/social-sharing.liquid +185 -0
- package/test-theme/templates/account/dashboard.liquid +401 -0
- package/test-theme/templates/account/loyalty-redemption.liquid +405 -0
- package/test-theme/templates/account/loyalty.liquid +588 -0
- package/test-theme/templates/account/order-detail.liquid +230 -0
- package/test-theme/templates/account/orders.liquid +349 -0
- package/test-theme/templates/account/profile.liquid +758 -0
- package/test-theme/templates/account/register.liquid +232 -0
- package/test-theme/templates/account/return-orders.liquid +348 -0
- package/test-theme/templates/account/store-credit.liquid +464 -0
- package/test-theme/templates/account/subscriptions.liquid +601 -0
- package/test-theme/templates/account/wishlist.liquid +419 -0
- package/test-theme/templates/address-book.liquid +1092 -0
- package/test-theme/templates/categories.liquid +452 -0
- package/test-theme/templates/checkout.liquid +4511 -0
- package/test-theme/templates/error.liquid +384 -0
- package/test-theme/templates/index.liquid +11 -0
- package/test-theme/templates/login.liquid +185 -0
- package/test-theme/templates/order-confirmation.liquid +720 -0
- package/test-theme/templates/page.liquid +297 -0
- package/test-theme/templates/product-detail.liquid +4363 -0
- package/test-theme/templates/products.liquid +518 -0
- package/test-theme/templates/search.liquid +922 -0
- package/test-theme/theme.json.example +19 -0
- package/test-theme/widgets/brand-carousel.liquid +676 -0
- package/test-theme/widgets/brand.liquid +245 -0
- package/test-theme/widgets/carousel.liquid +843 -0
- package/test-theme/widgets/category-list-carousel.liquid +656 -0
- package/test-theme/widgets/category-list.liquid +340 -0
- package/test-theme/widgets/category.liquid +475 -0
- package/test-theme/widgets/discount-time.liquid +176 -0
- package/test-theme/widgets/footer-menu.liquid +695 -0
- package/test-theme/widgets/footer.liquid +179 -0
- package/test-theme/widgets/gallery.liquid +271 -0
- package/test-theme/widgets/header-menu.liquid +932 -0
- package/test-theme/widgets/header.liquid +159 -0
- package/test-theme/widgets/html.liquid +214 -0
- package/test-theme/widgets/news.liquid +217 -0
- package/test-theme/widgets/product-canvas.liquid +235 -0
- package/test-theme/widgets/product-carousel.liquid +502 -0
- package/test-theme/widgets/product.liquid +45 -0
- package/test-theme/widgets/recently-viewed.liquid +26 -0
- package/test-theme/widgets/shared/product-grid.liquid +339 -0
- package/test-theme/widgets/simple-product.liquid +42 -0
- package/test-theme/widgets/single-product.liquid +610 -0
- package/test-theme/widgets/spacebar-carousel.liquid +663 -0
- package/test-theme/widgets/spacebar.liquid +279 -0
- package/test-theme/widgets/splash.liquid +378 -0
- 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;
|