@zohofinance/commerce 2.0.0 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zohofinance/commerce",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "main": "ts-dist/index.js",
5
5
  "bin": {
6
6
  "zcommerce": "ts-dist/index.js"
@@ -1 +1 @@
1
- {"version":3,"file":"serve-theme.d.ts","sourceRoot":"","sources":["../../src/commands/serve-theme.ts"],"names":[],"mappings":"AA4DA,iBAAe,UAAU,CACvB,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,OAAO,EACnB,aAAa,CAAC,EAAE,OAAO,EACvB,OAAO,UAAO,EACd,QAAQ,CAAC,EAAE,MAAM,iBA28ClB;AAED,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"serve-theme.d.ts","sourceRoot":"","sources":["../../src/commands/serve-theme.ts"],"names":[],"mappings":"AA4DA,iBAAe,UAAU,CACvB,UAAU,CAAC,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,OAAO,EACnB,aAAa,CAAC,EAAE,OAAO,EACvB,OAAO,UAAO,EACd,QAAQ,CAAC,EAAE,MAAM,iBA2oDlB;AAED,eAAe,UAAU,CAAC"}
@@ -749,7 +749,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
749
749
  // Runtime fields added by the CLI on section/block root objects — must NOT be written to disk.
750
750
  // NOTE: display_name inside settings values (e.g. {display_name:"Full Width",value:"full"}) is
751
751
  // 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']);
752
+ const COMPONENT_RUNTIME_FIELDS = new Set(['id', 'display_name', 'settingid', 'zcommerce_attributes', 'block_key', 'available_sections', 'available_blocks']);
753
753
  // Clean a single section or block object: strip runtime fields from root, recurse into blocks,
754
754
  // but leave settings values entirely untouched (they may contain legitimate display_name/handle).
755
755
  function cleanComponentForDisk(obj) {
@@ -774,11 +774,11 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
774
774
  }
775
775
  return out;
776
776
  }
777
- // Clean the full page/group JSON: only touches the sections map, leaves order/name/type intact.
778
777
  function cleanFullJsonForDisk(json) {
779
778
  const sections = json.sections;
780
- if (!sections || typeof sections !== 'object' || Array.isArray(sections))
779
+ if (!sections || typeof sections !== 'object' || Array.isArray(sections)) {
781
780
  return json;
781
+ }
782
782
  const cleanedSections = {};
783
783
  for (const [sk, sv] of Object.entries(sections)) {
784
784
  cleanedSections[sk] = sv && typeof sv === 'object' && !Array.isArray(sv)
@@ -991,6 +991,48 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
991
991
  next(error);
992
992
  }
993
993
  });
