@o2vend/theme-cli 1.0.32 → 1.0.34
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/lib/lib/dev-server.js +912 -69
- package/lib/lib/liquid-engine.js +83 -11
- package/lib/lib/mock-data.js +45 -7
- package/package.json +1 -1
package/lib/lib/liquid-engine.js
CHANGED
|
@@ -280,13 +280,25 @@ function registerCustomTags(liquid, themePath) {
|
|
|
280
280
|
};
|
|
281
281
|
|
|
282
282
|
// Override the include tag to handle widget and snippet paths
|
|
283
|
+
// CRITICAL: Include tag must parse hash arguments like 'product: product' to match production behavior
|
|
283
284
|
liquid.registerTag('include', {
|
|
284
285
|
parse: function(tagToken, remainTokens) {
|
|
285
|
-
// Parse arguments - handle
|
|
286
|
+
// Parse arguments - handle 'snippets/product-card', product: product format
|
|
286
287
|
const args = tagToken.args.trim();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
288
|
+
|
|
289
|
+
// Split by comma to separate file path from hash parameters
|
|
290
|
+
const commaIndex = args.indexOf(',');
|
|
291
|
+
if (commaIndex > 0) {
|
|
292
|
+
this.fileArg = args.substring(0, commaIndex).trim();
|
|
293
|
+
this.hashArgs = args.substring(commaIndex + 1).trim();
|
|
294
|
+
} else {
|
|
295
|
+
this.fileArg = args;
|
|
296
|
+
this.hashArgs = '';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Extract file path - remove quotes if present
|
|
300
|
+
const fileMatch = this.fileArg.match(/^['"]([^'"]+)['"]/);
|
|
301
|
+
this.file = fileMatch ? fileMatch[1] : this.fileArg.replace(/^['"]|['"]$/g, '');
|
|
290
302
|
},
|
|
291
303
|
render: async function(scope, hash) {
|
|
292
304
|
let filePath = this.file;
|
|
@@ -298,7 +310,7 @@ function registerCustomTags(liquid, themePath) {
|
|
|
298
310
|
// Clean up file path
|
|
299
311
|
filePath = filePath.trim().replace(/^['"]|['"]$/g, '');
|
|
300
312
|
|
|
301
|
-
// Get full context for snippets/widgets
|
|
313
|
+
// Get full context for snippets/widgets - merge parent scope (for shop, settings, etc.)
|
|
302
314
|
const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
|
|
303
315
|
const primaryScope = scopeContexts.length > 0
|
|
304
316
|
? scopeContexts[0]
|
|
@@ -309,12 +321,37 @@ function registerCustomTags(liquid, themePath) {
|
|
|
309
321
|
? { ...currentRenderingContext, ...primaryScope }
|
|
310
322
|
: primaryScope;
|
|
311
323
|
|
|
324
|
+
// CRITICAL: Parse hash arguments if provided (e.g., "product: product")
|
|
325
|
+
// Hash params override parent scope values, matching LiquidJS include behavior
|
|
326
|
+
const includeContext = { ...fullContext };
|
|
327
|
+
if (this.hashArgs) {
|
|
328
|
+
const hashPairs = this.hashArgs.split(',').map(pair => pair.trim());
|
|
329
|
+
for (const pair of hashPairs) {
|
|
330
|
+
const colonIndex = pair.indexOf(':');
|
|
331
|
+
if (colonIndex > 0) {
|
|
332
|
+
const key = pair.substring(0, colonIndex).trim();
|
|
333
|
+
const valueVar = pair.substring(colonIndex + 1).trim();
|
|
334
|
+
if (key && valueVar) {
|
|
335
|
+
// Use scope.get() to resolve the value variable (handles loop variables correctly!)
|
|
336
|
+
try {
|
|
337
|
+
const value = await scope.get(valueVar.split('.'));
|
|
338
|
+
if (value !== undefined && value !== null) {
|
|
339
|
+
includeContext[key] = value;
|
|
340
|
+
}
|
|
341
|
+
} catch (error) {
|
|
342
|
+
// Silently skip unresolved hash params
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
312
349
|
// Handle widget paths
|
|
313
350
|
if (filePath.startsWith('widgets/')) {
|
|
314
351
|
const widgetName = filePath.replace(/^widgets\//, '').replace(/\.liquid$/, '');
|
|
315
352
|
const widgetPath = path.join(themePath, 'widgets', `${widgetName}.liquid`);
|
|
316
353
|
if (fs.existsSync(widgetPath)) {
|
|
317
|
-
return await liquid.parseAndRender(fs.readFileSync(widgetPath, 'utf8'),
|
|
354
|
+
return await liquid.parseAndRender(fs.readFileSync(widgetPath, 'utf8'), includeContext);
|
|
318
355
|
}
|
|
319
356
|
}
|
|
320
357
|
|
|
@@ -323,7 +360,7 @@ function registerCustomTags(liquid, themePath) {
|
|
|
323
360
|
const snippetName = filePath.replace(/^snippets\//, '').replace(/\.liquid$/, '');
|
|
324
361
|
const snippetPath = path.join(themePath, 'snippets', `${snippetName}.liquid`);
|
|
325
362
|
if (fs.existsSync(snippetPath)) {
|
|
326
|
-
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'),
|
|
363
|
+
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), includeContext);
|
|
327
364
|
} else {
|
|
328
365
|
console.warn(`[INCLUDE] Snippet not found: ${snippetPath}`);
|
|
329
366
|
}
|
|
@@ -332,19 +369,19 @@ function registerCustomTags(liquid, themePath) {
|
|
|
332
369
|
// Try to resolve file in snippets directory (default location)
|
|
333
370
|
const snippetPath = path.join(themePath, 'snippets', `${filePath}.liquid`);
|
|
334
371
|
if (fs.existsSync(snippetPath)) {
|
|
335
|
-
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'),
|
|
372
|
+
return await liquid.parseAndRender(fs.readFileSync(snippetPath, 'utf8'), includeContext);
|
|
336
373
|
}
|
|
337
374
|
|
|
338
375
|
// Try direct path
|
|
339
376
|
const resolvedPath = path.join(themePath, filePath);
|
|
340
377
|
if (fs.existsSync(resolvedPath)) {
|
|
341
|
-
return await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'),
|
|
378
|
+
return await liquid.parseAndRender(fs.readFileSync(resolvedPath, 'utf8'), includeContext);
|
|
342
379
|
}
|
|
343
380
|
|
|
344
381
|
// Try with .liquid extension
|
|
345
382
|
const resolvedPathWithExt = `${resolvedPath}.liquid`;
|
|
346
383
|
if (fs.existsSync(resolvedPathWithExt)) {
|
|
347
|
-
return await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'),
|
|
384
|
+
return await liquid.parseAndRender(fs.readFileSync(resolvedPathWithExt, 'utf8'), includeContext);
|
|
348
385
|
}
|
|
349
386
|
|
|
350
387
|
console.warn(`[INCLUDE] File not found: ${filePath} (tried: ${snippetPath}, ${resolvedPath}, ${resolvedPathWithExt})`);
|
|
@@ -686,9 +723,44 @@ async function renderWithLayout(liquid, templatePath, context, themePath) {
|
|
|
686
723
|
categories: context.categories?.length || 0,
|
|
687
724
|
brands: context.brands?.length || 0,
|
|
688
725
|
menus: context.menus?.length || 0,
|
|
689
|
-
cart: context.cart?.itemCount || 0
|
|
726
|
+
cart: context.cart?.itemCount || 0,
|
|
727
|
+
collection: context.collection ? {
|
|
728
|
+
title: context.collection.title || context.collection.name,
|
|
729
|
+
products: context.collection.products?.length || 0
|
|
730
|
+
} : null
|
|
690
731
|
};
|
|
691
732
|
console.log(`[RENDER] ${templatePath} - Context:`, JSON.stringify(contextSummary));
|
|
733
|
+
|
|
734
|
+
// Detailed product logging
|
|
735
|
+
if (context.products && context.products.length > 0) {
|
|
736
|
+
const sampleProduct = context.products[0];
|
|
737
|
+
console.log(`[RENDER] ${templatePath} - Sample product data:`, {
|
|
738
|
+
id: sampleProduct.id,
|
|
739
|
+
title: sampleProduct.title || sampleProduct.name,
|
|
740
|
+
url: sampleProduct.url || sampleProduct.link,
|
|
741
|
+
price: sampleProduct.price || sampleProduct.sellingPrice,
|
|
742
|
+
stock: sampleProduct.stock,
|
|
743
|
+
inStock: sampleProduct.inStock,
|
|
744
|
+
hasImage: !!(sampleProduct.imageUrl || sampleProduct.thumbnailImage1),
|
|
745
|
+
imageUrl: sampleProduct.imageUrl || sampleProduct.thumbnailImage1?.url,
|
|
746
|
+
categoryId: sampleProduct.categoryId
|
|
747
|
+
});
|
|
748
|
+
} else {
|
|
749
|
+
console.warn(`[RENDER] ${templatePath} - ⚠️ No products in context!`);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Check collection products
|
|
753
|
+
if (context.collection && context.collection.products) {
|
|
754
|
+
console.log(`[RENDER] ${templatePath} - Collection products: ${context.collection.products.length}`);
|
|
755
|
+
if (context.collection.products.length > 0) {
|
|
756
|
+
const sample = context.collection.products[0];
|
|
757
|
+
console.log(`[RENDER] ${templatePath} - Sample collection product:`, {
|
|
758
|
+
id: sample.id,
|
|
759
|
+
title: sample.title || sample.name,
|
|
760
|
+
url: sample.url || sample.link
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
692
764
|
|
|
693
765
|
let templateContent = fs.readFileSync(fullTemplatePath, 'utf-8');
|
|
694
766
|
|
package/lib/lib/mock-data.js
CHANGED
|
@@ -65,6 +65,25 @@ function generateMockProducts(count = 20) {
|
|
|
65
65
|
console.log(`[MOCK DATA] Generating ${count} mock products with variations and stock...`);
|
|
66
66
|
const products = [];
|
|
67
67
|
|
|
68
|
+
// Category mapping - map category names to category IDs (matching generateMockCategories order)
|
|
69
|
+
const categoryMap = {
|
|
70
|
+
'Electronics': 'category-1',
|
|
71
|
+
'Clothing': 'category-2',
|
|
72
|
+
'Accessories': 'category-3',
|
|
73
|
+
'Home & Garden': 'category-4',
|
|
74
|
+
'Home': 'category-4', // Alias for Home & Garden
|
|
75
|
+
'Sports & Fitness': 'category-5',
|
|
76
|
+
'Sports': 'category-5', // Alias for Sports & Fitness
|
|
77
|
+
'Books & Media': 'category-6',
|
|
78
|
+
'Toys & Games': 'category-7',
|
|
79
|
+
'Beauty & Health': 'category-8',
|
|
80
|
+
'Automotive': 'category-9',
|
|
81
|
+
'Food & Beverages': 'category-10',
|
|
82
|
+
'Furniture': 'category-4', // Map Furniture to Home & Garden
|
|
83
|
+
'Office': 'category-6', // Map Office to Books & Media (or could be separate)
|
|
84
|
+
'Footwear': 'category-2' // Map Footwear to Clothing
|
|
85
|
+
};
|
|
86
|
+
|
|
68
87
|
// Product data with images from picsum.photos (reliable placeholder service)
|
|
69
88
|
const productData = [
|
|
70
89
|
{ name: 'Wireless Headphones', category: 'Electronics', imageId: 1 },
|
|
@@ -115,7 +134,13 @@ function generateMockProducts(count = 20) {
|
|
|
115
134
|
const productId = `product-${i + 1}`;
|
|
116
135
|
const basePrice = Math.floor(Math.random() * 50000) + 1000; // $10 to $500 (in cents)
|
|
117
136
|
const comparePrice = basePrice * 1.5;
|
|
118
|
-
const
|
|
137
|
+
const productTemplate = productData[i % productData.length];
|
|
138
|
+
const productName = productTemplate.name;
|
|
139
|
+
const productCategory = productTemplate.category;
|
|
140
|
+
const productImageId = productTemplate.imageId;
|
|
141
|
+
|
|
142
|
+
// Get categoryId from category name using categoryMap
|
|
143
|
+
const categoryId = categoryMap[productCategory] || 'category-1'; // Default to Electronics if not found
|
|
119
144
|
|
|
120
145
|
// Determine stock quantity - mix of low stock, high stock, and out of stock
|
|
121
146
|
const stockType = i % 5;
|
|
@@ -278,18 +303,19 @@ function generateMockProducts(count = 20) {
|
|
|
278
303
|
stock: totalStock,
|
|
279
304
|
stockQuantity: totalStock, // Alternative property for stock
|
|
280
305
|
sku: `SKU-${i + 1}`,
|
|
281
|
-
categoryId:
|
|
306
|
+
categoryId: categoryId, // Use mapped categoryId based on product category
|
|
307
|
+
category: productCategory, // Also include category name for reference
|
|
282
308
|
brandId: `brand-${(i % 8) + 1}`,
|
|
283
309
|
// Primary thumbnail for product cards
|
|
284
310
|
thumbnailImage1: {
|
|
285
|
-
url: `https://picsum.photos/seed/${
|
|
311
|
+
url: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
286
312
|
altText: productName
|
|
287
313
|
},
|
|
288
|
-
imageUrl: `https://picsum.photos/seed/${
|
|
314
|
+
imageUrl: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
289
315
|
images: [
|
|
290
316
|
{
|
|
291
317
|
id: `img-${i + 1}-1`,
|
|
292
|
-
url: `https://picsum.photos/seed/${
|
|
318
|
+
url: `https://picsum.photos/seed/${productImageId}/800/800`,
|
|
293
319
|
alt: productName,
|
|
294
320
|
altText: productName,
|
|
295
321
|
width: 800,
|
|
@@ -297,7 +323,7 @@ function generateMockProducts(count = 20) {
|
|
|
297
323
|
},
|
|
298
324
|
{
|
|
299
325
|
id: `img-${i + 1}-2`,
|
|
300
|
-
url: `https://picsum.photos/seed/${
|
|
326
|
+
url: `https://picsum.photos/seed/${productImageId + 100}/800/800`,
|
|
301
327
|
alt: `${productName} - View 2`,
|
|
302
328
|
altText: `${productName} - View 2`,
|
|
303
329
|
width: 800,
|
|
@@ -305,7 +331,7 @@ function generateMockProducts(count = 20) {
|
|
|
305
331
|
},
|
|
306
332
|
{
|
|
307
333
|
id: `img-${i + 1}-3`,
|
|
308
|
-
url: `https://picsum.photos/seed/${
|
|
334
|
+
url: `https://picsum.photos/seed/${productImageId + 200}/800/800`,
|
|
309
335
|
alt: `${productName} - View 3`,
|
|
310
336
|
altText: `${productName} - View 3`,
|
|
311
337
|
width: 800,
|
|
@@ -342,10 +368,22 @@ function generateMockProducts(count = 20) {
|
|
|
342
368
|
const productsWithVariations = products.filter(p => (p.variants?.length || 0) > 1).length;
|
|
343
369
|
const productsWithOptions = products.filter(p => (p.options?.length || 0) > 0).length;
|
|
344
370
|
const outOfStock = products.filter(p => !p.inStock || (p.stock || 0) === 0).length;
|
|
371
|
+
|
|
372
|
+
// Log category distribution
|
|
373
|
+
const categoryDistribution = {};
|
|
374
|
+
products.forEach(p => {
|
|
375
|
+
const cat = p.categoryId || 'unknown';
|
|
376
|
+
categoryDistribution[cat] = (categoryDistribution[cat] || 0) + 1;
|
|
377
|
+
});
|
|
378
|
+
const categorySummary = Object.entries(categoryDistribution)
|
|
379
|
+
.map(([cat, count]) => `${cat}: ${count}`)
|
|
380
|
+
.join(', ');
|
|
381
|
+
|
|
345
382
|
console.log(`[MOCK DATA] ✅ Generated ${products.length} products:`);
|
|
346
383
|
console.log(` - With variations: ${productsWithVariations}`);
|
|
347
384
|
console.log(` - With options: ${productsWithOptions}`);
|
|
348
385
|
console.log(` - Out of stock: ${outOfStock}`);
|
|
386
|
+
console.log(` - Category distribution: ${categorySummary}`);
|
|
349
387
|
|
|
350
388
|
return products;
|
|
351
389
|
}
|