@zohofinance/commerce 1.0.1 → 2.0.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/README.md +0 -2
- package/package.json +2 -2
- package/public/iframe-bridge.js +1303 -0
- package/ts-dist/commands/serve-theme.d.ts.map +1 -1
- package/ts-dist/commands/serve-theme.js +488 -2
- package/ts-dist/custom-tags/sections-tag.d.ts.map +1 -1
- package/ts-dist/custom-tags/sections-tag.js +34 -8
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/SKILL.md +2 -2
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/ask-developer.md +1 -1
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/dev-server-and-urls.md +14 -5
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/subagent-escalation.md +1 -1
- package/ts-dist/mocks/init-theme/.claude/skills/theme-from-figma/visual-verify-loop.md +1 -1
- package/ts-dist/mocks/init-theme/.cursor/agents/visual-compare.md +2 -2
- package/ts-dist/mocks/init-theme/.cursor/commands/build-theme.md +1 -1
- package/ts-dist/mocks/init-theme/.cursor/commands/compare-section.md +1 -1
- package/ts-dist/mocks/init-theme/.cursor/skills/browser-verify.md +1 -1
- package/ts-dist/utils/editor-attributes.d.ts +3 -0
- package/ts-dist/utils/editor-attributes.d.ts.map +1 -0
- package/ts-dist/utils/editor-attributes.js +24 -0
- package/ts-dist/utils/prepare-section-data.d.ts +4 -1
- package/ts-dist/utils/prepare-section-data.d.ts.map +1 -1
- package/ts-dist/utils/prepare-section-data.js +24 -1
- package/ts-dist/utils/resolve-schema-translations.d.ts +3 -0
- package/ts-dist/utils/resolve-schema-translations.d.ts.map +1 -0
- package/ts-dist/utils/resolve-schema-translations.js +61 -0
- package/ts-dist/utils/template-engine.d.ts.map +1 -1
- package/ts-dist/utils/template-engine.js +19 -5
- package/ts-dist/utils/theme-mocks.d.ts +2 -0
- package/ts-dist/utils/theme-mocks.d.ts.map +1 -0
- package/ts-dist/utils/theme-mocks.js +40 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve-theme.d.ts","sourceRoot":"","sources":["../../src/commands/serve-theme.ts"],"names":[],"mappings":"
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import open from 'open';
|
|
4
5
|
import openEditor from 'open-editor';
|
|
5
6
|
import chalk from 'chalk';
|
|
@@ -15,6 +16,10 @@ import StackTrace from '../utils/stack-trace.js';
|
|
|
15
16
|
import { validateTheme, formatValidationIssuesForConsole } from '../utils/theme-validation.js';
|
|
16
17
|
import { buildValidationErrorReport, buildRuntimeErrorReport, renderDevErrorPage } from '../utils/error-report.js';
|
|
17
18
|
import { STOREFRONT_ROUTES, resolveApiPath, resolvePageTemplate } from '../routes/storefront-routes.js';
|
|
19
|
+
import { resolveSchemaSettings } from '../utils/resolve-schema-settings.js';
|
|
20
|
+
import { resolveSchemaTranslations as resolveSchemaT, resolveSchemaName } from '../utils/resolve-schema-translations.js';
|
|
21
|
+
import { prepareSectionData } from '../utils/prepare-section-data.js';
|
|
22
|
+
import { buildSectionComponentAttr } from '../utils/editor-attributes.js';
|
|
18
23
|
function normalizeStoreUrl(input) {
|
|
19
24
|
if (!input)
|
|
20
25
|
return undefined;
|
|
@@ -183,7 +188,11 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
183
188
|
try {
|
|
184
189
|
const pageTemplate = resolvePageTemplate(routePageTemplate);
|
|
185
190
|
const data = await fetchPageData(apiPath);
|
|
191
|
+
// Always render in editor mode so sections get data-zcommerce-component attributes
|
|
192
|
+
engine.engine.__editorMode = true;
|
|
193
|
+
engine.engine.__editorJsonType = 'template';
|
|
186
194
|
const result = await renderPage(pageTemplate, data, req);
|
|
195
|
+
engine.engine.__editorMode = false;
|
|
187
196
|
if ('json' in result && result.json) {
|
|
188
197
|
res
|
|
189
198
|
.status(result.status ?? 200)
|
|
@@ -191,9 +200,18 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
191
200
|
.send(JSON.stringify(result.json, null, 2));
|
|
192
201
|
return;
|
|
193
202
|
}
|
|
194
|
-
|
|
203
|
+
let html = result.html ?? '';
|
|
204
|
+
const bridgeTag = '<script src="/sb-bridge.js"></script>';
|
|
205
|
+
if (html.includes('</body>')) {
|
|
206
|
+
html = html.replace('</body>', `${bridgeTag}</body>`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
html += bridgeTag;
|
|
210
|
+
}
|
|
211
|
+
res.status(result.status ?? 200).send(html);
|
|
195
212
|
}
|
|
196
213
|
catch (error) {
|
|
214
|
+
engine.engine.__editorMode = false;
|
|
197
215
|
next(error);
|
|
198
216
|
}
|
|
199
217
|
}
|
|
@@ -201,6 +219,20 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
201
219
|
const app = express();
|
|
202
220
|
const upload = multer();
|
|
203
221
|
app.use(express.json());
|
|
222
|
+
app.use((_req, res, next) => {
|
|
223
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
224
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
225
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
226
|
+
if (_req.method === 'OPTIONS') {
|
|
227
|
+
res.sendStatus(204);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
next();
|
|
231
|
+
});
|
|
232
|
+
app.get('/sb-bridge.js', (_req, res) => {
|
|
233
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
234
|
+
res.type('application/javascript').sendFile(path.resolve(__dirname, '../../public/iframe-bridge.js'));
|
|
235
|
+
});
|
|
204
236
|
app.engine('liquid', engine.express());
|
|
205
237
|
app.set('views', ['./pages']);
|
|
206
238
|
app.set('view engine', 'liquid');
|
|
@@ -714,6 +746,460 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
714
746
|
next(error);
|
|
715
747
|
}
|
|
716
748
|
});
|
|
749
|
+
// Runtime fields added by the CLI on section/block root objects — must NOT be written to disk.
|
|
750
|
+
// NOTE: display_name inside settings values (e.g. {display_name:"Full Width",value:"full"}) is
|
|
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']);
|
|
753
|
+
// Clean a single section or block object: strip runtime fields from root, recurse into blocks,
|
|
754
|
+
// but leave settings values entirely untouched (they may contain legitimate display_name/handle).
|
|
755
|
+
function cleanComponentForDisk(obj) {
|
|
756
|
+
const out = {};
|
|
757
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
758
|
+
if (COMPONENT_RUNTIME_FIELDS.has(k))
|
|
759
|
+
continue;
|
|
760
|
+
if (k === 'blocks' && v && typeof v === 'object' && !Array.isArray(v)) {
|
|
761
|
+
// blocks is an object map — clean each block recursively
|
|
762
|
+
const cleanedBlocks = {};
|
|
763
|
+
for (const [bk, bv] of Object.entries(v)) {
|
|
764
|
+
cleanedBlocks[bk] = bv && typeof bv === 'object' && !Array.isArray(bv)
|
|
765
|
+
? cleanComponentForDisk(bv)
|
|
766
|
+
: bv;
|
|
767
|
+
}
|
|
768
|
+
out[k] = cleanedBlocks;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// settings, type, block_order, etc. — pass through unchanged
|
|
772
|
+
out[k] = v;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return out;
|
|
776
|
+
}
|
|
777
|
+
// Clean the full page/group JSON: only touches the sections map, leaves order/name/type intact.
|
|
778
|
+
function cleanFullJsonForDisk(json) {
|
|
779
|
+
const sections = json.sections;
|
|
780
|
+
if (!sections || typeof sections !== 'object' || Array.isArray(sections))
|
|
781
|
+
return json;
|
|
782
|
+
const cleanedSections = {};
|
|
783
|
+
for (const [sk, sv] of Object.entries(sections)) {
|
|
784
|
+
cleanedSections[sk] = sv && typeof sv === 'object' && !Array.isArray(sv)
|
|
785
|
+
? cleanComponentForDisk(sv)
|
|
786
|
+
: sv;
|
|
787
|
+
}
|
|
788
|
+
return { ...json, sections: cleanedSections };
|
|
789
|
+
}
|
|
790
|
+
function getJsonFilePath(jsonType, pageName) {
|
|
791
|
+
if (jsonType === 'header')
|
|
792
|
+
return path.join(cwd, 'sections', 'header-group.json');
|
|
793
|
+
if (jsonType === 'footer')
|
|
794
|
+
return path.join(cwd, 'sections', 'footer-group.json');
|
|
795
|
+
const page = pageName === 'home' ? 'index' : pageName || 'index';
|
|
796
|
+
return path.join(cwd, 'pages', `${page}.json`);
|
|
797
|
+
}
|
|
798
|
+
function persistFullJsonToDisk(jsonType, pageName, json) {
|
|
799
|
+
const filePath = getJsonFilePath(jsonType, pageName);
|
|
800
|
+
try {
|
|
801
|
+
fs.writeFileSync(filePath, JSON.stringify(cleanFullJsonForDisk(json), null, 2), 'utf-8');
|
|
802
|
+
}
|
|
803
|
+
catch (e) {
|
|
804
|
+
console.warn('[dev] Failed to persist full JSON:', e);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// Cache parsed section schemas (type → { name, blocks }) to avoid re-reading files.
|
|
808
|
+
const sectionSchemaCache = new Map();
|
|
809
|
+
function getSectionSchema(sectionType) {
|
|
810
|
+
if (sectionSchemaCache.has(sectionType))
|
|
811
|
+
return sectionSchemaCache.get(sectionType);
|
|
812
|
+
const liquidPath = path.join(cwd, 'sections', `${sectionType}.liquid`);
|
|
813
|
+
try {
|
|
814
|
+
const content = fs.existsSync(liquidPath) ? fs.readFileSync(liquidPath, 'utf-8') : '';
|
|
815
|
+
const match = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
816
|
+
const schema = match ? JSON.parse(match[1].trim()) : {};
|
|
817
|
+
const result = { name: schema.name, blocks: schema.blocks };
|
|
818
|
+
sectionSchemaCache.set(sectionType, result);
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
catch {
|
|
822
|
+
sectionSchemaCache.set(sectionType, {});
|
|
823
|
+
return {};
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function addSectionIds(json) {
|
|
827
|
+
const sections = json.sections;
|
|
828
|
+
if (sections) {
|
|
829
|
+
for (const [key, section] of Object.entries(sections)) {
|
|
830
|
+
section.id = `section_${key}`;
|
|
831
|
+
const sectionType = section.type || key;
|
|
832
|
+
const schema = getSectionSchema(sectionType);
|
|
833
|
+
if (!section.display_name) {
|
|
834
|
+
section.display_name = resolveSchemaName(schema.name, sectionType, cwd);
|
|
835
|
+
}
|
|
836
|
+
const blocks = section.blocks;
|
|
837
|
+
if (blocks && !Array.isArray(blocks) && typeof blocks === 'object') {
|
|
838
|
+
const blockSchemaByType = new Map((schema.blocks || []).map((b) => [b.type, b]));
|
|
839
|
+
for (const [blockKey, block] of Object.entries(blocks)) {
|
|
840
|
+
if (block && typeof block === 'object') {
|
|
841
|
+
if (!block.id) {
|
|
842
|
+
block.id = `block_${blockKey}_section_${key}`;
|
|
843
|
+
}
|
|
844
|
+
if (!block.display_name) {
|
|
845
|
+
const blockType = block.type || blockKey;
|
|
846
|
+
const blockSchema = blockSchemaByType.get(blockType);
|
|
847
|
+
block.display_name = resolveSchemaName(blockSchema?.name, blockType, cwd);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return json;
|
|
855
|
+
}
|
|
856
|
+
app.get('/_editor/themeinfo', (_req, res) => {
|
|
857
|
+
res.json({ name: path.basename(cwd) });
|
|
858
|
+
});
|
|
859
|
+
app.get('/_editor/availablepages', (_req, res, next) => {
|
|
860
|
+
try {
|
|
861
|
+
const pagesDir = path.join(cwd, 'pages');
|
|
862
|
+
if (!fs.existsSync(pagesDir)) {
|
|
863
|
+
res.json([]);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
const PAGE_NAMES = {
|
|
867
|
+
index: 'Home', product: 'Product', collection: 'Category',
|
|
868
|
+
cart: 'Cart', checkout: 'Checkout', search: 'Search',
|
|
869
|
+
wishlist: 'Wishlist', account: 'Account', blog: 'Blog',
|
|
870
|
+
article: 'Article', page: 'Page', order: 'Order',
|
|
871
|
+
};
|
|
872
|
+
const pages = fs.readdirSync(pagesDir)
|
|
873
|
+
.filter((f) => f.endsWith('.json'))
|
|
874
|
+
.map((f) => {
|
|
875
|
+
const key = f.replace('.json', '');
|
|
876
|
+
return { key, name: PAGE_NAMES[key] || key.charAt(0).toUpperCase() + key.slice(1), url: `/${key === 'index' ? '' : key}` };
|
|
877
|
+
});
|
|
878
|
+
res.json(pages);
|
|
879
|
+
}
|
|
880
|
+
catch (error) {
|
|
881
|
+
next(error);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
app.get('/_editor/sectionsschema', (_req, res, next) => {
|
|
885
|
+
try {
|
|
886
|
+
const sectionsDir = path.join(cwd, 'sections');
|
|
887
|
+
if (!fs.existsSync(sectionsDir)) {
|
|
888
|
+
res.json({ sections_schema: [] });
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const files = fs.readdirSync(sectionsDir).filter((f) => f.endsWith('.liquid'));
|
|
892
|
+
const sections_schema = files.map((file) => {
|
|
893
|
+
const sectionType = file.replace('.liquid', '');
|
|
894
|
+
const content = fs.readFileSync(path.join(sectionsDir, file), 'utf-8');
|
|
895
|
+
const schemaMatch = content.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
896
|
+
let schema = { settings: [], blocks: [] };
|
|
897
|
+
if (schemaMatch) {
|
|
898
|
+
try {
|
|
899
|
+
schema = JSON.parse(schemaMatch[1].trim());
|
|
900
|
+
}
|
|
901
|
+
catch { /* skip invalid schema */ }
|
|
902
|
+
}
|
|
903
|
+
const resolved = resolveSchemaT({ settings: schema.settings || [], name: schema.name || sectionType, blocks: schema.blocks || [] }, cwd);
|
|
904
|
+
const blocks = (resolved.blocks || []).map((b) => ({
|
|
905
|
+
...b,
|
|
906
|
+
id: `setting_section_${sectionType}.${b.type}`,
|
|
907
|
+
settings: b.settings || [],
|
|
908
|
+
}));
|
|
909
|
+
return {
|
|
910
|
+
id: `setting_section_${sectionType}`,
|
|
911
|
+
name: resolved.name || sectionType,
|
|
912
|
+
type: sectionType,
|
|
913
|
+
settings: resolved.settings || [],
|
|
914
|
+
blocks,
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
res.json({ sections_schema });
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
next(error);
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
app.get('/_editor/templatejson', (req, res, next) => {
|
|
924
|
+
try {
|
|
925
|
+
const pageName = req.query.page || 'index';
|
|
926
|
+
const jsonPath = path.join(cwd, 'pages', `${pageName}.json`);
|
|
927
|
+
if (!fs.existsSync(jsonPath)) {
|
|
928
|
+
res.status(404).json({ error: `pages/${pageName}.json not found` });
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
932
|
+
res.json(addSectionIds(json));
|
|
933
|
+
}
|
|
934
|
+
catch (error) {
|
|
935
|
+
next(error);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
app.get('/_editor/headerjson', (req, res, next) => {
|
|
939
|
+
try {
|
|
940
|
+
const jsonPath = path.join(cwd, 'sections', 'header-group.json');
|
|
941
|
+
if (!fs.existsSync(jsonPath)) {
|
|
942
|
+
res.status(404).json({ error: 'sections/header-group.json not found' });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
946
|
+
res.json(addSectionIds(json));
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
next(error);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
app.get('/_editor/footerjson', (req, res, next) => {
|
|
953
|
+
try {
|
|
954
|
+
const jsonPath = path.join(cwd, 'sections', 'footer-group.json');
|
|
955
|
+
if (!fs.existsSync(jsonPath)) {
|
|
956
|
+
res.status(404).json({ error: 'sections/footer-group.json not found' });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'));
|
|
960
|
+
res.json(addSectionIds(json));
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
next(error);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
app.get('/_editor/page', async (req, res, next) => {
|
|
967
|
+
try {
|
|
968
|
+
const pageKey = req.query.page || 'home';
|
|
969
|
+
// Match by pageTemplate, exact expressPath, or expressPath prefix (e.g. 'product' → '/products/...')
|
|
970
|
+
const route = STOREFRONT_ROUTES.find((r) => r.pageTemplate === pageKey)
|
|
971
|
+
|| STOREFRONT_ROUTES.find((r) => r.expressPath === `/${pageKey}`)
|
|
972
|
+
|| STOREFRONT_ROUTES.find((r) => r.expressPath.split('/')[1]?.startsWith(pageKey))
|
|
973
|
+
|| STOREFRONT_ROUTES[0];
|
|
974
|
+
const apiPath = resolveApiPath(route, {});
|
|
975
|
+
const data = await fetchPageData(apiPath);
|
|
976
|
+
engine.engine.__editorMode = true;
|
|
977
|
+
engine.engine.__editorJsonType = 'template';
|
|
978
|
+
const { html, error } = await engine.renderJSONTemplates({
|
|
979
|
+
pathName: resolvePageTemplate(route.pageTemplate),
|
|
980
|
+
data
|
|
981
|
+
});
|
|
982
|
+
engine.engine.__editorMode = false;
|
|
983
|
+
if (error) {
|
|
984
|
+
res.status(500).json({ error: String(error) });
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
res.send(html);
|
|
988
|
+
}
|
|
989
|
+
catch (error) {
|
|
990
|
+
engine.engine.__editorMode = false;
|
|
991
|
+
next(error);
|
|
992
|
+
}
|
|
993
|
+
});
|
|
994
|
+
app.post('/_editor/rendersection', async (req, res, next) => {
|
|
995
|
+
try {
|
|
996
|
+
const body = req.body;
|
|
997
|
+
const sectionName = body.sectionName;
|
|
998
|
+
const jsonType = body.jsonType || 'template';
|
|
999
|
+
if (!sectionName) {
|
|
1000
|
+
res.status(400).json({ error: 'sectionName required' });
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const sectionJsonRaw = body.sectionJson;
|
|
1004
|
+
const sectionJson = typeof sectionJsonRaw === 'string' ? JSON.parse(sectionJsonRaw) : sectionJsonRaw;
|
|
1005
|
+
const sectionType = sectionJson.type || sectionName;
|
|
1006
|
+
const filePath = path.join(cwd, 'sections', `${sectionType}.liquid`);
|
|
1007
|
+
if (!fs.existsSync(filePath)) {
|
|
1008
|
+
res.status(404).json({ error: `Section ${sectionType} not found` });
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
1012
|
+
const schemaMatch = fileContent.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1013
|
+
let schemaData;
|
|
1014
|
+
if (schemaMatch) {
|
|
1015
|
+
try {
|
|
1016
|
+
schemaData = JSON.parse(schemaMatch[1].trim());
|
|
1017
|
+
}
|
|
1018
|
+
catch { /* ignore */ }
|
|
1019
|
+
}
|
|
1020
|
+
// Deep copy BEFORE any mutation (resolveSchemaSettings, prepareSectionData both mutate sectionJson).
|
|
1021
|
+
// This copy goes back to the editor and is later saved to disk — must have original unmodified URLs.
|
|
1022
|
+
const editorSectionJson = JSON.parse(JSON.stringify(sectionJson));
|
|
1023
|
+
const data = { ...runtimeGlobals };
|
|
1024
|
+
if (schemaData?.settings && sectionJson.settings) {
|
|
1025
|
+
await resolveSchemaSettings(schemaData.settings, sectionJson.settings, data, engine.fetcher);
|
|
1026
|
+
}
|
|
1027
|
+
editorSectionJson.id = `section_${sectionName}`;
|
|
1028
|
+
editorSectionJson.display_name = resolveSchemaName(schemaData?.name, sectionType, cwd);
|
|
1029
|
+
const editorBlocks = editorSectionJson.blocks;
|
|
1030
|
+
if (editorBlocks && !Array.isArray(editorBlocks) && typeof editorBlocks === 'object') {
|
|
1031
|
+
const blockSchemaByType = new Map((schemaData?.blocks || []).map((b) => [b.type, b]));
|
|
1032
|
+
for (const [blockKey, block] of Object.entries(editorBlocks)) {
|
|
1033
|
+
if (block && typeof block === 'object') {
|
|
1034
|
+
if (!block.id) {
|
|
1035
|
+
block.id = `block_${blockKey}_section_${sectionName}`;
|
|
1036
|
+
}
|
|
1037
|
+
if (!block.display_name) {
|
|
1038
|
+
const blockType = block.type || blockKey;
|
|
1039
|
+
const blockSchema = blockSchemaByType.get(blockType);
|
|
1040
|
+
block.display_name = resolveSchemaName(blockSchema?.name, blockType, cwd);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const preparedSection = prepareSectionData(sectionJson, schemaData, sectionName, { jsonType }, cwd);
|
|
1046
|
+
const resolvedDisplayName = resolveSchemaName(schemaData?.name, sectionType, cwd);
|
|
1047
|
+
preparedSection.settingid = `setting_section_${sectionType}`;
|
|
1048
|
+
preparedSection.display_name = resolvedDisplayName;
|
|
1049
|
+
const sectionHtml = await engine.renderFile(engine.getFilePath(`sections/${sectionType}.liquid`), {
|
|
1050
|
+
...data,
|
|
1051
|
+
section: {
|
|
1052
|
+
...preparedSection,
|
|
1053
|
+
id: `section_${sectionName}`,
|
|
1054
|
+
settingid: `setting_section_${sectionType}`,
|
|
1055
|
+
display_name: resolvedDisplayName
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
const attrJson = buildSectionComponentAttr(sectionType, sectionName, jsonType, resolvedDisplayName);
|
|
1059
|
+
const wrappedHtml = `<div data-zcommerce-component='${attrJson.replace(/'/g, ''')}' style="display:contents">${sectionHtml}</div>`;
|
|
1060
|
+
res.json({ html: wrappedHtml, sectionJson: editorSectionJson });
|
|
1061
|
+
}
|
|
1062
|
+
catch (error) {
|
|
1063
|
+
next(error);
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
app.post('/_editor/sectionschema', async (req, res, next) => {
|
|
1067
|
+
try {
|
|
1068
|
+
const body = req.body;
|
|
1069
|
+
const sectionJsonRaw = body.sectionJson;
|
|
1070
|
+
const sectionJson = typeof sectionJsonRaw === 'string' ? JSON.parse(sectionJsonRaw) : sectionJsonRaw;
|
|
1071
|
+
const sectionType = sectionJson?.type;
|
|
1072
|
+
if (!sectionType) {
|
|
1073
|
+
res.status(400).json({ error: 'sectionJson.type required' });
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const filePath = path.join(cwd, 'sections', `${sectionType}.liquid`);
|
|
1077
|
+
if (!fs.existsSync(filePath)) {
|
|
1078
|
+
res.status(404).json({ error: `Section ${sectionType} not found` });
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
1082
|
+
const schemaMatch = fileContent.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1083
|
+
if (!schemaMatch) {
|
|
1084
|
+
res.json({ schema: { settings: [], name: sectionType } });
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const schema = JSON.parse(schemaMatch[1].trim());
|
|
1088
|
+
res.json({ schema: resolveSchemaT({ settings: schema.settings || [], name: schema.name || sectionType, blocks: schema.blocks || [] }, cwd) });
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
next(error);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
app.post('/_editor/blockschema', async (req, res, next) => {
|
|
1095
|
+
try {
|
|
1096
|
+
const body = req.body;
|
|
1097
|
+
const sectionJsonRaw = body.sectionJson;
|
|
1098
|
+
const blockJsonRaw = body.blockJson;
|
|
1099
|
+
const sectionJson = typeof sectionJsonRaw === 'string' ? JSON.parse(sectionJsonRaw) : sectionJsonRaw;
|
|
1100
|
+
const blockJson = typeof blockJsonRaw === 'string' ? JSON.parse(blockJsonRaw) : blockJsonRaw;
|
|
1101
|
+
const sectionType = sectionJson?.type;
|
|
1102
|
+
const blockType = blockJson?.type;
|
|
1103
|
+
if (!sectionType || !blockType) {
|
|
1104
|
+
res.status(400).json({ error: 'sectionJson.type and blockJson.type required' });
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const filePath = path.join(cwd, 'sections', `${sectionType}.liquid`);
|
|
1108
|
+
if (!fs.existsSync(filePath)) {
|
|
1109
|
+
res.status(404).json({ error: `Section ${sectionType} not found` });
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
|
1113
|
+
const schemaMatch = fileContent.match(/{%\s*schema\s*%}([\s\S]*?){%\s*endschema\s*%}/);
|
|
1114
|
+
if (!schemaMatch) {
|
|
1115
|
+
res.json({ schema: { settings: [], name: blockType } });
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const schema = JSON.parse(schemaMatch[1].trim());
|
|
1119
|
+
const blockSchema = (schema.blocks || []).find((b) => b.type === blockType);
|
|
1120
|
+
res.json({ schema: resolveSchemaT({ settings: blockSchema?.settings || [], name: blockSchema?.name || blockType }, cwd) });
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
next(error);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
// Returns list of available sections (from sections/ dir) with optional rendered HTML.
|
|
1127
|
+
app.get('/_editor/availablesections', async (req, res, next) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const needHtml = req.query.needHtml === 'true';
|
|
1130
|
+
const sectionsDir = path.join(cwd, 'sections');
|
|
1131
|
+
if (!fs.existsSync(sectionsDir)) {
|
|
1132
|
+
res.json([]);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const files = fs.readdirSync(sectionsDir).filter((f) => f.endsWith('.liquid'));
|
|
1136
|
+
const sections = files.map((file) => {
|
|
1137
|
+
const sectionType = file.replace('.liquid', '');
|
|
1138
|
+
const content = fs.readFileSync(path.join(sectionsDir, file), 'utf-8');
|
|
1139
|
+
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;
|
|
1148
|
+
}
|
|
1149
|
+
catch { /* skip */ }
|
|
1150
|
+
}
|
|
1151
|
+
return { section_name: sectionType, name, category };
|
|
1152
|
+
});
|
|
1153
|
+
if (!needHtml) {
|
|
1154
|
+
res.json(sections);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
// Render HTML for requested section names using default section data.
|
|
1158
|
+
const sectionNames = req.query.sectionNames
|
|
1159
|
+
? 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.
|
|
1163
|
+
const assetsDir = path.join(cwd, 'assets');
|
|
1164
|
+
const cssLinks = fs.existsSync(assetsDir)
|
|
1165
|
+
? fs.readdirSync(assetsDir)
|
|
1166
|
+
.filter((f) => f.endsWith('.css'))
|
|
1167
|
+
.map((f) => `<link rel="stylesheet" href="http://localhost:${port}/assets/${f}">`)
|
|
1168
|
+
.join('')
|
|
1169
|
+
: '';
|
|
1170
|
+
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] || '')
|
|
1174
|
+
.replace(/(['"(])\/assets\//g, `$1${cliOrigin}/assets/`)
|
|
1175
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '');
|
|
1176
|
+
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 };
|
|
1178
|
+
});
|
|
1179
|
+
res.json(results);
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
next(error);
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
// Persists the full editor JSON to disk (for structural changes: add/delete/reorder sections).
|
|
1186
|
+
app.post('/_editor/savejson', (req, res, next) => {
|
|
1187
|
+
try {
|
|
1188
|
+
const body = req.body;
|
|
1189
|
+
const jsonType = body.jsonType || 'template';
|
|
1190
|
+
const pageName = body.pageName || 'index';
|
|
1191
|
+
const json = body.json;
|
|
1192
|
+
if (!json || typeof json !== 'object') {
|
|
1193
|
+
res.status(400).json({ error: 'json required' });
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
persistFullJsonToDisk(jsonType, pageName, json);
|
|
1197
|
+
res.json({ ok: true });
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
next(error);
|
|
1201
|
+
}
|
|
1202
|
+
});
|
|
717
1203
|
app.get('/_cli/editor', async (req, res) => {
|
|
718
1204
|
try {
|
|
719
1205
|
const { file } = req.query;
|
|
@@ -752,7 +1238,7 @@ async function serveTheme(portNumber, openInWeb, livereloadOff, useMock = true,
|
|
|
752
1238
|
}
|
|
753
1239
|
res.set('Cache-Control', 'no-store').send('OK');
|
|
754
1240
|
});
|
|
755
|
-
app.use((err, _req, res
|
|
1241
|
+
app.use((err, _req, res) => {
|
|
756
1242
|
if (res.headersSent) {
|
|
757
1243
|
return;
|
|
758
1244
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sections-tag.d.ts","sourceRoot":"","sources":["../../src/custom-tags/sections-tag.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"sections-tag.d.ts","sourceRoot":"","sources":["../../src/custom-tags/sections-tag.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAYzE,OAAO,KAAK,EAAoB,YAAY,EAAE,MAAM,cAAc,CAAC;AAQnE,MAAM,CAAC,OAAO,MAAO,SAAQ,GAAG;IAC9B,OAAO,CAAC,QAAQ,CAAS;gBAEb,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM;IAM1E,WAAW,CAAC,QAAQ,EAAE,MAAM;IAItB,MAAM,CAAC,OAAO,EAAE,OAAO;IAevB,cAAc,CAAC,EACnB,aAAa,EACb,QAAQ,EACR,IAAI,EACL,EAAE;QACD,aAAa,EAAE,MAAM,EAAE,CAAC;QACxB,QAAQ,EAAE,YAAY,CAAC;QACvB,IAAI,EAAE,GAAG,CAAC;KACX;CA6GF"}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { Tag } from 'liquidjs';
|
|
2
2
|
import { readFileContent, readJSONFileContent } from '../utils/file-operations.js';
|
|
3
3
|
import { prepareSectionData } from '../utils/prepare-section-data.js';
|
|
4
|
+
import { buildSectionComponentAttr } from '../utils/editor-attributes.js';
|
|
4
5
|
import { resolveSchemaSettings } from '../utils/resolve-schema-settings.js';
|
|
6
|
+
import { resolveSchemaName } from '../utils/resolve-schema-translations.js';
|
|
5
7
|
import { renderTrackedLiquidFile } from '../utils/liquid-render-frame.js';
|
|
8
|
+
function inferJsonType(sectionsFileName) {
|
|
9
|
+
if (sectionsFileName.startsWith('header'))
|
|
10
|
+
return 'header';
|
|
11
|
+
if (sectionsFileName.startsWith('footer'))
|
|
12
|
+
return 'footer';
|
|
13
|
+
return 'template';
|
|
14
|
+
}
|
|
6
15
|
export default class extends Tag {
|
|
7
16
|
sections;
|
|
8
17
|
constructor(token, remainTokens, liquid) {
|
|
@@ -15,6 +24,10 @@ export default class extends Tag {
|
|
|
15
24
|
}
|
|
16
25
|
async render(context) {
|
|
17
26
|
let { order, sections } = (await readJSONFileContent(`sections/${this.sections}.json`));
|
|
27
|
+
const editorMode = this.liquid.__editorMode;
|
|
28
|
+
if (editorMode) {
|
|
29
|
+
this.liquid.__editorJsonType = inferJsonType(this.sections);
|
|
30
|
+
}
|
|
18
31
|
return this.renderSections({
|
|
19
32
|
sections,
|
|
20
33
|
section_order: order,
|
|
@@ -22,6 +35,8 @@ export default class extends Tag {
|
|
|
22
35
|
});
|
|
23
36
|
}
|
|
24
37
|
async renderSections({ section_order, sections, data }) {
|
|
38
|
+
const editorMode = this.liquid.__editorMode;
|
|
39
|
+
const editorJsonType = this.liquid.__editorJsonType || 'template';
|
|
25
40
|
let renderedSectionsArr = (section_order || []).map(async (section) => {
|
|
26
41
|
let sectionToRender = sections[section].type || '';
|
|
27
42
|
let fileContent = await readFileContent(this.getFilePath(`sections/${sectionToRender}.liquid`));
|
|
@@ -52,14 +67,25 @@ export default class extends Tag {
|
|
|
52
67
|
/* ignore */
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
const cwd = process.cwd();
|
|
71
|
+
sections[section] = prepareSectionData(sections[section], schemaData, section, editorMode ? { jsonType: editorJsonType } : undefined, cwd);
|
|
72
|
+
const displayName = editorMode
|
|
73
|
+
? resolveSchemaName(schemaData?.name, sectionToRender, cwd)
|
|
74
|
+
: '';
|
|
75
|
+
const sectionContext = {
|
|
76
|
+
...sections[section],
|
|
77
|
+
id: `section_${section}`,
|
|
78
|
+
...(editorMode ? {
|
|
79
|
+
settingid: `setting_section_${sectionToRender}`,
|
|
80
|
+
display_name: displayName
|
|
81
|
+
} : {})
|
|
82
|
+
};
|
|
83
|
+
const sectionHtml = await renderTrackedLiquidFile(this.liquid, this.getFilePath(`sections/${sectionToRender}.liquid`), { ...data, section: sectionContext });
|
|
84
|
+
if (editorMode) {
|
|
85
|
+
const attrJson = buildSectionComponentAttr(sectionToRender, section, editorJsonType, displayName);
|
|
86
|
+
return `<div data-zcommerce-component='${attrJson.replace(/'/g, ''')}' style="display:contents">${sectionHtml}</div>`;
|
|
87
|
+
}
|
|
88
|
+
return sectionHtml;
|
|
63
89
|
});
|
|
64
90
|
const arr = await Promise.all(renderedSectionsArr);
|
|
65
91
|
return arr.join('');
|
|
@@ -116,7 +116,7 @@ The skill runs these checks before Stage 0. If any fail, stop and surface to the
|
|
|
116
116
|
|
|
117
117
|
1. **Ask developer for `storeUrl`.** Do not hardcode a demo store. Record in `dev-config.json`. See `dev-server-and-urls.md`.
|
|
118
118
|
2. **Confirm page URLs before any browser navigation.** Home is often `/home` not `/`. Product/collection handles vary by store. Ask once; write confirmed URLs to `dev-config.json`.
|
|
119
|
-
3. **Dev server.** Ask developer to start
|
|
119
|
+
3. **Dev server.** Ask the developer to start the local theme preview, or start it yourself with `npx @zohofinance/commerce theme dev --storeUrl=<your store url>` (value from `dev-config.json`). Notify the developer either way. Without `--storeUrl`, preview uses mock data only.
|
|
120
120
|
4. **`npx @zohofinance/commerce --version` works from inside the theme folder.** If not, run `npm install @zohofinance/commerce` in the theme directory.
|
|
121
121
|
5. **Figma MCP authenticated.** Otherwise: ask user to open Figma desktop with the first frame selected.
|
|
122
122
|
6. **Chrome DevTools MCP available** (`.cursor/mcp.json`). See `.cursor/skills/browser-verify.md`.
|
|
@@ -228,7 +228,7 @@ The pipeline is the right tool for: building a new theme from scratch with full
|
|
|
228
228
|
|
|
229
229
|
It is the **wrong** tool for, and should not be used for:
|
|
230
230
|
- **Bug fix in an existing theme** → just open the file and fix it. No skill needed.
|
|
231
|
-
- **One-off section change** → edit the section directly, then `cd <theme> &&
|
|
231
|
+
- **One-off section change** → edit the section directly, then `cd <theme> && npx @zohofinance/commerce theme dev --storeUrl=<your store url>` to verify visually. No skill needed.
|
|
232
232
|
- **Adding a theme setting** → edit `settings/schema.json` + `theme.json` directly.
|
|
233
233
|
- **A new section that doesn't fit any of the 5 pages this skill handles** → also direct edit; the lint rules in `code-quality.md` still apply, but you don't need the orchestration.
|
|
234
234
|
|
|
@@ -22,7 +22,7 @@ When the answer IS in any of those three sources — look it up. Don't ask.
|
|
|
22
22
|
- Product: `http://localhost:3000/products/item-1`
|
|
23
23
|
- Collection: `http://localhost:3000/collections/all-products`
|
|
24
24
|
Write confirmed URLs to `dev-config.json`.
|
|
25
|
-
- **Dev server not running** → ask developer to start it, or start it yourself with
|
|
25
|
+
- **Dev server not running** → ask developer to start it, or start it yourself with `npx @zohofinance/commerce theme dev --storeUrl=<your store url>` (their confirmed value) and notify them.
|
|
26
26
|
|
|
27
27
|
### Section identification (page orchestrator Phase 2)
|
|
28
28
|
|