@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.
Potentially problematic release.
This version of @o2vend/theme-cli might be problematic. Click here for more details.
- 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,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O2VEND Widget Service
|
|
3
|
+
* Standalone implementation - copied and adapted from webstore solution
|
|
4
|
+
* Update manually when webstore solution changes
|
|
5
|
+
*
|
|
6
|
+
* Source: o2vend-webstore/services/widget-service.js
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const NodeCache = require('node-cache');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Widget Service for O2VEND API
|
|
15
|
+
* Manages widgets for liquid theming and dynamic content
|
|
16
|
+
*/
|
|
17
|
+
class WidgetService {
|
|
18
|
+
constructor(apiClient, options = {}) {
|
|
19
|
+
this.apiClient = apiClient;
|
|
20
|
+
this.themeName = options.theme || 'default';
|
|
21
|
+
this.themePath = options.themePath || process.cwd();
|
|
22
|
+
this.cache = new NodeCache({
|
|
23
|
+
stdTTL: 300, // 5 minutes cache
|
|
24
|
+
checkperiod: 60
|
|
25
|
+
});
|
|
26
|
+
this.widgetTemplateMap = this.loadWidgetTemplateMap();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load widget template mapping from config
|
|
31
|
+
* @returns {Object} widget type to template slug map
|
|
32
|
+
*/
|
|
33
|
+
loadWidgetTemplateMap() {
|
|
34
|
+
try {
|
|
35
|
+
// Try to load from CLI config directory first
|
|
36
|
+
const configPath = path.join(__dirname, '../../config/widget-map.json');
|
|
37
|
+
if (fs.existsSync(configPath)) {
|
|
38
|
+
const fileContent = fs.readFileSync(configPath, 'utf8');
|
|
39
|
+
const map = JSON.parse(fileContent);
|
|
40
|
+
console.log(`[WidgetService] Loaded widget map with ${Object.keys(map).length} entries`);
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
console.warn('[WidgetService] Widget map file not found at:', configPath);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn('[WidgetService] Failed to load widget map:', error.message);
|
|
46
|
+
}
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalize keys recursively from PascalCase to camelCase
|
|
52
|
+
* @param {*} obj - Object to normalize
|
|
53
|
+
* @returns {*} Object with normalized keys
|
|
54
|
+
*/
|
|
55
|
+
normalizeContentKeys(obj) {
|
|
56
|
+
if (obj === null || obj === undefined) {
|
|
57
|
+
return obj;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(obj)) {
|
|
61
|
+
return obj.map(item => this.normalizeContentKeys(item));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (typeof obj === 'object') {
|
|
65
|
+
const normalized = {};
|
|
66
|
+
for (const key in obj) {
|
|
67
|
+
if (obj.hasOwnProperty(key)) {
|
|
68
|
+
const normalizedKey = key.charAt(0).toLowerCase() + key.slice(1);
|
|
69
|
+
normalized[normalizedKey] = this.normalizeContentKeys(obj[key]);
|
|
70
|
+
// Keep original PascalCase for compatibility
|
|
71
|
+
if (key !== normalizedKey) {
|
|
72
|
+
normalized[key] = normalized[normalizedKey];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return obj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Normalize widget object
|
|
84
|
+
* @param {Object} widget
|
|
85
|
+
* @returns {Object}
|
|
86
|
+
*/
|
|
87
|
+
normalizeWidget(widget) {
|
|
88
|
+
if (!widget || typeof widget !== 'object') {
|
|
89
|
+
return widget;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Normalize widget ID
|
|
93
|
+
if (!widget.id) {
|
|
94
|
+
if (widget._id) {
|
|
95
|
+
widget.id = typeof widget._id === 'object' && widget._id.$oid
|
|
96
|
+
? widget._id.$oid
|
|
97
|
+
: String(widget._id);
|
|
98
|
+
} else if (widget.Id) {
|
|
99
|
+
widget.id = String(widget.Id);
|
|
100
|
+
} else if (widget.WidgetInstanceId) {
|
|
101
|
+
widget.id = String(widget.WidgetInstanceId);
|
|
102
|
+
} else {
|
|
103
|
+
widget.id = `${widget.type || 'widget'}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
widget.id = String(widget.id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Initialize settings and data
|
|
110
|
+
if (!widget.settings || typeof widget.settings !== 'object') {
|
|
111
|
+
widget.settings = {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!widget.data || typeof widget.data !== 'object') {
|
|
115
|
+
widget.data = widget.payload || {};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Parse widget.content if it's a JSON string
|
|
119
|
+
let parsedContent = null;
|
|
120
|
+
if (widget.content) {
|
|
121
|
+
if (typeof widget.content === 'string') {
|
|
122
|
+
try {
|
|
123
|
+
parsedContent = JSON.parse(widget.content);
|
|
124
|
+
parsedContent = this.normalizeContentKeys(parsedContent);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn(`[WidgetService] Failed to parse widget.content JSON:`, error.message);
|
|
127
|
+
parsedContent = {};
|
|
128
|
+
}
|
|
129
|
+
} else if (typeof widget.content === 'object') {
|
|
130
|
+
parsedContent = this.normalizeContentKeys(widget.content);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
parsedContent = {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Move Title/Subtitle to settings
|
|
137
|
+
if (widget.Title && !widget.settings.title) {
|
|
138
|
+
widget.settings.title = widget.Title;
|
|
139
|
+
}
|
|
140
|
+
if (widget.title && !widget.settings.title) {
|
|
141
|
+
widget.settings.title = widget.title;
|
|
142
|
+
}
|
|
143
|
+
if (widget.Subtitle && !widget.settings.subtitle) {
|
|
144
|
+
widget.settings.subtitle = widget.Subtitle;
|
|
145
|
+
}
|
|
146
|
+
if (widget.subtitle && !widget.settings.subtitle) {
|
|
147
|
+
widget.settings.subtitle = widget.subtitle;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Store content
|
|
151
|
+
widget.data.content = parsedContent;
|
|
152
|
+
widget.content = parsedContent; // Legacy support
|
|
153
|
+
|
|
154
|
+
// Handle HtmlContent
|
|
155
|
+
if (widget.htmlContent !== undefined && widget.htmlContent !== null && widget.htmlContent !== '') {
|
|
156
|
+
widget.data.htmlContent = widget.htmlContent;
|
|
157
|
+
} else if (widget.HtmlContent !== undefined && widget.HtmlContent !== null && widget.HtmlContent !== '') {
|
|
158
|
+
widget.data.htmlContent = widget.HtmlContent;
|
|
159
|
+
widget.htmlContent = widget.HtmlContent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Set section
|
|
163
|
+
if (!widget.section) {
|
|
164
|
+
widget.section = widget.sectionName || widget.area || 'content';
|
|
165
|
+
}
|
|
166
|
+
widget.section = String(widget.section || 'content').toLowerCase();
|
|
167
|
+
|
|
168
|
+
// Set template path
|
|
169
|
+
widget.template = this.getTemplateSlug(widget.type);
|
|
170
|
+
widget.template_path = `widgets/${widget.template}`;
|
|
171
|
+
widget.handle = `${widget.section}-${widget.template}-${widget.id}`.replace(/\s+/g, '-').toLowerCase();
|
|
172
|
+
|
|
173
|
+
return widget;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get template slug for widget type
|
|
178
|
+
* @param {string} widgetType - Widget type
|
|
179
|
+
* @returns {string} Template slug
|
|
180
|
+
*/
|
|
181
|
+
getTemplateSlug(widgetType) {
|
|
182
|
+
if (!widgetType) return 'default';
|
|
183
|
+
|
|
184
|
+
const slug = this.widgetTemplateMap[widgetType] ||
|
|
185
|
+
this.widgetTemplateMap[widgetType + 'Widget'] ||
|
|
186
|
+
widgetType.toLowerCase().replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '');
|
|
187
|
+
|
|
188
|
+
return slug;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Normalize array of widgets
|
|
193
|
+
* @param {Array} widgets
|
|
194
|
+
* @returns {Array}
|
|
195
|
+
*/
|
|
196
|
+
normalizeWidgetArray(widgets = []) {
|
|
197
|
+
if (!Array.isArray(widgets)) {
|
|
198
|
+
return widgets;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const normalized = widgets.map(widget => this.normalizeWidget(widget));
|
|
202
|
+
|
|
203
|
+
// Sort by Position, order, or priority
|
|
204
|
+
normalized.sort((a, b) => {
|
|
205
|
+
const positionA = a.Position !== undefined ? a.Position : (a.position !== undefined ? a.position : (a.order || a.priority || 0));
|
|
206
|
+
const positionB = b.Position !== undefined ? b.Position : (b.position !== undefined ? b.position : (b.order || b.priority || 0));
|
|
207
|
+
return positionA - positionB;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
return normalized;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get widgets by section
|
|
215
|
+
* @param {string} section - Section name
|
|
216
|
+
* @param {Object} options - Options (pageId, etc.)
|
|
217
|
+
* @returns {Promise<Array>} Widgets array
|
|
218
|
+
*/
|
|
219
|
+
async getWidgetsBySection(section, options = {}) {
|
|
220
|
+
try {
|
|
221
|
+
if (!this.apiClient) {
|
|
222
|
+
// If no API client, return empty array (mock mode will handle)
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const params = {
|
|
227
|
+
section,
|
|
228
|
+
status: 'active',
|
|
229
|
+
...options
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const response = await this.apiClient.getWidgetsBySection(options.pageId, section);
|
|
233
|
+
const widgets = Array.isArray(response) ? response : (response.widgets || []);
|
|
234
|
+
|
|
235
|
+
return this.normalizeWidgetArray(widgets);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
console.warn(`[WidgetService] Error getting widgets for section ${section}:`, error.message);
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get all widgets organized by section
|
|
244
|
+
* @param {Object} options - Options (pageId, etc.)
|
|
245
|
+
* @returns {Promise<Object>} Widgets organized by section
|
|
246
|
+
*/
|
|
247
|
+
async getWidgetsBySections(options = {}) {
|
|
248
|
+
try {
|
|
249
|
+
if (!this.apiClient) {
|
|
250
|
+
return {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const params = {
|
|
254
|
+
status: 'active',
|
|
255
|
+
...options
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const response = await this.apiClient.getWidgets(params);
|
|
259
|
+
const widgets = Array.isArray(response) ? response : (response.widgets || []);
|
|
260
|
+
|
|
261
|
+
const normalized = this.normalizeWidgetArray(widgets);
|
|
262
|
+
|
|
263
|
+
// Organize by section
|
|
264
|
+
const widgetsBySection = {};
|
|
265
|
+
normalized.forEach(widget => {
|
|
266
|
+
const section = widget.section || 'content';
|
|
267
|
+
if (!widgetsBySection[section]) {
|
|
268
|
+
widgetsBySection[section] = [];
|
|
269
|
+
}
|
|
270
|
+
widgetsBySection[section].push(widget);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
return widgetsBySection;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.warn('[WidgetService] Error getting widgets:', error.message);
|
|
276
|
+
return {};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if widget template exists in theme
|
|
282
|
+
* @param {string} widgetType - Widget type
|
|
283
|
+
* @param {string} themePath - Theme path
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
widgetTemplateExists(widgetType, themePath = null) {
|
|
287
|
+
const templateSlug = this.getTemplateSlug(widgetType);
|
|
288
|
+
const widgetPath = path.join(themePath || this.themePath, 'widgets', `${templateSlug}.liquid`);
|
|
289
|
+
return fs.existsSync(widgetPath);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = WidgetService;
|