@zohofinance/commerce 2.0.0 → 2.2.0
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/VALIDATIONS.md +122 -0
- package/package.json +2 -2
- package/public/dev-error.liquid +49 -0
- package/ts-dist/commands/build-theme.d.ts.map +1 -1
- package/ts-dist/commands/build-theme.js +38 -11
- package/ts-dist/commands/serve-theme.d.ts.map +1 -1
- package/ts-dist/commands/serve-theme.js +414 -70
- package/ts-dist/index.js +0 -0
- package/ts-dist/mocks/global-objects.d.ts.map +1 -1
- package/ts-dist/mocks/global-objects.js +1 -0
- package/ts-dist/mocks/init-theme/i18n/en.schema.json +2 -1
- package/ts-dist/mocks/init-theme/sections/footer.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/header-topbar.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/product-collection.liquid +3 -1
- package/ts-dist/mocks/init-theme/sections/promo-banner.liquid +5 -1
- package/ts-dist/mocks/reviews.d.ts +6 -0
- package/ts-dist/mocks/reviews.d.ts.map +1 -0
- package/ts-dist/mocks/reviews.js +52 -0
- package/ts-dist/routes/storefront-routes.js +1 -1
- package/ts-dist/utils/api-handler.d.ts +6 -1
- package/ts-dist/utils/api-handler.d.ts.map +1 -1
- package/ts-dist/utils/api-handler.js +91 -35
- package/ts-dist/utils/editor-attributes.d.ts.map +1 -1
- package/ts-dist/utils/editor-attributes.js +2 -4
- package/ts-dist/utils/error-report.d.ts +1 -1
- package/ts-dist/utils/error-report.d.ts.map +1 -1
- package/ts-dist/utils/error-report.js +58 -18
- package/ts-dist/utils/load-theme-globals.d.ts +1 -1
- package/ts-dist/utils/load-theme-globals.d.ts.map +1 -1
- package/ts-dist/utils/load-theme-globals.js +42 -2
- package/ts-dist/utils/resolve-schema-settings.d.ts.map +1 -1
- package/ts-dist/utils/resolve-schema-settings.js +0 -1
- package/ts-dist/utils/resolve-schema-translations.d.ts +1 -0
- package/ts-dist/utils/resolve-schema-translations.d.ts.map +1 -1
- package/ts-dist/utils/resolve-schema-translations.js +3 -0
- package/ts-dist/utils/schema-validation.d.ts +34 -0
- package/ts-dist/utils/schema-validation.d.ts.map +1 -0
- package/ts-dist/utils/schema-validation.js +544 -0
- package/ts-dist/utils/serve-theme-utils.d.ts +1 -1
- package/ts-dist/utils/serve-theme-utils.d.ts.map +1 -1
- package/ts-dist/utils/serve-theme-utils.js +1 -1
- package/ts-dist/utils/template-reference-validation.d.ts +11 -0
- package/ts-dist/utils/template-reference-validation.d.ts.map +1 -0
- package/ts-dist/utils/template-reference-validation.js +466 -0
- package/ts-dist/utils/theme-block-schema.d.ts +5 -0
- package/ts-dist/utils/theme-block-schema.d.ts.map +1 -0
- package/ts-dist/utils/theme-block-schema.js +35 -0
- package/ts-dist/utils/theme-validation.d.ts +8 -1
- package/ts-dist/utils/theme-validation.d.ts.map +1 -1
- package/ts-dist/utils/theme-validation.js +84 -143
|
@@ -13,13 +13,14 @@ import { findAvailablePort, watchFolders } from '../utils/serve-theme-utils.js';
|
|
|
13
13
|
import { APIHandler } from '../utils/api-handler.js';
|
|
14
14
|
import { loadThemeGlobals, mergePageData } from '../utils/load-theme-globals.js';
|
|
15
15
|
import StackTrace from '../utils/stack-trace.js';
|
|
16
|
-
import { validateTheme, formatValidationIssuesForConsole } from '../utils/theme-validation.js';
|
|
16
|
+
import { validateTheme, formatValidationIssuesForConsole, validationWarnings } from '../utils/theme-validation.js';
|
|
17
17
|
import { buildValidationErrorReport, buildRuntimeErrorReport, renderDevErrorPage } from '../utils/error-report.js';
|
|
18
18
|
import { STOREFRONT_ROUTES, resolveApiPath, resolvePageTemplate } from '../routes/storefront-routes.js';
|
|
19
19
|
import { resolveSchemaSettings } from '../utils/resolve-schema-settings.js';
|
|
20
|
-
import { resolveSchemaTranslations as resolveSchemaT, resolveSchemaName } from '../utils/resolve-schema-translations.js';
|
|
20
|
+
import { resolveSchemaTranslations as resolveSchemaT, resolveSchemaName, invalidateSchemaLocaleCache } from '../utils/resolve-schema-translations.js';
|
|
21
21
|
import { prepareSectionData } from '../utils/prepare-section-data.js';
|
|
22
22
|
import { buildSectionComponentAttr } from '../utils/editor-attributes.js';
|
|
23
|
+
import { withThemeBlockSchema } from '../utils/theme-block-schema.js';
|
|
23
24
|
function normalizeStoreUrl(input) {
|
|
24
25
|
if (!input)
|
|
25
26
|
return undefined;
|
|
@@ -39,8 +40,6 @@ function normalizeStoreUrl(input) {
|
|
|
39
40
|
async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true, storeUrl) {
|
|
40
41
|
const liveReloadPort = await findAvailablePort();
|
|
41
42
|
const stackTrace = new StackTrace();
|
|
42
|
-
const themeGlobals = loadThemeGlobals();
|
|
43
|
-
let runtimeGlobals = themeGlobals;
|
|
44
43
|
const normalizedStoreUrl = normalizeStoreUrl(storeUrl);
|
|
45
44
|
const shouldUseMock = useMock || !normalizedStoreUrl;
|
|
46
45
|
const cwd = process.cwd();
|
|
@@ -50,7 +49,11 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
50
49
|
let validationResult = await validateTheme(cwd, { mode: 'dev' });
|
|
51
50
|
if (!validationResult.valid) {
|
|
52
51
|
console.error(chalk.yellow('\nTheme validation failed. Pages will show validation errors until the issues are fixed.'));
|
|
53
|
-
console.error(
|
|
52
|
+
console.error(formatValidationIssuesForConsole(validationResult.issues));
|
|
53
|
+
}
|
|
54
|
+
else if (validationWarnings(validationResult.issues).length > 0) {
|
|
55
|
+
console.log(chalk.yellow('\nTheme validation passed with warnings. Preview is still available.'));
|
|
56
|
+
console.log(formatValidationIssuesForConsole(validationResult.issues));
|
|
54
57
|
}
|
|
55
58
|
else {
|
|
56
59
|
console.log(chalk.green('Theme validation passed.'));
|
|
@@ -59,18 +62,78 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
59
62
|
useMock: shouldUseMock,
|
|
60
63
|
baseURL: normalizedStoreUrl ?? ''
|
|
61
64
|
});
|
|
62
|
-
|
|
65
|
+
// Page-specific keys of the live /home response; they must not leak into other pages.
|
|
66
|
+
const PAGE_SPECIFIC_KEYS = [
|
|
67
|
+
'product',
|
|
68
|
+
'products',
|
|
69
|
+
'collection',
|
|
70
|
+
'collections',
|
|
71
|
+
'category',
|
|
72
|
+
'categories',
|
|
73
|
+
'blog',
|
|
74
|
+
'blogs',
|
|
75
|
+
'page',
|
|
76
|
+
'page_context',
|
|
77
|
+
'cart',
|
|
78
|
+
'checkout',
|
|
79
|
+
'order',
|
|
80
|
+
'comments',
|
|
81
|
+
'page_title',
|
|
82
|
+
'seo'
|
|
83
|
+
];
|
|
84
|
+
let liveGlobals = null;
|
|
85
|
+
async function ensureLiveGlobals() {
|
|
86
|
+
if (shouldUseMock || liveGlobals)
|
|
87
|
+
return;
|
|
63
88
|
try {
|
|
64
|
-
const homeData =
|
|
65
|
-
|
|
89
|
+
const homeData = {
|
|
90
|
+
...(await apiHandler.fetch('/home'))
|
|
91
|
+
};
|
|
92
|
+
for (const key of PAGE_SPECIFIC_KEYS) {
|
|
93
|
+
delete homeData[key];
|
|
94
|
+
}
|
|
95
|
+
liveGlobals = homeData;
|
|
66
96
|
}
|
|
67
97
|
catch (e) {
|
|
68
|
-
console.warn('Could not load live storefront globals.
|
|
98
|
+
console.warn('Could not load live storefront globals. Will retry on the next request.', e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Rebuilt on every access so edits to settings/theme.json show up without a restart.
|
|
102
|
+
function getRuntimeGlobals() {
|
|
103
|
+
const themeGlobals = loadThemeGlobals(!shouldUseMock);
|
|
104
|
+
return liveGlobals ? mergePageData(themeGlobals, liveGlobals) : themeGlobals;
|
|
105
|
+
}
|
|
106
|
+
await ensureLiveGlobals();
|
|
107
|
+
const productHandleById = new Map();
|
|
108
|
+
function indexProductHandles(value, depth = 0) {
|
|
109
|
+
if (depth > 8 || !value || typeof value !== 'object')
|
|
110
|
+
return;
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
for (const item of value)
|
|
113
|
+
indexProductHandles(item, depth + 1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const record = value;
|
|
117
|
+
const { id, handle } = record;
|
|
118
|
+
if ((typeof id === 'string' || typeof id === 'number') &&
|
|
119
|
+
typeof handle === 'string' &&
|
|
120
|
+
handle) {
|
|
121
|
+
productHandleById.set(String(id), handle);
|
|
122
|
+
}
|
|
123
|
+
for (const nested of Object.values(record)) {
|
|
124
|
+
indexProductHandles(nested, depth + 1);
|
|
69
125
|
}
|
|
70
126
|
}
|
|
71
|
-
|
|
127
|
+
function resolveProductPath(productId) {
|
|
128
|
+
return productHandleById.get(productId) ?? productId;
|
|
129
|
+
}
|
|
130
|
+
const engine = new TemplateEngine({ globals: getRuntimeGlobals() }, stackTrace);
|
|
72
131
|
if (!shouldUseMock && normalizedStoreUrl) {
|
|
73
|
-
engine.fetcher = (url) =>
|
|
132
|
+
engine.fetcher = async (url) => {
|
|
133
|
+
const data = await apiHandler.fetch(url);
|
|
134
|
+
indexProductHandles(data);
|
|
135
|
+
return data;
|
|
136
|
+
};
|
|
74
137
|
}
|
|
75
138
|
function parseSectionsParam(req) {
|
|
76
139
|
const raw = req.body?.sections ?? req.query?.sections;
|
|
@@ -144,11 +207,19 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
144
207
|
}
|
|
145
208
|
return sections;
|
|
146
209
|
}
|
|
210
|
+
// Declared before revalidateTheme so a file change can reset it.
|
|
211
|
+
const sectionSchemaCache = new Map();
|
|
147
212
|
async function revalidateTheme() {
|
|
213
|
+
sectionSchemaCache.clear();
|
|
214
|
+
invalidateSchemaLocaleCache();
|
|
148
215
|
validationResult = await validateTheme(cwd, { mode: 'dev' });
|
|
149
216
|
if (!validationResult.valid) {
|
|
150
217
|
console.error(chalk.yellow('\nTheme validation failed after a file change.'));
|
|
151
|
-
console.error(
|
|
218
|
+
console.error(formatValidationIssuesForConsole(validationResult.issues));
|
|
219
|
+
}
|
|
220
|
+
else if (validationWarnings(validationResult.issues).length > 0) {
|
|
221
|
+
console.log(chalk.yellow('\nTheme validation passed with warnings after a file change.'));
|
|
222
|
+
console.log(formatValidationIssuesForConsole(validationResult.issues));
|
|
152
223
|
}
|
|
153
224
|
else {
|
|
154
225
|
console.log(chalk.green('Theme validation passed.'));
|
|
@@ -158,9 +229,24 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
158
229
|
const report = buildValidationErrorReport(validationResult, cwd);
|
|
159
230
|
return renderDevErrorPage(report);
|
|
160
231
|
}
|
|
161
|
-
async function fetchPageData(apiPath) {
|
|
162
|
-
|
|
163
|
-
|
|
232
|
+
async function fetchPageData(apiPath, query = '') {
|
|
233
|
+
await ensureLiveGlobals();
|
|
234
|
+
// Sort, pagination and filter params must reach the live store.
|
|
235
|
+
const pageData = await apiHandler.fetch(query ? `${apiPath}?${query}` : apiPath);
|
|
236
|
+
indexProductHandles(pageData);
|
|
237
|
+
return mergePageData(getRuntimeGlobals(), pageData);
|
|
238
|
+
}
|
|
239
|
+
function pageQueryString(req) {
|
|
240
|
+
const params = new URLSearchParams();
|
|
241
|
+
for (const [key, value] of Object.entries(req.query)) {
|
|
242
|
+
if (key === 'format' || key === 'sections')
|
|
243
|
+
continue;
|
|
244
|
+
for (const v of Array.isArray(value) ? value : [value]) {
|
|
245
|
+
if (typeof v === 'string')
|
|
246
|
+
params.append(key, v);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return params.toString();
|
|
164
250
|
}
|
|
165
251
|
async function renderPage(pathName, data, req) {
|
|
166
252
|
if (req?.query.format === 'json') {
|
|
@@ -187,7 +273,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
187
273
|
async function handlePageRoute(routePageTemplate, apiPath, req, res, next) {
|
|
188
274
|
try {
|
|
189
275
|
const pageTemplate = resolvePageTemplate(routePageTemplate);
|
|
190
|
-
const data = await fetchPageData(apiPath);
|
|
276
|
+
const data = await fetchPageData(apiPath, pageQueryString(req));
|
|
191
277
|
// Always render in editor mode so sections get data-zcommerce-component attributes
|
|
192
278
|
engine.engine.__editorMode = true;
|
|
193
279
|
engine.engine.__editorJsonType = 'template';
|
|
@@ -208,7 +294,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
208
294
|
else {
|
|
209
295
|
html += bridgeTag;
|
|
210
296
|
}
|
|
211
|
-
res.status(result.status ?? 200).send(html);
|
|
297
|
+
res.status(result.status ?? 200).set('Cache-Control', 'no-store').send(html);
|
|
212
298
|
}
|
|
213
299
|
catch (error) {
|
|
214
300
|
engine.engine.__editorMode = false;
|
|
@@ -236,11 +322,14 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
236
322
|
app.engine('liquid', engine.express());
|
|
237
323
|
app.set('views', ['./pages']);
|
|
238
324
|
app.set('view engine', 'liquid');
|
|
239
|
-
if (
|
|
325
|
+
if (livereloadOff) {
|
|
326
|
+
watchFolders(undefined, revalidateTheme);
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
240
329
|
app.use(connectLiveReload({ port: liveReloadPort }));
|
|
241
330
|
const liveReloadServer = livereload.createServer({ port: liveReloadPort });
|
|
331
|
+
watchFolders(liveReloadServer, revalidateTheme);
|
|
242
332
|
liveReloadServer.server.once('connection', () => {
|
|
243
|
-
watchFolders(liveReloadServer, revalidateTheme);
|
|
244
333
|
setTimeout(() => {
|
|
245
334
|
liveReloadServer.refresh('/');
|
|
246
335
|
}, 100);
|
|
@@ -250,12 +339,62 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
250
339
|
app.get('/', (_req, res) => {
|
|
251
340
|
res.redirect('/home');
|
|
252
341
|
});
|
|
342
|
+
const PREDICTIVE_SEARCH_SECTION = 'search-result';
|
|
343
|
+
function withSearchContext(data, req) {
|
|
344
|
+
const existing = data.search && typeof data.search === 'object' && !Array.isArray(data.search)
|
|
345
|
+
? { ...data.search }
|
|
346
|
+
: {};
|
|
347
|
+
if (existing.products === undefined) {
|
|
348
|
+
existing.products = data.products ?? [];
|
|
349
|
+
}
|
|
350
|
+
if (existing.q === undefined) {
|
|
351
|
+
existing.q = typeof req.query.q === 'string' ? req.query.q : '';
|
|
352
|
+
}
|
|
353
|
+
return { ...data, search: existing };
|
|
354
|
+
}
|
|
355
|
+
async function handlePredictiveSearch(req, res, next) {
|
|
356
|
+
try {
|
|
357
|
+
const requested = parseSectionsParam(req);
|
|
358
|
+
const sectionNames = requested.length > 0 ? requested : [PREDICTIVE_SEARCH_SECTION];
|
|
359
|
+
const data = await fetchPageData('/search', pageQueryString(req));
|
|
360
|
+
const sections = await renderSectionsForResponse(sectionNames, withSearchContext(data, req));
|
|
361
|
+
res.json({ sections });
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
next(error);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
app.get('/home/search', handlePredictiveSearch);
|
|
368
|
+
// /search is the search page as well, so only a sections param marks it as
|
|
369
|
+
// an API call; a plain navigation falls through to the page route below.
|
|
370
|
+
app.get('/search', (req, res, next) => {
|
|
371
|
+
if (parseSectionsParam(req).length === 0) {
|
|
372
|
+
next();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
return handlePredictiveSearch(req, res, next);
|
|
376
|
+
});
|
|
253
377
|
for (const route of STOREFRONT_ROUTES) {
|
|
254
378
|
app.get(route.expressPath, (req, res, next) => {
|
|
255
379
|
const apiPath = resolveApiPath(route, req.params);
|
|
256
380
|
return handlePageRoute(route.pageTemplate, apiPath, req, res, next);
|
|
257
381
|
});
|
|
258
382
|
}
|
|
383
|
+
// Live stores link to singular /category/<handle> and /collection/<handle> URLs.
|
|
384
|
+
const ROUTE_ALIASES = [
|
|
385
|
+
['/category/:category_name', '/categories/:category_name'],
|
|
386
|
+
['/collection/:collection_name', '/collections/:collection_name'],
|
|
387
|
+
['/product/:product_id', '/products/:product_id']
|
|
388
|
+
];
|
|
389
|
+
for (const [aliasPath, targetPath] of ROUTE_ALIASES) {
|
|
390
|
+
const route = STOREFRONT_ROUTES.find((r) => r.expressPath === targetPath);
|
|
391
|
+
if (!route)
|
|
392
|
+
continue;
|
|
393
|
+
app.get(aliasPath, (req, res, next) => {
|
|
394
|
+
const apiPath = resolveApiPath(route, req.params);
|
|
395
|
+
return handlePageRoute(route.pageTemplate, apiPath, req, res, next);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
259
398
|
app.post('/addtocart', upload.none(), async (req, res, next) => {
|
|
260
399
|
try {
|
|
261
400
|
const response = (await apiHandler.addToCart(req.body));
|
|
@@ -263,7 +402,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
263
402
|
if (sectionNames.length > 0) {
|
|
264
403
|
const cartData = await resolveCartForSections(response);
|
|
265
404
|
const sections = await renderSectionsForResponse(sectionNames, {
|
|
266
|
-
...
|
|
405
|
+
...getRuntimeGlobals(),
|
|
267
406
|
cart: cartData
|
|
268
407
|
});
|
|
269
408
|
res.send({ ...response, sections });
|
|
@@ -283,7 +422,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
283
422
|
if (sectionNames.length > 0) {
|
|
284
423
|
const cartData = await resolveCartForSections(response);
|
|
285
424
|
const sections = await renderSectionsForResponse(sectionNames, {
|
|
286
|
-
...
|
|
425
|
+
...getRuntimeGlobals(),
|
|
287
426
|
cart: cartData
|
|
288
427
|
});
|
|
289
428
|
res.send({ ...response, sections });
|
|
@@ -303,7 +442,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
303
442
|
if (sectionNames.length > 0) {
|
|
304
443
|
const cartData = await resolveCartForSections(response);
|
|
305
444
|
const sections = await renderSectionsForResponse(sectionNames, {
|
|
306
|
-
...
|
|
445
|
+
...getRuntimeGlobals(),
|
|
307
446
|
cart: cartData
|
|
308
447
|
});
|
|
309
448
|
res.send({ ...response, sections });
|
|
@@ -396,18 +535,14 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
396
535
|
const sectionNames = parseSectionsParam(req);
|
|
397
536
|
if (sectionNames.length === 0) {
|
|
398
537
|
res.status(400).json({
|
|
399
|
-
code: 1,
|
|
400
538
|
message: 'Missing sections query param',
|
|
401
539
|
sections: {}
|
|
402
540
|
});
|
|
403
541
|
return;
|
|
404
542
|
}
|
|
405
|
-
const data = await fetchPageData(`/products/${req.params.product_id}`);
|
|
543
|
+
const data = await fetchPageData(`/products/${encodeURIComponent(resolveProductPath(req.params.product_id))}`);
|
|
406
544
|
const sections = await renderSectionsForResponse(sectionNames, data);
|
|
407
|
-
res.json({
|
|
408
|
-
code: 0,
|
|
409
|
-
sections
|
|
410
|
-
});
|
|
545
|
+
res.json({ sections });
|
|
411
546
|
}
|
|
412
547
|
catch (error) {
|
|
413
548
|
next(error);
|
|
@@ -415,7 +550,34 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
415
550
|
});
|
|
416
551
|
app.get('/products/:product_id/reviews', async (req, res, next) => {
|
|
417
552
|
try {
|
|
418
|
-
|
|
553
|
+
const params = new URLSearchParams();
|
|
554
|
+
const response = (await apiHandler.getProductReviews(req.params.product_id, params.toString()));
|
|
555
|
+
res.json(response);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
next(error);
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
app.post('/products/:product_id/reviews', async (req, res, next) => {
|
|
563
|
+
try {
|
|
564
|
+
res.json(await apiHandler.submitProductReview('POST', req.params.product_id, req.body));
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
next(error);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
app.put('/products/:product_id/reviews', async (req, res, next) => {
|
|
571
|
+
try {
|
|
572
|
+
res.json(await apiHandler.submitProductReview('PUT', req.params.product_id, req.body));
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
next(error);
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
app.get('/editreview/:product_id', async (req, res, next) => {
|
|
579
|
+
try {
|
|
580
|
+
res.json(await apiHandler.getReviewForEdit(req.params.product_id));
|
|
419
581
|
}
|
|
420
582
|
catch (error) {
|
|
421
583
|
next(error);
|
|
@@ -437,7 +599,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
437
599
|
if (sectionNames.length > 0) {
|
|
438
600
|
const cartData = await resolveCartForSections(response);
|
|
439
601
|
const sections = await renderSectionsForResponse(sectionNames, {
|
|
440
|
-
...
|
|
602
|
+
...getRuntimeGlobals(),
|
|
441
603
|
cart: cartData
|
|
442
604
|
});
|
|
443
605
|
res.send({ ...response, sections });
|
|
@@ -459,7 +621,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
459
621
|
const cartData = await resolveCartForSections(response);
|
|
460
622
|
const saveForLaterData = resolveSaveForLaterForSections(response);
|
|
461
623
|
const sectionData = {
|
|
462
|
-
...
|
|
624
|
+
...getRuntimeGlobals(),
|
|
463
625
|
cart: cartData
|
|
464
626
|
};
|
|
465
627
|
if (saveForLaterData) {
|
|
@@ -485,7 +647,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
485
647
|
const cartData = await resolveCartForSections(response);
|
|
486
648
|
const saveForLaterData = resolveSaveForLaterForSections(response);
|
|
487
649
|
const sectionData = {
|
|
488
|
-
...
|
|
650
|
+
...getRuntimeGlobals(),
|
|
489
651
|
cart: cartData
|
|
490
652
|
};
|
|
491
653
|
if (saveForLaterData) {
|
|
@@ -511,7 +673,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
511
673
|
const cartData = await resolveCartForSections(response);
|
|
512
674
|
const saveForLaterData = resolveSaveForLaterForSections(response);
|
|
513
675
|
const sectionData = {
|
|
514
|
-
...
|
|
676
|
+
...getRuntimeGlobals(),
|
|
515
677
|
cart: cartData
|
|
516
678
|
};
|
|
517
679
|
if (saveForLaterData) {
|
|
@@ -537,7 +699,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
537
699
|
const cartData = await resolveCartForSections(response);
|
|
538
700
|
const saveForLaterData = resolveSaveForLaterForSections(response);
|
|
539
701
|
const sectionData = {
|
|
540
|
-
...
|
|
702
|
+
...getRuntimeGlobals(),
|
|
541
703
|
cart: cartData
|
|
542
704
|
};
|
|
543
705
|
if (saveForLaterData) {
|
|
@@ -749,7 +911,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
749
911
|
// Runtime fields added by the CLI on section/block root objects — must NOT be written to disk.
|
|
750
912
|
// NOTE: display_name inside settings values (e.g. {display_name:"Full Width",value:"full"}) is
|
|
751
913
|
// legitimate theme data — we only strip it at the section/block root level, not inside settings.
|
|
752
|
-
const COMPONENT_RUNTIME_FIELDS = new Set(['id', 'display_name', 'settingid', 'zcommerce_attributes', 'block_key']);
|
|
914
|
+
const COMPONENT_RUNTIME_FIELDS = new Set(['id', 'display_name', 'settingid', 'zcommerce_attributes', 'block_key', 'available_sections', 'available_blocks']);
|
|
753
915
|
// Clean a single section or block object: strip runtime fields from root, recurse into blocks,
|
|
754
916
|
// but leave settings values entirely untouched (they may contain legitimate display_name/handle).
|
|
755
917
|
function cleanComponentForDisk(obj) {
|
|
@@ -774,11 +936,11 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
774
936
|
}
|
|
775
937
|
return out;
|
|
776
938
|
}
|
|
777
|
-
// Clean the full page/group JSON: only touches the sections map, leaves order/name/type intact.
|
|
778
939
|
function cleanFullJsonForDisk(json) {
|
|
779
940
|
const sections = json.sections;
|
|
780
|
-
if (!sections || typeof sections !== 'object' || Array.isArray(sections))
|
|
941
|
+
if (!sections || typeof sections !== 'object' || Array.isArray(sections)) {
|
|
781
942
|
return json;
|
|
943
|
+
}
|
|
782
944
|
const cleanedSections = {};
|
|
783
945
|
for (const [sk, sv] of Object.entries(sections)) {
|
|
784
946
|
cleanedSections[sk] = sv && typeof sv === 'object' && !Array.isArray(sv)
|
|
@@ -804,8 +966,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
804
966
|
console.warn('[dev] Failed to persist full JSON:', e);
|
|
805
967
|
}
|
|
806
968
|
}
|
|
807
|
-
//
|
|
808
|
-
const sectionSchemaCache = new Map();
|
|
969
|
+
// sectionSchemaCache (declared above) caches parsed section schemas (type → { name, blocks }).
|
|
809
970
|
function getSectionSchema(sectionType) {
|
|
810
971
|
if (sectionSchemaCache.has(sectionType))
|
|
811
972
|
return sectionSchemaCache.get(sectionType);
|
|
@@ -901,11 +1062,15 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
901
1062
|
catch { /* skip invalid schema */ }
|
|
902
1063
|
}
|
|
903
1064
|
const resolved = resolveSchemaT({ settings: schema.settings || [], name: schema.name || sectionType, blocks: schema.blocks || [] }, cwd);
|
|
904
|
-
const blocks = (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1065
|
+
const blocks = (schema.blocks || []).map((rawBlock) => {
|
|
1066
|
+
const merged = withThemeBlockSchema(cwd, rawBlock, rawBlock.type) ?? rawBlock;
|
|
1067
|
+
const b = resolveSchemaT(merged, cwd);
|
|
1068
|
+
return {
|
|
1069
|
+
...b,
|
|
1070
|
+
id: `setting_section_${sectionType}.${b.type}`,
|
|
1071
|
+
settings: b.settings || [],
|
|
1072
|
+
};
|
|
1073
|
+
});
|
|
909
1074
|
return {
|
|
910
1075
|
id: `setting_section_${sectionType}`,
|
|
911
1076
|
name: resolved.name || sectionType,
|
|
@@ -991,6 +1156,45 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
991
1156
|
next(error);
|
|
992
1157
|
}
|
|
993
1158
|
});
|
|
1159
|
+
// Re-renders the full page with in-memory settings override (no disk write).
|
|
1160
|
+
// Used by the editor when theme settings change so the preview updates immediately.
|
|
1161
|
+
app.post('/_editor/renderpage', async (req, res, next) => {
|
|
1162
|
+
const engineInternal = engine.engine;
|
|
1163
|
+
try {
|
|
1164
|
+
const body = req.body;
|
|
1165
|
+
const settingsData = body.settingsData;
|
|
1166
|
+
const pageName = body.pageName || 'index';
|
|
1167
|
+
const route = STOREFRONT_ROUTES.find((r) => r.pageTemplate === pageName)
|
|
1168
|
+
|| STOREFRONT_ROUTES.find((r) => r.expressPath === `/${pageName}`)
|
|
1169
|
+
|| STOREFRONT_ROUTES.find((r) => r.expressPath.split('/')[1]?.startsWith(pageName))
|
|
1170
|
+
|| STOREFRONT_ROUTES[0];
|
|
1171
|
+
const apiPath = resolveApiPath(route, {});
|
|
1172
|
+
const data = await fetchPageData(apiPath);
|
|
1173
|
+
// Override the settings on this request's data so the engine renders with the new values.
|
|
1174
|
+
if (settingsData)
|
|
1175
|
+
data.settings = settingsData;
|
|
1176
|
+
engineInternal.__editorMode = true;
|
|
1177
|
+
engineInternal.__editorJsonType = 'template';
|
|
1178
|
+
try {
|
|
1179
|
+
const { html, error } = await engine.renderJSONTemplates({
|
|
1180
|
+
pathName: resolvePageTemplate(route.pageTemplate),
|
|
1181
|
+
data,
|
|
1182
|
+
});
|
|
1183
|
+
if (error) {
|
|
1184
|
+
res.status(500).json({ error: String(error) });
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
res.json({ html });
|
|
1188
|
+
}
|
|
1189
|
+
finally {
|
|
1190
|
+
engineInternal.__editorMode = false;
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
engineInternal.__editorMode = false;
|
|
1195
|
+
next(error);
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
994
1198
|
app.post('/_editor/rendersection', async (req, res, next) => {
|
|
995
1199
|
try {
|
|
996
1200
|
const body = req.body;
|
|
@@ -1020,7 +1224,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1020
1224
|
// Deep copy BEFORE any mutation (resolveSchemaSettings, prepareSectionData both mutate sectionJson).
|
|
1021
1225
|
// This copy goes back to the editor and is later saved to disk — must have original unmodified URLs.
|
|
1022
1226
|
const editorSectionJson = JSON.parse(JSON.stringify(sectionJson));
|
|
1023
|
-
const data = { ...
|
|
1227
|
+
const data = { ...getRuntimeGlobals() };
|
|
1024
1228
|
if (schemaData?.settings && sectionJson.settings) {
|
|
1025
1229
|
await resolveSchemaSettings(schemaData.settings, sectionJson.settings, data, engine.fetcher);
|
|
1026
1230
|
}
|
|
@@ -1112,11 +1316,13 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1112
1316
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
1113
1317
|
const schemaMatch = fileContent.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1114
1318
|
if (!schemaMatch) {
|
|
1115
|
-
|
|
1319
|
+
const themeBlock = withThemeBlockSchema(cwd, undefined, blockType);
|
|
1320
|
+
res.json({ schema: resolveSchemaT({ settings: themeBlock?.settings || [], name: themeBlock?.name || blockType }, cwd) });
|
|
1116
1321
|
return;
|
|
1117
1322
|
}
|
|
1118
1323
|
const schema = JSON.parse(schemaMatch[1].trim());
|
|
1119
|
-
const
|
|
1324
|
+
const sectionBlockEntry = (schema.blocks || []).find((b) => b.type === blockType);
|
|
1325
|
+
const blockSchema = withThemeBlockSchema(cwd, sectionBlockEntry, blockType);
|
|
1120
1326
|
res.json({ schema: resolveSchemaT({ settings: blockSchema?.settings || [], name: blockSchema?.name || blockType }, cwd) });
|
|
1121
1327
|
}
|
|
1122
1328
|
catch (error) {
|
|
@@ -1127,39 +1333,106 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1127
1333
|
app.get('/_editor/availablesections', async (req, res, next) => {
|
|
1128
1334
|
try {
|
|
1129
1335
|
const needHtml = req.query.needHtml === 'true';
|
|
1336
|
+
const jsonType = req.query.jsonType || 'template';
|
|
1337
|
+
const pageName = req.query.pageName || 'index';
|
|
1130
1338
|
const sectionsDir = path.join(cwd, 'sections');
|
|
1131
1339
|
if (!fs.existsSync(sectionsDir)) {
|
|
1132
1340
|
res.json([]);
|
|
1133
1341
|
return;
|
|
1134
1342
|
}
|
|
1343
|
+
let templateName;
|
|
1344
|
+
if (jsonType === 'header') {
|
|
1345
|
+
templateName = 'header-group';
|
|
1346
|
+
}
|
|
1347
|
+
else if (jsonType === 'footer') {
|
|
1348
|
+
templateName = 'footer-group';
|
|
1349
|
+
}
|
|
1350
|
+
else {
|
|
1351
|
+
templateName = pageName === 'home' ? 'index' : pageName;
|
|
1352
|
+
}
|
|
1353
|
+
let currentTemplateJson = { sections: {}, order: [] };
|
|
1354
|
+
try {
|
|
1355
|
+
let jsonPath;
|
|
1356
|
+
if (jsonType === 'header') {
|
|
1357
|
+
jsonPath = path.join(cwd, 'sections', 'header-group.json');
|
|
1358
|
+
}
|
|
1359
|
+
else if (jsonType === 'footer') {
|
|
1360
|
+
jsonPath = path.join(cwd, 'sections', 'footer-group.json');
|
|
1361
|
+
}
|
|
1362
|
+
else {
|
|
1363
|
+
jsonPath = path.join(cwd, 'pages', `${templateName}.json`);
|
|
1364
|
+
}
|
|
1365
|
+
if (fs.existsSync(jsonPath)) {
|
|
1366
|
+
currentTemplateJson = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
catch { /* use defaults */ }
|
|
1370
|
+
const currentSections = (currentTemplateJson.sections || {});
|
|
1371
|
+
const currentOrder = (currentTemplateJson.order || []);
|
|
1372
|
+
const availableSections = [];
|
|
1135
1373
|
const files = fs.readdirSync(sectionsDir).filter((f) => f.endsWith('.liquid'));
|
|
1136
|
-
const
|
|
1374
|
+
for (const file of files) {
|
|
1137
1375
|
const sectionType = file.replace('.liquid', '');
|
|
1138
1376
|
const content = fs.readFileSync(path.join(sectionsDir, file), 'utf-8');
|
|
1139
1377
|
const schemaMatch = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1378
|
+
if (!schemaMatch)
|
|
1379
|
+
continue;
|
|
1380
|
+
let schema;
|
|
1381
|
+
try {
|
|
1382
|
+
schema = JSON.parse(schemaMatch[1].trim());
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
const templates = schema?.available_in?.templates;
|
|
1388
|
+
if (!Array.isArray(templates) || !templates.includes(templateName)) {
|
|
1389
|
+
continue;
|
|
1390
|
+
}
|
|
1391
|
+
const presets = schema.presets;
|
|
1392
|
+
if (!Array.isArray(presets) || presets.length === 0) {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof schema.limit === 'number') {
|
|
1396
|
+
let count = 0;
|
|
1397
|
+
for (const key of currentOrder) {
|
|
1398
|
+
if (currentSections[key]?.type === sectionType)
|
|
1399
|
+
count++;
|
|
1400
|
+
}
|
|
1401
|
+
if (count >= schema.limit) {
|
|
1402
|
+
continue;
|
|
1148
1403
|
}
|
|
1149
|
-
catch { /* skip */ }
|
|
1150
1404
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1405
|
+
for (let p = 0; p < presets.length; p++) {
|
|
1406
|
+
const preset = presets[p];
|
|
1407
|
+
if (!preset)
|
|
1408
|
+
continue;
|
|
1409
|
+
const uuid = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1410
|
+
const sectionName = `${sectionType}-additional_section-${uuid}`;
|
|
1411
|
+
const resolved = resolveSchemaT({ name: preset.name || sectionType, settings: [], blocks: [] }, cwd);
|
|
1412
|
+
const displayName = resolved.name || preset.name || sectionType;
|
|
1413
|
+
availableSections.push({
|
|
1414
|
+
section_name: sectionName,
|
|
1415
|
+
display_name: displayName,
|
|
1416
|
+
name: displayName,
|
|
1417
|
+
preset_index: p,
|
|
1418
|
+
category: schema.category,
|
|
1419
|
+
_sectionType: sectionType,
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1153
1423
|
if (!needHtml) {
|
|
1154
|
-
res.json(
|
|
1424
|
+
res.json(availableSections.map(({ _sectionType: _, ...s }) => s));
|
|
1155
1425
|
return;
|
|
1156
1426
|
}
|
|
1157
|
-
|
|
1158
|
-
const sectionNames = req.query.sectionNames
|
|
1427
|
+
const requestedNames = req.query.sectionNames
|
|
1159
1428
|
? JSON.parse(req.query.sectionNames)
|
|
1160
|
-
:
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1429
|
+
: availableSections.map((s) => s.section_name);
|
|
1430
|
+
const baseTypeFor = (name) => {
|
|
1431
|
+
const idx = name.indexOf('-additional_section-');
|
|
1432
|
+
return idx >= 0 ? name.substring(0, idx) : name;
|
|
1433
|
+
};
|
|
1434
|
+
const typesToRender = [...new Set(requestedNames.map(baseTypeFor))];
|
|
1435
|
+
const renderedMap = await renderSectionsForResponse(typesToRender, {});
|
|
1163
1436
|
const assetsDir = path.join(cwd, 'assets');
|
|
1164
1437
|
const cssLinks = fs.existsSync(assetsDir)
|
|
1165
1438
|
? fs.readdirSync(assetsDir)
|
|
@@ -1168,13 +1441,45 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1168
1441
|
.join('')
|
|
1169
1442
|
: '';
|
|
1170
1443
|
const cliOrigin = `http://localhost:${port}`;
|
|
1171
|
-
const
|
|
1172
|
-
|
|
1173
|
-
|
|
1444
|
+
const displayNameForType = new Map();
|
|
1445
|
+
for (const entry of availableSections) {
|
|
1446
|
+
if (!displayNameForType.has(entry._sectionType)) {
|
|
1447
|
+
displayNameForType.set(entry._sectionType, entry.display_name);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
const presetSettingsForType = new Map();
|
|
1451
|
+
for (const entry of availableSections) {
|
|
1452
|
+
if (!presetSettingsForType.has(entry._sectionType)) {
|
|
1453
|
+
presetSettingsForType.set(entry._sectionType, {});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
for (const [sType] of presetSettingsForType) {
|
|
1457
|
+
try {
|
|
1458
|
+
const content = fs.readFileSync(path.join(sectionsDir, `${sType}.liquid`), 'utf-8');
|
|
1459
|
+
const sm = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1460
|
+
if (sm) {
|
|
1461
|
+
const sc = JSON.parse(sm[1].trim());
|
|
1462
|
+
if (Array.isArray(sc.presets) && sc.presets[0]?.settings) {
|
|
1463
|
+
presetSettingsForType.set(sType, sc.presets[0].settings);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
catch { /* skip */ }
|
|
1468
|
+
}
|
|
1469
|
+
const results = requestedNames.map((reqName) => {
|
|
1470
|
+
const baseType = baseTypeFor(reqName);
|
|
1471
|
+
const sectionHtml = (renderedMap[baseType] || '')
|
|
1174
1472
|
.replace(/(['"(])\/assets\//g, `$1${cliOrigin}/assets/`)
|
|
1175
1473
|
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
1176
1474
|
const html = `<!DOCTYPE html><html><head><meta charset="utf-8">${cssLinks}</head><body>${sectionHtml}</body></html>`;
|
|
1177
|
-
|
|
1475
|
+
const displayName = displayNameForType.get(baseType) || baseType;
|
|
1476
|
+
const sectionJson = {
|
|
1477
|
+
type: baseType,
|
|
1478
|
+
settings: presetSettingsForType.get(baseType) || {},
|
|
1479
|
+
blocks: {},
|
|
1480
|
+
block_order: [],
|
|
1481
|
+
};
|
|
1482
|
+
return { section_name: reqName, display_name: displayName, name: displayName, sectionJson, html };
|
|
1178
1483
|
});
|
|
1179
1484
|
res.json(results);
|
|
1180
1485
|
}
|
|
@@ -1182,6 +1487,44 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1182
1487
|
next(error);
|
|
1183
1488
|
}
|
|
1184
1489
|
});
|
|
1490
|
+
app.get('/_editor/themesettings', (req, res, next) => {
|
|
1491
|
+
try {
|
|
1492
|
+
const settingsDir = path.join(cwd, 'settings');
|
|
1493
|
+
let settings_schema = [];
|
|
1494
|
+
const schemaPath = path.join(settingsDir, 'schema.json');
|
|
1495
|
+
if (fs.existsSync(schemaPath)) {
|
|
1496
|
+
try {
|
|
1497
|
+
const raw = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
|
|
1498
|
+
settings_schema = resolveSchemaT(raw, cwd);
|
|
1499
|
+
}
|
|
1500
|
+
catch { /* use empty */ }
|
|
1501
|
+
}
|
|
1502
|
+
let settings_data = { selected: {}, presets: {} };
|
|
1503
|
+
const themePath = path.join(settingsDir, 'theme.json');
|
|
1504
|
+
if (fs.existsSync(themePath)) {
|
|
1505
|
+
try {
|
|
1506
|
+
settings_data = JSON.parse(fs.readFileSync(themePath, 'utf-8'));
|
|
1507
|
+
if (!settings_data.selected)
|
|
1508
|
+
settings_data.selected = {};
|
|
1509
|
+
const { selected, presets } = settings_data;
|
|
1510
|
+
if (selected && typeof selected.preset_name === 'string' && presets) {
|
|
1511
|
+
const preset = presets[selected.preset_name];
|
|
1512
|
+
if (preset && typeof preset === 'object') {
|
|
1513
|
+
settings_data = {
|
|
1514
|
+
...settings_data,
|
|
1515
|
+
selected: { ...preset, preset_name: selected.preset_name },
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
catch { /* use defaults */ }
|
|
1521
|
+
}
|
|
1522
|
+
res.json({ settings_schema, settings_data });
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
next(error);
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1185
1528
|
// Persists the full editor JSON to disk (for structural changes: add/delete/reorder sections).
|
|
1186
1529
|
app.post('/_editor/savejson', (req, res, next) => {
|
|
1187
1530
|
try {
|
|
@@ -1238,7 +1581,8 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
1238
1581
|
}
|
|
1239
1582
|
res.set('Cache-Control', 'no-store').send('OK');
|
|
1240
1583
|
});
|
|
1241
|
-
|
|
1584
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1585
|
+
app.use((err, _req, res, _next) => {
|
|
1242
1586
|
if (res.headersSent) {
|
|
1243
1587
|
return;
|
|
1244
1588
|
}
|