@o2vend/theme-cli 1.0.34 → 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 +474 -316
- package/lib/lib/webstoreapi-fetcher.js +322 -0
- package/package.json +16 -16
- package/test-theme/layout/theme.liquid +195 -195
|
@@ -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"
|