994
+ // Re-renders the full page with in-memory settings override (no disk write).
995
+ // Used by the editor when theme settings change so the preview updates immediately.
996
+ app.post('/_editor/renderpage', async (req, res, next) => {
997
+ const engineInternal = engine.engine;
998
+ try {
999
+ const body = req.body;
1000
+ const settingsData = body.settingsData;
1001
+ const pageName = body.pageName || 'index';
1002
+ const route = STOREFRONT_ROUTES.find((r) => r.pageTemplate === pageName)
1003
+ || STOREFRONT_ROUTES.find((r) => r.expressPath === `/${pageName}`)
1004
+ || STOREFRONT_ROUTES.find((r) => r.expressPath.split('/')[1]?.startsWith(pageName))
1005
+ || STOREFRONT_ROUTES[0];
1006
+ const apiPath = resolveApiPath(route, {});
1007
+ const data = await fetchPageData(apiPath);
1008
+ // Temporarily override runtimeGlobals.settings so the engine renders with
1009
+ // the new values. Restored in the finally block regardless of outcome.
1010
+ const savedSettings = runtimeGlobals.settings;
1011
+ if (settingsData)
1012
+ runtimeGlobals.settings = settingsData;
1013
+ engineInternal.__editorMode = true;
1014
+ engineInternal.__editorJsonType = 'template';
1015
+ try {
1016
+ const { html, error } = await engine.renderJSONTemplates({
1017
+ pathName: resolvePageTemplate(route.pageTemplate),
1018
+ data,
1019
+ });
1020
+ if (error) {
1021
+ res.status(500).json({ error: String(error) });
1022
+ return;
1023
+ }
1024
+ res.json({ html });
1025
+ }
1026
+ finally {
1027
+ runtimeGlobals.settings = savedSettings;
1028
+ engineInternal.__editorMode = false;
1029
+ }
1030
+ }
1031
+ catch (error) {
1032
+ engineInternal.__editorMode = false;
1033
+ next(error);
1034
+ }
1035
+ });
994
1036
  app.post('/_editor/rendersection', async (req, res, next) => {
995
1037
  try {
996
1038
  const body = req.body;
@@ -1127,39 +1169,106 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
1127
1169
  app.get('/_editor/availablesections', async (req, res, next) => {
1128
1170
  try {
1129
1171
  const needHtml = req.query.needHtml === 'true';
1172
+ const jsonType = req.query.jsonType || 'template';
1173
+ const pageName = req.query.pageName || 'index';
1130
1174
  const sectionsDir = path.join(cwd, 'sections');
1131
1175
  if (!fs.existsSync(sectionsDir)) {
1132
1176
  res.json([]);
1133
1177
  return;
1134
1178
  }
1179
+ let templateName;
1180
+ if (jsonType === 'header') {
1181
+ templateName = 'header-group';
1182
+ }
1183
+ else if (jsonType === 'footer') {
1184
+ templateName = 'footer-group';
1185
+ }
1186
+ else {
1187
+ templateName = pageName === 'home' ? 'index' : pageName;
1188
+ }
1189
+ let currentTemplateJson = { sections: {}, order: [] };
1190
+ try {
1191
+ let jsonPath;
1192
+ if (jsonType === 'header') {
1193
+ jsonPath = path.join(cwd, 'sections', 'header-group.json');
1194
+ }
1195
+ else if (jsonType === 'footer') {
1196
+ jsonPath = path.join(cwd, 'sections', 'footer-group.json');
1197
+ }
1198
+ else {
1199
+ jsonPath = path.join(cwd, 'pages', `${templateName}.json`);
1200
+ }
1201
+ if (fs.existsSync(jsonPath)) {
1202
+ currentTemplateJson = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
1203
+ }
1204
+ }
1205
+ catch { /* use defaults */ }
1206
+ const currentSections = (currentTemplateJson.sections || {});
1207
+ const currentOrder = (currentTemplateJson.order || []);
1208
+ const availableSections = [];
1135
1209
  const files = fs.readdirSync(sectionsDir).filter((f) => f.endsWith('.liquid'));
1136
- const sections = files.map((file) => {
1210
+ for (const file of files) {
1137
1211
  const sectionType = file.replace('.liquid', '');
1138
1212
  const content = fs.readFileSync(path.join(sectionsDir, file), 'utf-8');
1139
1213
  const schemaMatch = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
1140
- let name = sectionType;
1141
- let category;
1142
- if (schemaMatch) {
1143
- try {
1144
- const schema = JSON.parse(schemaMatch[1].trim());
1145
- const resolved = resolveSchemaT({ name: schema.name || sectionType, settings: [], blocks: [] }, cwd);
1146
- name = resolved.name || sectionType;
1147
- category = schema.category;
1214
+ if (!schemaMatch)
1215
+ continue;
1216
+ let schema;
1217
+ try {
1218
+ schema = JSON.parse(schemaMatch[1].trim());
1219
+ }
1220
+ catch {
1221
+ continue;
1222
+ }
1223
+ const templates = schema?.available_in?.templates;
1224
+ if (!Array.isArray(templates) || !templates.includes(templateName)) {
1225
+ continue;
1226
+ }
1227
+ const presets = schema.presets;
1228
+ if (!Array.isArray(presets) || presets.length === 0) {
1229
+ continue;
1230
+ }
1231
+ if (typeof schema.limit === 'number') {
1232
+ let count = 0;
1233
+ for (const key of currentOrder) {
1234
+ if (currentSections[key]?.type === sectionType)
1235
+ count++;
1236
+ }
1237
+ if (count >= schema.limit) {
1238
+ continue;
1148
1239
  }
1149
- catch { /* skip */ }
1150
1240
  }
1151
- return { section_name: sectionType, name, category };
1152
- });
1241
+ for (let p = 0; p < presets.length; p++) {
1242
+ const preset = presets[p];
1243
+ if (!preset)
1244
+ continue;
1245
+ const uuid = Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
1246
+ const sectionName = `${sectionType}-additional_section-${uuid}`;
1247
+ const resolved = resolveSchemaT({ name: preset.name || sectionType, settings: [], blocks: [] }, cwd);
1248
+ const displayName = resolved.name || preset.name || sectionType;
1249
+ availableSections.push({
1250
+ section_name: sectionName,
1251
+ display_name: displayName,
1252
+ name: displayName,
1253
+ preset_index: p,
1254
+ category: schema.category,
1255
+ _sectionType: sectionType,
1256
+ });
1257
+ }
1258
+ }
1153
1259
  if (!needHtml) {
1154
- res.json(sections);
1260
+ res.json(availableSections.map(({ _sectionType: _, ...s }) => s));
1155
1261
  return;
1156
1262
  }
1157
- // Render HTML for requested section names using default section data.
1158
- const sectionNames = req.query.sectionNames
1263
+ const requestedNames = req.query.sectionNames
1159
1264
  ? JSON.parse(req.query.sectionNames)
1160
- : sections.map((s) => s.section_name);
1161
- const renderedMap = await renderSectionsForResponse(sectionNames, {});
1162
- // Build CSS link tags from assets directory so previews render with theme styles.
1265
+ : availableSections.map((s) => s.section_name);
1266
+ const baseTypeFor = (name) => {
1267
+ const idx = name.indexOf('-additional_section-');
1268
+ return idx >= 0 ? name.substring(0, idx) : name;
1269
+ };
1270
+ const typesToRender = [...new Set(requestedNames.map(baseTypeFor))];
1271
+ const renderedMap = await renderSectionsForResponse(typesToRender, {});
1163
1272
  const assetsDir = path.join(cwd, 'assets');
1164
1273
  const cssLinks = fs.existsSync(assetsDir)
1165
1274
  ? fs.readdirSync(assetsDir)
@@ -1168,13 +1277,45 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
1168
1277
  .join('')
1169
1278
  : '';
1170
1279
  const cliOrigin = `http://localhost:${port}`;
1171
- const results = sectionNames.map((sectionType) => {
1172
- const entry = sections.find((s) => s.section_name === sectionType);
1173
- const sectionHtml = (renderedMap[sectionType] || '')
1280
+ const displayNameForType = new Map();
1281
+ for (const entry of availableSections) {
1282
+ if (!displayNameForType.has(entry._sectionType)) {
1283
+ displayNameForType.set(entry._sectionType, entry.display_name);
1284
+ }
1285
+ }
1286
+ const presetSettingsForType = new Map();
1287
+ for (const entry of availableSections) {
1288
+ if (!presetSettingsForType.has(entry._sectionType)) {
1289
+ presetSettingsForType.set(entry._sectionType, {});
1290
+ }
1291
+ }
1292
+ for (const [sType] of presetSettingsForType) {
1293
+ try {
1294
+ const content = fs.readFileSync(path.join(sectionsDir, `${sType}.liquid`), 'utf-8');
1295
+ const sm = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
1296
+ if (sm) {
1297
+ const sc = JSON.parse(sm[1].trim());
1298
+ if (Array.isArray(sc.presets) && sc.presets[0]?.settings) {
1299
+ presetSettingsForType.set(sType, sc.presets[0].settings);
1300
+ }
1301
+ }
1302
+ }
1303
+ catch { /* skip */ }
1304
+ }
1305
+ const results = requestedNames.map((reqName) => {
1306
+ const baseType = baseTypeFor(reqName);
1307
+ const sectionHtml = (renderedMap[baseType] || '')
1174
1308
  .replace(/(['"(])\/assets\//g, `$1${cliOrigin}/assets/`)
1175
1309
  .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
1176
1310
  const html = `<!DOCTYPE html><html><head><meta charset="utf-8">${cssLinks}</head><body>${sectionHtml}</body></html>`;
1177
- return { section_name: sectionType, display_name: entry?.name || sectionType, name: entry?.name || sectionType, category: entry?.category, html };
1311
+ const displayName = displayNameForType.get(baseType) || baseType;
1312
+ const sectionJson = {
1313
+ type: baseType,
1314
+ settings: presetSettingsForType.get(baseType) || {},
1315
+ blocks: {},
1316
+ block_order: [],
1317
+ };
1318
+ return { section_name: reqName, display_name: displayName, name: displayName, sectionJson, html };
1178
1319
  });
1179
1320
  res.json(results);
1180
1321
  }
@@ -1182,6 +1323,44 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
1182
1323
  next(error);
1183
1324
  }
1184
1325
  });
1326
+ app.get('/_editor/themesettings', (req, res, next) => {
1327
+ try {
1328
+ const settingsDir = path.join(cwd, 'settings');
1329
+ let settings_schema = [];
1330
+ const schemaPath = path.join(settingsDir, 'schema.json');
1331
+ if (fs.existsSync(schemaPath)) {
1332
+ try {
1333
+ const raw = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
1334
+ settings_schema = resolveSchemaT(raw, cwd);
1335
+ }
1336
+ catch { /* use empty */ }
1337
+ }
1338
+ let settings_data = { selected: {}, presets: {} };
1339
+ const themePath = path.join(settingsDir, 'theme.json');
1340
+ if (fs.existsSync(themePath)) {
1341
+ try {
1342
+ settings_data = JSON.parse(fs.readFileSync(themePath, 'utf-8'));
1343
+ if (!settings_data.selected)
1344
+ settings_data.selected = {};
1345
+ const { selected, presets } = settings_data;
1346
+ if (selected && typeof selected.preset_name === 'string' && presets) {
1347
+ const preset = presets[selected.preset_name];
1348
+ if (preset && typeof preset === 'object') {
1349
+ settings_data = {
1350
+ ...settings_data,
1351
+ selected: { ...preset, preset_name: selected.preset_name },
1352
+ };
1353
+ }
1354
+ }
1355
+ }
1356
+ catch { /* use defaults */ }
1357
+ }
1358
+ res.json({ settings_schema, settings_data });
1359
+ }
1360
+ catch (error) {
1361
+ next(error);
1362
+ }
1363
+ });
1185
1364
  // Persists the full editor JSON to disk (for structural changes: add/delete/reorder sections).
1186
1365
  app.post('/_editor/savejson', (req, res, next) => {
1187
1366
  try {
@@ -1238,7 +1417,8 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
1238
1417
  }
1239
1418
  res.set('Cache-Control', 'no-store').send('OK');
1240
1419
  });
1241
- app.use((err, _req, res) => {
1420
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1421
+ app.use((err, _req, res, _next) => {
1242
1422
  if (res.headersSent) {
1243
1423
  return;
1244
1424
  }
@@ -1 +1 @@
1
- {"version":3,"file":"editor-attributes.d.ts","sourceRoot":"","sources":["../../src/utils/editor-attributes.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAUR;AAED,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAYR"}
1
+ {"version":3,"file":"editor-attributes.d.ts","sourceRoot":"","sources":["../../src/utils/editor-attributes.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CASR;AAED,wBAAgB,6BAA6B,CAC3C,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,GACnB,MAAM,CAWR"}
@@ -2,10 +2,9 @@ export function buildSectionComponentAttr(sectionType, sectionKey, jsonType, dis
2
2
  return JSON.stringify({
3
3
  id: `section_${sectionKey}`,
4
4
  'data-zcommerce-section-id': '',
5
- 'data-zcommerce-component-name': sectionType,
5
+ 'data-zcommerce-component-name': displayName || sectionType,
6
6
  'data-zcommerce-component-key': `section_${sectionKey}`,
7
7
  'data-zcommerce-json-type': jsonType,
8
- 'data-zcommerce-display-name': displayName || sectionType,
9
8
  settingid: `setting_section_${sectionType}`
10
9
  });
11
10
  }
@@ -14,10 +13,9 @@ export function buildBlockZcommerceAttributes(blockType, blockKey, sectionKey, j
14
13
  const attrJson = JSON.stringify({
15
14
  id,
16
15
  'data-zcommerce-section-id': `section_${sectionKey}`,
17
- 'data-zcommerce-component-name': blockType,
16
+ 'data-zcommerce-component-name': displayName || blockType,
18
17
  'data-zcommerce-component-key': id,
19
18
  'data-zcommerce-json-type': jsonType,
20
- 'data-zcommerce-display-name': displayName || blockType,
21
19
  settingid: `setting_block_${blockType}`
22
20
  }).replace(/'/g, '&#39;');
23
21
  return `data-zcommerce-component='${attrJson}'`;