@o2vend/theme-cli 1.0.33 → 1.0.35
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 +916 -347
- package/lib/lib/liquid-engine.js +83 -11
- package/lib/lib/webstoreapi-fetcher.js +322 -0
- package/package.json +16 -16
- package/test-theme/layout/theme.liquid +195 -195
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
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebstoreAPI Fetcher (Real Mode)
|
|
3
|
+
* Thin HTTP client for public /webstoreapi/* endpoints only.
|
|
4
|
+
* No webstore internals, business logic, or service abstractions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const axios = require('axios');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SECTIONS = ['hero', 'products', 'footer', 'content', 'header'];
|
|
10
|
+
const TIMEOUT = parseInt(process.env.O2VEND_API_TIMEOUT, 10) || 10000;
|
|
11
|
+
const DEBUG = process.env.DEBUG_WEBSTOREAPI === 'true' || process.env.DEBUG_WEBSTOREAPI === '1';
|
|
12
|
+
|
|
13
|
+
function logRequest(method, path, status, extra = {}) {
|
|
14
|
+
const msg = `[webstoreapi] ${method} ${path} ${status}`;
|
|
15
|
+
if (DEBUG || status >= 400) {
|
|
16
|
+
const parts = [msg];
|
|
17
|
+
if (extra.bodySnippet) parts.push(extra.bodySnippet);
|
|
18
|
+
if (extra.headers) parts.push(JSON.stringify(extra.headers));
|
|
19
|
+
console.log(parts.join(' '));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function truncate(v, max = 300) {
|
|
24
|
+
const s = typeof v === 'string' ? v : JSON.stringify(v);
|
|
25
|
+
return s.length <= max ? s : s.slice(0, max) + '...';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Derive webstoreapi base URL from O2VEND_API_BASE_URL.
|
|
30
|
+
* Strips /shopfront/api/v2 if present.
|
|
31
|
+
* @returns {string} e.g. https://sareesdemo.myo2vend.com/webstoreapi
|
|
32
|
+
*/
|
|
33
|
+
function getWebstoreapiBaseUrl() {
|
|
34
|
+
let base = process.env.O2VEND_API_BASE_URL || '';
|
|
35
|
+
base = base.replace(/\/$/, '');
|
|
36
|
+
base = base.replace(/\/shopfront\/api\/v2\/?$/i, '');
|
|
37
|
+
base = base.replace(/\/webstoreapi\/?$/i, '');
|
|
38
|
+
return base ? `${base}/webstoreapi` : '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Derive shopfront API base URL from O2VEND_API_BASE_URL.
|
|
43
|
+
* Used only for GET /storeinfo (store logo, favicon, name).
|
|
44
|
+
* @returns {string} e.g. https://sareesdemo.myo2vend.com/shopfront/api/v2
|
|
45
|
+
*/
|
|
46
|
+
function getShopfrontBaseUrl() {
|
|
47
|
+
let base = process.env.O2VEND_API_BASE_URL || '';
|
|
48
|
+
base = base.replace(/\/$/, '');
|
|
49
|
+
base = base.replace(/\/webstoreapi\/?$/i, '');
|
|
50
|
+
if (/\/shopfront\/api\/v2\/?$/i.test(base)) return base.replace(/\/$/, '');
|
|
51
|
+
base = base.replace(/\/shopfront\/api\/v2\/?$/i, '');
|
|
52
|
+
return base ? `${base}/shopfront/api/v2` : '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fetch store info from shopfront API (GET /storeinfo).
|
|
57
|
+
* Used in real mode for shop.logo, shop.favicon, shop.name.
|
|
58
|
+
* Requires O2VEND_API_KEY. On failure, returns null.
|
|
59
|
+
* @returns {Promise<{ logoUrl?: string, favouriteIconUrl?: string, name?: string, settings?: object } | null>}
|
|
60
|
+
*/
|
|
61
|
+
async function fetchStoreInfo() {
|
|
62
|
+
const base = getShopfrontBaseUrl();
|
|
63
|
+
const apiKey = process.env.O2VEND_API_KEY;
|
|
64
|
+
if (!base || !apiKey) return null;
|
|
65
|
+
const client = axios.create({
|
|
66
|
+
baseURL: base,
|
|
67
|
+
timeout: TIMEOUT,
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'X-O2VEND-SHOPFRONT-API-KEY': apiKey,
|
|
71
|
+
'User-Agent': 'O2VEND-Theme-CLI/1.0'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
try {
|
|
75
|
+
const res = await client.get('/storeinfo');
|
|
76
|
+
const data = res.data || {};
|
|
77
|
+
const logoUrl = data.logoUrl ?? data.logourl;
|
|
78
|
+
const favouriteIconUrl = data.favouriteIconUrl ?? data.favouriteiconurl;
|
|
79
|
+
const name = data.name ?? data.Name;
|
|
80
|
+
const settings = data.settings || data.Settings || {};
|
|
81
|
+
return {
|
|
82
|
+
logoUrl: logoUrl || null,
|
|
83
|
+
favouriteIconUrl: favouriteIconUrl || null,
|
|
84
|
+
name: name || null,
|
|
85
|
+
settings
|
|
86
|
+
};
|
|
87
|
+
} catch (e) {
|
|
88
|
+
if (DEBUG) console.log('[webstoreapi] GET /storeinfo failed:', e.response?.status || e.message);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create axios instance for webstoreapi calls.
|
|
95
|
+
* @param {Object} [opts] - { headers: { Cookie } }
|
|
96
|
+
* @returns {{ baseUrl: string, get: Function, post: Function }}
|
|
97
|
+
*/
|
|
98
|
+
function createFetcher(opts = {}) {
|
|
99
|
+
const baseUrl = getWebstoreapiBaseUrl();
|
|
100
|
+
const client = axios.create({
|
|
101
|
+
baseURL: baseUrl,
|
|
102
|
+
timeout: TIMEOUT,
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'User-Agent': 'O2VEND-Theme-CLI/1.0',
|
|
106
|
+
...(opts.headers || {})
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
client.interceptors.response.use(
|
|
111
|
+
(r) => {
|
|
112
|
+
const method = (r.config?.method || 'get').toUpperCase();
|
|
113
|
+
const path = r.config?.url || r.config?.baseURL || '';
|
|
114
|
+
logRequest(method, path, r.status);
|
|
115
|
+
return r;
|
|
116
|
+
},
|
|
117
|
+
(err) => {
|
|
118
|
+
const method = (err.config?.method || 'get').toUpperCase();
|
|
119
|
+
const path = err.config?.url || err.config?.baseURL || '';
|
|
120
|
+
const status = err.response?.status;
|
|
121
|
+
const data = err.response?.data;
|
|
122
|
+
const bodySnippet = data != null ? truncate(data) : err.message;
|
|
123
|
+
logRequest(method, path, status || 'ERR', { bodySnippet });
|
|
124
|
+
if (status >= 400 && typeof data === 'object' && data.message) {
|
|
125
|
+
console.log(`[webstoreapi] error message: ${data.message}`);
|
|
126
|
+
}
|
|
127
|
+
return Promise.reject(err);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
baseUrl,
|
|
133
|
+
get: (path, config = {}) => client.get(path, config).then((r) => r.data),
|
|
134
|
+
post: (path, body, config = {}) => client.post(path, body, config).then((r) => r.data)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convert products query (POST-style) to query string for GET /webstoreapi/products.
|
|
140
|
+
* @param {Object} p - { limit, offset, filter, searchText, ... }
|
|
141
|
+
* @returns {string}
|
|
142
|
+
*/
|
|
143
|
+
function productsQueryToParams(p) {
|
|
144
|
+
const q = new URLSearchParams();
|
|
145
|
+
const limit = p.limit != null ? parseInt(p.limit, 10) : 20;
|
|
146
|
+
const page = p.page != null ? parseInt(p.page, 10) : (p.offset != null ? Math.floor(parseInt(p.offset, 10) / limit) + 1 : 1);
|
|
147
|
+
q.set('page', String(Math.max(1, page)));
|
|
148
|
+
q.set('limit', String(Math.max(1, limit)));
|
|
149
|
+
if (p.searchText) q.set('searchText', p.searchText);
|
|
150
|
+
const cat = p.categoryId || (p.filter && p.filter.category) || '';
|
|
151
|
+
const brand = p.brandId || (p.filter && p.filter.brand) || '';
|
|
152
|
+
q.set('filter[category]', String(cat));
|
|
153
|
+
q.set('filter[brand]', String(brand));
|
|
154
|
+
if (p.sort && typeof p.sort === 'object' && p.sort.field) {
|
|
155
|
+
q.set('sort[field]', p.sort.field);
|
|
156
|
+
}
|
|
157
|
+
return q.toString();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Convert categories query to query string for GET /webstoreapi/categories.
|
|
162
|
+
* @param {Object} p - { limit, offset }
|
|
163
|
+
* @returns {string}
|
|
164
|
+
*/
|
|
165
|
+
function categoriesQueryToParams(p) {
|
|
166
|
+
const q = new URLSearchParams();
|
|
167
|
+
if (p.limit != null) q.set('limit', String(p.limit));
|
|
168
|
+
if (p.offset != null) q.set('offset', String(p.offset));
|
|
169
|
+
return q.toString();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Fetch products. GET /webstoreapi/products?...
|
|
174
|
+
* @param {Object} params - { limit, offset, filter, searchText, ... }
|
|
175
|
+
* @param {Object} [opts] - { headers: { Cookie } }
|
|
176
|
+
* @returns {Promise<{ products?: Array, Products?: Array, totalCount?: number, TotalCount?: number }>}
|
|
177
|
+
*/
|
|
178
|
+
async function fetchProducts(params = {}, opts = {}) {
|
|
179
|
+
const { get } = createFetcher(opts);
|
|
180
|
+
const qs = productsQueryToParams(params);
|
|
181
|
+
const path = qs ? `/products?${qs}` : '/products';
|
|
182
|
+
return get(path);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Fetch product by ID. GET /webstoreapi/products/:productId
|
|
187
|
+
* @param {string} id
|
|
188
|
+
* @param {Object} [opts]
|
|
189
|
+
* @returns {Promise<Object>}
|
|
190
|
+
*/
|
|
191
|
+
async function fetchProductById(id, opts = {}) {
|
|
192
|
+
const { get } = createFetcher(opts);
|
|
193
|
+
return get(`/products/${encodeURIComponent(id)}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Fetch categories. GET /webstoreapi/categories?...
|
|
198
|
+
* @param {Object} params - { limit, offset }
|
|
199
|
+
* @param {Object} [opts]
|
|
200
|
+
* @returns {Promise<{ categories?: Array, data?: { categories?: Array } }>}
|
|
201
|
+
*/
|
|
202
|
+
async function fetchCategories(params = {}, opts = {}) {
|
|
203
|
+
const { get } = createFetcher(opts);
|
|
204
|
+
const qs = categoriesQueryToParams(params);
|
|
205
|
+
const path = qs ? `/categories?${qs}` : '/categories';
|
|
206
|
+
return get(path);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Fetch brands. POST /webstoreapi/brands with body.
|
|
211
|
+
* @param {Object} params - { limit, offset }
|
|
212
|
+
* @param {Object} [opts]
|
|
213
|
+
* @returns {Promise<{ brands?: Array, data?: { brands?: Array } }>}
|
|
214
|
+
*/
|
|
215
|
+
async function fetchBrands(params = {}, opts = {}) {
|
|
216
|
+
const { post } = createFetcher(opts);
|
|
217
|
+
return post('/brands', params || {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Fetch menus. GET /webstoreapi/menus
|
|
222
|
+
* @param {Object} [opts]
|
|
223
|
+
* @returns {Promise<Array|{ data?: { menus?: Array }, menus?: Array }>}
|
|
224
|
+
*/
|
|
225
|
+
async function fetchMenus(opts = {}) {
|
|
226
|
+
const { get } = createFetcher(opts);
|
|
227
|
+
const data = await get('/menus');
|
|
228
|
+
if (Array.isArray(data)) return data;
|
|
229
|
+
return data?.data?.menus || data?.menus || [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Fetch menu by ID. GET /webstoreapi/menus/:id
|
|
234
|
+
* @param {string} id
|
|
235
|
+
* @param {Object} [opts]
|
|
236
|
+
* @returns {Promise<Object>}
|
|
237
|
+
*/
|
|
238
|
+
async function fetchMenuById(id, opts = {}) {
|
|
239
|
+
const { get } = createFetcher(opts);
|
|
240
|
+
return get(`/menus/${encodeURIComponent(id)}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Fetch cart. GET /webstoreapi/carts (session-based; pass Cookie).
|
|
245
|
+
* @param {Object} [opts] - { headers: { Cookie: req.headers.cookie } }
|
|
246
|
+
* @returns {Promise<{ data?: Object }>}
|
|
247
|
+
*/
|
|
248
|
+
async function fetchCart(opts = {}) {
|
|
249
|
+
const { get } = createFetcher(opts);
|
|
250
|
+
return get('/carts');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Fetch widgets for a section. GET /webstoreapi/sections/widgets/:page/:section
|
|
255
|
+
* @param {string} page - e.g. 'home'
|
|
256
|
+
* @param {string} section - e.g. 'hero', 'products'
|
|
257
|
+
* @param {Object} [opts]
|
|
258
|
+
* @returns {Promise<{ widgets?: Array }>}
|
|
259
|
+
*/
|
|
260
|
+
async function fetchSectionsWidgets(page, section, opts = {}) {
|
|
261
|
+
const { get } = createFetcher(opts);
|
|
262
|
+
const data = await get(`/sections/widgets/${encodeURIComponent(page)}/${encodeURIComponent(section)}`);
|
|
263
|
+
const list = Array.isArray(data?.widgets) ? data.widgets : [];
|
|
264
|
+
return { widgets: list };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Fetch all sections widgets and merge into { hero: [], products: [], ... }.
|
|
269
|
+
* @param {string} [page] - e.g. 'home'
|
|
270
|
+
* @param {string[]} [sections]
|
|
271
|
+
* @param {Object} [opts]
|
|
272
|
+
* @returns {Promise<Object>}
|
|
273
|
+
*/
|
|
274
|
+
async function fetchWidgetsBySections(page = 'home', sections = DEFAULT_SECTIONS, opts = {}) {
|
|
275
|
+
const out = {};
|
|
276
|
+
for (const s of sections) {
|
|
277
|
+
out[s] = [];
|
|
278
|
+
}
|
|
279
|
+
for (const section of sections) {
|
|
280
|
+
try {
|
|
281
|
+
const { widgets } = await fetchSectionsWidgets(page, section, opts);
|
|
282
|
+
out[section] = Array.isArray(widgets) ? widgets : [];
|
|
283
|
+
} catch (e) {
|
|
284
|
+
out[section] = [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Fetch CMS page. GET /webstoreapi/pages/:handle
|
|
292
|
+
* @param {string} handle
|
|
293
|
+
* @param {Object} [opts]
|
|
294
|
+
* @returns {Promise<Object|null>} null on 404
|
|
295
|
+
*/
|
|
296
|
+
async function fetchPage(handle, opts = {}) {
|
|
297
|
+
const { get } = createFetcher(opts);
|
|
298
|
+
try {
|
|
299
|
+
return await get(`/pages/${encodeURIComponent(handle)}`);
|
|
300
|
+
} catch (e) {
|
|
301
|
+
if (e.response?.status === 404) return null;
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
getWebstoreapiBaseUrl,
|
|
308
|
+
getShopfrontBaseUrl,
|
|
309
|
+
createFetcher,
|
|
310
|
+
fetchProducts,
|
|
311
|
+
fetchProductById,
|
|
312
|
+
fetchCategories,
|
|
313
|
+
fetchBrands,
|
|
314
|
+
fetchMenus,
|
|
315
|
+
fetchMenuById,
|
|
316
|
+
fetchCart,
|
|
317
|
+
fetchSectionsWidgets,
|
|
318
|
+
fetchWidgetsBySections,
|
|
319
|
+
fetchPage,
|
|
320
|
+
fetchStoreInfo,
|
|
321
|
+
DEFAULT_SECTIONS
|
|
322
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@o2vend/theme-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
4
|
"description": "O2VEND Theme Development CLI - Standalone tool for local theme development",
|
|
5
5
|
"bin": {
|
|
6
6
|
"o2vend": "./bin/o2vend"
|
|
@@ -22,28 +22,28 @@
|
|
|
22
22
|
"unpublish-and-republish": "node scripts/unpublish-and-republish.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"archiver": "^6.0.1",
|
|
26
|
+
"axios": "^1.6.0",
|
|
27
|
+
"boxen": "^7.1.1",
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"chokidar": "^3.5.3",
|
|
25
30
|
"commander": "^11.0.0",
|
|
31
|
+
"dotenv": "^16.3.1",
|
|
26
32
|
"express": "^4.18.2",
|
|
33
|
+
"fs-extra": "^11.1.1",
|
|
34
|
+
"glob": "^10.3.10",
|
|
35
|
+
"http-proxy-middleware": "^2.0.6",
|
|
36
|
+
"inquirer": "^9.2.0",
|
|
27
37
|
"liquidjs": "^10.7.0",
|
|
28
|
-
"
|
|
38
|
+
"listr": "^0.14.3",
|
|
29
39
|
"lodash": "^4.17.21",
|
|
30
40
|
"moment": "^2.29.4",
|
|
41
|
+
"node-cache": "^5.1.2",
|
|
42
|
+
"open": "^8.4.2",
|
|
43
|
+
"ora": "^5.4.1",
|
|
31
44
|
"slugify": "^1.6.6",
|
|
32
|
-
"chokidar": "^3.5.3",
|
|
33
45
|
"socket.io": "^4.7.0",
|
|
34
|
-
"
|
|
35
|
-
"inquirer": "^9.2.0",
|
|
36
|
-
"ora": "^5.4.1",
|
|
37
|
-
"boxen": "^7.1.1",
|
|
38
|
-
"table": "^6.8.3",
|
|
39
|
-
"listr": "^0.14.3",
|
|
40
|
-
"dotenv": "^16.3.1",
|
|
41
|
-
"archiver": "^6.0.1",
|
|
42
|
-
"glob": "^10.3.10",
|
|
43
|
-
"fs-extra": "^11.1.1",
|
|
44
|
-
"node-cache": "^5.1.2",
|
|
45
|
-
"http-proxy-middleware": "^2.0.6",
|
|
46
|
-
"open": "^8.4.2"
|
|
46
|
+
"table": "^6.8.3"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=18.0.0"
|