@zalify/cli 0.12.1 → 0.14.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/dist/cli.js +243 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12836,6 +12836,64 @@ async function shopifyImport(storeDir, options = {}) {
|
|
|
12836
12836
|
console.log(` ✓ pruned ${node.handle}`);
|
|
12837
12837
|
}
|
|
12838
12838
|
}
|
|
12839
|
+
for (const disc of catalog.discounts ?? []) {
|
|
12840
|
+
const existing = await gql(auth, `query($q: String!) { codeDiscountNodes(first: 1, query: $q) { nodes { id } } }`, { q: `code:${disc.code}` });
|
|
12841
|
+
if (existing.codeDiscountNodes.nodes.length) {
|
|
12842
|
+
console.log(` - discount ${disc.code} exists, skipped`);
|
|
12843
|
+
continue;
|
|
12844
|
+
}
|
|
12845
|
+
const res = await gql(auth, `mutation($discount: DiscountCodeBasicInput!) {
|
|
12846
|
+
discountCodeBasicCreate(basicCodeDiscount: $discount) {
|
|
12847
|
+
userErrors { field message }
|
|
12848
|
+
}
|
|
12849
|
+
}`, {
|
|
12850
|
+
discount: {
|
|
12851
|
+
title: disc.title ?? `${disc.code} — ${disc.percentOff}% off`,
|
|
12852
|
+
code: disc.code,
|
|
12853
|
+
startsAt: new Date().toISOString(),
|
|
12854
|
+
customerSelection: { all: true },
|
|
12855
|
+
customerGets: {
|
|
12856
|
+
value: { percentage: disc.percentOff / 100 },
|
|
12857
|
+
items: { all: true }
|
|
12858
|
+
}
|
|
12859
|
+
}
|
|
12860
|
+
});
|
|
12861
|
+
const errs = res.discountCodeBasicCreate.userErrors;
|
|
12862
|
+
if (errs.length)
|
|
12863
|
+
console.error(` ✗ discount ${disc.code}: ${joinErrors(errs)}`);
|
|
12864
|
+
else
|
|
12865
|
+
console.log(` ✓ discount ${disc.code} created (${disc.percentOff}% off)`);
|
|
12866
|
+
}
|
|
12867
|
+
for (const pg of catalog.pages ?? []) {
|
|
12868
|
+
const existing = await gql(auth, `query($q: String!) { pages(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${pg.handle}` });
|
|
12869
|
+
const node = existing.pages.nodes[0];
|
|
12870
|
+
if (node?.handle === pg.handle) {
|
|
12871
|
+
const res = await gql(auth, `mutation($id: ID!, $page: PageUpdateInput!) {
|
|
12872
|
+
pageUpdate(id: $id, page: $page) { userErrors { field message } }
|
|
12873
|
+
}`, { id: node.id, page: { title: pg.title, body: pg.bodyHtml } });
|
|
12874
|
+
const errs = res.pageUpdate.userErrors;
|
|
12875
|
+
if (errs.length)
|
|
12876
|
+
console.error(` ✗ page ${pg.handle}: ${joinErrors(errs)}`);
|
|
12877
|
+
else
|
|
12878
|
+
console.log(` ✓ page ${pg.handle} updated`);
|
|
12879
|
+
} else {
|
|
12880
|
+
const res = await gql(auth, `mutation($page: PageCreateInput!) {
|
|
12881
|
+
pageCreate(page: $page) { userErrors { field message } }
|
|
12882
|
+
}`, {
|
|
12883
|
+
page: {
|
|
12884
|
+
title: pg.title,
|
|
12885
|
+
handle: pg.handle,
|
|
12886
|
+
body: pg.bodyHtml,
|
|
12887
|
+
isPublished: true
|
|
12888
|
+
}
|
|
12889
|
+
});
|
|
12890
|
+
const errs = res.pageCreate.userErrors;
|
|
12891
|
+
if (errs.length)
|
|
12892
|
+
console.error(` ✗ page ${pg.handle}: ${joinErrors(errs)}`);
|
|
12893
|
+
else
|
|
12894
|
+
console.log(` ✓ page ${pg.handle} created`);
|
|
12895
|
+
}
|
|
12896
|
+
}
|
|
12839
12897
|
const collectionIds = [];
|
|
12840
12898
|
for (const c of catalog.collections ?? []) {
|
|
12841
12899
|
const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
|
|
@@ -12872,6 +12930,40 @@ async function shopifyImport(storeDir, options = {}) {
|
|
|
12872
12930
|
console.log(` ✓ collection ${c.handle}`);
|
|
12873
12931
|
}
|
|
12874
12932
|
}
|
|
12933
|
+
const menuEntries = Object.entries(catalog.menus ?? {});
|
|
12934
|
+
if (menuEntries.length) {
|
|
12935
|
+
const toMenuItems = (items) => items.map((i) => ({
|
|
12936
|
+
title: i.title,
|
|
12937
|
+
type: "HTTP",
|
|
12938
|
+
url: i.url,
|
|
12939
|
+
items: i.items ? toMenuItems(i.items) : []
|
|
12940
|
+
}));
|
|
12941
|
+
const titleize = (handle) => handle.replaceAll("-", " ").replace(/^./, (c) => c.toUpperCase());
|
|
12942
|
+
const list = await gql(auth, `{ menus(first: 50) { nodes { id handle title } } }`);
|
|
12943
|
+
for (const [handle, items] of menuEntries) {
|
|
12944
|
+
const existing = list.menus.nodes.find((n) => n.handle === handle);
|
|
12945
|
+
const input = toMenuItems(items);
|
|
12946
|
+
if (existing) {
|
|
12947
|
+
const res = await gql(auth, `mutation($id: ID!, $title: String!, $items: [MenuItemUpdateInput!]!) {
|
|
12948
|
+
menuUpdate(id: $id, title: $title, items: $items) { userErrors { field message } }
|
|
12949
|
+
}`, { id: existing.id, title: existing.title, items: input });
|
|
12950
|
+
const errs = res.menuUpdate.userErrors;
|
|
12951
|
+
if (errs.length)
|
|
12952
|
+
console.error(` ✗ menu ${handle}: ${joinErrors(errs)}`);
|
|
12953
|
+
else
|
|
12954
|
+
console.log(` ✓ menu ${handle} updated (${items.length} top-level items)`);
|
|
12955
|
+
} else {
|
|
12956
|
+
const res = await gql(auth, `mutation($title: String!, $handle: String!, $items: [MenuItemCreateInput!]!) {
|
|
12957
|
+
menuCreate(title: $title, handle: $handle, items: $items) { userErrors { field message } }
|
|
12958
|
+
}`, { title: titleize(handle), handle, items: input });
|
|
12959
|
+
const errs = res.menuCreate.userErrors;
|
|
12960
|
+
if (errs.length)
|
|
12961
|
+
console.error(` ✗ menu ${handle}: ${joinErrors(errs)}`);
|
|
12962
|
+
else
|
|
12963
|
+
console.log(` ✓ menu ${handle} created (${items.length} top-level items)`);
|
|
12964
|
+
}
|
|
12965
|
+
}
|
|
12966
|
+
}
|
|
12875
12967
|
try {
|
|
12876
12968
|
const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
|
|
12877
12969
|
const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
|
|
@@ -13006,6 +13098,24 @@ alone. Validate anytime with \`zalify brand validate\` (defaults to cwd).
|
|
|
13006
13098
|
- Prices are strings with 2 decimals, in the store currency. Honest
|
|
13007
13099
|
markdowns only (15–25% off typical).
|
|
13008
13100
|
- Colors and variants get evocative names per brand.md — never "Gray 02".
|
|
13101
|
+
- **Pages** (\`pages\`): the standard store pages, upserted to Shopify by
|
|
13102
|
+
\`brand push\` and linked from the footer menu. Keep the scaffold's six
|
|
13103
|
+
handles (about, shipping-delivery, returns-exchanges, faq,
|
|
13104
|
+
privacy-policy, terms-of-service); write \`bodyHtml\` in the brand
|
|
13105
|
+
voice (simple HTML: h2/p/ul/li). The \`contact\` page already exists in
|
|
13106
|
+
every Shopify store — link it, don't create it.
|
|
13107
|
+
- **Discounts** (\`discounts\`): code discounts created by \`brand push\`
|
|
13108
|
+
(\`{ code, percentOff, title? }\`). Keep the scaffold's \`WELCOME20\` —
|
|
13109
|
+
it is the code the theme's cart voucher banner shows by default, so
|
|
13110
|
+
Apply actually works. Codes: 4-20 uppercase letters/digits.
|
|
13111
|
+
- **Menus** (\`menus\`): \`main-menu\` (header nav) and \`footer\`, upserted
|
|
13112
|
+
by \`brand push\`; the catalog is the source of truth (a push replaces
|
|
13113
|
+
the menu's items). Items are \`{ title, url, items? }\`, max 3 levels.
|
|
13114
|
+
A top-level main-menu item with children opens the theme's mega
|
|
13115
|
+
panel; footer top-level items with children become footer columns
|
|
13116
|
+
(Shop / Help / Company is the standard trio). URLs are relative
|
|
13117
|
+
(\`/collections/<handle>\`, \`/pages/<handle>\`) and must point at
|
|
13118
|
+
handles that exist in this file.
|
|
13009
13119
|
|
|
13010
13120
|
## images/manifest.json rules
|
|
13011
13121
|
|
|
@@ -13127,7 +13237,90 @@ var CATALOG_JSON = (vendor, currency) => JSON.stringify({
|
|
|
13127
13237
|
descriptionHtml: "<p>Real markdowns on favorites.</p>",
|
|
13128
13238
|
rule: { tag: "collection:sale" }
|
|
13129
13239
|
}
|
|
13130
|
-
]
|
|
13240
|
+
],
|
|
13241
|
+
pages: [
|
|
13242
|
+
{
|
|
13243
|
+
handle: "about",
|
|
13244
|
+
title: "About",
|
|
13245
|
+
bodyHtml: "<p><Two-three paragraphs in the brand voice: who we are, what we make, why it matters.></p>"
|
|
13246
|
+
},
|
|
13247
|
+
{
|
|
13248
|
+
handle: "shipping-delivery",
|
|
13249
|
+
title: "Shipping & Delivery",
|
|
13250
|
+
bodyHtml: "<p><Shipping promise in the brand voice: threshold for free shipping, carriers, timing, tracking.></p>"
|
|
13251
|
+
},
|
|
13252
|
+
{
|
|
13253
|
+
handle: "returns-exchanges",
|
|
13254
|
+
title: "Returns & Exchanges",
|
|
13255
|
+
bodyHtml: "<p><Return window, condition rules, how to start a return, refund timing — friendly and concrete.></p>"
|
|
13256
|
+
},
|
|
13257
|
+
{
|
|
13258
|
+
handle: "faq",
|
|
13259
|
+
title: "FAQ",
|
|
13260
|
+
bodyHtml: "<h2>Question one?</h2><p>Answer.</p><h2>Question two?</h2><p>Answer. 5-8 real questions a shopper would ask.</p>"
|
|
13261
|
+
},
|
|
13262
|
+
{
|
|
13263
|
+
handle: "privacy-policy",
|
|
13264
|
+
title: "Privacy Policy",
|
|
13265
|
+
bodyHtml: "<p><Plain-language privacy policy: what's collected, why, cookies, contact email.></p>"
|
|
13266
|
+
},
|
|
13267
|
+
{
|
|
13268
|
+
handle: "terms-of-service",
|
|
13269
|
+
title: "Terms of Service",
|
|
13270
|
+
bodyHtml: "<p><Plain-language terms: orders, pricing, liability, governing law.></p>"
|
|
13271
|
+
}
|
|
13272
|
+
],
|
|
13273
|
+
discounts: [
|
|
13274
|
+
{
|
|
13275
|
+
code: "WELCOME20",
|
|
13276
|
+
percentOff: 20,
|
|
13277
|
+
title: "Welcome — 20% off the first order"
|
|
13278
|
+
}
|
|
13279
|
+
],
|
|
13280
|
+
menus: {
|
|
13281
|
+
"main-menu": [
|
|
13282
|
+
{ title: "New in", url: "/collections/all" },
|
|
13283
|
+
{
|
|
13284
|
+
title: "Shop",
|
|
13285
|
+
url: "/collections/all",
|
|
13286
|
+
items: [
|
|
13287
|
+
{ title: "Best sellers", url: "/collections/best-sellers" },
|
|
13288
|
+
{ title: "Sale", url: "/collections/sale" }
|
|
13289
|
+
]
|
|
13290
|
+
},
|
|
13291
|
+
{ title: "Sale", url: "/collections/sale" }
|
|
13292
|
+
],
|
|
13293
|
+
footer: [
|
|
13294
|
+
{
|
|
13295
|
+
title: "Shop",
|
|
13296
|
+
url: "/collections/all",
|
|
13297
|
+
items: [
|
|
13298
|
+
{ title: "Shop all", url: "/collections/all" },
|
|
13299
|
+
{ title: "Best sellers", url: "/collections/best-sellers" },
|
|
13300
|
+
{ title: "Sale", url: "/collections/sale" }
|
|
13301
|
+
]
|
|
13302
|
+
},
|
|
13303
|
+
{
|
|
13304
|
+
title: "Help",
|
|
13305
|
+
url: "/pages/shipping-delivery",
|
|
13306
|
+
items: [
|
|
13307
|
+
{ title: "Shipping & Delivery", url: "/pages/shipping-delivery" },
|
|
13308
|
+
{ title: "Returns & Exchanges", url: "/pages/returns-exchanges" },
|
|
13309
|
+
{ title: "FAQ", url: "/pages/faq" },
|
|
13310
|
+
{ title: "Contact", url: "/pages/contact" }
|
|
13311
|
+
]
|
|
13312
|
+
},
|
|
13313
|
+
{
|
|
13314
|
+
title: "Company",
|
|
13315
|
+
url: "/pages/about",
|
|
13316
|
+
items: [
|
|
13317
|
+
{ title: "About", url: "/pages/about" },
|
|
13318
|
+
{ title: "Privacy Policy", url: "/pages/privacy-policy" },
|
|
13319
|
+
{ title: "Terms of Service", url: "/pages/terms-of-service" }
|
|
13320
|
+
]
|
|
13321
|
+
}
|
|
13322
|
+
]
|
|
13323
|
+
}
|
|
13131
13324
|
}, null, 2) + `
|
|
13132
13325
|
`;
|
|
13133
13326
|
var MANIFEST_JSON = JSON.stringify({
|
|
@@ -13284,6 +13477,55 @@ function brandValidate(dir2 = ".") {
|
|
|
13284
13477
|
}
|
|
13285
13478
|
}
|
|
13286
13479
|
}
|
|
13480
|
+
const pageHandles = new Set;
|
|
13481
|
+
for (const pg of catalog.pages ?? []) {
|
|
13482
|
+
const where = `page "${pg.handle ?? pg.title ?? "?"}"`;
|
|
13483
|
+
if (!pg.handle || !pg.title || !pg.bodyHtml) {
|
|
13484
|
+
problems.push(`${where}: handle, title, and bodyHtml are required`);
|
|
13485
|
+
}
|
|
13486
|
+
if (pg.handle) {
|
|
13487
|
+
if (pageHandles.has(pg.handle))
|
|
13488
|
+
problems.push(`${where}: duplicate handle`);
|
|
13489
|
+
pageHandles.add(pg.handle);
|
|
13490
|
+
}
|
|
13491
|
+
if (pg.bodyHtml?.includes("<p><")) {
|
|
13492
|
+
warnings.push(`${where}: bodyHtml still contains a scaffold placeholder`);
|
|
13493
|
+
}
|
|
13494
|
+
}
|
|
13495
|
+
const collectionHandles = new Set((catalog.collections ?? []).map((c) => c.handle).filter(Boolean));
|
|
13496
|
+
const checkMenu = (nodes, menu, depth) => {
|
|
13497
|
+
if (depth > 3) {
|
|
13498
|
+
problems.push(`menu "${menu}": nested deeper than 3 levels`);
|
|
13499
|
+
return;
|
|
13500
|
+
}
|
|
13501
|
+
for (const node of nodes) {
|
|
13502
|
+
const where = `menu "${menu}" item "${node.title ?? "?"}"`;
|
|
13503
|
+
if (!node.title || !node.url)
|
|
13504
|
+
problems.push(`${where}: title and url required`);
|
|
13505
|
+
const col = node.url?.match(/^\/collections\/([a-z0-9-]+)$/)?.[1];
|
|
13506
|
+
if (col && col !== "all" && !collectionHandles.has(col)) {
|
|
13507
|
+
problems.push(`${where}: collection "${col}" not in catalog.json`);
|
|
13508
|
+
}
|
|
13509
|
+
const pageRef = node.url?.match(/^\/pages\/([a-z0-9-]+)$/)?.[1];
|
|
13510
|
+
if (pageRef && pageRef !== "contact" && !pageHandles.has(pageRef)) {
|
|
13511
|
+
problems.push(`${where}: page "${pageRef}" not in catalog.json pages`);
|
|
13512
|
+
}
|
|
13513
|
+
if (node.items?.length)
|
|
13514
|
+
checkMenu(node.items, menu, depth + 1);
|
|
13515
|
+
}
|
|
13516
|
+
};
|
|
13517
|
+
for (const [menuHandle, nodes] of Object.entries(catalog.menus ?? {})) {
|
|
13518
|
+
checkMenu(nodes ?? [], menuHandle, 1);
|
|
13519
|
+
}
|
|
13520
|
+
for (const disc of catalog.discounts ?? []) {
|
|
13521
|
+
const where = `discount "${disc.code ?? "?"}"`;
|
|
13522
|
+
if (!disc.code || !/^[A-Z0-9]{4,20}$/.test(disc.code)) {
|
|
13523
|
+
problems.push(`${where}: code must be 4-20 uppercase letters/digits`);
|
|
13524
|
+
}
|
|
13525
|
+
if (typeof disc.percentOff !== "number" || !Number.isInteger(disc.percentOff) || disc.percentOff < 1 || disc.percentOff > 99) {
|
|
13526
|
+
problems.push(`${where}: percentOff must be an integer 1-99`);
|
|
13527
|
+
}
|
|
13528
|
+
}
|
|
13287
13529
|
}
|
|
13288
13530
|
const manifest = readJson2("images/manifest.json");
|
|
13289
13531
|
if (manifest) {
|