@zalify/cli 0.8.0 → 0.9.1
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 +494 -315
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11293,7 +11293,7 @@ function updateNotifier(options) {
|
|
|
11293
11293
|
|
|
11294
11294
|
// src/cli.ts
|
|
11295
11295
|
import { createRequire as createRequire2 } from "node:module";
|
|
11296
|
-
import { dirname as dirname2, join as
|
|
11296
|
+
import { dirname as dirname2, join as join7 } from "node:path";
|
|
11297
11297
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
11298
11298
|
|
|
11299
11299
|
// src/auth.ts
|
|
@@ -12061,272 +12061,24 @@ async function imagesGenerate(storeDir) {
|
|
|
12061
12061
|
}
|
|
12062
12062
|
|
|
12063
12063
|
// src/shopify.ts
|
|
12064
|
-
import { readFileSync as
|
|
12065
|
-
import { basename, join as
|
|
12066
|
-
|
|
12067
|
-
|
|
12068
|
-
|
|
12069
|
-
|
|
12070
|
-
|
|
12071
|
-
` + ` Reconnect the store (one-time approval): ${json.reconnectUrl ?? `${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`}
|
|
12072
|
-
` + ` Then re-run this command.`);
|
|
12073
|
-
}
|
|
12074
|
-
if (status === 404) {
|
|
12075
|
-
return new Error("Shopify Admin proxy not found (404) — the /api/shopify/admin endpoint " + "is probably not deployed yet on this Zalify app. Try again after the next deploy.");
|
|
12076
|
-
}
|
|
12077
|
-
if (status === 402 || json.code === "UPGRADE_REQUIRED") {
|
|
12078
|
-
return new Error(`${json.error ?? "Paid plan required."}
|
|
12079
|
-
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
12080
|
-
}
|
|
12081
|
-
if (status === 409) {
|
|
12082
|
-
return new Error(`${json.error ?? "No Shopify store is connected to this workspace."}
|
|
12083
|
-
` + ` Connect one: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`);
|
|
12084
|
-
}
|
|
12085
|
-
return new Error(`shopify/admin ${status}: ${JSON.stringify(json)}`);
|
|
12086
|
-
}
|
|
12087
|
-
async function gql(auth, query, variables = {}) {
|
|
12088
|
-
const res = await fetch(`${auth.appUrl}/api/shopify/admin`, {
|
|
12089
|
-
method: "POST",
|
|
12090
|
-
headers: {
|
|
12091
|
-
"Content-Type": "application/json",
|
|
12092
|
-
Authorization: `Bearer ${auth.key}`
|
|
12093
|
-
},
|
|
12094
|
-
body: JSON.stringify({ workspaceId: auth.workspaceId, query, variables })
|
|
12095
|
-
});
|
|
12096
|
-
const json = await res.json().catch(() => ({}));
|
|
12097
|
-
if (!res.ok)
|
|
12098
|
-
throw proxyError(auth, res.status, json);
|
|
12099
|
-
if (json.errors)
|
|
12100
|
-
throw new Error(JSON.stringify(json.errors));
|
|
12101
|
-
if (!json.data)
|
|
12102
|
-
throw new Error(`shopify/admin: empty response`);
|
|
12103
|
-
return json.data;
|
|
12104
|
-
}
|
|
12105
|
-
function readJson(path9, label) {
|
|
12106
|
-
if (!existsSync4(path9)) {
|
|
12107
|
-
throw new Error(`No ${label} at ${path9}`);
|
|
12108
|
-
}
|
|
12109
|
-
return JSON.parse(readFileSync4(path9, "utf8"));
|
|
12110
|
-
}
|
|
12111
|
-
function joinErrors(errs) {
|
|
12112
|
-
return errs.map((e) => e.message).join("; ");
|
|
12113
|
-
}
|
|
12114
|
-
var PRODUCT_SET = `
|
|
12115
|
-
mutation productSet($input: ProductSetInput!) {
|
|
12116
|
-
productSet(input: $input, synchronous: true) {
|
|
12117
|
-
product { id handle }
|
|
12118
|
-
userErrors { field message }
|
|
12119
|
-
}
|
|
12120
|
-
}`;
|
|
12121
|
-
var COLLECTION_CREATE = `
|
|
12122
|
-
mutation collectionCreate($input: CollectionInput!) {
|
|
12123
|
-
collectionCreate(input: $input) {
|
|
12124
|
-
collection { id handle }
|
|
12125
|
-
userErrors { field message }
|
|
12126
|
-
}
|
|
12127
|
-
}`;
|
|
12128
|
-
async function findProductId(auth, handle) {
|
|
12129
|
-
const data = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${handle}` });
|
|
12130
|
-
const node = data.products.nodes[0];
|
|
12131
|
-
return node && node.handle === handle ? node.id : null;
|
|
12132
|
-
}
|
|
12133
|
-
async function shopifyImport(storeDir, options = {}) {
|
|
12134
|
-
const auth = requireActive();
|
|
12135
|
-
const dir2 = resolve3(storeDir);
|
|
12136
|
-
const catalog = readJson(join4(dir2, "catalog.json"), "catalog.json");
|
|
12137
|
-
const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
|
|
12138
|
-
const locationId = locations.nodes[0]?.id;
|
|
12139
|
-
console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
|
|
12140
|
-
const productIds = [];
|
|
12141
|
-
for (const p of catalog.products) {
|
|
12142
|
-
const options_ = p.options ?? [];
|
|
12143
|
-
const optionValues = options_.map((name, idx) => ({
|
|
12144
|
-
name,
|
|
12145
|
-
position: idx + 1,
|
|
12146
|
-
values: [...new Set(p.variants.map((v) => v.options[idx]))].map((v) => ({ name: v }))
|
|
12147
|
-
}));
|
|
12148
|
-
const input = {
|
|
12149
|
-
handle: p.handle,
|
|
12150
|
-
title: p.title,
|
|
12151
|
-
descriptionHtml: p.descriptionHtml ?? "",
|
|
12152
|
-
vendor: p.vendor ?? catalog.vendor,
|
|
12153
|
-
productType: p.type ?? "",
|
|
12154
|
-
tags: p.tags ?? [],
|
|
12155
|
-
status: "ACTIVE",
|
|
12156
|
-
productOptions: optionValues.length ? optionValues : [{ name: "Title", position: 1, values: [{ name: "Default Title" }] }],
|
|
12157
|
-
variants: p.variants.map((v) => ({
|
|
12158
|
-
optionValues: options_.length ? options_.map((name, idx) => ({ optionName: name, name: v.options[idx] })) : [{ optionName: "Title", name: "Default Title" }],
|
|
12159
|
-
price: v.price,
|
|
12160
|
-
compareAtPrice: v.compareAtPrice ?? null,
|
|
12161
|
-
inventoryItem: {
|
|
12162
|
-
sku: v.sku ?? "",
|
|
12163
|
-
tracked: true,
|
|
12164
|
-
measurement: { weight: { value: v.grams ?? 0, unit: "GRAMS" } }
|
|
12165
|
-
},
|
|
12166
|
-
inventoryQuantities: locationId ? [{ locationId, name: "available", quantity: v.inventory ?? 25 }] : []
|
|
12167
|
-
}))
|
|
12168
|
-
};
|
|
12169
|
-
const existingId = await findProductId(auth, p.handle);
|
|
12170
|
-
if (existingId)
|
|
12171
|
-
input.id = existingId;
|
|
12172
|
-
const data = await gql(auth, PRODUCT_SET, { input });
|
|
12173
|
-
const errs = data.productSet.userErrors;
|
|
12174
|
-
if (errs.length) {
|
|
12175
|
-
console.error(` ✗ ${p.handle}: ${joinErrors(errs)}`);
|
|
12176
|
-
} else if (data.productSet.product) {
|
|
12177
|
-
productIds.push(data.productSet.product.id);
|
|
12178
|
-
console.log(` ✓ ${existingId ? "updated" : "created"} ${p.handle} (${p.variants.length} variants)`);
|
|
12179
|
-
}
|
|
12180
|
-
}
|
|
12181
|
-
if (options.prune) {
|
|
12182
|
-
const wanted = new Set(catalog.products.map((p) => p.handle));
|
|
12183
|
-
const data = await gql(auth, `query($q: String!) { products(first: 100, query: $q) { nodes { id handle } } }`, { q: `vendor:${catalog.vendor}` });
|
|
12184
|
-
for (const node of data.products.nodes) {
|
|
12185
|
-
if (wanted.has(node.handle))
|
|
12186
|
-
continue;
|
|
12187
|
-
const del = await gql(auth, `mutation($input: ProductDeleteInput!) {
|
|
12188
|
-
productDelete(input: $input) { deletedProductId userErrors { field message } }
|
|
12189
|
-
}`, { input: { id: node.id } });
|
|
12190
|
-
const errs = del.productDelete.userErrors;
|
|
12191
|
-
if (errs.length)
|
|
12192
|
-
console.error(` ✗ prune ${node.handle}: ${joinErrors(errs)}`);
|
|
12193
|
-
else
|
|
12194
|
-
console.log(` ✓ pruned ${node.handle}`);
|
|
12195
|
-
}
|
|
12196
|
-
}
|
|
12197
|
-
for (const c of catalog.collections ?? []) {
|
|
12198
|
-
const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
|
|
12199
|
-
if (existing.collections.nodes[0]?.handle === c.handle) {
|
|
12200
|
-
console.log(` - collection ${c.handle} exists, skipped`);
|
|
12201
|
-
continue;
|
|
12202
|
-
}
|
|
12203
|
-
const data = await gql(auth, COLLECTION_CREATE, {
|
|
12204
|
-
input: {
|
|
12205
|
-
handle: c.handle,
|
|
12206
|
-
title: c.title,
|
|
12207
|
-
descriptionHtml: c.descriptionHtml ?? "",
|
|
12208
|
-
ruleSet: {
|
|
12209
|
-
appliedDisjunctively: false,
|
|
12210
|
-
rules: [
|
|
12211
|
-
{
|
|
12212
|
-
column: "TAG",
|
|
12213
|
-
relation: "EQUALS",
|
|
12214
|
-
condition: c.rule?.tag ?? `collection:${c.handle}`
|
|
12215
|
-
}
|
|
12216
|
-
]
|
|
12217
|
-
}
|
|
12218
|
-
}
|
|
12219
|
-
});
|
|
12220
|
-
const errs = data.collectionCreate.userErrors;
|
|
12221
|
-
if (errs.length)
|
|
12222
|
-
console.error(` ✗ collection ${c.handle}: ${joinErrors(errs)}`);
|
|
12223
|
-
else
|
|
12224
|
-
console.log(` ✓ collection ${c.handle}`);
|
|
12225
|
-
}
|
|
12226
|
-
try {
|
|
12227
|
-
const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
|
|
12228
|
-
const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
|
|
12229
|
-
if (online) {
|
|
12230
|
-
for (const id of productIds) {
|
|
12231
|
-
await gql(auth, `mutation($id: ID!, $input: [PublicationInput!]!) {
|
|
12232
|
-
publishablePublish(id: $id, input: $input) { userErrors { field message } }
|
|
12233
|
-
}`, { id, input: [{ publicationId: online.id }] });
|
|
12234
|
-
}
|
|
12235
|
-
console.log(` ✓ published ${productIds.length} products to Online Store`);
|
|
12236
|
-
}
|
|
12237
|
-
} catch (e) {
|
|
12238
|
-
console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
|
|
12239
|
-
}
|
|
12240
|
-
console.log("Done.");
|
|
12241
|
-
}
|
|
12242
|
-
async function shopifyUploadImages(storeDir) {
|
|
12243
|
-
const auth = requireActive();
|
|
12244
|
-
const dir2 = resolve3(storeDir);
|
|
12245
|
-
const manifest = readJson(join4(dir2, "images", "manifest.json"), "images/manifest.json");
|
|
12246
|
-
console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
|
|
12247
|
-
for (const entry of manifest.images) {
|
|
12248
|
-
const filePath = join4(dir2, "images", entry.file);
|
|
12249
|
-
if (!existsSync4(filePath)) {
|
|
12250
|
-
console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
|
|
12251
|
-
continue;
|
|
12252
|
-
}
|
|
12253
|
-
const found = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle
|
|
12254
|
-
media(first: 50) { nodes { alt ... on MediaImage { image { url } } } } } } }`, { q: `handle:${entry.handle}` });
|
|
12255
|
-
const product = found.products.nodes[0];
|
|
12256
|
-
if (!product || product.handle !== entry.handle) {
|
|
12257
|
-
console.error(` ✗ ${entry.handle}: product not found in store`);
|
|
12258
|
-
continue;
|
|
12259
|
-
}
|
|
12260
|
-
const stem = basename(entry.file, ".png");
|
|
12261
|
-
const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
|
|
12262
|
-
if (already) {
|
|
12263
|
-
console.log(` - ${entry.file}: already attached, skipped`);
|
|
12264
|
-
continue;
|
|
12265
|
-
}
|
|
12266
|
-
const bytes = readFileSync4(filePath);
|
|
12267
|
-
const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
|
|
12268
|
-
stagedUploadsCreate(input: $input) {
|
|
12269
|
-
stagedTargets { url resourceUrl parameters { name value } }
|
|
12270
|
-
userErrors { field message }
|
|
12271
|
-
}
|
|
12272
|
-
}`, {
|
|
12273
|
-
input: [
|
|
12274
|
-
{
|
|
12275
|
-
resource: "IMAGE",
|
|
12276
|
-
filename: basename(entry.file),
|
|
12277
|
-
mimeType: "image/png",
|
|
12278
|
-
httpMethod: "PUT",
|
|
12279
|
-
fileSize: String(bytes.length)
|
|
12280
|
-
}
|
|
12281
|
-
]
|
|
12282
|
-
});
|
|
12283
|
-
const target = staged.stagedUploadsCreate.stagedTargets?.[0];
|
|
12284
|
-
if (!target) {
|
|
12285
|
-
console.error(` ✗ ${entry.handle}: staged upload failed: ${JSON.stringify(staged.stagedUploadsCreate.userErrors)}`);
|
|
12286
|
-
continue;
|
|
12287
|
-
}
|
|
12288
|
-
const headers = { "Content-Type": "image/png" };
|
|
12289
|
-
for (const p of target.parameters)
|
|
12290
|
-
headers[p.name] = p.value;
|
|
12291
|
-
const up = await fetch(target.url, {
|
|
12292
|
-
method: "PUT",
|
|
12293
|
-
headers,
|
|
12294
|
-
body: new Uint8Array(bytes)
|
|
12295
|
-
});
|
|
12296
|
-
if (!up.ok) {
|
|
12297
|
-
console.error(` ✗ ${entry.handle}: upload PUT failed (${up.status})`);
|
|
12298
|
-
continue;
|
|
12299
|
-
}
|
|
12300
|
-
const media = await gql(auth, `mutation($productId: ID!, $media: [CreateMediaInput!]!) {
|
|
12301
|
-
productCreateMedia(productId: $productId, media: $media) {
|
|
12302
|
-
media { alt }
|
|
12303
|
-
mediaUserErrors { field message }
|
|
12304
|
-
}
|
|
12305
|
-
}`, {
|
|
12306
|
-
productId: product.id,
|
|
12307
|
-
media: [
|
|
12308
|
-
{ originalSource: target.resourceUrl, alt: entry.alt ?? "", mediaContentType: "IMAGE" }
|
|
12309
|
-
]
|
|
12310
|
-
});
|
|
12311
|
-
const errs = media.productCreateMedia.mediaUserErrors;
|
|
12312
|
-
if (errs.length)
|
|
12313
|
-
console.error(` ✗ ${entry.handle}: ${joinErrors(errs)}`);
|
|
12314
|
-
else
|
|
12315
|
-
console.log(` ✓ ${entry.file}`);
|
|
12316
|
-
}
|
|
12317
|
-
console.log("Done.");
|
|
12318
|
-
}
|
|
12064
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6 } from "node:fs";
|
|
12065
|
+
import { basename as basename3, join as join6, resolve as resolve5 } from "node:path";
|
|
12066
|
+
|
|
12067
|
+
// src/storefront.ts
|
|
12068
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
12069
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
12070
|
+
import { basename as basename2, join as join5, resolve as resolve4 } from "node:path";
|
|
12319
12071
|
|
|
12320
12072
|
// src/theme.ts
|
|
12321
12073
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
12322
12074
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
12323
12075
|
import {
|
|
12324
12076
|
cpSync,
|
|
12325
|
-
existsSync as
|
|
12077
|
+
existsSync as existsSync4,
|
|
12326
12078
|
mkdirSync as mkdirSync2,
|
|
12327
12079
|
mkdtempSync,
|
|
12328
12080
|
readdirSync as readdirSync2,
|
|
12329
|
-
readFileSync as
|
|
12081
|
+
readFileSync as readFileSync4,
|
|
12330
12082
|
renameSync,
|
|
12331
12083
|
rmSync as rmSync3,
|
|
12332
12084
|
statSync,
|
|
@@ -12334,7 +12086,7 @@ import {
|
|
|
12334
12086
|
writeFileSync as writeFileSync5
|
|
12335
12087
|
} from "node:fs";
|
|
12336
12088
|
import { tmpdir } from "node:os";
|
|
12337
|
-
import { basename
|
|
12089
|
+
import { basename, dirname, join as join4, resolve as resolve3 } from "node:path";
|
|
12338
12090
|
var TEMPLATES_PKG = "@zalify/theme-templates";
|
|
12339
12091
|
var REGISTRY = "https://registry.npmjs.org";
|
|
12340
12092
|
var MANIFEST_PATH = ".zalify/theme.json";
|
|
@@ -12360,27 +12112,27 @@ async function downloadTarball(url) {
|
|
|
12360
12112
|
const res = await fetch(url);
|
|
12361
12113
|
if (!res.ok)
|
|
12362
12114
|
throw new Error(`tarball download ${res.status}: ${url}`);
|
|
12363
|
-
const file2 =
|
|
12115
|
+
const file2 = join4(mkdtempSync(join4(tmpdir(), "zalify-theme-")), "pkg.tgz");
|
|
12364
12116
|
writeFileSync5(file2, Buffer.from(await res.arrayBuffer()));
|
|
12365
12117
|
return file2;
|
|
12366
12118
|
}
|
|
12367
12119
|
function extractTarball(file2) {
|
|
12368
|
-
const dir2 = mkdtempSync(
|
|
12369
|
-
const result = spawnSync2("tar", ["-xzf",
|
|
12120
|
+
const dir2 = mkdtempSync(join4(tmpdir(), "zalify-theme-x-"));
|
|
12121
|
+
const result = spawnSync2("tar", ["-xzf", resolve3(file2), "-C", dir2], {
|
|
12370
12122
|
encoding: "utf8"
|
|
12371
12123
|
});
|
|
12372
12124
|
if (result.error || result.status !== 0) {
|
|
12373
12125
|
throw new Error(`tar extract failed for ${file2}: ${result.stderr || result.error?.message}`);
|
|
12374
12126
|
}
|
|
12375
|
-
const root =
|
|
12376
|
-
if (!
|
|
12127
|
+
const root = join4(dir2, "package");
|
|
12128
|
+
if (!existsSync4(root))
|
|
12377
12129
|
throw new Error(`unexpected tarball layout in ${file2} (no package/)`);
|
|
12378
12130
|
return root;
|
|
12379
12131
|
}
|
|
12380
12132
|
async function acquireTemplatePkg(localTarball, version) {
|
|
12381
12133
|
if (localTarball) {
|
|
12382
12134
|
const root = extractTarball(localTarball);
|
|
12383
|
-
const v = JSON.parse(
|
|
12135
|
+
const v = JSON.parse(readFileSync4(join4(root, "package.json"), "utf8")).version;
|
|
12384
12136
|
return { root, version: v };
|
|
12385
12137
|
}
|
|
12386
12138
|
const packument = await fetchPackument();
|
|
@@ -12391,11 +12143,11 @@ async function acquireTemplatePkg(localTarball, version) {
|
|
|
12391
12143
|
return { root: extractTarball(await downloadTarball(entry.dist.tarball)), version: target };
|
|
12392
12144
|
}
|
|
12393
12145
|
function readTemplateManifest(pkgRoot, template) {
|
|
12394
|
-
const path9 =
|
|
12395
|
-
if (!
|
|
12146
|
+
const path9 = join4(pkgRoot, "templates", template, MANIFEST_PATH);
|
|
12147
|
+
if (!existsSync4(path9)) {
|
|
12396
12148
|
throw new Error(`template "${template}" has no ${MANIFEST_PATH} — this ${TEMPLATES_PKG} version predates the upgrade contract`);
|
|
12397
12149
|
}
|
|
12398
|
-
return JSON.parse(
|
|
12150
|
+
return JSON.parse(readFileSync4(path9, "utf8"));
|
|
12399
12151
|
}
|
|
12400
12152
|
function resolveVariant(manifest, variant) {
|
|
12401
12153
|
const resolved = { ...manifest, files: { ...manifest.files }, variant };
|
|
@@ -12409,48 +12161,48 @@ function templateFilePath(templateDir, rel, variant) {
|
|
|
12409
12161
|
const stashed = rel.replace(/(^|\/)\.gitignore$/, "$1gitignore");
|
|
12410
12162
|
if (variant === "editor") {
|
|
12411
12163
|
for (const candidate of [rel, stashed]) {
|
|
12412
|
-
const overlay =
|
|
12413
|
-
if (
|
|
12164
|
+
const overlay = join4(templateDir, "__editor__", candidate);
|
|
12165
|
+
if (existsSync4(overlay))
|
|
12414
12166
|
return overlay;
|
|
12415
12167
|
}
|
|
12416
12168
|
}
|
|
12417
12169
|
for (const candidate of [rel, stashed]) {
|
|
12418
|
-
const path9 =
|
|
12419
|
-
if (
|
|
12170
|
+
const path9 = join4(templateDir, candidate);
|
|
12171
|
+
if (existsSync4(path9))
|
|
12420
12172
|
return path9;
|
|
12421
12173
|
}
|
|
12422
12174
|
throw new Error(`file listed in manifest but missing from template: ${rel}`);
|
|
12423
12175
|
}
|
|
12424
12176
|
function readProjectManifest(dir2) {
|
|
12425
|
-
const path9 =
|
|
12426
|
-
if (!
|
|
12177
|
+
const path9 = join4(dir2, MANIFEST_PATH);
|
|
12178
|
+
if (!existsSync4(path9)) {
|
|
12427
12179
|
throw new Error(`no ${MANIFEST_PATH} in ${dir2} — not a scaffolded Zalify storefront.
|
|
12428
12180
|
` + ` Scaffold one with \`zalify theme create\`.`);
|
|
12429
12181
|
}
|
|
12430
|
-
return JSON.parse(
|
|
12182
|
+
return JSON.parse(readFileSync4(path9, "utf8"));
|
|
12431
12183
|
}
|
|
12432
12184
|
function writeJson(path9, value) {
|
|
12433
12185
|
writeFileSync5(path9, JSON.stringify(value, null, 2) + `
|
|
12434
12186
|
`);
|
|
12435
12187
|
}
|
|
12436
12188
|
async function themeCreate(dir2, opts) {
|
|
12437
|
-
const targetDir =
|
|
12438
|
-
if (
|
|
12189
|
+
const targetDir = resolve3(dir2);
|
|
12190
|
+
if (existsSync4(targetDir) && readdirSync2(targetDir).length > 0) {
|
|
12439
12191
|
throw new Error(`${dir2} already exists and is not empty`);
|
|
12440
12192
|
}
|
|
12441
12193
|
const { root, version } = await acquireTemplatePkg(opts.tarball, opts.to);
|
|
12442
|
-
const frameworks = JSON.parse(
|
|
12194
|
+
const frameworks = JSON.parse(readFileSync4(join4(root, "templates.json"), "utf8"));
|
|
12443
12195
|
const template = opts.template ?? "nextjs";
|
|
12444
12196
|
const meta = frameworks.frameworks[template];
|
|
12445
12197
|
if (!meta) {
|
|
12446
12198
|
throw new Error(`unknown template "${template}" — expected one of: ${Object.keys(frameworks.frameworks).join(", ")}`);
|
|
12447
12199
|
}
|
|
12448
|
-
const templateDir =
|
|
12200
|
+
const templateDir = join4(root, "templates", template);
|
|
12449
12201
|
console.log(`Scaffolding ${meta.label} (${TEMPLATES_PKG}@${version}) into ${dir2}`);
|
|
12450
12202
|
mkdirSync2(targetDir, { recursive: true });
|
|
12451
12203
|
cpSync(templateDir, targetDir, { recursive: true });
|
|
12452
|
-
const overlayDir =
|
|
12453
|
-
const useEditor = Boolean(opts.editor) &&
|
|
12204
|
+
const overlayDir = join4(targetDir, "__editor__");
|
|
12205
|
+
const useEditor = Boolean(opts.editor) && existsSync4(overlayDir);
|
|
12454
12206
|
if (opts.editor && !useEditor) {
|
|
12455
12207
|
console.log(` ! --editor has no effect for ${template} — no editor overlay in this template`);
|
|
12456
12208
|
}
|
|
@@ -12459,16 +12211,16 @@ async function themeCreate(dir2, opts) {
|
|
|
12459
12211
|
rmSync3(overlayDir, { recursive: true, force: true });
|
|
12460
12212
|
(function restoreGitignores(d) {
|
|
12461
12213
|
for (const entry of readdirSync2(d)) {
|
|
12462
|
-
const full =
|
|
12214
|
+
const full = join4(d, entry);
|
|
12463
12215
|
if (statSync(full).isDirectory())
|
|
12464
12216
|
restoreGitignores(full);
|
|
12465
12217
|
else if (entry === "gitignore")
|
|
12466
|
-
renameSync(full,
|
|
12218
|
+
renameSync(full, join4(d, ".gitignore"));
|
|
12467
12219
|
}
|
|
12468
12220
|
})(targetDir);
|
|
12469
|
-
const pkgPath =
|
|
12470
|
-
const pkg = JSON.parse(
|
|
12471
|
-
pkg.name =
|
|
12221
|
+
const pkgPath = join4(targetDir, "package.json");
|
|
12222
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
12223
|
+
pkg.name = basename(targetDir).toLowerCase().replaceAll(/[^a-z0-9-_.]+/g, "-").replaceAll(/^[-.]+|[-.]+$/g, "") || "zalify-storefront";
|
|
12472
12224
|
writeJson(pkgPath, pkg);
|
|
12473
12225
|
if (meta.env) {
|
|
12474
12226
|
const lines = [
|
|
@@ -12480,12 +12232,12 @@ async function themeCreate(dir2, opts) {
|
|
|
12480
12232
|
if (meta.env.sessionSecret) {
|
|
12481
12233
|
lines.push("# Session cookie signing key — unique to this project.", `${meta.env.sessionSecret}=${randomBytes2(32).toString("hex")}`);
|
|
12482
12234
|
}
|
|
12483
|
-
writeFileSync5(
|
|
12235
|
+
writeFileSync5(join4(targetDir, ".env"), lines.join(`
|
|
12484
12236
|
`) + `
|
|
12485
12237
|
`);
|
|
12486
12238
|
}
|
|
12487
12239
|
const manifest = readProjectManifest(targetDir);
|
|
12488
|
-
writeJson(
|
|
12240
|
+
writeJson(join4(targetDir, MANIFEST_PATH), resolveVariant(manifest, useEditor ? "editor" : "default"));
|
|
12489
12241
|
if (opts.git !== false) {
|
|
12490
12242
|
const git = (args) => spawnSync2("git", args, { cwd: targetDir, stdio: "ignore" });
|
|
12491
12243
|
if (git(["init", "-b", "main"]).status === 0) {
|
|
@@ -12518,17 +12270,17 @@ Runs on mock.shop demo data out of the box — edit .env to connect your store.`
|
|
|
12518
12270
|
console.log("Your theme customizations live in theme/ — see theme/README.md.");
|
|
12519
12271
|
}
|
|
12520
12272
|
function themeStatus(dir2 = ".") {
|
|
12521
|
-
const projectDir =
|
|
12273
|
+
const projectDir = resolve3(dir2);
|
|
12522
12274
|
const manifest = readProjectManifest(projectDir);
|
|
12523
12275
|
console.log(`${manifest.template} theme, ${TEMPLATES_PKG}@${manifest.version}` + (manifest.variant === "editor" ? " (editor variant)" : ""));
|
|
12524
12276
|
let clean = 0;
|
|
12525
12277
|
const edited = [];
|
|
12526
12278
|
const missing = [];
|
|
12527
12279
|
for (const [rel, expected] of Object.entries(manifest.files)) {
|
|
12528
|
-
const full =
|
|
12529
|
-
if (!
|
|
12280
|
+
const full = join4(projectDir, rel);
|
|
12281
|
+
if (!existsSync4(full))
|
|
12530
12282
|
missing.push(rel);
|
|
12531
|
-
else if (sha256(
|
|
12283
|
+
else if (sha256(readFileSync4(full)) !== expected)
|
|
12532
12284
|
edited.push(rel);
|
|
12533
12285
|
else
|
|
12534
12286
|
clean++;
|
|
@@ -12546,7 +12298,7 @@ function looksBinary(...buffers) {
|
|
|
12546
12298
|
return buffers.some((b) => b.subarray(0, 8000).includes(0));
|
|
12547
12299
|
}
|
|
12548
12300
|
async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
12549
|
-
const projectDir =
|
|
12301
|
+
const projectDir = resolve3(dir2);
|
|
12550
12302
|
const local = readProjectManifest(projectDir);
|
|
12551
12303
|
const variant = local.variant ?? "default";
|
|
12552
12304
|
const target = await acquireTemplatePkg(opts.tarball, opts.to);
|
|
@@ -12560,8 +12312,8 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12560
12312
|
}
|
|
12561
12313
|
const oldManifest = resolveVariant(readTemplateManifest(base.root, local.template), variant);
|
|
12562
12314
|
const newManifest = resolveVariant(readTemplateManifest(target.root, local.template), variant);
|
|
12563
|
-
const oldDir =
|
|
12564
|
-
const newDir =
|
|
12315
|
+
const oldDir = join4(base.root, "templates", local.template);
|
|
12316
|
+
const newDir = join4(target.root, "templates", local.template);
|
|
12565
12317
|
console.log(`Upgrading ${local.template} theme ${local.version} → ${target.version}` + (opts.dryRun ? " (dry run)" : ""));
|
|
12566
12318
|
const counts = {
|
|
12567
12319
|
overwrite: 0,
|
|
@@ -12579,9 +12331,9 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12579
12331
|
continue;
|
|
12580
12332
|
const oldHash = oldManifest.files[rel];
|
|
12581
12333
|
const newHash = newManifest.files[rel];
|
|
12582
|
-
const localPath =
|
|
12583
|
-
const localExists =
|
|
12584
|
-
const localBytes = localExists ?
|
|
12334
|
+
const localPath = join4(projectDir, rel);
|
|
12335
|
+
const localExists = existsSync4(localPath);
|
|
12336
|
+
const localBytes = localExists ? readFileSync4(localPath) : null;
|
|
12585
12337
|
const localHash = localBytes ? sha256(localBytes) : null;
|
|
12586
12338
|
const write = (bytes) => {
|
|
12587
12339
|
if (opts.dryRun)
|
|
@@ -12595,7 +12347,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12595
12347
|
console.log(` ! missing ${rel} (theme file was deleted locally; unchanged upstream — leaving as is)`);
|
|
12596
12348
|
continue;
|
|
12597
12349
|
}
|
|
12598
|
-
const newBytes =
|
|
12350
|
+
const newBytes = readFileSync4(templateFilePath(newDir, rel, variant));
|
|
12599
12351
|
if (!localExists) {
|
|
12600
12352
|
write(newBytes);
|
|
12601
12353
|
counts.add++;
|
|
@@ -12605,7 +12357,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12605
12357
|
counts.overwrite++;
|
|
12606
12358
|
console.log(` ✓ ${opts.dryRun ? "would update" : "updated"} ${rel}`);
|
|
12607
12359
|
} else if (localHash === newHash) {} else {
|
|
12608
|
-
const oldBytes =
|
|
12360
|
+
const oldBytes = readFileSync4(templateFilePath(oldDir, rel, variant));
|
|
12609
12361
|
if (looksBinary(localBytes, oldBytes, newBytes)) {
|
|
12610
12362
|
if (!opts.dryRun)
|
|
12611
12363
|
writeFileSync5(`${localPath}.new`, newBytes);
|
|
@@ -12619,9 +12371,9 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12619
12371
|
console.log(` ~ would merge ${rel}`);
|
|
12620
12372
|
continue;
|
|
12621
12373
|
}
|
|
12622
|
-
const scratch = mkdtempSync(
|
|
12623
|
-
const basePath =
|
|
12624
|
-
const newPath =
|
|
12374
|
+
const scratch = mkdtempSync(join4(tmpdir(), "zalify-merge-"));
|
|
12375
|
+
const basePath = join4(scratch, "base");
|
|
12376
|
+
const newPath = join4(scratch, "new");
|
|
12625
12377
|
writeFileSync5(basePath, oldBytes);
|
|
12626
12378
|
writeFileSync5(newPath, newBytes);
|
|
12627
12379
|
const result = spawnSync2("git", ["merge-file", "-L", "current", "-L", `base (${base.version})`, "-L", `new (${target.version})`, localPath, basePath, newPath], { encoding: "utf8" });
|
|
@@ -12654,7 +12406,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12654
12406
|
}
|
|
12655
12407
|
} else if (!oldHash && newHash) {
|
|
12656
12408
|
if (!localExists) {
|
|
12657
|
-
write(
|
|
12409
|
+
write(readFileSync4(templateFilePath(newDir, rel, variant)));
|
|
12658
12410
|
counts.add++;
|
|
12659
12411
|
console.log(` + ${opts.dryRun ? "would add" : "added"} ${rel}`);
|
|
12660
12412
|
} else if (localHash !== newHash) {
|
|
@@ -12665,7 +12417,7 @@ async function themeUpgrade(dir2 = ".", opts = {}) {
|
|
|
12665
12417
|
}
|
|
12666
12418
|
const depsChanged = mergePackageJson(projectDir, oldDir, newDir, opts.dryRun ?? false);
|
|
12667
12419
|
if (!opts.dryRun) {
|
|
12668
|
-
writeJson(
|
|
12420
|
+
writeJson(join4(projectDir, MANIFEST_PATH), { ...newManifest, version: target.version });
|
|
12669
12421
|
}
|
|
12670
12422
|
const summary = Object.entries(counts).filter(([, n]) => n > 0).map(([k, n]) => `${n} ${k.replace("-", " ")}`).join(", ");
|
|
12671
12423
|
console.log(`
|
|
@@ -12686,7 +12438,7 @@ ${opts.dryRun ? "Would apply" : "Done"}: ${summary || "no file changes"}.`);
|
|
|
12686
12438
|
}
|
|
12687
12439
|
}
|
|
12688
12440
|
function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
|
|
12689
|
-
const read = (p) => JSON.parse(
|
|
12441
|
+
const read = (p) => JSON.parse(readFileSync4(join4(p, "package.json"), "utf8"));
|
|
12690
12442
|
const localPkg = read(projectDir);
|
|
12691
12443
|
const oldPkg = read(oldDir);
|
|
12692
12444
|
const newPkg = read(newDir);
|
|
@@ -12735,20 +12487,437 @@ function mergePackageJson(projectDir, oldDir, newDir, dryRun) {
|
|
|
12735
12487
|
console.log(` ✓ package.json ${key} updated`);
|
|
12736
12488
|
}
|
|
12737
12489
|
if (changed && !dryRun) {
|
|
12738
|
-
writeFileSync5(
|
|
12490
|
+
writeFileSync5(join4(projectDir, "package.json"), JSON.stringify(localPkg, null, 2) + `
|
|
12739
12491
|
`);
|
|
12740
12492
|
}
|
|
12741
12493
|
return depsChanged;
|
|
12742
12494
|
}
|
|
12743
12495
|
|
|
12744
|
-
// src/
|
|
12745
|
-
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12749
|
-
|
|
12750
|
-
|
|
12751
|
-
|
|
12496
|
+
// src/storefront.ts
|
|
12497
|
+
function apiError2(status, json, appUrl) {
|
|
12498
|
+
if (status === 402) {
|
|
12499
|
+
return new Error(`${json.error ?? "Upgrade required"}
|
|
12500
|
+
` + ` Storefront hosting is a paid feature — upgrade the workspace in ${appUrl}`);
|
|
12501
|
+
}
|
|
12502
|
+
if (json.code === "NO_CONNECTION") {
|
|
12503
|
+
return new Error(`${json.error}
|
|
12504
|
+
Connect the workspace to its Shopify store first, then re-run.`);
|
|
12505
|
+
}
|
|
12506
|
+
if (json.code === "SLUG_TAKEN") {
|
|
12507
|
+
return new Error(`${json.error}
|
|
12508
|
+
Pick a different directory/slug name.`);
|
|
12509
|
+
}
|
|
12510
|
+
if (status === 404) {
|
|
12511
|
+
return new Error("The storefronts API is not deployed yet (404). Update app.zalify.com first.");
|
|
12512
|
+
}
|
|
12513
|
+
if (status === 503) {
|
|
12514
|
+
return new Error(json.error ?? "Storefront provisioning is not configured on the server.");
|
|
12515
|
+
}
|
|
12516
|
+
return new Error(`storefronts API ${status}: ${JSON.stringify(json)}`);
|
|
12517
|
+
}
|
|
12518
|
+
async function post(auth, path9, body) {
|
|
12519
|
+
const res = await fetch(`${auth.appUrl}${path9}`, {
|
|
12520
|
+
method: "POST",
|
|
12521
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${auth.key}` },
|
|
12522
|
+
body: JSON.stringify({ workspaceId: auth.workspaceId, ...body })
|
|
12523
|
+
});
|
|
12524
|
+
const json = await res.json().catch(() => ({}));
|
|
12525
|
+
if (!res.ok)
|
|
12526
|
+
throw apiError2(res.status, json, auth.appUrl);
|
|
12527
|
+
return json;
|
|
12528
|
+
}
|
|
12529
|
+
async function get(auth, path9) {
|
|
12530
|
+
const sep = path9.includes("?") ? "&" : "?";
|
|
12531
|
+
const res = await fetch(`${auth.appUrl}${path9}${sep}workspaceId=${auth.workspaceId}`, {
|
|
12532
|
+
headers: { Authorization: `Bearer ${auth.key}` }
|
|
12533
|
+
});
|
|
12534
|
+
const json = await res.json().catch(() => ({}));
|
|
12535
|
+
if (!res.ok)
|
|
12536
|
+
throw apiError2(res.status, json, auth.appUrl);
|
|
12537
|
+
return json;
|
|
12538
|
+
}
|
|
12539
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
12540
|
+
async function pollUntilReady(auth, id) {
|
|
12541
|
+
let lastMessage = "";
|
|
12542
|
+
const deadline = Date.now() + 10 * 60000;
|
|
12543
|
+
while (Date.now() < deadline) {
|
|
12544
|
+
const view = await get(auth, `/api/storefronts/${id}`);
|
|
12545
|
+
const message = view.statusMessage ?? view.status;
|
|
12546
|
+
if (message !== lastMessage) {
|
|
12547
|
+
console.log(` … ${message}`);
|
|
12548
|
+
lastMessage = message;
|
|
12549
|
+
}
|
|
12550
|
+
if (view.status === "READY")
|
|
12551
|
+
return view;
|
|
12552
|
+
if (view.status === "FAILED") {
|
|
12553
|
+
throw new Error(`Provisioning failed: ${view.statusMessage ?? "unknown reason"}`);
|
|
12554
|
+
}
|
|
12555
|
+
await sleep2(3000);
|
|
12556
|
+
}
|
|
12557
|
+
throw new Error("Provisioning timed out after 10 minutes — check the Temporal UI.");
|
|
12558
|
+
}
|
|
12559
|
+
async function storefrontCreate(dir2, opts) {
|
|
12560
|
+
const auth = requireActive();
|
|
12561
|
+
const slug = basename2(resolve4(dir2)).toLowerCase().replaceAll(/[^a-z0-9-]+/g, "-").replaceAll(/^-+|-+$/g, "") || "storefront";
|
|
12562
|
+
console.log(`Provisioning storefront "${slug}" (workspace "${auth.workspaceName}")`);
|
|
12563
|
+
const { storefrontId } = await post(auth, "/api/storefronts", {
|
|
12564
|
+
slug,
|
|
12565
|
+
theme: opts.theme ?? "nextjs",
|
|
12566
|
+
...opts.to ? { themeVersion: opts.to } : {}
|
|
12567
|
+
});
|
|
12568
|
+
const view = await pollUntilReady(auth, storefrontId);
|
|
12569
|
+
console.log(` ✓ provisioned: ${view.githubRepo} → https://${view.domain}`);
|
|
12570
|
+
await themeCreate(dir2, {
|
|
12571
|
+
template: view.theme,
|
|
12572
|
+
editor: opts.editor,
|
|
12573
|
+
storeDomain: view.storeDomain ?? "",
|
|
12574
|
+
storefrontToken: view.storefrontToken ?? "",
|
|
12575
|
+
to: opts.to,
|
|
12576
|
+
install: opts.install,
|
|
12577
|
+
git: opts.git
|
|
12578
|
+
});
|
|
12579
|
+
if (opts.git !== false && view.githubRepo) {
|
|
12580
|
+
const { token, repo } = await post(auth, `/api/storefronts/${storefrontId}/push-token`, {});
|
|
12581
|
+
const run = (args) => spawnSync3("git", args, { cwd: resolve4(dir2), stdio: "ignore" });
|
|
12582
|
+
run(["remote", "add", "origin", `https://github.com/${repo}.git`]);
|
|
12583
|
+
const push = spawnSync3("git", ["push", `https://x-access-token:${token}@github.com/${repo}.git`, "main"], { cwd: resolve4(dir2), stdio: "inherit" });
|
|
12584
|
+
if (push.status !== 0) {
|
|
12585
|
+
throw new Error(`git push to ${repo} failed — resolve and push manually.`);
|
|
12586
|
+
}
|
|
12587
|
+
run(["update-ref", "refs/remotes/origin/main", "HEAD"]);
|
|
12588
|
+
run(["config", "branch.main.remote", "origin"]);
|
|
12589
|
+
run(["config", "branch.main.merge", "refs/heads/main"]);
|
|
12590
|
+
console.log(` ✓ pushed to github.com/${repo} — Vercel deploys on push`);
|
|
12591
|
+
}
|
|
12592
|
+
if (view.domain) {
|
|
12593
|
+
console.log(` Waiting for https://${view.domain} …`);
|
|
12594
|
+
const deadline = Date.now() + 5 * 60000;
|
|
12595
|
+
while (Date.now() < deadline) {
|
|
12596
|
+
try {
|
|
12597
|
+
const res = await fetch(`https://${view.domain}/`, { redirect: "manual" });
|
|
12598
|
+
if (res.ok) {
|
|
12599
|
+
console.log(`
|
|
12600
|
+
Live: https://${view.domain}`);
|
|
12601
|
+
break;
|
|
12602
|
+
}
|
|
12603
|
+
} catch {}
|
|
12604
|
+
await sleep2(1e4);
|
|
12605
|
+
}
|
|
12606
|
+
}
|
|
12607
|
+
console.log(`
|
|
12608
|
+
Next steps:
|
|
12609
|
+
` + ` - author your theme in ${dir2}/theme/ (see theme/README.md), commit + push
|
|
12610
|
+
` + ` - zalify shopify import stores/<slug> --prune # catalog (auto-flushes the site cache)
|
|
12611
|
+
` + ` - zalify theme status ${dir2} # drift check anytime`);
|
|
12612
|
+
}
|
|
12613
|
+
async function storefrontList() {
|
|
12614
|
+
const auth = requireActive();
|
|
12615
|
+
const { storefronts } = await get(auth, "/api/storefronts");
|
|
12616
|
+
if (!storefronts.length) {
|
|
12617
|
+
console.log(`No storefronts in workspace "${auth.workspaceName}".`);
|
|
12618
|
+
return;
|
|
12619
|
+
}
|
|
12620
|
+
for (const s of storefronts) {
|
|
12621
|
+
const site = s.domain ? `https://${s.domain}` : "-";
|
|
12622
|
+
console.log(`${s.status === "READY" ? "✓" : s.status === "FAILED" ? "✗" : "…"} ${s.slug} ${site} (${s.status.toLowerCase()}${s.githubRepo ? `, ${s.githubRepo}` : ""})`);
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
12625
|
+
function resolveSlug(slugOrDir) {
|
|
12626
|
+
const storeJson = join5(resolve4(slugOrDir), "store.json");
|
|
12627
|
+
if (existsSync5(storeJson)) {
|
|
12628
|
+
const parsed = JSON.parse(readFileSync5(storeJson, "utf8"));
|
|
12629
|
+
if (parsed.slug)
|
|
12630
|
+
return parsed.slug;
|
|
12631
|
+
}
|
|
12632
|
+
return basename2(slugOrDir);
|
|
12633
|
+
}
|
|
12634
|
+
async function storefrontRevalidate(slugOrDir) {
|
|
12635
|
+
const auth = requireActive();
|
|
12636
|
+
const slug = resolveSlug(slugOrDir);
|
|
12637
|
+
const { storefronts } = await get(auth, "/api/storefronts");
|
|
12638
|
+
const match = storefronts.find((s) => s.slug === slug);
|
|
12639
|
+
if (!match)
|
|
12640
|
+
throw new Error(`No storefront "${slug}" in workspace "${auth.workspaceName}".`);
|
|
12641
|
+
await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
|
|
12642
|
+
console.log(`✓ flushed cache on https://${match.domain}`);
|
|
12643
|
+
}
|
|
12644
|
+
async function maybeRevalidateStorefront(auth, storeDir) {
|
|
12645
|
+
try {
|
|
12646
|
+
const slug = resolveSlug(storeDir);
|
|
12647
|
+
const { storefronts } = await get(auth, "/api/storefronts");
|
|
12648
|
+
const match = storefronts.find((s) => s.slug === slug && s.status === "READY");
|
|
12649
|
+
if (!match)
|
|
12650
|
+
return;
|
|
12651
|
+
await post(auth, `/api/storefronts/${match.id}/revalidate`, {});
|
|
12652
|
+
console.log(`✓ site cache flushed (https://${match.domain})`);
|
|
12653
|
+
} catch {}
|
|
12654
|
+
}
|
|
12655
|
+
|
|
12656
|
+
// src/shopify.ts
|
|
12657
|
+
function proxyError(auth, status, json) {
|
|
12658
|
+
if (json.code === "SCOPES_MISSING") {
|
|
12659
|
+
const granted = new Set(json.grantedScopes ?? []);
|
|
12660
|
+
const missing = (json.requiredScopes ?? []).filter((s) => !granted.has(s));
|
|
12661
|
+
return new Error(`Your Shopify connection is missing newly added permissions: ${missing.join(", ") || "(unknown)"}.
|
|
12662
|
+
` + ` Reconnect the store (one-time approval): ${json.reconnectUrl ?? `${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`}
|
|
12663
|
+
` + ` Then re-run this command.`);
|
|
12664
|
+
}
|
|
12665
|
+
if (status === 404) {
|
|
12666
|
+
return new Error("Shopify Admin proxy not found (404) — the /api/shopify/admin endpoint " + "is probably not deployed yet on this Zalify app. Try again after the next deploy.");
|
|
12667
|
+
}
|
|
12668
|
+
if (status === 402 || json.code === "UPGRADE_REQUIRED") {
|
|
12669
|
+
return new Error(`${json.error ?? "Paid plan required."}
|
|
12670
|
+
Upgrade: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/billing`);
|
|
12671
|
+
}
|
|
12672
|
+
if (status === 409) {
|
|
12673
|
+
return new Error(`${json.error ?? "No Shopify store is connected to this workspace."}
|
|
12674
|
+
` + ` Connect one: ${auth.appUrl}/store/${auth.workspaceSlug}/settings/integrations`);
|
|
12675
|
+
}
|
|
12676
|
+
return new Error(`shopify/admin ${status}: ${JSON.stringify(json)}`);
|
|
12677
|
+
}
|
|
12678
|
+
async function gql(auth, query, variables = {}) {
|
|
12679
|
+
const res = await fetch(`${auth.appUrl}/api/shopify/admin`, {
|
|
12680
|
+
method: "POST",
|
|
12681
|
+
headers: {
|
|
12682
|
+
"Content-Type": "application/json",
|
|
12683
|
+
Authorization: `Bearer ${auth.key}`
|
|
12684
|
+
},
|
|
12685
|
+
body: JSON.stringify({ workspaceId: auth.workspaceId, query, variables })
|
|
12686
|
+
});
|
|
12687
|
+
const json = await res.json().catch(() => ({}));
|
|
12688
|
+
if (!res.ok)
|
|
12689
|
+
throw proxyError(auth, res.status, json);
|
|
12690
|
+
if (json.errors)
|
|
12691
|
+
throw new Error(JSON.stringify(json.errors));
|
|
12692
|
+
if (!json.data)
|
|
12693
|
+
throw new Error(`shopify/admin: empty response`);
|
|
12694
|
+
return json.data;
|
|
12695
|
+
}
|
|
12696
|
+
function readJson(path9, label) {
|
|
12697
|
+
if (!existsSync6(path9)) {
|
|
12698
|
+
throw new Error(`No ${label} at ${path9}`);
|
|
12699
|
+
}
|
|
12700
|
+
return JSON.parse(readFileSync6(path9, "utf8"));
|
|
12701
|
+
}
|
|
12702
|
+
function joinErrors(errs) {
|
|
12703
|
+
return errs.map((e) => e.message).join("; ");
|
|
12704
|
+
}
|
|
12705
|
+
var PRODUCT_SET = `
|
|
12706
|
+
mutation productSet($input: ProductSetInput!) {
|
|
12707
|
+
productSet(input: $input, synchronous: true) {
|
|
12708
|
+
product { id handle }
|
|
12709
|
+
userErrors { field message }
|
|
12710
|
+
}
|
|
12711
|
+
}`;
|
|
12712
|
+
var COLLECTION_CREATE = `
|
|
12713
|
+
mutation collectionCreate($input: CollectionInput!) {
|
|
12714
|
+
collectionCreate(input: $input) {
|
|
12715
|
+
collection { id handle }
|
|
12716
|
+
userErrors { field message }
|
|
12717
|
+
}
|
|
12718
|
+
}`;
|
|
12719
|
+
async function findProductId(auth, handle) {
|
|
12720
|
+
const data = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${handle}` });
|
|
12721
|
+
const node = data.products.nodes[0];
|
|
12722
|
+
return node && node.handle === handle ? node.id : null;
|
|
12723
|
+
}
|
|
12724
|
+
async function shopifyImport(storeDir, options = {}) {
|
|
12725
|
+
const auth = requireActive();
|
|
12726
|
+
const dir2 = resolve5(storeDir);
|
|
12727
|
+
const catalog = readJson(join6(dir2, "catalog.json"), "catalog.json");
|
|
12728
|
+
const { locations } = await gql(auth, `{ locations(first: 1) { nodes { id name } } }`);
|
|
12729
|
+
const locationId = locations.nodes[0]?.id;
|
|
12730
|
+
console.log(`Importing ${catalog.products.length} product(s) into the Shopify store connected to ` + `workspace "${auth.workspaceName}" (location: ${locations.nodes[0]?.name ?? "none"})`);
|
|
12731
|
+
const productIds = [];
|
|
12732
|
+
for (const p of catalog.products) {
|
|
12733
|
+
const options_ = p.options ?? [];
|
|
12734
|
+
const optionValues = options_.map((name, idx) => ({
|
|
12735
|
+
name,
|
|
12736
|
+
position: idx + 1,
|
|
12737
|
+
values: [...new Set(p.variants.map((v) => v.options[idx]))].map((v) => ({ name: v }))
|
|
12738
|
+
}));
|
|
12739
|
+
const input = {
|
|
12740
|
+
handle: p.handle,
|
|
12741
|
+
title: p.title,
|
|
12742
|
+
descriptionHtml: p.descriptionHtml ?? "",
|
|
12743
|
+
vendor: p.vendor ?? catalog.vendor,
|
|
12744
|
+
productType: p.type ?? "",
|
|
12745
|
+
tags: p.tags ?? [],
|
|
12746
|
+
status: "ACTIVE",
|
|
12747
|
+
productOptions: optionValues.length ? optionValues : [{ name: "Title", position: 1, values: [{ name: "Default Title" }] }],
|
|
12748
|
+
variants: p.variants.map((v) => ({
|
|
12749
|
+
optionValues: options_.length ? options_.map((name, idx) => ({ optionName: name, name: v.options[idx] })) : [{ optionName: "Title", name: "Default Title" }],
|
|
12750
|
+
price: v.price,
|
|
12751
|
+
compareAtPrice: v.compareAtPrice ?? null,
|
|
12752
|
+
inventoryItem: {
|
|
12753
|
+
sku: v.sku ?? "",
|
|
12754
|
+
tracked: true,
|
|
12755
|
+
measurement: { weight: { value: v.grams ?? 0, unit: "GRAMS" } }
|
|
12756
|
+
},
|
|
12757
|
+
inventoryQuantities: locationId ? [{ locationId, name: "available", quantity: v.inventory ?? 25 }] : []
|
|
12758
|
+
}))
|
|
12759
|
+
};
|
|
12760
|
+
const existingId = await findProductId(auth, p.handle);
|
|
12761
|
+
if (existingId)
|
|
12762
|
+
input.id = existingId;
|
|
12763
|
+
const data = await gql(auth, PRODUCT_SET, { input });
|
|
12764
|
+
const errs = data.productSet.userErrors;
|
|
12765
|
+
if (errs.length) {
|
|
12766
|
+
console.error(` ✗ ${p.handle}: ${joinErrors(errs)}`);
|
|
12767
|
+
} else if (data.productSet.product) {
|
|
12768
|
+
productIds.push(data.productSet.product.id);
|
|
12769
|
+
console.log(` ✓ ${existingId ? "updated" : "created"} ${p.handle} (${p.variants.length} variants)`);
|
|
12770
|
+
}
|
|
12771
|
+
}
|
|
12772
|
+
if (options.prune) {
|
|
12773
|
+
const wanted = new Set(catalog.products.map((p) => p.handle));
|
|
12774
|
+
const data = await gql(auth, `query($q: String!) { products(first: 100, query: $q) { nodes { id handle } } }`, { q: `vendor:${catalog.vendor}` });
|
|
12775
|
+
for (const node of data.products.nodes) {
|
|
12776
|
+
if (wanted.has(node.handle))
|
|
12777
|
+
continue;
|
|
12778
|
+
const del = await gql(auth, `mutation($input: ProductDeleteInput!) {
|
|
12779
|
+
productDelete(input: $input) { deletedProductId userErrors { field message } }
|
|
12780
|
+
}`, { input: { id: node.id } });
|
|
12781
|
+
const errs = del.productDelete.userErrors;
|
|
12782
|
+
if (errs.length)
|
|
12783
|
+
console.error(` ✗ prune ${node.handle}: ${joinErrors(errs)}`);
|
|
12784
|
+
else
|
|
12785
|
+
console.log(` ✓ pruned ${node.handle}`);
|
|
12786
|
+
}
|
|
12787
|
+
}
|
|
12788
|
+
for (const c of catalog.collections ?? []) {
|
|
12789
|
+
const existing = await gql(auth, `query($q: String!) { collections(first: 1, query: $q) { nodes { id handle } } }`, { q: `handle:${c.handle}` });
|
|
12790
|
+
if (existing.collections.nodes[0]?.handle === c.handle) {
|
|
12791
|
+
console.log(` - collection ${c.handle} exists, skipped`);
|
|
12792
|
+
continue;
|
|
12793
|
+
}
|
|
12794
|
+
const data = await gql(auth, COLLECTION_CREATE, {
|
|
12795
|
+
input: {
|
|
12796
|
+
handle: c.handle,
|
|
12797
|
+
title: c.title,
|
|
12798
|
+
descriptionHtml: c.descriptionHtml ?? "",
|
|
12799
|
+
ruleSet: {
|
|
12800
|
+
appliedDisjunctively: false,
|
|
12801
|
+
rules: [
|
|
12802
|
+
{
|
|
12803
|
+
column: "TAG",
|
|
12804
|
+
relation: "EQUALS",
|
|
12805
|
+
condition: c.rule?.tag ?? `collection:${c.handle}`
|
|
12806
|
+
}
|
|
12807
|
+
]
|
|
12808
|
+
}
|
|
12809
|
+
}
|
|
12810
|
+
});
|
|
12811
|
+
const errs = data.collectionCreate.userErrors;
|
|
12812
|
+
if (errs.length)
|
|
12813
|
+
console.error(` ✗ collection ${c.handle}: ${joinErrors(errs)}`);
|
|
12814
|
+
else
|
|
12815
|
+
console.log(` ✓ collection ${c.handle}`);
|
|
12816
|
+
}
|
|
12817
|
+
try {
|
|
12818
|
+
const pubs = await gql(auth, `{ publications(first: 10) { nodes { id name } } }`);
|
|
12819
|
+
const online = pubs.publications.nodes.find((n) => n.name === "Online Store");
|
|
12820
|
+
if (online) {
|
|
12821
|
+
for (const id of productIds) {
|
|
12822
|
+
await gql(auth, `mutation($id: ID!, $input: [PublicationInput!]!) {
|
|
12823
|
+
publishablePublish(id: $id, input: $input) { userErrors { field message } }
|
|
12824
|
+
}`, { id, input: [{ publicationId: online.id }] });
|
|
12825
|
+
}
|
|
12826
|
+
console.log(` ✓ published ${productIds.length} products to Online Store`);
|
|
12827
|
+
}
|
|
12828
|
+
} catch (e) {
|
|
12829
|
+
console.warn(` ! could not publish to Online Store (write_publications scope missing?): ${e instanceof Error ? e.message : String(e)}`);
|
|
12830
|
+
}
|
|
12831
|
+
console.log("Done.");
|
|
12832
|
+
await maybeRevalidateStorefront(auth, dir2);
|
|
12833
|
+
}
|
|
12834
|
+
async function shopifyUploadImages(storeDir) {
|
|
12835
|
+
const auth = requireActive();
|
|
12836
|
+
const dir2 = resolve5(storeDir);
|
|
12837
|
+
const manifest = readJson(join6(dir2, "images", "manifest.json"), "images/manifest.json");
|
|
12838
|
+
console.log(`Uploading images to the Shopify store connected to workspace "${auth.workspaceName}"`);
|
|
12839
|
+
for (const entry of manifest.images) {
|
|
12840
|
+
const filePath = join6(dir2, "images", entry.file);
|
|
12841
|
+
if (!existsSync6(filePath)) {
|
|
12842
|
+
console.log(` - ${entry.handle}: ${entry.file} not generated yet, skipped`);
|
|
12843
|
+
continue;
|
|
12844
|
+
}
|
|
12845
|
+
const found = await gql(auth, `query($q: String!) { products(first: 1, query: $q) { nodes { id handle
|
|
12846
|
+
media(first: 50) { nodes { alt ... on MediaImage { image { url } } } } } } }`, { q: `handle:${entry.handle}` });
|
|
12847
|
+
const product = found.products.nodes[0];
|
|
12848
|
+
if (!product || product.handle !== entry.handle) {
|
|
12849
|
+
console.error(` ✗ ${entry.handle}: product not found in store`);
|
|
12850
|
+
continue;
|
|
12851
|
+
}
|
|
12852
|
+
const stem = basename3(entry.file, ".png");
|
|
12853
|
+
const already = product.media.nodes.some((m) => m.image?.url?.includes(stem) || entry.alt && m.alt === entry.alt);
|
|
12854
|
+
if (already) {
|
|
12855
|
+
console.log(` - ${entry.file}: already attached, skipped`);
|
|
12856
|
+
continue;
|
|
12857
|
+
}
|
|
12858
|
+
const bytes = readFileSync6(filePath);
|
|
12859
|
+
const staged = await gql(auth, `mutation($input: [StagedUploadInput!]!) {
|
|
12860
|
+
stagedUploadsCreate(input: $input) {
|
|
12861
|
+
stagedTargets { url resourceUrl parameters { name value } }
|
|
12862
|
+
userErrors { field message }
|
|
12863
|
+
}
|
|
12864
|
+
}`, {
|
|
12865
|
+
input: [
|
|
12866
|
+
{
|
|
12867
|
+
resource: "IMAGE",
|
|
12868
|
+
filename: basename3(entry.file),
|
|
12869
|
+
mimeType: "image/png",
|
|
12870
|
+
httpMethod: "PUT",
|
|
12871
|
+
fileSize: String(bytes.length)
|
|
12872
|
+
}
|
|
12873
|
+
]
|
|
12874
|
+
});
|
|
12875
|
+
const target = staged.stagedUploadsCreate.stagedTargets?.[0];
|
|
12876
|
+
if (!target) {
|
|
12877
|
+
console.error(` ✗ ${entry.handle}: staged upload failed: ${JSON.stringify(staged.stagedUploadsCreate.userErrors)}`);
|
|
12878
|
+
continue;
|
|
12879
|
+
}
|
|
12880
|
+
const headers = { "Content-Type": "image/png" };
|
|
12881
|
+
for (const p of target.parameters)
|
|
12882
|
+
headers[p.name] = p.value;
|
|
12883
|
+
const up = await fetch(target.url, {
|
|
12884
|
+
method: "PUT",
|
|
12885
|
+
headers,
|
|
12886
|
+
body: new Uint8Array(bytes)
|
|
12887
|
+
});
|
|
12888
|
+
if (!up.ok) {
|
|
12889
|
+
console.error(` ✗ ${entry.handle}: upload PUT failed (${up.status})`);
|
|
12890
|
+
continue;
|
|
12891
|
+
}
|
|
12892
|
+
const media = await gql(auth, `mutation($productId: ID!, $media: [CreateMediaInput!]!) {
|
|
12893
|
+
productCreateMedia(productId: $productId, media: $media) {
|
|
12894
|
+
media { alt }
|
|
12895
|
+
mediaUserErrors { field message }
|
|
12896
|
+
}
|
|
12897
|
+
}`, {
|
|
12898
|
+
productId: product.id,
|
|
12899
|
+
media: [
|
|
12900
|
+
{ originalSource: target.resourceUrl, alt: entry.alt ?? "", mediaContentType: "IMAGE" }
|
|
12901
|
+
]
|
|
12902
|
+
});
|
|
12903
|
+
const errs = media.productCreateMedia.mediaUserErrors;
|
|
12904
|
+
if (errs.length)
|
|
12905
|
+
console.error(` ✗ ${entry.handle}: ${joinErrors(errs)}`);
|
|
12906
|
+
else
|
|
12907
|
+
console.log(` ✓ ${entry.file}`);
|
|
12908
|
+
}
|
|
12909
|
+
console.log("Done.");
|
|
12910
|
+
await maybeRevalidateStorefront(auth, dir2);
|
|
12911
|
+
}
|
|
12912
|
+
|
|
12913
|
+
// src/cli.ts
|
|
12914
|
+
var __dirname4 = dirname2(fileURLToPath3(import.meta.url));
|
|
12915
|
+
var require2 = createRequire2(import.meta.url);
|
|
12916
|
+
var pkg = require2(join7(__dirname4, "..", "package.json"));
|
|
12917
|
+
try {
|
|
12918
|
+
updateNotifier({ pkg }).notify({
|
|
12919
|
+
isGlobal: true,
|
|
12920
|
+
message: "Update available {currentVersion} → {latestVersion}\nRun `zalify self-update`"
|
|
12752
12921
|
});
|
|
12753
12922
|
} catch {}
|
|
12754
12923
|
var program2 = new Command;
|
|
@@ -12799,6 +12968,16 @@ shopify.command("import <store-dir>").description("Import <store-dir>/catalog.js
|
|
|
12799
12968
|
shopify.command("upload-images <store-dir>").description("Upload <store-dir>/images/* to matching products per images/manifest.json (skips files already attached)").action(async (storeDir) => {
|
|
12800
12969
|
await shopifyUploadImages(storeDir);
|
|
12801
12970
|
});
|
|
12971
|
+
var storefront = program2.command("storefront").description("Provision and manage fully-hosted standalone storefronts (repo + Vercel via Zalify)");
|
|
12972
|
+
storefront.command("create <dir>").description("One-command launch: Zalify provisions the GitHub repo + Vercel project (env, domain, storefront token), then scaffolds locally and pushes").option("-t, --theme <name>", "theme", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--to <version>", "template version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init/push").action(async (dir2, options) => {
|
|
12973
|
+
await storefrontCreate(dir2, options);
|
|
12974
|
+
});
|
|
12975
|
+
storefront.command("list", { isDefault: true }).description("List the workspace's provisioned storefronts").action(async () => {
|
|
12976
|
+
await storefrontList();
|
|
12977
|
+
});
|
|
12978
|
+
storefront.command("revalidate <slug-or-store-dir>").description("Flush the storefront site's data cache (secret stays server-side)").action(async (slugOrDir) => {
|
|
12979
|
+
await storefrontRevalidate(slugOrDir);
|
|
12980
|
+
});
|
|
12802
12981
|
var theme = program2.command("theme").description("Scaffold and upgrade standalone Zalify storefronts (Shopify-theme model)");
|
|
12803
12982
|
theme.command("create <dir>").description("Scaffold a standalone storefront from the published theme templates").option("-t, --template <name>", "liquid | hydrogen | nextjs", "nextjs").option("--editor", "include the Zalify canvas-editor (z1) wiring").option("--store-domain <domain>", "your-store.myshopify.com (default: mock.shop demo data)").option("--storefront-token <token>", "public Storefront API access token").option("--to <version>", "template version (default: latest)").option("--no-install", "skip dependency install").option("--no-git", "skip git init + initial commit").option("--tarball <path>", "use a local @zalify/theme-templates .tgz instead of npm").action(async (dir2, options) => {
|
|
12804
12983
|
await themeCreate(dir2, options);
|