@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.
@@ -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 both 'snippets/pagination' and 'snippets/pagination', pagination formats
286
+ // Parse arguments - handle 'snippets/product-card', product: product format
286
287
  const args = tagToken.args.trim();
287
- // Extract file path (first argument before comma if present)
288
- const fileMatch = args.match(/^['"]([^'"]+)['"]/);
289
- this.file = fileMatch ? fileMatch[1] : args.split(',')[0].trim().replace(/^['"]|['"]$/g, '');
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'), fullContext);
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'), fullContext);
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'), fullContext);
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'), fullContext);
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'), fullContext);
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
 
@@ -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 productName = productNames[i % productNames.length];
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: `category-${(i % 10) + 1}`,
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/${productData[i % productData.length].imageId}/800/800`,
311
+ url: `https://picsum.photos/seed/${productImageId}/800/800`,
286
312
  altText: productName
287
313
  },
288
- imageUrl: `https://picsum.photos/seed/${productData[i % productData.length].imageId}/800/800`,
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/${productData[i % productData.length].imageId}/800/800`,
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/${productData[i % productData.length].imageId + 100}/800/800`,
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/${productData[i % productData.length].imageId + 200}/800/800`,
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o2vend/theme-cli",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "O2VEND Theme Development CLI - Standalone tool for local theme development",
5
5
  "bin": {
6
6
  "o2vend": "./bin/o2vend"