@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.
@@ -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>&copy; {{ '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;