@o2vend/theme-cli 1.0.35 → 1.0.37

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.
@@ -101,63 +101,50 @@ function createLiquidEngine(themePath, options = {}) {
101
101
  * @param {string} themePath - Theme path
102
102
  */
103
103
  function registerCustomTags(liquid, themePath) {
104
- // Register section tag
105
104
  liquid.registerTag('section', {
106
105
  parse: function(tagToken, remainTokens) {
107
106
  this.sectionPath = tagToken.args.trim().replace(/^['"]|['"]$/g, '');
108
107
  },
109
108
  render: async function(scope, hash) {
110
109
  let sectionPath = this.sectionPath;
111
- if (!sectionPath) {
112
- console.error('[SECTION] No section path provided');
113
- return '';
114
- }
115
-
110
+ if (!sectionPath) return '';
111
+
116
112
  try {
117
- // Normalize section path - remove 'sections/' prefix if present
118
- // Handle both 'hero' and 'sections/hero' formats
119
113
  sectionPath = sectionPath.replace(/^sections\//, '').replace(/^sections\\/, '');
120
-
114
+
121
115
  const sectionFile = path.join(themePath, 'sections', `${sectionPath}.liquid`);
122
- if (fs.existsSync(sectionFile)) {
123
- let sectionContent = fs.readFileSync(sectionFile, 'utf8');
124
-
125
- // Pre-process section to convert render_widget filter to tag
126
- const originalContent = sectionContent;
127
- try {
128
- sectionContent = preprocessTemplate(sectionContent);
129
- } catch (error) {
130
- console.error(chalk.red(`[SECTION] ❌ Error preprocessing ${sectionPath}: ${error.message}`));
131
- sectionContent = originalContent;
132
- }
133
-
134
- const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
135
- const primaryScope = scopeContexts.length > 0
136
- ? scopeContexts[0]
137
- : (scope?.environments || scope?.context || {});
138
-
139
- // Merge with current rendering context to ensure all data is available
140
- const fullContext = currentRenderingContext
141
- ? { ...currentRenderingContext, ...primaryScope }
142
- : primaryScope;
143
-
144
- const context = {
145
- ...fullContext,
146
- section: fullContext.section || {}
147
- };
148
-
149
- // Render section
150
- try {
151
- const rendered = await liquid.parseAndRender(sectionContent, context);
152
- return rendered;
153
- } catch (renderError) {
154
- console.error(chalk.red(`[SECTION] ❌ Error rendering ${sectionPath}: ${renderError.message}`));
155
- throw renderError;
156
- }
157
- } else {
116
+ if (!fs.existsSync(sectionFile)) {
158
117
  console.warn(`[SECTION] Section file not found: ${sectionFile}`);
159
118
  return '';
160
119
  }
120
+
121
+ let sectionContent = fs.readFileSync(sectionFile, 'utf8');
122
+
123
+ try {
124
+ sectionContent = preprocessTemplate(sectionContent);
125
+ } catch (e) {
126
+ // Keep original content if preprocessing fails
127
+ }
128
+
129
+ const scopeContexts = Array.isArray(scope?.contexts) ? scope.contexts : [];
130
+ const primaryScope = scopeContexts.length > 0
131
+ ? scopeContexts[0]
132
+ : (scope?.environments || scope?.context || {});
133
+
134
+ const fullContext = currentRenderingContext
135
+ ? { ...currentRenderingContext, ...primaryScope }
136
+ : primaryScope;
137
+
138
+ const context = {
139
+ ...fullContext,
140
+ section: {
141
+ id: sectionPath.replace(/\//g, '-'),
142
+ name: sectionPath.split('/').pop(),
143
+ settings: fullContext.section?.settings || {}
144
+ }
145
+ };
146
+
147
+ return await liquid.parseAndRender(sectionContent, context);
161
148
  } catch (error) {
162
149
  console.error(`[SECTION] Error rendering section ${sectionPath}:`, error.message);
163
150
  return '';
@@ -202,16 +189,30 @@ function registerCustomTags(liquid, themePath) {
202
189
  }
203
190
  });
204
191
 
205
- // Register endschema tag (closing tag for schema)
206
192
  liquid.registerTag('endschema', {
193
+ parse: function(tagToken, remainTokens) {},
194
+ render: async function(scope, hash) { return ''; }
195
+ });
196
+
197
+ liquid.registerTag('cache', {
207
198
  parse: function(tagToken, remainTokens) {
208
- // No-op - just marks the end of schema block
199
+ this.tpls = [];
200
+ const stream = liquid.parser.parseStream(remainTokens);
201
+ stream.on('tag:endcache', () => stream.stop());
202
+ stream.on('template', (tpl) => this.tpls.push(tpl));
203
+ stream.on('end', () => { throw new Error('tag cache not closed'); });
204
+ stream.start();
209
205
  },
210
206
  render: async function(scope, hash) {
211
- return '';
207
+ return liquid.renderer.renderTemplates(this.tpls, scope);
212
208
  }
213
209
  });
214
210
 
211
+ liquid.registerTag('endcache', {
212
+ parse: function() {},
213
+ render: async function() { return ''; }
214
+ });
215
+
215
216
  // Register render_widget tag as an alternative to the filter
216
217
  // This allows templates to use: {% render_widget widget %}
217
218
  liquid.registerTag('render_widget', {
@@ -283,7 +284,6 @@ function registerCustomTags(liquid, themePath) {
283
284
  // CRITICAL: Include tag must parse hash arguments like 'product: product' to match production behavior
284
285
  liquid.registerTag('include', {
285
286
  parse: function(tagToken, remainTokens) {
286
- // Parse arguments - handle 'snippets/product-card', product: product format
287
287
  const args = tagToken.args.trim();
288
288
 
289
289
  // Split by comma to separate file path from hash parameters
@@ -296,12 +296,26 @@ function registerCustomTags(liquid, themePath) {
296
296
  this.hashArgs = '';
297
297
  }
298
298
 
299
+ // Handle "| default:" filter in file argument
300
+ const pipeIndex = this.fileArg.indexOf('|');
301
+ if (pipeIndex > 0) {
302
+ const filterPart = this.fileArg.substring(pipeIndex + 1).trim();
303
+ this.fileArg = this.fileArg.substring(0, pipeIndex).trim();
304
+ const defaultMatch = filterPart.match(/^default:\s*['"]([^'"]+)['"]/);
305
+ this.defaultFile = defaultMatch ? defaultMatch[1] : null;
306
+ } else {
307
+ this.defaultFile = null;
308
+ }
309
+
299
310
  // Extract file path - remove quotes if present
300
311
  const fileMatch = this.fileArg.match(/^['"]([^'"]+)['"]/);
301
312
  this.file = fileMatch ? fileMatch[1] : this.fileArg.replace(/^['"]|['"]$/g, '');
302
313
  },
303
314
  render: async function(scope, hash) {
304
315
  let filePath = this.file;
316
+ if (!filePath && this.defaultFile) {
317
+ filePath = this.defaultFile;
318
+ }
305
319
  if (!filePath) {
306
320
  console.warn('[INCLUDE] No file path provided');
307
321
  return '';
@@ -392,8 +406,6 @@ function registerCustomTags(liquid, themePath) {
392
406
  // Override the render tag (similar to include but with isolated scope)
393
407
  liquid.registerTag('render', {
394
408
  parse: function(tagToken, remainTokens) {
395
- // Parse the tag arguments
396
- // Format: {% render 'path' %} or {% render variable.path, param: value %}
397
409
  const args = tagToken.args.trim();
398
410
 
399
411
  // Split by comma to separate file path from hash parameters
@@ -405,21 +417,28 @@ function registerCustomTags(liquid, themePath) {
405
417
  this.fileArg = args;
406
418
  this.hashArgs = '';
407
419
  }
420
+
421
+ // Handle "| default:" filter on the file argument
422
+ // e.g., footer_menu_widget.template_path | default: 'widgets/footer-menu'
423
+ this.defaultPath = null;
424
+ const pipeIndex = this.fileArg.indexOf('|');
425
+ if (pipeIndex > 0) {
426
+ const filterPart = this.fileArg.substring(pipeIndex + 1).trim();
427
+ this.fileArg = this.fileArg.substring(0, pipeIndex).trim();
428
+ const defaultMatch = filterPart.match(/^default:\s*['"]([^'"]+)['"]/);
429
+ if (defaultMatch) {
430
+ this.defaultPath = defaultMatch[1];
431
+ }
432
+ }
408
433
  },
409
434
  render: async function(scope, hash) {
410
- // RECURSION GUARD: Check render depth to prevent infinite loops
411
435
  currentRenderDepth++;
412
436
  if (currentRenderDepth > MAX_RENDER_DEPTH) {
413
- console.error(chalk.red(`[RENDER TAG] Maximum render depth (${MAX_RENDER_DEPTH}) exceeded - possible infinite loop`));
414
- console.error(chalk.red(`[RENDER TAG] Current rendering stack: ${Array.from(renderingStack).join(' -> ')}`));
437
+ console.error(chalk.red(`[RENDER TAG] Maximum render depth (${MAX_RENDER_DEPTH}) exceeded`));
415
438
  currentRenderDepth--;
416
439
  return `<div class="widget-error">Error: Maximum render depth exceeded</div>`;
417
440
  }
418
441
 
419
- // CRITICAL: Use scope.get() to properly resolve variables from the LiquidJS scope chain
420
- // This correctly handles loop variables like 'widget' in {% for widget in widgets.hero %}
421
-
422
- // Build context starting with currentRenderingContext
423
442
  let fullContext = {};
424
443
  if (currentRenderingContext) {
425
444
  fullContext = { ...currentRenderingContext };
@@ -428,24 +447,28 @@ function registerCustomTags(liquid, themePath) {
428
447
  // Resolve file path - could be a string literal or a variable
429
448
  let filePath = this.fileArg;
430
449
 
431
- // If it's wrapped in quotes, it's a string literal
432
450
  if ((filePath.startsWith('"') && filePath.endsWith('"')) ||
433
451
  (filePath.startsWith("'") && filePath.endsWith("'"))) {
434
452
  filePath = filePath.slice(1, -1);
435
453
  } else {
436
- // It's a variable - use scope.get() to resolve it properly (this handles loop variables!)
454
+ // Variable resolution with | default: fallback support
437
455
  try {
438
- // For paths like 'widget.template_path', we need to resolve via scope.get()
439
456
  const resolvedValue = await scope.get(filePath.split('.'));
440
457
  if (resolvedValue && typeof resolvedValue === 'string') {
441
458
  filePath = resolvedValue;
459
+ } else if (this.defaultPath) {
460
+ filePath = this.defaultPath;
442
461
  } else {
443
462
  currentRenderDepth--;
444
463
  return '';
445
464
  }
446
465
  } catch (error) {
447
- currentRenderDepth--;
448
- return '';
466
+ if (this.defaultPath) {
467
+ filePath = this.defaultPath;
468
+ } else {
469
+ currentRenderDepth--;
470
+ return '';
471
+ }
449
472
  }
450
473
  }
451
474
 
@@ -741,8 +764,8 @@ async function renderWithLayout(liquid, templatePath, context, themePath) {
741
764
  price: sampleProduct.price || sampleProduct.sellingPrice,
742
765
  stock: sampleProduct.stock,
743
766
  inStock: sampleProduct.inStock,
744
- hasImage: !!(sampleProduct.imageUrl || sampleProduct.thumbnailImage1),
745
- imageUrl: sampleProduct.imageUrl || sampleProduct.thumbnailImage1?.url,
767
+ hasImage: !!(sampleProduct.imageUrl || sampleProduct.thumbnailImage),
768
+ imageUrl: sampleProduct.imageUrl || sampleProduct.thumbnailImage,
746
769
  categoryId: sampleProduct.categoryId
747
770
  });
748
771
  } else {
@@ -528,25 +528,22 @@ class LiquidHelperService {
528
528
  return `<img src="${input}" alt="${alt}" ${attributes}>`;
529
529
  }
530
530
 
531
- // Product filters
532
531
  productUrlFilter(product) {
533
532
  if (!product) return '#';
534
- // Use handle, slug, or id - ensure it's a string and handle URL encoding
535
- const identifier = product.handle || product.slug || product.id;
536
- if (!identifier) return '#';
537
- // Ensure it's a string and remove any leading slashes
538
- const handle = String(identifier).replace(/^\//, '');
539
- return `/products/${handle}`;
533
+ const slug = product.slug || product.handle || product.id;
534
+ return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
540
535
  }
541
536
 
542
537
  collectionUrlFilter(collection) {
543
538
  if (!collection) return '#';
544
- return `/collections/${collection.slug || collection.handle || collection.id}`;
539
+ const slug = collection.slug || collection.handle || collection.id;
540
+ return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
545
541
  }
546
542
 
547
543
  pageUrlFilter(page) {
548
544
  if (!page) return '#';
549
- return `/pages/${page.slug || page.handle || page.id}`;
545
+ const slug = page.slug || page.handle || page.id;
546
+ return slug ? `/${String(slug).replace(/^\//, '')}` : '#';
550
547
  }
551
548
 
552
549
  // Utility filters
@@ -591,26 +588,10 @@ class LiquidHelperService {
591
588
  */
592
589
  assetUrlFilter(input) {
593
590
  if (!input || typeof input !== 'string') return input;
594
-
595
- // Remove .min extension if present
596
- let url = input.replace(/\.min\.(css|js|jpg|jpeg|png|gif|svg|webp)$/, (match, ext) => `.${ext}`);
597
-
598
- // Remove /themes/default/ or /themes/theme-name/ prefixes if present
599
- // Handle both "/themes/default/assets/logo.png" and "themes/default/assets/logo.png"
600
- url = url.replace(/^\/?themes\/[^\/]+\//, '/');
601
-
602
- // Extract just the filename if path contains assets
603
- // Handle paths like "themes/default/assets/logo.png" -> "/assets/logo.png"
604
- // or "/themes/default/assets/logo.png" -> "/assets/logo.png"
605
- const assetsMatch = url.match(/assets\/(.+)$/);
606
- if (assetsMatch) {
607
- url = '/assets/' + assetsMatch[1];
608
- } else if (!url.startsWith('/assets/') && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('data:')) {
609
- // Ensure /assets/ prefix if not already present (skip if already absolute URL or data URI)
610
- url = '/assets/' + url.replace(/^\/+/, '');
611
- }
612
-
613
- return url;
591
+
592
+ if (input.startsWith('/assets/')) return input;
593
+
594
+ return `/assets/${input}`;
614
595
  }
615
596
 
616
597
  /**
@@ -66,22 +66,10 @@ class MockApiServer {
66
66
  // Paginate
67
67
  const paginated = products.slice(offset, offset + limit);
68
68
 
69
- // DEBUG: Verify products have stock/quantity before sending
70
69
  if (paginated.length > 0) {
71
- const sampleProduct = paginated[0];
72
- const hasStock = 'stock' in sampleProduct;
73
- // Use nullish coalescing to show 0 values correctly
74
- const stockValue = sampleProduct.stock ?? 'N/A';
70
+ const sp = paginated[0];
75
71
  console.log(`[MOCK API] Returning ${paginated.length} products (total: ${products.length})`);
76
- console.log(`[MOCK API DEBUG] Sample product - Title: ${sampleProduct.title}, Has stock field: ${hasStock}, Stock value: ${stockValue}`);
77
- console.log(`[MOCK API DEBUG] Sample product stock details: stock=${sampleProduct.stock}, inStock=${sampleProduct.inStock}`);
78
- if (sampleProduct.variants && sampleProduct.variants.length > 0) {
79
- const sampleVariant = sampleProduct.variants[0];
80
- const variantHasStock = 'stock' in sampleVariant;
81
- const variantStockValue = sampleVariant.stock ?? 'N/A';
82
- console.log(`[MOCK API DEBUG] Sample variant - Has stock field: ${variantHasStock}, Stock value: ${variantStockValue}`);
83
- console.log(`[MOCK API DEBUG] Sample variant stock details: stock=${sampleVariant.stock}, inStock=${sampleVariant.inStock}, available=${sampleVariant.available}`);
84
- }
72
+ console.log(`[MOCK API] Sample: ${sp.title}, stockQuantity=${sp.stockQuantity}, inStock=${sp.inStock}`);
85
73
  }
86
74
 
87
75
  res.json({
@@ -94,7 +82,10 @@ class MockApiServer {
94
82
 
95
83
  // Product by ID
96
84
  this.app.get('/shopfront/api/v2/products/:id', (req, res) => {
97
- const product = this.mockData.products.find(p => p.id === req.params.id);
85
+ const id = req.params.id;
86
+ const product = this.mockData.products.find(p =>
87
+ String(p.id) === id || String(p.productId) === id || p.slug === id || p.handle === id
88
+ );
98
89
  if (product) {
99
90
  res.json(product);
100
91
  } else {
@@ -138,7 +129,10 @@ class MockApiServer {
138
129
 
139
130
  // Category by ID
140
131
  this.app.get('/shopfront/api/v2/categories/:id', (req, res) => {
141
- const category = this.mockData.categories.find(c => c.id === req.params.id);
132
+ const id = req.params.id;
133
+ const category = this.mockData.categories.find(c =>
134
+ String(c.id) === id || String(c.categoryId) === id || c.slug === id || c.handle === id
135
+ );
142
136
  if (category) {
143
137
  res.json(category);
144
138
  } else {
@@ -165,17 +159,16 @@ class MockApiServer {
165
159
 
166
160
  let widgets = [...this.mockData.widgets];
167
161
 
168
- // Filter by section
162
+ if (pageId) {
163
+ widgets = widgets.filter(w => w.pageId === 'all' || w.pageId === pageId);
164
+ }
169
165
  if (section) {
170
166
  widgets = widgets.filter(w => w.section === section || w.sectionName === section);
171
167
  }
172
168
 
173
- // Filter by status (default to 'active' if not specified)
174
169
  const filterStatus = status || 'active';
175
170
  widgets = widgets.filter(w => w.status === filterStatus);
176
-
177
- // Sort by position
178
- widgets.sort((a, b) => (a.position || 0) - (b.position || 0));
171
+ widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
179
172
 
180
173
  res.json({
181
174
  widgets: widgets,
@@ -189,17 +182,16 @@ class MockApiServer {
189
182
 
190
183
  let widgets = [...this.mockData.widgets];
191
184
 
192
- // Filter by section
185
+ if (pageId) {
186
+ widgets = widgets.filter(w => w.pageId === 'all' || w.pageId === pageId);
187
+ }
193
188
  if (section) {
194
189
  widgets = widgets.filter(w => w.section === section || w.sectionName === section);
195
190
  }
196
191
 
197
- // Filter by status (default to 'active' if not specified)
198
192
  const filterStatus = status || 'active';
199
193
  widgets = widgets.filter(w => w.status === filterStatus);
200
-
201
- // Sort by position
202
- widgets.sort((a, b) => (a.position || 0) - (b.position || 0));
194
+ widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
203
195
 
204
196
  res.json({
205
197
  widgets: widgets,
@@ -207,6 +199,21 @@ class MockApiServer {
207
199
  });
208
200
  });
209
201
 
202
+ // Page-section widgets endpoint (matches production: POST /pages/{pageId}/sections/{section}/widgets)
203
+ this.app.post('/pages/:pageId/sections/:section/widgets', (req, res) => {
204
+ const { pageId, section } = req.params;
205
+
206
+ let widgets = this.mockData.widgets.filter(w => {
207
+ const matchesPage = w.pageId === 'all' || w.pageId === pageId;
208
+ const matchesSection = w.section === section || w.sectionName === section;
209
+ const isActive = w.status === 'active';
210
+ return matchesPage && matchesSection && isActive;
211
+ });
212
+
213
+ widgets.sort((a, b) => (a.Position || a.position || 0) - (b.Position || b.position || 0));
214
+ res.json(widgets);
215
+ });
216
+
210
217
  // Cart endpoints
211
218
  this.app.get('/shopfront/api/v2/cart/:cartId', (req, res) => {
212
219
  res.json(this.mockData.cart);
@@ -239,86 +246,35 @@ class MockApiServer {
239
246
  {
240
247
  id: 'main-menu',
241
248
  name: 'Main Menu',
242
- type: 'Main Menu', // Type field for filtering
249
+ type: 'Main Menu',
243
250
  items: [
244
- {
245
- id: '1',
246
- name: 'Home',
247
- title: 'Home',
248
- link: '/',
249
- url: '/',
250
- displayOrder: 1,
251
- childItems: []
252
- },
253
- {
254
- id: '2',
255
- name: 'Products',
256
- title: 'Products',
257
- link: '/products',
258
- url: '/products',
259
- displayOrder: 2,
260
- childItems: [
261
- { id: '2-1', name: 'Electronics', title: 'Electronics', link: '/products?category=electronics', url: '/products?category=electronics', displayOrder: 1 },
262
- { id: '2-2', name: 'Clothing', title: 'Clothing', link: '/products?category=clothing', url: '/products?category=clothing', displayOrder: 2 },
263
- { id: '2-3', name: 'Accessories', title: 'Accessories', link: '/products?category=accessories', url: '/products?category=accessories', displayOrder: 3 },
264
- { id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/products?category=home-garden', url: '/products?category=home-garden', displayOrder: 4 }
265
- ]
266
- },
267
- {
268
- id: '3',
269
- name: 'Collections',
270
- title: 'Collections',
271
- link: '/collections',
272
- url: '/collections',
273
- displayOrder: 3,
251
+ { id: '1', name: 'Home', title: 'Home', link: '/', displayOrder: 1, childItems: [] },
252
+ {
253
+ id: '2', name: 'Products', title: 'Products', link: '/categories', displayOrder: 2,
274
254
  childItems: [
275
- { id: '3-1', name: 'New Arrivals', title: 'New Arrivals', link: '/collections/new-arrivals', url: '/collections/new-arrivals', displayOrder: 1 },
276
- { id: '3-2', name: 'Best Sellers', title: 'Best Sellers', link: '/collections/best-sellers', url: '/collections/best-sellers', displayOrder: 2 },
277
- { id: '3-3', name: 'On Sale', title: 'On Sale', link: '/collections/sale', url: '/collections/sale', displayOrder: 3 }
255
+ { id: '2-1', name: 'Electronics', title: 'Electronics', link: '/electronics', displayOrder: 1 },
256
+ { id: '2-2', name: 'Clothing', title: 'Clothing', link: '/clothing', displayOrder: 2 },
257
+ { id: '2-3', name: 'Accessories', title: 'Accessories', link: '/accessories', displayOrder: 3 },
258
+ { id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/home-and-garden', displayOrder: 4 }
278
259
  ]
279
260
  },
280
- {
281
- id: '4',
282
- name: 'About',
283
- title: 'About',
284
- link: '/page/about',
285
- url: '/page/about',
286
- displayOrder: 4,
287
- childItems: []
288
- },
289
- {
290
- id: '5',
291
- name: 'Contact',
292
- title: 'Contact',
293
- link: '/page/contact',
294
- url: '/page/contact',
295
- displayOrder: 5,
296
- childItems: []
297
- }
261
+ { id: '3', name: 'About', title: 'About', link: '/about', displayOrder: 3, childItems: [] },
262
+ { id: '4', name: 'Contact', title: 'Contact', link: '/contact', displayOrder: 4, childItems: [] }
298
263
  ]
299
264
  },
300
265
  {
301
266
  id: 'footer-menu',
302
267
  name: 'Footer Menu',
303
- type: 'Footer Menu', // Type field for filtering
268
+ type: 'Footer Menu',
304
269
  items: [
305
- { id: '6', name: 'Privacy Policy', title: 'Privacy Policy', link: '/pages/privacy', url: '/pages/privacy', displayOrder: 1 },
306
- { id: '7', name: 'Terms of Service', title: 'Terms of Service', link: '/pages/terms', url: '/pages/terms', displayOrder: 2 },
307
- { id: '8', name: 'Shipping', title: 'Shipping', link: '/pages/shipping', url: '/pages/shipping', displayOrder: 3 },
308
- { id: '9', name: 'Returns', title: 'Returns', link: '/pages/returns', url: '/pages/returns', displayOrder: 4 }
270
+ { id: '6', name: 'Privacy Policy', title: 'Privacy Policy', link: '/privacy', displayOrder: 1 },
271
+ { id: '7', name: 'Terms of Service', title: 'Terms of Service', link: '/terms', displayOrder: 2 },
272
+ { id: '8', name: 'Shipping', title: 'Shipping', link: '/shipping', displayOrder: 3 },
273
+ { id: '9', name: 'Returns', title: 'Returns', link: '/returns', displayOrder: 4 }
309
274
  ]
310
275
  }
311
276
  ];
312
-
313
- // Support both array format (for frontend) and object format (for backward compatibility)
314
- if (req.query.format === 'object') {
315
- res.json({
316
- success: true,
317
- data: { menus }
318
- });
319
- } else {
320
- res.json(menus);
321
- }
277
+ res.json(menus);
322
278
  });
323
279
 
324
280
  this.app.get('/webstoreapi/menus/:menuId', (req, res) => {
@@ -329,60 +285,18 @@ class MockApiServer {
329
285
  name: 'Main Menu',
330
286
  type: 'Main Menu',
331
287
  items: [
332
- {
333
- id: '1',
334
- name: 'Home',
335
- title: 'Home',
336
- link: '/',
337
- url: '/',
338
- displayOrder: 1,
339
- childItems: []
340
- },
341
- {
342
- id: '2',
343
- name: 'Products',
344
- title: 'Products',
345
- link: '/products',
346
- url: '/products',
347
- displayOrder: 2,
288
+ { id: '1', name: 'Home', title: 'Home', link: '/', displayOrder: 1, childItems: [] },
289
+ {
290
+ id: '2', name: 'Products', title: 'Products', link: '/categories', displayOrder: 2,
348
291
  childItems: [
349
- { id: '2-1', name: 'Electronics', title: 'Electronics', link: '/products?category=electronics', url: '/products?category=electronics', displayOrder: 1 },
350
- { id: '2-2', name: 'Clothing', title: 'Clothing', link: '/products?category=clothing', url: '/products?category=clothing', displayOrder: 2 },
351
- { id: '2-3', name: 'Accessories', title: 'Accessories', link: '/products?category=accessories', url: '/products?category=accessories', displayOrder: 3 },
352
- { id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/products?category=home-garden', url: '/products?category=home-garden', displayOrder: 4 }
292
+ { id: '2-1', name: 'Electronics', title: 'Electronics', link: '/electronics', displayOrder: 1 },
293
+ { id: '2-2', name: 'Clothing', title: 'Clothing', link: '/clothing', displayOrder: 2 },
294
+ { id: '2-3', name: 'Accessories', title: 'Accessories', link: '/accessories', displayOrder: 3 },
295
+ { id: '2-4', name: 'Home & Garden', title: 'Home & Garden', link: '/home-and-garden', displayOrder: 4 }
353
296
  ]
354
297
  },
355
- {
356
- id: '3',
357
- name: 'Collections',
358
- title: 'Collections',
359
- link: '/collections',
360
- url: '/collections',
361
- displayOrder: 3,
362
- childItems: [
363
- { id: '3-1', name: 'New Arrivals', title: 'New Arrivals', link: '/collections/new-arrivals', url: '/collections/new-arrivals', displayOrder: 1 },
364
- { id: '3-2', name: 'Best Sellers', title: 'Best Sellers', link: '/collections/best-sellers', url: '/collections/best-sellers', displayOrder: 2 },
365
- { id: '3-3', name: 'On Sale', title: 'On Sale', link: '/collections/sale', url: '/collections/sale', displayOrder: 3 }
366
- ]
367
- },
368
- {
369
- id: '4',
370
- name: 'About',
371
- title: 'About',
372
- link: '/page/about',
373
- url: '/page/about',
374
- displayOrder: 4,
375
- childItems: []
376
- },
377
- {
378
- id: '5',
379
- name: 'Contact',
380
- title: 'Contact',
381
- link: '/page/contact',
382
- url: '/page/contact',
383
- displayOrder: 5,
384
- childItems: []
385
- }
298
+ { id: '3', name: 'About', title: 'About', link: '/about', displayOrder: 3, childItems: [] },
299
+ { id: '4', name: 'Contact', title: 'Contact', link: '/contact', displayOrder: 4, childItems: [] }
386
300
  ]
387
301
  },
388
302
  'footer-menu': {