@popcraft/cli 0.15.0 → 0.16.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/popcraft.mjs +629 -84
- package/package.json +2 -2
package/dist/popcraft.mjs
CHANGED
|
@@ -1186,7 +1186,7 @@ function operations(spec = openapi) {
|
|
|
1186
1186
|
}
|
|
1187
1187
|
async function call(target2, method, path, opts = {}) {
|
|
1188
1188
|
const qs = new URLSearchParams(opts.query ?? {}).toString();
|
|
1189
|
-
const headers = { "user-agent": `popcraft-cli/${"0.
|
|
1189
|
+
const headers = { "user-agent": `popcraft-cli/${"0.16.0"}`, ...opts.headers };
|
|
1190
1190
|
if (target2.token) headers.authorization = `Bearer ${target2.token}`;
|
|
1191
1191
|
if (opts.contentType) headers["content-type"] = opts.contentType;
|
|
1192
1192
|
try {
|
|
@@ -32553,6 +32553,7 @@ var init_en2 = __esm({
|
|
|
32553
32553
|
"command:export.pptx": "Export as PowerPoint (.pptx)",
|
|
32554
32554
|
"command:export.storefront": "Export storefront\u2026",
|
|
32555
32555
|
"command:export.storefront#next-commerce": "Export storefront as Next.js Commerce components",
|
|
32556
|
+
"command:export.storefront#next-commerce-store": "Export storefront as a Next.js Commerce store",
|
|
32556
32557
|
"command:export.storefront#shopify-theme": "Export storefront as a Shopify theme",
|
|
32557
32558
|
"command:export.web": "Export as web code\u2026",
|
|
32558
32559
|
"command:export.webm": "Export animation as WebM",
|
|
@@ -35655,6 +35656,18 @@ var init_en2 = __esm({
|
|
|
35655
35656
|
"stateMachine.true": "true",
|
|
35656
35657
|
"stateMachine.whenAllHold": "Taken when all of these hold:",
|
|
35657
35658
|
"stateMachine.wholeFrame": "The whole frame",
|
|
35659
|
+
"storeSection.about": "Deploy it as a store that sells: a Next.js Commerce store on Vercel, with your Shopify store's products, cart and checkout.",
|
|
35660
|
+
"storeSection.building": "Building:",
|
|
35661
|
+
"storeSection.connect": "Connect Shopify and Vercel\u2026",
|
|
35662
|
+
"storeSection.deploy": "Deploy store",
|
|
35663
|
+
"storeSection.deployAgain": "Deploy store again",
|
|
35664
|
+
"storeSection.deploying": "Deploying store\u2026",
|
|
35665
|
+
"storeSection.hostedOn": "Hosted on Vercel",
|
|
35666
|
+
"storeSection.live": "Live:",
|
|
35667
|
+
"storeSection.noHost": "Not connected",
|
|
35668
|
+
"storeSection.noShop": "Not connected",
|
|
35669
|
+
"storeSection.sellsFrom": "Sells from Shopify",
|
|
35670
|
+
"storeSection.title": "Store",
|
|
35658
35671
|
"strokeEndpointDropdown.circle": "Circle",
|
|
35659
35672
|
"strokeEndpointDropdown.diamond": "Diamond",
|
|
35660
35673
|
"strokeEndpointDropdown.line": "Line",
|
|
@@ -254729,7 +254742,7 @@ function hrefOf(link2, nodes, routes) {
|
|
|
254729
254742
|
if (!link2) return null;
|
|
254730
254743
|
switch (link2.kind) {
|
|
254731
254744
|
case "url":
|
|
254732
|
-
return safeExternalUrl(link2.url);
|
|
254745
|
+
return sitePath(link2.url) ?? safeExternalUrl(link2.url);
|
|
254733
254746
|
case "email":
|
|
254734
254747
|
return /^[^\s@]+@[^\s@]+$/.test(link2.value.trim()) ? `mailto:${link2.value.trim()}` : null;
|
|
254735
254748
|
case "tel": {
|
|
@@ -254742,6 +254755,10 @@ function hrefOf(link2, nodes, routes) {
|
|
|
254742
254755
|
return nodes[link2.nodeId] ? `#${anchorOf(nodes[link2.nodeId])}` : null;
|
|
254743
254756
|
}
|
|
254744
254757
|
}
|
|
254758
|
+
function sitePath(raw) {
|
|
254759
|
+
const path = raw.trim().replace(/[\x00-\x1f\x7f]/g, "");
|
|
254760
|
+
return /^\/(?![/\\])/.test(path) ? path : null;
|
|
254761
|
+
}
|
|
254745
254762
|
var anchorOf, cleanAnchor, opensNewTab;
|
|
254746
254763
|
var init_semantics = __esm({
|
|
254747
254764
|
"lib/document/semantics.ts"() {
|
|
@@ -254749,7 +254766,7 @@ var init_semantics = __esm({
|
|
|
254749
254766
|
init_safe_url();
|
|
254750
254767
|
anchorOf = (node2) => node2.semantics?.anchor || `pc-${node2.id.replace(/[^A-Za-z0-9_-]/g, "-")}`;
|
|
254751
254768
|
cleanAnchor = (raw) => raw.trim().toLowerCase().replace(/^#/, "").replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
254752
|
-
opensNewTab = (link2) => link2.kind === "url" && link2.newTab !== false;
|
|
254769
|
+
opensNewTab = (link2) => link2.kind === "url" && link2.newTab !== false && !sitePath(link2.url);
|
|
254753
254770
|
}
|
|
254754
254771
|
});
|
|
254755
254772
|
|
|
@@ -305519,7 +305536,6 @@ function applyTextPath(layout2, path, tp, rebase = true) {
|
|
|
305519
305536
|
var defaultCircleRadius, ownsPath, mapPoint;
|
|
305520
305537
|
var init_text_path = __esm({
|
|
305521
305538
|
"lib/engine/text/text-path.ts"() {
|
|
305522
|
-
"use strict";
|
|
305523
305539
|
init_scene_graph();
|
|
305524
305540
|
init_transform();
|
|
305525
305541
|
init_shape_to_path();
|
|
@@ -305637,7 +305653,6 @@ function areaBounds(rings) {
|
|
|
305637
305653
|
}
|
|
305638
305654
|
var init_text_area = __esm({
|
|
305639
305655
|
"lib/engine/text/text-area.ts"() {
|
|
305640
|
-
"use strict";
|
|
305641
305656
|
}
|
|
305642
305657
|
});
|
|
305643
305658
|
|
|
@@ -310306,7 +310321,6 @@ function videoPixelSize(resolution, aspect) {
|
|
|
310306
310321
|
var VIDEO_ASPECTS, VIDEO_RESOLUTIONS, SEEDANCE_ASPECTS, range, VIDEO_MODELS, DEFAULT_VIDEO_MODEL, videoModel, VIDEO_LIMITS;
|
|
310307
310322
|
var init_video_models = __esm({
|
|
310308
310323
|
"lib/ai/video-models.ts"() {
|
|
310309
|
-
"use strict";
|
|
310310
310324
|
VIDEO_ASPECTS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
|
|
310311
310325
|
VIDEO_RESOLUTIONS = ["480p", "720p", "1080p"];
|
|
310312
310326
|
SEEDANCE_ASPECTS = ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
|
|
@@ -310795,6 +310809,7 @@ function agentToolList() {
|
|
|
310795
310809
|
var AGENT_CAPABILITIES, legacyCommandId, legacyTools, blobToDataUrl3, provided, legacyTool, legacyToolList;
|
|
310796
310810
|
var init_agent = __esm({
|
|
310797
310811
|
"lib/commands/agent.ts"() {
|
|
310812
|
+
"use strict";
|
|
310798
310813
|
init_types5();
|
|
310799
310814
|
init_registry();
|
|
310800
310815
|
init_errors();
|
|
@@ -314648,6 +314663,11 @@ var init_storefront_targets = __esm({
|
|
|
314648
314663
|
"lib/io/web/project/storefront-targets.ts"() {
|
|
314649
314664
|
"use strict";
|
|
314650
314665
|
STOREFRONT_TARGET_INFO = {
|
|
314666
|
+
"next-commerce-store": {
|
|
314667
|
+
label: "Next.js Commerce store",
|
|
314668
|
+
short: "Next.js Commerce",
|
|
314669
|
+
description: "A whole Next.js Commerce store drawn by the design: deploy it with a Shopify store's domain and Storefront token, and it sells."
|
|
314670
|
+
},
|
|
314651
314671
|
"next-commerce": {
|
|
314652
314672
|
label: "Next.js Commerce components",
|
|
314653
314673
|
short: "Next.js Commerce",
|
|
@@ -319522,7 +319542,7 @@ var init_package = __esm({
|
|
|
319522
319542
|
"packages/runtime/package.json"() {
|
|
319523
319543
|
package_default = {
|
|
319524
319544
|
name: "@popcraft/runtime",
|
|
319525
|
-
version: "0.
|
|
319545
|
+
version: "0.16.0",
|
|
319526
319546
|
description: "What code exported from PopCraft shares: the meaning of expressions (operators and functions) and, as it grows, the parts of a design's behaviour that are common code rather than generated React.",
|
|
319527
319547
|
license: "MIT",
|
|
319528
319548
|
homepage: "https://popcraft.app/docs/exporting/nextjs",
|
|
@@ -346083,6 +346103,7 @@ var components_exports = {};
|
|
|
346083
346103
|
__export(components_exports, {
|
|
346084
346104
|
componentFiles: () => componentFiles,
|
|
346085
346105
|
componentMedia: () => componentMedia,
|
|
346106
|
+
componentPlan: () => componentPlan,
|
|
346086
346107
|
componentScreens: () => componentScreens
|
|
346087
346108
|
});
|
|
346088
346109
|
function componentScreens(doc2, pageId2 = doc2.pages[0], nodeCap = 2e4) {
|
|
@@ -346097,6 +346118,35 @@ function componentScreens(doc2, pageId2 = doc2.pages[0], nodeCap = 2e4) {
|
|
|
346097
346118
|
return { id: s.id, name: s.name, tree: media2.length ? { ...tree, rules: [...tree.rules ?? [], ...media2] } : tree, ...about ? { about } : {} };
|
|
346098
346119
|
});
|
|
346099
346120
|
}
|
|
346121
|
+
function componentPlan(doc2, screens) {
|
|
346122
|
+
const names = /* @__PURE__ */ new Set();
|
|
346123
|
+
return screens.map((s) => {
|
|
346124
|
+
const base = (componentName(s.name) || "Screen").replace(/(page|screen)?$/i, "Page");
|
|
346125
|
+
let name2 = base;
|
|
346126
|
+
for (let n = 2; names.has(name2); n++) name2 = `${base}${n}`;
|
|
346127
|
+
names.add(name2);
|
|
346128
|
+
const props = [];
|
|
346129
|
+
const propName2 = (b) => {
|
|
346130
|
+
let p2 = b;
|
|
346131
|
+
for (let n = 2; props.some((x2) => x2.name === p2) || p2 === "item"; n++) p2 = `${b}${n}`;
|
|
346132
|
+
return p2;
|
|
346133
|
+
};
|
|
346134
|
+
if (s.about) {
|
|
346135
|
+
const schema = s.about.schema;
|
|
346136
|
+
props.push({ name: propName2(camel3(singular(COMMERCE_PROP[schema].name))), type: COMMERCE_PROP[schema].type, doc: `The ${singular(s.about.name).toLowerCase()} this page is about; none: the design's first sample.`, schema, about: true, collectionId: s.about.id });
|
|
346137
|
+
}
|
|
346138
|
+
const seen = /* @__PURE__ */ new Set();
|
|
346139
|
+
for (const n of all3(s.tree.root)) {
|
|
346140
|
+
const c2 = n.repeat && doc2.collections?.[n.repeat.collectionId];
|
|
346141
|
+
if (!c2?.schema || seen.has(c2.id)) continue;
|
|
346142
|
+
seen.add(c2.id);
|
|
346143
|
+
const { type, one } = COMMERCE_PROP[c2.schema];
|
|
346144
|
+
props.push({ name: propName2(one ? COMMERCE_PROP[c2.schema].name : camel3(c2.name)), type: one ? type : `${type}[]`, doc: `${c2.name}; none: the design's samples.`, schema: c2.schema, collectionId: c2.id });
|
|
346145
|
+
}
|
|
346146
|
+
for (const a of new Set(all3(s.tree.root).map((n) => n.action).filter((a2) => !!a2))) props.push({ name: COMMERCE_ACTION_PROP[a], type: "(formData: FormData) => void | Promise<void>", doc: `What sending its ${a} form does (a server action).`, action: a });
|
|
346147
|
+
return { screen: s, name: name2, props };
|
|
346148
|
+
});
|
|
346149
|
+
}
|
|
346100
346150
|
function componentMedia(doc2, screens) {
|
|
346101
346151
|
const text = JSON.stringify(screens.map((s) => s.tree)) + JSON.stringify(doc2.records ?? {});
|
|
346102
346152
|
return [...new Set(text.match(MEDIA_REF2) ?? [])].sort();
|
|
@@ -346114,46 +346164,27 @@ function componentFiles(doc2, screens, o = {}) {
|
|
|
346114
346164
|
doc2 = { ...doc2, records: located(doc2.records) };
|
|
346115
346165
|
const holder = { pages: screens.map((s) => ({ tree: located(s.tree) })) };
|
|
346116
346166
|
const pictures = [...externalizeBitmaps(holder).map((f2) => ({ ...f2, path: `public/popcraft/${f2.path}` })), ...media2];
|
|
346117
|
-
const names = /* @__PURE__ */ new Set();
|
|
346118
346167
|
const files = [];
|
|
346119
346168
|
const index2 = [];
|
|
346120
|
-
screens.forEach((
|
|
346169
|
+
componentPlan(doc2, screens).forEach(({ name: name2, props }, i2) => {
|
|
346170
|
+
const s = screens[i2];
|
|
346121
346171
|
const tree = { ...holder.pages[i2].tree, root: withAssetBase(holder.pages[i2].tree.root, "/popcraft/") };
|
|
346122
|
-
const base = (componentName(s.name) || "Screen").replace(/(page|screen)?$/i, "Page");
|
|
346123
|
-
let name2 = base;
|
|
346124
|
-
for (let n = 2; names.has(name2); n++) name2 = `${base}${n}`;
|
|
346125
|
-
names.add(name2);
|
|
346126
|
-
const props = [];
|
|
346127
346172
|
const types = /* @__PURE__ */ new Set();
|
|
346128
346173
|
const samples = [];
|
|
346129
346174
|
const prelude = [];
|
|
346130
346175
|
const listProp = /* @__PURE__ */ new Map();
|
|
346131
|
-
const
|
|
346132
|
-
|
|
346133
|
-
|
|
346134
|
-
|
|
346135
|
-
|
|
346136
|
-
|
|
346137
|
-
|
|
346138
|
-
|
|
346139
|
-
|
|
346140
|
-
|
|
346141
|
-
|
|
346142
|
-
|
|
346143
|
-
prelude.push(`const item = ${p2} ? commerceItem(${JSON.stringify(schema)}, ${p2}) : SAMPLE_${p2.toUpperCase()}`);
|
|
346144
|
-
}
|
|
346145
|
-
for (const n of all3(tree.root)) {
|
|
346146
|
-
const c2 = n.repeat && doc2.collections?.[n.repeat.collectionId];
|
|
346147
|
-
if (!c2?.schema || listProp.has(c2.id)) continue;
|
|
346148
|
-
const { type, one } = COMMERCE_PROP[c2.schema];
|
|
346149
|
-
const p2 = propName2(one ? COMMERCE_PROP[c2.schema].name : camel3(c2.name));
|
|
346150
|
-
types.add(type);
|
|
346151
|
-
listProp.set(c2.id, p2);
|
|
346152
|
-
props.push({ name: p2, type: one ? type : `${type}[]`, doc: `${c2.name}; none: the design's samples.` });
|
|
346153
|
-
samples.push(sampleConst(`SAMPLE_${p2.toUpperCase()}`, doc2, c2));
|
|
346154
|
-
}
|
|
346155
|
-
const actions = [...new Set(all3(tree.root).map((n) => n.action).filter((a) => !!a))];
|
|
346156
|
-
for (const a of actions) props.push({ name: COMMERCE_ACTION_PROP[a], type: "(formData: FormData) => void | Promise<void>", doc: `What sending its ${a} form does (a server action).` });
|
|
346176
|
+
for (const p2 of props) {
|
|
346177
|
+
if (!p2.schema || !p2.collectionId) continue;
|
|
346178
|
+
types.add(COMMERCE_PROP[p2.schema].type);
|
|
346179
|
+
const c2 = doc2.collections[p2.collectionId];
|
|
346180
|
+
if (p2.about) {
|
|
346181
|
+
samples.push(sampleConst(`SAMPLE_${p2.name.toUpperCase()}`, doc2, c2, 1));
|
|
346182
|
+
prelude.push(`const item = ${p2.name} ? commerceItem(${JSON.stringify(p2.schema)}, ${p2.name}) : SAMPLE_${p2.name.toUpperCase()}`);
|
|
346183
|
+
} else {
|
|
346184
|
+
listProp.set(c2.id, p2.name);
|
|
346185
|
+
samples.push(sampleConst(`SAMPLE_${p2.name.toUpperCase()}`, doc2, c2));
|
|
346186
|
+
}
|
|
346187
|
+
}
|
|
346157
346188
|
const body = serializeReact(tree, {
|
|
346158
346189
|
ids: true,
|
|
346159
346190
|
name: name2,
|
|
@@ -346235,8 +346266,35 @@ var init_components = __esm({
|
|
|
346235
346266
|
}
|
|
346236
346267
|
});
|
|
346237
346268
|
|
|
346238
|
-
// lib/io/web/project/
|
|
346239
|
-
|
|
346269
|
+
// lib/io/web/project/storefront-prepare.ts
|
|
346270
|
+
var storefront_prepare_exports = {};
|
|
346271
|
+
__export(storefront_prepare_exports, {
|
|
346272
|
+
storefrontInputs: () => storefrontInputs
|
|
346273
|
+
});
|
|
346274
|
+
async function storefrontInputs(doc2, pageId2, nodeCap) {
|
|
346275
|
+
const [{ componentMedia: componentMedia2, componentScreens: componentScreens2 }, { flatten: flatten3, resolveWebAssets: resolveWebAssets2 }, { rendererAssetResolvers: rendererAssetResolvers2 }, { loadMediaBlob: loadMediaBlob2 }] = await Promise.all([
|
|
346276
|
+
Promise.resolve().then(() => (init_components(), components_exports)),
|
|
346277
|
+
Promise.resolve().then(() => (init_web(), web_exports)),
|
|
346278
|
+
Promise.resolve().then(() => (init_resolvers(), resolvers_exports)),
|
|
346279
|
+
Promise.resolve().then(() => (init_media_store(), media_store_exports))
|
|
346280
|
+
]);
|
|
346281
|
+
const screens = componentScreens2(doc2, pageId2, nodeCap);
|
|
346282
|
+
const resolvers = rendererAssetResolvers2(doc2);
|
|
346283
|
+
for (const s of screens) if (flatten3(s.tree.root).some((n) => n.asset)) await resolveWebAssets2(s.tree, resolvers);
|
|
346284
|
+
const media2 = {};
|
|
346285
|
+
await Promise.all(componentMedia2(doc2, screens).map(async (ref3) => {
|
|
346286
|
+
const blob = await loadMediaBlob2(ref3).catch(() => null);
|
|
346287
|
+
if (blob) media2[ref3] = { bytes: new Uint8Array(await blob.arrayBuffer()), type: blob.type };
|
|
346288
|
+
}));
|
|
346289
|
+
return { screens, media: media2 };
|
|
346290
|
+
}
|
|
346291
|
+
var init_storefront_prepare = __esm({
|
|
346292
|
+
"lib/io/web/project/storefront-prepare.ts"() {
|
|
346293
|
+
}
|
|
346294
|
+
});
|
|
346295
|
+
|
|
346296
|
+
// lib/io/web/project/storefront-pages.ts
|
|
346297
|
+
function storefrontPageKind(s, doc2, route2) {
|
|
346240
346298
|
if (s.about?.schema === "commerce.product") return "product";
|
|
346241
346299
|
if (s.about?.schema === "commerce.collection") return "collection";
|
|
346242
346300
|
const nodes = every2(s.tree.root);
|
|
@@ -346245,6 +346303,20 @@ function kindOf(s, doc2, route2) {
|
|
|
346245
346303
|
if (route2 === "/") return "index";
|
|
346246
346304
|
return "page";
|
|
346247
346305
|
}
|
|
346306
|
+
var every2;
|
|
346307
|
+
var init_storefront_pages = __esm({
|
|
346308
|
+
"lib/io/web/project/storefront-pages.ts"() {
|
|
346309
|
+
"use strict";
|
|
346310
|
+
every2 = (n, out = []) => {
|
|
346311
|
+
out.push(n);
|
|
346312
|
+
n.children.forEach((c2) => every2(c2, out));
|
|
346313
|
+
n.repeat?.template.forEach((c2) => every2(c2, out));
|
|
346314
|
+
return out;
|
|
346315
|
+
};
|
|
346316
|
+
}
|
|
346317
|
+
});
|
|
346318
|
+
|
|
346319
|
+
// lib/io/web/project/shopify-theme.ts
|
|
346248
346320
|
function liquidNode(node2, schema, v) {
|
|
346249
346321
|
const fields = LIQUID[schema](v);
|
|
346250
346322
|
const out = boundNode(node2, (key5) => key5 in fields ? `{{ ${fields[key5]} }}` : key5 === "id" ? `{{ ${v}.id }}` : "");
|
|
@@ -346280,7 +346352,7 @@ function shopifyThemeFiles(doc2, screens, o = {}) {
|
|
|
346280
346352
|
const sections = [];
|
|
346281
346353
|
for (const [i2, s] of screens.entries()) {
|
|
346282
346354
|
const route2 = doc2.nodes?.[s.id] ? doc2.nodes[s.id].route?.path : void 0;
|
|
346283
|
-
let kind =
|
|
346355
|
+
let kind = storefrontPageKind(s, doc2, route2 ?? (i2 === 0 ? "/" : void 0));
|
|
346284
346356
|
if (kind !== "page" && used.has(kind)) kind = "page";
|
|
346285
346357
|
used.add(kind);
|
|
346286
346358
|
const slug3 = fileBase(s.name).toLowerCase() || `page-${i2 + 1}`;
|
|
@@ -346388,10 +346460,11 @@ ${JSON.stringify({ name: `${title} (PopCraft)`, settings: [] }, null, 2)}
|
|
|
346388
346460
|
files.push({ path: "locales/en.default.json", content: "{}\n" });
|
|
346389
346461
|
return files;
|
|
346390
346462
|
}
|
|
346391
|
-
var MEDIA_REF3, MEDIA_EXT2, LIQUID, LOOP_VAR, FORMS
|
|
346463
|
+
var MEDIA_REF3, MEDIA_EXT2, LIQUID, LOOP_VAR, FORMS;
|
|
346392
346464
|
var init_shopify_theme = __esm({
|
|
346393
346465
|
"lib/io/web/project/shopify-theme.ts"() {
|
|
346394
346466
|
"use strict";
|
|
346467
|
+
init_storefront_pages();
|
|
346395
346468
|
init_fonts3();
|
|
346396
346469
|
init_html();
|
|
346397
346470
|
init_naming();
|
|
@@ -346433,11 +346506,384 @@ var init_shopify_theme = __esm({
|
|
|
346433
346506
|
checkout: { action: "/cart", method: "post", fields: '<input type="hidden" name="checkout" value="1" />' },
|
|
346434
346507
|
search: { action: "/search", method: "get" }
|
|
346435
346508
|
};
|
|
346436
|
-
|
|
346437
|
-
|
|
346438
|
-
|
|
346439
|
-
|
|
346440
|
-
|
|
346509
|
+
}
|
|
346510
|
+
});
|
|
346511
|
+
|
|
346512
|
+
// lib/io/web/generated/next-commerce-shell.ts
|
|
346513
|
+
var NEXT_COMMERCE_SHELL;
|
|
346514
|
+
var init_next_commerce_shell = __esm({
|
|
346515
|
+
"lib/io/web/generated/next-commerce-shell.ts"() {
|
|
346516
|
+
"use strict";
|
|
346517
|
+
NEXT_COMMERCE_SHELL = {
|
|
346518
|
+
".env.example": 'COMPANY_NAME="Vercel Inc."\nSITE_NAME="Next.js Commerce"\nSHOPIFY_REVALIDATION_SECRET=""\nSHOPIFY_STOREFRONT_ACCESS_TOKEN=""\nSHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"\n',
|
|
346519
|
+
".gitignore": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n.playwright\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*\n!.env.example\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env*.local\n",
|
|
346520
|
+
"README.md": "# Next.js Commerce, drawn by a PopCraft design\n\n[Next.js Commerce](https://github.com/vercel/commerce) with its pages rendered by components exported from\n[PopCraft](https://popcraft.app). The store does what Next.js Commerce does \u2014 Shopify's Storefront API, the cart\ncookie, server actions, checkout, search, revalidation \u2014 and the design is PopCraft's: the **Fieldwork** storefront\ntemplate, its layout at every width, its type, colours and motion.\n\nA designer changes the shop in PopCraft; `npm run design:sync` exports it again, and the store renders the new design\nwith its own products. No component is rewritten by hand.\n\n## How it fits together\n\n| Path | What it is |\n| --- | --- |\n| `design/fieldwork.popcraft` | The design: open it in PopCraft (**Import as new file**), change it, save it back here |\n| `design/media/` | Its pictures (the hero, the story, the sample products), by content hash |\n| `components/popcraft/` | The design exported as components: `HomePage`, `SearchPage`, `CategoryPage`, `ProductPage`, `CartPage` |\n| `public/popcraft/` | The pictures they draw |\n| `app/` | Next.js Commerce's routes, each fetching from Shopify and rendering a PopCraft component |\n| `app/actions.ts` | The design's forms (add to cart, remove, checkout, search) as server actions over Next.js Commerce's cart |\n| `lib/shopify/`, `components/cart/actions.ts` | Next.js Commerce's Shopify client and cart actions, unchanged |\n| `lib/popcraft.ts` | What every page passes: the visitor's cart (an empty one when there is none) and the menu |\n\nA page is Next.js Commerce's data handed to a component:\n\n```tsx\nexport default async function Product(props: { params: Promise<{ handle: string }> }) {\n const product = await getProduct((await props.params).handle);\n if (!product) return notFound();\n const [related, frame] = await Promise.all([getProductRecommendations(product.id), storeFrame()]);\n return <ProductPage product={product} products={related} onAddToCart={addToCart} {...frame} />;\n}\n```\n\nThe components take Next.js Commerce's own types (`Product`, `Cart`, `CartItem`, `Menu`) from\n`@popcraft/runtime/commerce`, so nothing is mapped by hand. See\n[Storefront components](https://popcraft.app/docs/exporting/commerce) for how a design is bound.\n\n## Run it\n\nYou need a Shopify store with the Headless channel (or the Storefront API) and Node 24 or later.\n\n```bash\ncp .env.example .env.local # your store's domain and Storefront access token\nnpm install\nnpm run dev\n```\n\n| Variable | What it is |\n| --- | --- |\n| `SHOPIFY_STORE_DOMAIN` | `your-store.myshopify.com` |\n| `SHOPIFY_STOREFRONT_ACCESS_TOKEN` | From the Headless channel or a custom app with Storefront API access |\n| `SHOPIFY_REVALIDATION_SECRET` | Any secret: Shopify's webhooks call `/api/revalidate?secret=\u2026` when products change |\n| `SITE_NAME`, `COMPANY_NAME` | The store's name in titles and the footer |\n\nThe footer draws Shopify's menu with the handle `next-js-frontend-footer-menu`, as Next.js Commerce does. Deploy it like\nany Next.js app; on Vercel, add the variables to the project.\n\n## Change the design\n\n1. Open `design/fieldwork.popcraft` in PopCraft, or start from another storefront template (**New file \u203A Web \u203A\n Kiln storefront**) and save it here.\n2. Change anything: the layout, type, colours, motion, copy. Keep the lists and forms bound (Collections, Repeat and\n the forms' **Sending it**): they are how the store's data gets in.\n3. Export again:\n\n```bash\nnpm run design:sync # popcraft storefront design/fieldwork.popcraft --target next-commerce --out .\n```\n\nProduct pictures, titles and prices come from Shopify; what the design draws itself (the hero, the story) comes from\n`design/media`.\n\n## What stays Next.js Commerce's\n\nVariant selection, the cart's optimistic updates, sorting and filtering, and checkout are the store's. The design's add\nto cart sends the first available variant; a store that sells options renders Next.js Commerce's variant selector\nbeside the component.\n\nNext.js Commerce is MIT licensed (`license.md`).\n",
|
|
346521
|
+
"app/actions.ts": '"use server";\n\n// The design\'s storefront forms (add to cart, remove, checkout, search) as server actions. Each takes the form\'s own\n// fields \u2014 the names the design gave them \u2014 and calls Next.js Commerce\'s cart actions.\nimport {\n addItem,\n createCartAndSetCookie,\n redirectToCheckout,\n removeItem,\n updateItemQuantity,\n} from "components/cart/actions";\nimport { cookies } from "next/headers";\nimport { redirect } from "next/navigation";\n\nconst field = (form: FormData, name: string) => String(form.get(name) ?? "");\n\nexport async function addToCart(form: FormData) {\n if (!(await cookies()).get("cartId")) await createCartAndSetCookie();\n await addItem(null, field(form, "merchandiseId"));\n}\n\nexport async function removeCartLine(form: FormData) {\n await removeItem(null, field(form, "merchandiseId"));\n}\n\nexport async function updateCartLine(form: FormData) {\n await updateItemQuantity(null, {\n merchandiseId: field(form, "merchandiseId"),\n quantity: Number(field(form, "quantity")),\n });\n}\n\nexport async function checkout() {\n await redirectToCheckout();\n}\n\nexport async function search(form: FormData) {\n const q = field(form, "q").trim();\n redirect(q ? `/search?q=${encodeURIComponent(q)}` : "/search");\n}\n',
|
|
346522
|
+
"app/api/revalidate/route.ts": 'import { revalidate } from "lib/shopify";\nimport { NextRequest, NextResponse } from "next/server";\n\nexport async function POST(req: NextRequest): Promise<NextResponse> {\n return revalidate(req);\n}\n',
|
|
346523
|
+
"app/error.tsx": '"use client";\n\nexport default function Error({ reset }: { reset: () => void }) {\n return (\n <div style={{ maxWidth: 560, margin: "64px auto", padding: 32, fontFamily: "system-ui, sans-serif" }}>\n <h2 style={{ margin: 0 }}>Something went wrong</h2>\n <p>There was an issue with the store. Please try again.</p>\n <button onClick={() => reset()} style={{ padding: "12px 20px", borderRadius: 8, border: 0, background: "#2B3A55", color: "#fff", cursor: "pointer" }}>\n Try again\n </button>\n </div>\n );\n}\n',
|
|
346524
|
+
"app/globals.css": "html,\nbody {\n margin: 0;\n min-height: 100%;\n}\n\nimg,\npicture,\nsvg,\nvideo {\n display: block;\n max-width: 100%;\n}\n",
|
|
346525
|
+
"app/layout.tsx": 'import { baseUrl } from "lib/utils";\nimport { ReactNode } from "react";\nimport "./globals.css";\n\nconst { SITE_NAME } = process.env;\n\nexport const metadata = {\n metadataBase: new URL(baseUrl),\n title: {\n default: SITE_NAME!,\n template: `%s | ${SITE_NAME}`,\n },\n robots: {\n follow: true,\n index: true,\n },\n};\n\n// The design draws the whole page \u2014 its bar, its footer \u2014 so the layout is only the document.\nexport default function RootLayout({ children }: { children: ReactNode }) {\n return (\n <html lang="en">\n <body>{children}</body>\n </html>\n );\n}\n',
|
|
346526
|
+
"app/robots.ts": 'import { baseUrl } from "lib/utils";\n\nexport default function robots() {\n return {\n rules: [\n {\n userAgent: "*",\n },\n ],\n sitemap: `${baseUrl}/sitemap.xml`,\n host: baseUrl,\n };\n}\n',
|
|
346527
|
+
"app/sitemap.ts": 'import { getCollections, getPages, getProducts } from "lib/shopify";\nimport { baseUrl, validateEnvironmentVariables } from "lib/utils";\nimport { MetadataRoute } from "next";\n\ntype Route = {\n url: string;\n lastModified: string;\n};\n\nexport const dynamic = "force-dynamic";\n\nexport default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n validateEnvironmentVariables();\n\n const routesMap = [""].map((route) => ({\n url: `${baseUrl}${route}`,\n lastModified: new Date().toISOString(),\n }));\n\n const collectionsPromise = getCollections().then((collections) =>\n collections.map((collection) => ({\n url: `${baseUrl}${collection.path}`,\n lastModified: collection.updatedAt,\n })),\n );\n\n const productsPromise = getProducts({}).then((products) =>\n products.map((product) => ({\n url: `${baseUrl}/product/${product.handle}`,\n lastModified: product.updatedAt,\n })),\n );\n\n const pagesPromise = getPages().then((pages) =>\n pages.map((page) => ({\n url: `${baseUrl}/${page.handle}`,\n lastModified: page.updatedAt,\n })),\n );\n\n let fetchedRoutes: Route[] = [];\n\n try {\n fetchedRoutes = (\n await Promise.all([collectionsPromise, productsPromise, pagesPromise])\n ).flat();\n } catch (error) {\n throw JSON.stringify(error, null, 2);\n }\n\n return [...routesMap, ...fetchedRoutes];\n}\n',
|
|
346528
|
+
"components/cart/actions.ts": `"use server";
|
|
346529
|
+
|
|
346530
|
+
import { TAGS } from "lib/constants";
|
|
346531
|
+
import {
|
|
346532
|
+
addToCart,
|
|
346533
|
+
createCart,
|
|
346534
|
+
getCart,
|
|
346535
|
+
removeFromCart,
|
|
346536
|
+
updateCart,
|
|
346537
|
+
} from "lib/shopify";
|
|
346538
|
+
import { updateTag } from "next/cache";
|
|
346539
|
+
import { cookies } from "next/headers";
|
|
346540
|
+
import { redirect } from "next/navigation";
|
|
346541
|
+
|
|
346542
|
+
export async function addItem(
|
|
346543
|
+
prevState: any,
|
|
346544
|
+
selectedVariantId: string | undefined
|
|
346545
|
+
) {
|
|
346546
|
+
if (!selectedVariantId) {
|
|
346547
|
+
return "Error adding item to cart";
|
|
346548
|
+
}
|
|
346549
|
+
|
|
346550
|
+
try {
|
|
346551
|
+
await addToCart([{ merchandiseId: selectedVariantId, quantity: 1 }]);
|
|
346552
|
+
updateTag(TAGS.cart);
|
|
346553
|
+
} catch (e) {
|
|
346554
|
+
return "Error adding item to cart";
|
|
346555
|
+
}
|
|
346556
|
+
}
|
|
346557
|
+
|
|
346558
|
+
export async function removeItem(prevState: any, merchandiseId: string) {
|
|
346559
|
+
try {
|
|
346560
|
+
const cart = await getCart();
|
|
346561
|
+
|
|
346562
|
+
if (!cart) {
|
|
346563
|
+
return "Error fetching cart";
|
|
346564
|
+
}
|
|
346565
|
+
|
|
346566
|
+
const lineItem = cart.lines.find(
|
|
346567
|
+
(line) => line.merchandise.id === merchandiseId
|
|
346568
|
+
);
|
|
346569
|
+
|
|
346570
|
+
if (lineItem && lineItem.id) {
|
|
346571
|
+
await removeFromCart([lineItem.id]);
|
|
346572
|
+
updateTag(TAGS.cart);
|
|
346573
|
+
} else {
|
|
346574
|
+
return "Item not found in cart";
|
|
346575
|
+
}
|
|
346576
|
+
} catch (e) {
|
|
346577
|
+
return "Error removing item from cart";
|
|
346578
|
+
}
|
|
346579
|
+
}
|
|
346580
|
+
|
|
346581
|
+
export async function updateItemQuantity(
|
|
346582
|
+
prevState: any,
|
|
346583
|
+
payload: {
|
|
346584
|
+
merchandiseId: string;
|
|
346585
|
+
quantity: number;
|
|
346586
|
+
}
|
|
346587
|
+
) {
|
|
346588
|
+
const { merchandiseId, quantity } = payload;
|
|
346589
|
+
|
|
346590
|
+
try {
|
|
346591
|
+
const cart = await getCart();
|
|
346592
|
+
|
|
346593
|
+
if (!cart) {
|
|
346594
|
+
return "Error fetching cart";
|
|
346595
|
+
}
|
|
346596
|
+
|
|
346597
|
+
const lineItem = cart.lines.find(
|
|
346598
|
+
(line) => line.merchandise.id === merchandiseId
|
|
346599
|
+
);
|
|
346600
|
+
|
|
346601
|
+
if (lineItem && lineItem.id) {
|
|
346602
|
+
if (quantity === 0) {
|
|
346603
|
+
await removeFromCart([lineItem.id]);
|
|
346604
|
+
} else {
|
|
346605
|
+
await updateCart([
|
|
346606
|
+
{
|
|
346607
|
+
id: lineItem.id,
|
|
346608
|
+
merchandiseId,
|
|
346609
|
+
quantity,
|
|
346610
|
+
},
|
|
346611
|
+
]);
|
|
346612
|
+
}
|
|
346613
|
+
} else if (quantity > 0) {
|
|
346614
|
+
// If the item doesn't exist in the cart and quantity > 0, add it
|
|
346615
|
+
await addToCart([{ merchandiseId, quantity }]);
|
|
346616
|
+
}
|
|
346617
|
+
|
|
346618
|
+
updateTag(TAGS.cart);
|
|
346619
|
+
} catch (e) {
|
|
346620
|
+
console.error(e);
|
|
346621
|
+
return "Error updating item quantity";
|
|
346622
|
+
}
|
|
346623
|
+
}
|
|
346624
|
+
|
|
346625
|
+
export async function redirectToCheckout() {
|
|
346626
|
+
let cart = await getCart();
|
|
346627
|
+
redirect(cart!.checkoutUrl);
|
|
346628
|
+
}
|
|
346629
|
+
|
|
346630
|
+
export async function createCartAndSetCookie() {
|
|
346631
|
+
let cart = await createCart();
|
|
346632
|
+
(await cookies()).set("cartId", cart.id!);
|
|
346633
|
+
}
|
|
346634
|
+
`,
|
|
346635
|
+
"lib/constants.ts": 'export type SortFilterItem = {\n title: string;\n slug: string | null;\n sortKey: "RELEVANCE" | "BEST_SELLING" | "CREATED_AT" | "PRICE";\n reverse: boolean;\n};\n\nexport const defaultSort: SortFilterItem = {\n title: "Relevance",\n slug: null,\n sortKey: "RELEVANCE",\n reverse: false,\n};\n\nexport const sorting: SortFilterItem[] = [\n defaultSort,\n {\n title: "Trending",\n slug: "trending-desc",\n sortKey: "BEST_SELLING",\n reverse: false,\n }, // asc\n {\n title: "Latest arrivals",\n slug: "latest-desc",\n sortKey: "CREATED_AT",\n reverse: true,\n },\n {\n title: "Price: Low to high",\n slug: "price-asc",\n sortKey: "PRICE",\n reverse: false,\n }, // asc\n {\n title: "Price: High to low",\n slug: "price-desc",\n sortKey: "PRICE",\n reverse: true,\n },\n];\n\nexport const TAGS = {\n collections: "collections",\n products: "products",\n cart: "cart",\n};\n\nexport const HIDDEN_PRODUCT_TAG = "nextjs-frontend-hidden";\nexport const DEFAULT_OPTION = "Default Title";\nexport const SHOPIFY_GRAPHQL_API_ENDPOINT = "/api/2023-01/graphql.json";\n',
|
|
346636
|
+
"lib/popcraft.ts": `// What every page hands its PopCraft component: the visitor's cart and the store's menu. A component given nothing
|
|
346637
|
+
// for a list draws the design's sample records, so an empty cart is passed as one, never left out.
|
|
346638
|
+
import { getCart, getCollections, getMenu } from "lib/shopify";
|
|
346639
|
+
import type { Cart } from "lib/shopify/types";
|
|
346640
|
+
|
|
346641
|
+
export const EMPTY_CART: Cart = {
|
|
346642
|
+
id: undefined,
|
|
346643
|
+
checkoutUrl: "",
|
|
346644
|
+
totalQuantity: 0,
|
|
346645
|
+
lines: [],
|
|
346646
|
+
cost: {
|
|
346647
|
+
subtotalAmount: { amount: "0", currencyCode: "USD" },
|
|
346648
|
+
totalAmount: { amount: "0", currencyCode: "USD" },
|
|
346649
|
+
totalTaxAmount: { amount: "0", currencyCode: "USD" },
|
|
346650
|
+
},
|
|
346651
|
+
};
|
|
346652
|
+
|
|
346653
|
+
export async function storeFrame() {
|
|
346654
|
+
const [cart, menu] = await Promise.all([getCart(), storeMenu()]);
|
|
346655
|
+
return { cart: cart ?? EMPTY_CART, menu };
|
|
346656
|
+
}
|
|
346657
|
+
|
|
346658
|
+
/**
|
|
346659
|
+
* The store's menu: Shopify's main menu (Online Store \u203A Navigation), or \u2014 a store that has none \u2014 every product and
|
|
346660
|
+
* its first categories, so the design's menu always goes to pages the store has.
|
|
346661
|
+
*/
|
|
346662
|
+
async function storeMenu() {
|
|
346663
|
+
const menu = await getMenu("main-menu");
|
|
346664
|
+
if (menu.length) return menu;
|
|
346665
|
+
const categories = await storeCategories();
|
|
346666
|
+
return [{ title: "Shop all", path: "/search" }, ...categories.slice(0, 3).map((c) => ({ title: c.title, path: c.path }))];
|
|
346667
|
+
}
|
|
346668
|
+
|
|
346669
|
+
/** The store's categories, as the search and category pages list them ("All" is Next.js Commerce's own, left out). */
|
|
346670
|
+
export async function storeCategories() {
|
|
346671
|
+
return (await getCollections()).filter((c) => c.handle !== "");
|
|
346672
|
+
}
|
|
346673
|
+
`,
|
|
346674
|
+
"lib/shopify/fragments/cart.ts": 'import productFragment from "./product";\n\nconst cartFragment = /* GraphQL */ `\n fragment cart on Cart {\n id\n checkoutUrl\n cost {\n subtotalAmount {\n amount\n currencyCode\n }\n totalAmount {\n amount\n currencyCode\n }\n totalTaxAmount {\n amount\n currencyCode\n }\n }\n lines(first: 100) {\n edges {\n node {\n id\n quantity\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n merchandise {\n ... on ProductVariant {\n id\n title\n selectedOptions {\n name\n value\n }\n product {\n ...product\n }\n }\n }\n }\n }\n }\n totalQuantity\n }\n ${productFragment}\n`;\n\nexport default cartFragment;\n',
|
|
346675
|
+
"lib/shopify/fragments/image.ts": "const imageFragment = /* GraphQL */ `\n fragment image on Image {\n url\n altText\n width\n height\n }\n`;\n\nexport default imageFragment;\n",
|
|
346676
|
+
"lib/shopify/fragments/product.ts": 'import imageFragment from "./image";\nimport seoFragment from "./seo";\n\nconst productFragment = /* GraphQL */ `\n fragment product on Product {\n id\n handle\n availableForSale\n title\n description\n descriptionHtml\n options {\n id\n name\n values\n }\n priceRange {\n maxVariantPrice {\n amount\n currencyCode\n }\n minVariantPrice {\n amount\n currencyCode\n }\n }\n variants(first: 250) {\n edges {\n node {\n id\n title\n availableForSale\n selectedOptions {\n name\n value\n }\n price {\n amount\n currencyCode\n }\n }\n }\n }\n featuredImage {\n ...image\n }\n images(first: 20) {\n edges {\n node {\n ...image\n }\n }\n }\n seo {\n ...seo\n }\n tags\n updatedAt\n }\n ${imageFragment}\n ${seoFragment}\n`;\n\nexport default productFragment;\n',
|
|
346677
|
+
"lib/shopify/fragments/seo.ts": "const seoFragment = /* GraphQL */ `\n fragment seo on SEO {\n description\n title\n }\n`;\n\nexport default seoFragment;\n",
|
|
346678
|
+
"lib/shopify/index.ts": 'import {\n HIDDEN_PRODUCT_TAG,\n SHOPIFY_GRAPHQL_API_ENDPOINT,\n TAGS,\n} from "lib/constants";\nimport { isShopifyError } from "lib/type-guards";\nimport { ensureStartsWith } from "lib/utils";\nimport {\n unstable_cacheLife as cacheLife,\n unstable_cacheTag as cacheTag,\n revalidateTag,\n} from "next/cache";\nimport { cookies, headers } from "next/headers";\nimport { NextRequest, NextResponse } from "next/server";\nimport {\n addToCartMutation,\n createCartMutation,\n editCartItemsMutation,\n removeFromCartMutation,\n} from "./mutations/cart";\nimport { getCartQuery } from "./queries/cart";\nimport {\n getCollectionProductsQuery,\n getCollectionQuery,\n getCollectionsQuery,\n} from "./queries/collection";\nimport { getMenuQuery } from "./queries/menu";\nimport { getPageQuery, getPagesQuery } from "./queries/page";\nimport {\n getProductQuery,\n getProductRecommendationsQuery,\n getProductsQuery,\n} from "./queries/product";\nimport {\n Cart,\n Collection,\n Connection,\n Image,\n Menu,\n Page,\n Product,\n ShopifyAddToCartOperation,\n ShopifyCart,\n ShopifyCartOperation,\n ShopifyCollection,\n ShopifyCollectionOperation,\n ShopifyCollectionProductsOperation,\n ShopifyCollectionsOperation,\n ShopifyCreateCartOperation,\n ShopifyMenuOperation,\n ShopifyPageOperation,\n ShopifyPagesOperation,\n ShopifyProduct,\n ShopifyProductOperation,\n ShopifyProductRecommendationsOperation,\n ShopifyProductsOperation,\n ShopifyRemoveFromCartOperation,\n ShopifyUpdateCartOperation,\n} from "./types";\n\nconst domain = process.env.SHOPIFY_STORE_DOMAIN\n ? ensureStartsWith(process.env.SHOPIFY_STORE_DOMAIN, "https://")\n : "";\nconst endpoint = domain ? `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}` : "";\nconst key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;\n\ntype ExtractVariables<T> = T extends { variables: object }\n ? T["variables"]\n : never;\n\nexport async function shopifyFetch<T>({\n headers,\n query,\n variables,\n}: {\n headers?: HeadersInit;\n query: string;\n variables?: ExtractVariables<T>;\n}): Promise<{ status: number; body: T } | never> {\n try {\n if (!endpoint) {\n throw new Error("SHOPIFY_STORE_DOMAIN environment variable is not set");\n }\n\n const result = await fetch(endpoint, {\n method: "POST",\n headers: {\n "Content-Type": "application/json",\n "X-Shopify-Storefront-Access-Token": key,\n ...headers,\n },\n body: JSON.stringify({\n ...(query && { query }),\n ...(variables && { variables }),\n }),\n });\n\n const body = await result.json();\n\n if (body.errors) {\n throw body.errors[0];\n }\n\n return {\n status: result.status,\n body,\n };\n } catch (e) {\n if (isShopifyError(e)) {\n throw {\n cause: e.cause?.toString() || "unknown",\n status: e.status || 500,\n message: e.message,\n query,\n };\n }\n\n throw {\n error: e,\n query,\n };\n }\n}\n\nconst removeEdgesAndNodes = <T>(array: Connection<T>): T[] => {\n return array.edges.map((edge) => edge?.node);\n};\n\nconst reshapeCart = (cart: ShopifyCart): Cart => {\n if (!cart.cost?.totalTaxAmount) {\n cart.cost.totalTaxAmount = {\n amount: "0.0",\n currencyCode: cart.cost.totalAmount.currencyCode,\n };\n }\n\n return {\n ...cart,\n lines: removeEdgesAndNodes(cart.lines),\n };\n};\n\nconst reshapeCollection = (\n collection: ShopifyCollection\n): Collection | undefined => {\n if (!collection) {\n return undefined;\n }\n\n return {\n ...collection,\n path: `/search/${collection.handle}`,\n };\n};\n\nconst reshapeCollections = (collections: ShopifyCollection[]) => {\n const reshapedCollections = [];\n\n for (const collection of collections) {\n if (collection) {\n const reshapedCollection = reshapeCollection(collection);\n\n if (reshapedCollection) {\n reshapedCollections.push(reshapedCollection);\n }\n }\n }\n\n return reshapedCollections;\n};\n\nconst reshapeImages = (images: Connection<Image>, productTitle: string) => {\n const flattened = removeEdgesAndNodes(images);\n\n return flattened.map((image) => {\n const filename = image.url.match(/.*\\/(.*)\\..*/)?.[1];\n return {\n ...image,\n altText: image.altText || `${productTitle} - ${filename}`,\n };\n });\n};\n\nconst reshapeProduct = (\n product: ShopifyProduct,\n filterHiddenProducts: boolean = true\n) => {\n if (\n !product ||\n (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))\n ) {\n return undefined;\n }\n\n const { images, variants, ...rest } = product;\n\n return {\n ...rest,\n images: reshapeImages(images, product.title),\n variants: removeEdgesAndNodes(variants),\n };\n};\n\nconst reshapeProducts = (products: ShopifyProduct[]) => {\n const reshapedProducts = [];\n\n for (const product of products) {\n if (product) {\n const reshapedProduct = reshapeProduct(product);\n\n if (reshapedProduct) {\n reshapedProducts.push(reshapedProduct);\n }\n }\n }\n\n return reshapedProducts;\n};\n\nexport async function createCart(): Promise<Cart> {\n const res = await shopifyFetch<ShopifyCreateCartOperation>({\n query: createCartMutation,\n });\n\n return reshapeCart(res.body.data.cartCreate.cart);\n}\n\nexport async function addToCart(\n lines: { merchandiseId: string; quantity: number }[]\n): Promise<Cart> {\n const cartId = (await cookies()).get("cartId")?.value!;\n const res = await shopifyFetch<ShopifyAddToCartOperation>({\n query: addToCartMutation,\n variables: {\n cartId,\n lines,\n },\n });\n return reshapeCart(res.body.data.cartLinesAdd.cart);\n}\n\nexport async function removeFromCart(lineIds: string[]): Promise<Cart> {\n const cartId = (await cookies()).get("cartId")?.value!;\n const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({\n query: removeFromCartMutation,\n variables: {\n cartId,\n lineIds,\n },\n });\n\n return reshapeCart(res.body.data.cartLinesRemove.cart);\n}\n\nexport async function updateCart(\n lines: { id: string; merchandiseId: string; quantity: number }[]\n): Promise<Cart> {\n const cartId = (await cookies()).get("cartId")?.value!;\n const res = await shopifyFetch<ShopifyUpdateCartOperation>({\n query: editCartItemsMutation,\n variables: {\n cartId,\n lines,\n },\n });\n\n return reshapeCart(res.body.data.cartLinesUpdate.cart);\n}\n\nexport async function getCart(): Promise<Cart | undefined> {\n "use cache: private";\n cacheTag(TAGS.cart);\n cacheLife("seconds");\n\n const cartId = (await cookies()).get("cartId")?.value;\n\n if (!cartId) {\n return undefined;\n }\n\n const res = await shopifyFetch<ShopifyCartOperation>({\n query: getCartQuery,\n variables: { cartId },\n });\n\n // Old carts becomes `null` when you checkout.\n if (!res.body.data.cart) {\n return undefined;\n }\n\n return reshapeCart(res.body.data.cart);\n}\n\nexport async function getCollection(\n handle: string\n): Promise<Collection | undefined> {\n "use cache";\n cacheTag(TAGS.collections);\n cacheLife("days");\n\n const res = await shopifyFetch<ShopifyCollectionOperation>({\n query: getCollectionQuery,\n variables: {\n handle,\n },\n });\n\n return reshapeCollection(res.body.data.collection);\n}\n\nexport async function getCollectionProducts({\n collection,\n reverse,\n sortKey,\n}: {\n collection: string;\n reverse?: boolean;\n sortKey?: string;\n}): Promise<Product[]> {\n "use cache";\n cacheTag(TAGS.collections, TAGS.products);\n cacheLife("days");\n\n if (!endpoint) {\n console.log(\n `Skipping getCollectionProducts for \'${collection}\' - Shopify not configured`\n );\n return [];\n }\n\n const res = await shopifyFetch<ShopifyCollectionProductsOperation>({\n query: getCollectionProductsQuery,\n variables: {\n handle: collection,\n reverse,\n sortKey: sortKey === "CREATED_AT" ? "CREATED" : sortKey,\n },\n });\n\n if (!res.body.data.collection) {\n console.log(`No collection found for \\`${collection}\\``);\n return [];\n }\n\n return reshapeProducts(\n removeEdgesAndNodes(res.body.data.collection.products)\n );\n}\n\nexport async function getCollections(): Promise<Collection[]> {\n "use cache";\n cacheTag(TAGS.collections);\n cacheLife("days");\n\n if (!endpoint) {\n console.log("Skipping getCollections - Shopify not configured");\n return [\n {\n handle: "",\n title: "All",\n description: "All products",\n seo: {\n title: "All",\n description: "All products",\n },\n path: "/search",\n updatedAt: new Date().toISOString(),\n },\n ];\n }\n\n const res = await shopifyFetch<ShopifyCollectionsOperation>({\n query: getCollectionsQuery,\n });\n const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);\n const collections = [\n {\n handle: "",\n title: "All",\n description: "All products",\n seo: {\n title: "All",\n description: "All products",\n },\n path: "/search",\n updatedAt: new Date().toISOString(),\n },\n // Filter out the `hidden` collections.\n // Collections that start with `hidden-*` need to be hidden on the search page.\n ...reshapeCollections(shopifyCollections).filter(\n (collection) => !collection.handle.startsWith("hidden")\n ),\n ];\n\n return collections;\n}\n\nexport async function getMenu(handle: string): Promise<Menu[]> {\n "use cache";\n cacheTag(TAGS.collections);\n cacheLife("days");\n\n if (!endpoint) {\n console.log(`Skipping getMenu for \'${handle}\' - Shopify not configured`);\n return [];\n }\n\n const res = await shopifyFetch<ShopifyMenuOperation>({\n query: getMenuQuery,\n variables: {\n handle,\n },\n });\n\n return (\n res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({\n title: item.title,\n path: item.url\n .replace(domain, "")\n .replace("/collections", "/search")\n .replace("/pages", ""),\n })) || []\n );\n}\n\nexport async function getPage(handle: string): Promise<Page> {\n const res = await shopifyFetch<ShopifyPageOperation>({\n query: getPageQuery,\n variables: { handle },\n });\n\n return res.body.data.pageByHandle;\n}\n\nexport async function getPages(): Promise<Page[]> {\n const res = await shopifyFetch<ShopifyPagesOperation>({\n query: getPagesQuery,\n });\n\n return removeEdgesAndNodes(res.body.data.pages);\n}\n\nexport async function getProduct(handle: string): Promise<Product | undefined> {\n "use cache";\n cacheTag(TAGS.products);\n cacheLife("days");\n\n if (!endpoint) {\n console.log(`Skipping getProduct for \'${handle}\' - Shopify not configured`);\n return undefined;\n }\n\n const res = await shopifyFetch<ShopifyProductOperation>({\n query: getProductQuery,\n variables: {\n handle,\n },\n });\n\n return reshapeProduct(res.body.data.product, false);\n}\n\nexport async function getProductRecommendations(\n productId: string\n): Promise<Product[]> {\n "use cache";\n cacheTag(TAGS.products);\n cacheLife("days");\n\n const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({\n query: getProductRecommendationsQuery,\n variables: {\n productId,\n },\n });\n\n return reshapeProducts(res.body.data.productRecommendations);\n}\n\nexport async function getProducts({\n query,\n reverse,\n sortKey,\n}: {\n query?: string;\n reverse?: boolean;\n sortKey?: string;\n}): Promise<Product[]> {\n "use cache";\n cacheTag(TAGS.products);\n cacheLife("days");\n\n const res = await shopifyFetch<ShopifyProductsOperation>({\n query: getProductsQuery,\n variables: {\n query,\n reverse,\n sortKey,\n },\n });\n\n return reshapeProducts(removeEdgesAndNodes(res.body.data.products));\n}\n\n// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.\nexport async function revalidate(req: NextRequest): Promise<NextResponse> {\n // We always need to respond with a 200 status code to Shopify,\n // otherwise it will continue to retry the request.\n const collectionWebhooks = [\n "collections/create",\n "collections/delete",\n "collections/update",\n ];\n const productWebhooks = [\n "products/create",\n "products/delete",\n "products/update",\n ];\n const topic = (await headers()).get("x-shopify-topic") || "unknown";\n const secret = req.nextUrl.searchParams.get("secret");\n const isCollectionUpdate = collectionWebhooks.includes(topic);\n const isProductUpdate = productWebhooks.includes(topic);\n\n if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) {\n console.error("Invalid revalidation secret.");\n return NextResponse.json({ status: 401 });\n }\n\n if (!isCollectionUpdate && !isProductUpdate) {\n // We don\'t need to revalidate anything for any other topics.\n return NextResponse.json({ status: 200 });\n }\n\n if (isCollectionUpdate) {\n revalidateTag(TAGS.collections, "seconds");\n }\n\n if (isProductUpdate) {\n revalidateTag(TAGS.products, "seconds");\n }\n\n return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });\n}\n',
|
|
346679
|
+
"lib/shopify/mutations/cart.ts": 'import cartFragment from "../fragments/cart";\n\nexport const addToCartMutation = /* GraphQL */ `\n mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) {\n cartLinesAdd(cartId: $cartId, lines: $lines) {\n cart {\n ...cart\n }\n }\n }\n ${cartFragment}\n`;\n\nexport const createCartMutation = /* GraphQL */ `\n mutation createCart($lineItems: [CartLineInput!]) {\n cartCreate(input: { lines: $lineItems }) {\n cart {\n ...cart\n }\n }\n }\n ${cartFragment}\n`;\n\nexport const editCartItemsMutation = /* GraphQL */ `\n mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) {\n cartLinesUpdate(cartId: $cartId, lines: $lines) {\n cart {\n ...cart\n }\n }\n }\n ${cartFragment}\n`;\n\nexport const removeFromCartMutation = /* GraphQL */ `\n mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) {\n cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {\n cart {\n ...cart\n }\n }\n }\n ${cartFragment}\n`;\n',
|
|
346680
|
+
"lib/shopify/queries/cart.ts": 'import cartFragment from "../fragments/cart";\n\nexport const getCartQuery = /* GraphQL */ `\n query getCart($cartId: ID!) {\n cart(id: $cartId) {\n ...cart\n }\n }\n ${cartFragment}\n`;\n',
|
|
346681
|
+
"lib/shopify/queries/collection.ts": 'import productFragment from "../fragments/product";\nimport seoFragment from "../fragments/seo";\n\nconst collectionFragment = /* GraphQL */ `\n fragment collection on Collection {\n handle\n title\n description\n seo {\n ...seo\n }\n updatedAt\n }\n ${seoFragment}\n`;\n\nexport const getCollectionQuery = /* GraphQL */ `\n query getCollection($handle: String!) {\n collection(handle: $handle) {\n ...collection\n }\n }\n ${collectionFragment}\n`;\n\nexport const getCollectionsQuery = /* GraphQL */ `\n query getCollections {\n collections(first: 100, sortKey: TITLE) {\n edges {\n node {\n ...collection\n }\n }\n }\n }\n ${collectionFragment}\n`;\n\nexport const getCollectionProductsQuery = /* GraphQL */ `\n query getCollectionProducts(\n $handle: String!\n $sortKey: ProductCollectionSortKeys\n $reverse: Boolean\n ) {\n collection(handle: $handle) {\n products(sortKey: $sortKey, reverse: $reverse, first: 100) {\n edges {\n node {\n ...product\n }\n }\n }\n }\n }\n ${productFragment}\n`;\n',
|
|
346682
|
+
"lib/shopify/queries/menu.ts": "export const getMenuQuery = /* GraphQL */ `\n query getMenu($handle: String!) {\n menu(handle: $handle) {\n items {\n title\n url\n }\n }\n }\n`;\n",
|
|
346683
|
+
"lib/shopify/queries/page.ts": 'import seoFragment from "../fragments/seo";\n\nconst pageFragment = /* GraphQL */ `\n fragment page on Page {\n ... on Page {\n id\n title\n handle\n body\n bodySummary\n seo {\n ...seo\n }\n createdAt\n updatedAt\n }\n }\n ${seoFragment}\n`;\n\nexport const getPageQuery = /* GraphQL */ `\n query getPage($handle: String!) {\n pageByHandle(handle: $handle) {\n ...page\n }\n }\n ${pageFragment}\n`;\n\nexport const getPagesQuery = /* GraphQL */ `\n query getPages {\n pages(first: 100) {\n edges {\n node {\n ...page\n }\n }\n }\n }\n ${pageFragment}\n`;\n',
|
|
346684
|
+
"lib/shopify/queries/product.ts": 'import productFragment from "../fragments/product";\n\nexport const getProductQuery = /* GraphQL */ `\n query getProduct($handle: String!) {\n product(handle: $handle) {\n ...product\n }\n }\n ${productFragment}\n`;\n\nexport const getProductsQuery = /* GraphQL */ `\n query getProducts(\n $sortKey: ProductSortKeys\n $reverse: Boolean\n $query: String\n ) {\n products(sortKey: $sortKey, reverse: $reverse, query: $query, first: 100) {\n edges {\n node {\n ...product\n }\n }\n }\n }\n ${productFragment}\n`;\n\nexport const getProductRecommendationsQuery = /* GraphQL */ `\n query getProductRecommendations($productId: ID!) {\n productRecommendations(productId: $productId) {\n ...product\n }\n }\n ${productFragment}\n`;\n',
|
|
346685
|
+
"lib/shopify/types.ts": 'export type Maybe<T> = T | null;\n\nexport type Connection<T> = {\n edges: Array<Edge<T>>;\n};\n\nexport type Edge<T> = {\n node: T;\n};\n\nexport type Cart = Omit<ShopifyCart, "lines"> & {\n lines: CartItem[];\n};\n\nexport type CartProduct = {\n id: string;\n handle: string;\n title: string;\n featuredImage: Image;\n};\n\nexport type CartItem = {\n id: string | undefined;\n quantity: number;\n cost: {\n totalAmount: Money;\n };\n merchandise: {\n id: string;\n title: string;\n selectedOptions: {\n name: string;\n value: string;\n }[];\n product: CartProduct;\n };\n};\n\nexport type Collection = ShopifyCollection & {\n path: string;\n};\n\nexport type Image = {\n url: string;\n altText: string;\n width: number;\n height: number;\n};\n\nexport type Menu = {\n title: string;\n path: string;\n};\n\nexport type Money = {\n amount: string;\n currencyCode: string;\n};\n\nexport type Page = {\n id: string;\n title: string;\n handle: string;\n body: string;\n bodySummary: string;\n seo?: SEO;\n createdAt: string;\n updatedAt: string;\n};\n\nexport type Product = Omit<ShopifyProduct, "variants" | "images"> & {\n variants: ProductVariant[];\n images: Image[];\n};\n\nexport type ProductOption = {\n id: string;\n name: string;\n values: string[];\n};\n\nexport type ProductVariant = {\n id: string;\n title: string;\n availableForSale: boolean;\n selectedOptions: {\n name: string;\n value: string;\n }[];\n price: Money;\n};\n\nexport type SEO = {\n title: string;\n description: string;\n};\n\nexport type ShopifyCart = {\n id: string | undefined;\n checkoutUrl: string;\n cost: {\n subtotalAmount: Money;\n totalAmount: Money;\n totalTaxAmount: Money;\n };\n lines: Connection<CartItem>;\n totalQuantity: number;\n};\n\nexport type ShopifyCollection = {\n handle: string;\n title: string;\n description: string;\n seo: SEO;\n updatedAt: string;\n};\n\nexport type ShopifyProduct = {\n id: string;\n handle: string;\n availableForSale: boolean;\n title: string;\n description: string;\n descriptionHtml: string;\n options: ProductOption[];\n priceRange: {\n maxVariantPrice: Money;\n minVariantPrice: Money;\n };\n variants: Connection<ProductVariant>;\n featuredImage: Image;\n images: Connection<Image>;\n seo: SEO;\n tags: string[];\n updatedAt: string;\n};\n\nexport type ShopifyCartOperation = {\n data: {\n cart: ShopifyCart;\n };\n variables: {\n cartId: string;\n };\n};\n\nexport type ShopifyCreateCartOperation = {\n data: { cartCreate: { cart: ShopifyCart } };\n};\n\nexport type ShopifyAddToCartOperation = {\n data: {\n cartLinesAdd: {\n cart: ShopifyCart;\n };\n };\n variables: {\n cartId: string;\n lines: {\n merchandiseId: string;\n quantity: number;\n }[];\n };\n};\n\nexport type ShopifyRemoveFromCartOperation = {\n data: {\n cartLinesRemove: {\n cart: ShopifyCart;\n };\n };\n variables: {\n cartId: string;\n lineIds: string[];\n };\n};\n\nexport type ShopifyUpdateCartOperation = {\n data: {\n cartLinesUpdate: {\n cart: ShopifyCart;\n };\n };\n variables: {\n cartId: string;\n lines: {\n id: string;\n merchandiseId: string;\n quantity: number;\n }[];\n };\n};\n\nexport type ShopifyCollectionOperation = {\n data: {\n collection: ShopifyCollection;\n };\n variables: {\n handle: string;\n };\n};\n\nexport type ShopifyCollectionProductsOperation = {\n data: {\n collection: {\n products: Connection<ShopifyProduct>;\n };\n };\n variables: {\n handle: string;\n reverse?: boolean;\n sortKey?: string;\n };\n};\n\nexport type ShopifyCollectionsOperation = {\n data: {\n collections: Connection<ShopifyCollection>;\n };\n};\n\nexport type ShopifyMenuOperation = {\n data: {\n menu?: {\n items: {\n title: string;\n url: string;\n }[];\n };\n };\n variables: {\n handle: string;\n };\n};\n\nexport type ShopifyPageOperation = {\n data: { pageByHandle: Page };\n variables: { handle: string };\n};\n\nexport type ShopifyPagesOperation = {\n data: {\n pages: Connection<Page>;\n };\n};\n\nexport type ShopifyProductOperation = {\n data: { product: ShopifyProduct };\n variables: {\n handle: string;\n };\n};\n\nexport type ShopifyProductRecommendationsOperation = {\n data: {\n productRecommendations: ShopifyProduct[];\n };\n variables: {\n productId: string;\n };\n};\n\nexport type ShopifyProductsOperation = {\n data: {\n products: Connection<ShopifyProduct>;\n };\n variables: {\n query?: string;\n reverse?: boolean;\n sortKey?: string;\n };\n};\n',
|
|
346686
|
+
"lib/type-guards.ts": 'export interface ShopifyErrorLike {\n status: number;\n message: Error;\n cause?: Error;\n}\n\nexport const isObject = (\n object: unknown,\n): object is Record<string, unknown> => {\n return (\n typeof object === "object" && object !== null && !Array.isArray(object)\n );\n};\n\nexport const isShopifyError = (error: unknown): error is ShopifyErrorLike => {\n if (!isObject(error)) return false;\n\n if (error instanceof Error) return true;\n\n return findError(error);\n};\n\nfunction findError<T extends object>(error: T): boolean {\n if (Object.prototype.toString.call(error) === "[object Error]") {\n return true;\n }\n\n const prototype = Object.getPrototypeOf(error) as T | null;\n\n return prototype === null ? false : findError(prototype);\n}\n',
|
|
346687
|
+
"lib/utils.ts": 'import { ReadonlyURLSearchParams } from "next/navigation";\n\nexport const baseUrl = process.env.VERCEL_PROJECT_PRODUCTION_URL\n ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`\n : "http://localhost:3000";\n\nexport const createUrl = (\n pathname: string,\n params: URLSearchParams | ReadonlyURLSearchParams,\n) => {\n const paramsString = params.toString();\n const queryString = `${paramsString.length ? "?" : ""}${paramsString}`;\n\n return `${pathname}${queryString}`;\n};\n\nexport const ensureStartsWith = (stringToCheck: string, startsWith: string) =>\n stringToCheck.startsWith(startsWith)\n ? stringToCheck\n : `${startsWith}${stringToCheck}`;\n\nexport const validateEnvironmentVariables = () => {\n const requiredEnvironmentVariables = [\n "SHOPIFY_STORE_DOMAIN",\n "SHOPIFY_STOREFRONT_ACCESS_TOKEN",\n ];\n const missingEnvironmentVariables = [] as string[];\n\n requiredEnvironmentVariables.forEach((envVar) => {\n if (!process.env[envVar]) {\n missingEnvironmentVariables.push(envVar);\n }\n });\n\n if (missingEnvironmentVariables.length) {\n throw new Error(\n `The following environment variables are missing. Your site will not work without them. Read more: https://vercel.com/docs/integrations/shopify#configure-environment-variables\\n\\n${missingEnvironmentVariables.join(\n "\\n",\n )}\\n`,\n );\n }\n\n if (\n process.env.SHOPIFY_STORE_DOMAIN?.includes("[") ||\n process.env.SHOPIFY_STORE_DOMAIN?.includes("]")\n ) {\n throw new Error(\n "Your `SHOPIFY_STORE_DOMAIN` environment variable includes brackets (ie. `[` and / or `]`). Your site will not work with them there. Please remove them.",\n );\n }\n};\n',
|
|
346688
|
+
"license.md": 'The MIT License (MIT)\n\nCopyright (c) 2025 Vercel, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n',
|
|
346689
|
+
"next.config.ts": 'export default {\n experimental: {\n ppr: true,\n inlineCss: true,\n useCache: true,\n },\n images: {\n formats: ["image/avif", "image/webp"],\n remotePatterns: [\n {\n protocol: "https",\n hostname: "cdn.shopify.com",\n pathname: "/s/files/**",\n },\n ],\n },\n};\n',
|
|
346690
|
+
"package.json": `{
|
|
346691
|
+
"name": "popcraft-store",
|
|
346692
|
+
"private": true,
|
|
346693
|
+
"description": "A Next.js Commerce store drawn by its PopCraft design: Shopify's products, cart and checkout, rendered by the design's components.",
|
|
346694
|
+
"scripts": {
|
|
346695
|
+
"dev": "next dev --turbopack",
|
|
346696
|
+
"build": "next build",
|
|
346697
|
+
"start": "next start",
|
|
346698
|
+
"test": "tsc --noEmit"
|
|
346699
|
+
},
|
|
346700
|
+
"dependencies": {
|
|
346701
|
+
"@popcraft/runtime": "^0.15.0",
|
|
346702
|
+
"next": "15.6.0-canary.60",
|
|
346703
|
+
"react": "19.0.0",
|
|
346704
|
+
"react-dom": "19.0.0"
|
|
346705
|
+
},
|
|
346706
|
+
"devDependencies": {
|
|
346707
|
+
"@types/node": "22.13.10",
|
|
346708
|
+
"@types/react": "19.0.12",
|
|
346709
|
+
"@types/react-dom": "19.0.4",
|
|
346710
|
+
"typescript": "5.8.2"
|
|
346711
|
+
}
|
|
346712
|
+
}
|
|
346713
|
+
`,
|
|
346714
|
+
"tsconfig.json": '{\n "compilerOptions": {\n "target": "es2015",\n "lib": ["dom", "dom.iterable", "esnext"],\n "downlevelIteration": true,\n "allowJs": true,\n "skipLibCheck": true,\n "strict": true,\n "forceConsistentCasingInFileNames": true,\n "noEmit": true,\n "esModuleInterop": true,\n "module": "esnext",\n "moduleResolution": "bundler",\n "resolveJsonModule": true,\n "isolatedModules": true,\n "jsx": "react-jsx",\n "incremental": true,\n "baseUrl": ".",\n "noUncheckedIndexedAccess": true,\n "plugins": [\n {\n "name": "next"\n }\n ]\n },\n "include": [\n "next-env.d.ts",\n "**/*.ts",\n "**/*.tsx",\n ".next/types/**/*.ts",\n ".next/dev/types/**/*.ts"\n ],\n "exclude": ["node_modules"]\n}\n',
|
|
346715
|
+
"types/css.d.ts": '// Stylesheets imported for their effect (app/globals.css, components/popcraft/popcraft.css). Next.js declares these\n// itself once it has run (next-env.d.ts); this lets `npm test` typecheck a fresh checkout too.\ndeclare module "*.css";\n'
|
|
346716
|
+
};
|
|
346717
|
+
}
|
|
346718
|
+
});
|
|
346719
|
+
|
|
346720
|
+
// lib/io/web/project/next-commerce.ts
|
|
346721
|
+
var next_commerce_exports = {};
|
|
346722
|
+
__export(next_commerce_exports, {
|
|
346723
|
+
nextCommerceFiles: () => nextCommerceFiles
|
|
346724
|
+
});
|
|
346725
|
+
function valueOf(p2) {
|
|
346726
|
+
if (p2.action) return ACTION[p2.action];
|
|
346727
|
+
if (p2.about) return p2.schema === "commerce.product" ? "product" : p2.schema === "commerce.collection" ? "collection" : null;
|
|
346728
|
+
switch (p2.schema) {
|
|
346729
|
+
case "commerce.product":
|
|
346730
|
+
return "products";
|
|
346731
|
+
case "commerce.collection":
|
|
346732
|
+
return "categories";
|
|
346733
|
+
case "commerce.cartLine":
|
|
346734
|
+
return "frame.cart.lines";
|
|
346735
|
+
case "commerce.cart":
|
|
346736
|
+
return "frame.cart";
|
|
346737
|
+
case "commerce.menu":
|
|
346738
|
+
return "frame.menu";
|
|
346739
|
+
default:
|
|
346740
|
+
return null;
|
|
346741
|
+
}
|
|
346742
|
+
}
|
|
346743
|
+
function pageSource(plan2, kind) {
|
|
346744
|
+
const given = plan2.props.map((p2) => [p2.name, valueOf(p2)]).filter((x2) => !!x2[1]);
|
|
346745
|
+
const uses = (v) => given.some(([, e]) => e === v);
|
|
346746
|
+
const actions = [...new Set(plan2.props.filter((p2) => p2.action).map((p2) => ACTION[p2.action]))].sort();
|
|
346747
|
+
const jsx = `<${plan2.name} ${given.map(([n, e]) => `${n}={${e}}`).join(" ")} />`;
|
|
346748
|
+
const productsFrom = {
|
|
346749
|
+
product: "getProductRecommendations(product.id)",
|
|
346750
|
+
collection: "getCollectionProducts({ collection: handle, sortKey, reverse })",
|
|
346751
|
+
search: "getProducts({ sortKey, reverse, query })",
|
|
346752
|
+
index: 'getProducts({ sortKey: "CREATED_AT", reverse: true })',
|
|
346753
|
+
cart: 'getProducts({ sortKey: "CREATED_AT", reverse: true })',
|
|
346754
|
+
page: 'getProducts({ sortKey: "CREATED_AT", reverse: true })'
|
|
346755
|
+
};
|
|
346756
|
+
const fetches = [
|
|
346757
|
+
...kind === "collection" ? [["collection", "getCollection(handle)"]] : [],
|
|
346758
|
+
...uses("products") ? [["products", productsFrom[kind]]] : [],
|
|
346759
|
+
...uses("categories") ? [["categories", "storeCategories()"]] : [],
|
|
346760
|
+
["frame", "storeFrame()"]
|
|
346761
|
+
];
|
|
346762
|
+
const shopify = [
|
|
346763
|
+
...kind === "product" ? ["getProduct"] : [],
|
|
346764
|
+
...kind === "collection" ? ["getCollection"] : [],
|
|
346765
|
+
...uses("products") ? [productsFrom[kind].slice(0, productsFrom[kind].indexOf("("))] : []
|
|
346766
|
+
];
|
|
346767
|
+
const sorted = (kind === "collection" || kind === "search") && uses("products");
|
|
346768
|
+
const head2 = [
|
|
346769
|
+
...actions.length ? [`import { ${actions.join(", ")} } from "app/actions";`] : [],
|
|
346770
|
+
`import { ${plan2.name} } from "components/popcraft";`,
|
|
346771
|
+
...sorted ? ['import { defaultSort, sorting } from "lib/constants";'] : [],
|
|
346772
|
+
`import { ${["storeFrame", ...uses("categories") ? ["storeCategories"] : []].sort().join(", ")} } from "lib/popcraft";`,
|
|
346773
|
+
...shopify.length ? [`import { ${[...new Set(shopify)].sort().join(", ")} } from "lib/shopify";`] : [],
|
|
346774
|
+
...kind === "product" || kind === "collection" ? ['import type { Metadata } from "next";', 'import { notFound } from "next/navigation";'] : [],
|
|
346775
|
+
""
|
|
346776
|
+
];
|
|
346777
|
+
const load2 = [
|
|
346778
|
+
` const [${fetches.map((f2) => f2[0]).join(", ")}] = await Promise.all([${fetches.map((f2) => f2[1]).join(", ")}]);`,
|
|
346779
|
+
...kind === "collection" ? [" if (!collection) return notFound();"] : [],
|
|
346780
|
+
` return ${jsx};`,
|
|
346781
|
+
"}",
|
|
346782
|
+
""
|
|
346783
|
+
];
|
|
346784
|
+
const sorting = sorted ? [" const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;"] : [];
|
|
346785
|
+
switch (kind) {
|
|
346786
|
+
case "product":
|
|
346787
|
+
return [
|
|
346788
|
+
...head2,
|
|
346789
|
+
"export async function generateMetadata(props: { params: Promise<{ handle: string }> }): Promise<Metadata> {",
|
|
346790
|
+
" const product = await getProduct((await props.params).handle);",
|
|
346791
|
+
" if (!product) return notFound();",
|
|
346792
|
+
" const { url, width, height, altText: alt } = product.featuredImage || {};",
|
|
346793
|
+
" return { title: product.seo.title || product.title, description: product.seo.description || product.description, openGraph: url ? { images: [{ url, width, height, alt }] } : null };",
|
|
346794
|
+
"}",
|
|
346795
|
+
"",
|
|
346796
|
+
"export default async function Product(props: { params: Promise<{ handle: string }> }) {",
|
|
346797
|
+
" const product = await getProduct((await props.params).handle);",
|
|
346798
|
+
" if (!product) return notFound();",
|
|
346799
|
+
...load2
|
|
346800
|
+
].join("\n");
|
|
346801
|
+
case "collection":
|
|
346802
|
+
return [
|
|
346803
|
+
...head2,
|
|
346804
|
+
"export async function generateMetadata(props: { params: Promise<{ collection: string }> }): Promise<Metadata> {",
|
|
346805
|
+
" const collection = await getCollection((await props.params).collection);",
|
|
346806
|
+
" if (!collection) return notFound();",
|
|
346807
|
+
" return { title: collection.seo?.title || collection.title, description: collection.seo?.description || collection.description || `${collection.title} products` };",
|
|
346808
|
+
"}",
|
|
346809
|
+
"",
|
|
346810
|
+
"export default async function Category(props: { params: Promise<{ collection: string }>; searchParams?: Promise<{ [key: string]: string | string[] | undefined }> }) {",
|
|
346811
|
+
" const { collection: handle } = await props.params;",
|
|
346812
|
+
...sorted ? [" const { sort } = ((await props.searchParams) ?? {}) as { [key: string]: string };"] : [],
|
|
346813
|
+
...sorting,
|
|
346814
|
+
...load2
|
|
346815
|
+
].join("\n");
|
|
346816
|
+
case "search":
|
|
346817
|
+
return [
|
|
346818
|
+
...head2,
|
|
346819
|
+
'export const metadata = { title: "Search", description: "Search for products in the store." };',
|
|
346820
|
+
"",
|
|
346821
|
+
"export default async function Search(props: { searchParams?: Promise<{ [key: string]: string | string[] | undefined }> }) {",
|
|
346822
|
+
...sorted ? [" const { sort, q: query } = ((await props.searchParams) ?? {}) as { [key: string]: string };"] : [" void props;"],
|
|
346823
|
+
...sorting,
|
|
346824
|
+
...load2
|
|
346825
|
+
].join("\n");
|
|
346826
|
+
case "cart":
|
|
346827
|
+
return [
|
|
346828
|
+
...head2,
|
|
346829
|
+
'export const metadata = { title: "Your cart" };',
|
|
346830
|
+
"",
|
|
346831
|
+
"export default async function Cart() {",
|
|
346832
|
+
...load2
|
|
346833
|
+
].join("\n");
|
|
346834
|
+
default:
|
|
346835
|
+
return [
|
|
346836
|
+
...head2,
|
|
346837
|
+
`export default async function ${kind === "index" ? "Home" : "Page"}() {`,
|
|
346838
|
+
...load2
|
|
346839
|
+
].join("\n");
|
|
346840
|
+
}
|
|
346841
|
+
}
|
|
346842
|
+
function nextCommerceFiles(doc2, screens, o = {}) {
|
|
346843
|
+
const routes = new Map(routesOf(screensOf(doc2, doc2.pages[0])));
|
|
346844
|
+
const pages2 = [];
|
|
346845
|
+
const taken = /* @__PURE__ */ new Set();
|
|
346846
|
+
componentPlan(doc2, screens).forEach((plan2, i2) => {
|
|
346847
|
+
const route2 = routes.get(plan2.screen.id) ?? (i2 === 0 ? "/" : void 0);
|
|
346848
|
+
const kind = storefrontPageKind(plan2.screen, doc2, route2);
|
|
346849
|
+
const path = kind === "page" ? route2 ? otherRoute(route2) : null : ROUTE[kind];
|
|
346850
|
+
if (!path || taken.has(path)) return;
|
|
346851
|
+
taken.add(path);
|
|
346852
|
+
pages2.push({ path, content: pageSource(plan2, kind) });
|
|
346853
|
+
});
|
|
346854
|
+
const name2 = o.siteName ?? doc2.name;
|
|
346855
|
+
const shell = Object.entries(NEXT_COMMERCE_SHELL).map(([path, content]) => {
|
|
346856
|
+
if (path === "package.json") {
|
|
346857
|
+
const pkg = JSON.parse(content);
|
|
346858
|
+
pkg.name = name2.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "store";
|
|
346859
|
+
pkg.description = `${name2}: a Next.js Commerce store drawn by its PopCraft design.`;
|
|
346860
|
+
return { path, content: `${JSON.stringify(pkg, null, 2)}
|
|
346861
|
+
` };
|
|
346862
|
+
}
|
|
346863
|
+
if (path === ".env.example") return { path, content: content.replace(/SITE_NAME=".*"/, `SITE_NAME=${JSON.stringify(name2)}`).replace(/COMPANY_NAME=".*"/, `COMPANY_NAME=${JSON.stringify(name2)}`) };
|
|
346864
|
+
return { path, content };
|
|
346865
|
+
});
|
|
346866
|
+
return [...shell, ...componentFiles(doc2, screens, { media: o.media, styling: o.styling }), ...pages2];
|
|
346867
|
+
}
|
|
346868
|
+
var ACTION, ROUTE, otherRoute;
|
|
346869
|
+
var init_next_commerce = __esm({
|
|
346870
|
+
"lib/io/web/project/next-commerce.ts"() {
|
|
346871
|
+
"use strict";
|
|
346872
|
+
init_routes();
|
|
346873
|
+
init_next_commerce_shell();
|
|
346874
|
+
init_components();
|
|
346875
|
+
init_storefront_pages();
|
|
346876
|
+
ACTION = { addToCart: "addToCart", updateCartLine: "updateCartLine", removeCartLine: "removeCartLine", checkout: "checkout", search: "search" };
|
|
346877
|
+
ROUTE = {
|
|
346878
|
+
index: "app/page.tsx",
|
|
346879
|
+
search: "app/search/page.tsx",
|
|
346880
|
+
collection: "app/search/[collection]/page.tsx",
|
|
346881
|
+
product: "app/product/[handle]/page.tsx",
|
|
346882
|
+
cart: "app/cart/page.tsx"
|
|
346883
|
+
};
|
|
346884
|
+
otherRoute = (route2) => {
|
|
346885
|
+
const path = route2.replace(/^\/+|\/+$/g, "");
|
|
346886
|
+
return path && /^[a-z0-9-]+(\/[a-z0-9-]+)*$/i.test(path) ? `app/${path}/page.tsx` : null;
|
|
346441
346887
|
};
|
|
346442
346888
|
}
|
|
346443
346889
|
});
|
|
@@ -346482,11 +346928,17 @@ var init_storefront = __esm({
|
|
|
346482
346928
|
"lib/io/web/project/storefront.ts"() {
|
|
346483
346929
|
init_components();
|
|
346484
346930
|
init_shopify_theme();
|
|
346931
|
+
init_next_commerce();
|
|
346485
346932
|
init_collections();
|
|
346486
346933
|
init_html();
|
|
346487
346934
|
init_storefront_targets();
|
|
346488
346935
|
init_storefront_targets();
|
|
346489
346936
|
STOREFRONT_TARGETS = {
|
|
346937
|
+
"next-commerce-store": {
|
|
346938
|
+
id: "next-commerce-store",
|
|
346939
|
+
...STOREFRONT_TARGET_INFO["next-commerce-store"],
|
|
346940
|
+
files: (doc2, screens, o) => nextCommerceFiles(doc2, screens, { media: o.media, styling: o.styling })
|
|
346941
|
+
},
|
|
346490
346942
|
"next-commerce": {
|
|
346491
346943
|
id: "next-commerce",
|
|
346492
346944
|
...STOREFRONT_TARGET_INFO["next-commerce"],
|
|
@@ -346656,6 +347108,24 @@ var init_board_export = __esm({
|
|
|
346656
347108
|
});
|
|
346657
347109
|
|
|
346658
347110
|
// lib/commands/export.ts
|
|
347111
|
+
var export_exports2 = {};
|
|
347112
|
+
__export(export_exports2, {
|
|
347113
|
+
EXPORT_NODE_CAP: () => EXPORT_NODE_CAP,
|
|
347114
|
+
exportBoard: () => exportBoard,
|
|
347115
|
+
exportCopy: () => exportCopy,
|
|
347116
|
+
exportGif: () => exportGif,
|
|
347117
|
+
exportLottie: () => exportLottie2,
|
|
347118
|
+
exportMp4: () => exportMp4,
|
|
347119
|
+
exportNodes: () => exportNodes,
|
|
347120
|
+
exportPages: () => exportPages,
|
|
347121
|
+
exportPdf: () => exportPdf,
|
|
347122
|
+
exportPngSequence: () => exportPngSequence,
|
|
347123
|
+
exportPptx: () => exportPptx,
|
|
347124
|
+
exportStorefront: () => exportStorefront,
|
|
347125
|
+
exportSvg: () => exportSvg,
|
|
347126
|
+
exportWeb: () => exportWeb,
|
|
347127
|
+
exportWebm: () => exportWebm
|
|
347128
|
+
});
|
|
346659
347129
|
function animationTarget(args2, ctx2, doc2) {
|
|
346660
347130
|
if (args2.composition) {
|
|
346661
347131
|
if (!doc2.compositions?.[args2.composition]) throw new ToolError(`There is no composition "${args2.composition}".`);
|
|
@@ -347177,7 +347647,7 @@ var init_export2 = __esm({
|
|
|
347177
347647
|
permission: "export",
|
|
347178
347648
|
needs: ["canvas"],
|
|
347179
347649
|
params: external_exports.object({
|
|
347180
|
-
target: external_exports.enum(STOREFRONT_TARGET_IDS).default("next-commerce").describe("next-commerce: React components a Next.js Commerce store renders with its own data and server actions. shopify-theme: an Online Store 2.0 theme (Liquid) to upload in the store's admin, running on Shopify with its own cart and checkout."),
|
|
347650
|
+
target: external_exports.enum(STOREFRONT_TARGET_IDS).default("next-commerce").describe("next-commerce-store: a whole Next.js Commerce store drawn by the design, to deploy with a Shopify store's domain and Storefront token (publish_store deploys it for you). next-commerce: React components a Next.js Commerce store renders with its own data and server actions. shopify-theme: an Online Store 2.0 theme (Liquid) to upload in the store's admin, running on Shopify with its own cart and checkout."),
|
|
347181
347651
|
styling: external_exports.enum(["inline", "tailwind"]).optional().describe("next-commerce: style objects (default) or Tailwind CSS v4 classes."),
|
|
347182
347652
|
dir: external_exports.string().max(200).optional().describe("next-commerce: where the components go in the store's project (default components/popcraft).")
|
|
347183
347653
|
}).strict(),
|
|
@@ -347186,34 +347656,26 @@ var init_export2 = __esm({
|
|
|
347186
347656
|
label: "Export storefront\u2026",
|
|
347187
347657
|
keywords: ["commerce", "shopify", "theme", "liquid", "store", "shop", "next", "react", "components", "export"],
|
|
347188
347658
|
variants: [
|
|
347659
|
+
{ id: "next-commerce-store", label: "Export storefront as a Next.js Commerce store", params: { target: "next-commerce-store" } },
|
|
347189
347660
|
{ id: "next-commerce", label: "Export storefront as Next.js Commerce components", params: { target: "next-commerce" } },
|
|
347190
347661
|
{ id: "shopify-theme", label: "Export storefront as a Shopify theme", params: { target: "shopify-theme" } }
|
|
347191
347662
|
]
|
|
347192
347663
|
},
|
|
347193
|
-
agent: { name: "export_storefront", readOnly: false, description: "Download a storefront design (a page whose lists and pages read collections with a commerce schema) as a store's project, in a zip. target next-commerce: a React component per screen, each list a typed prop of Next.js Commerce's own types, each storefront form (addToCart, updateCartLine, removeCartLine, checkout, search) an action prop for a server action. target shopify-theme: an Online Store 2.0 theme, each screen a section and template (home: index, a page about a product: product, about a category: collection, search, cart), each list a Liquid loop over the store's products, cart or menu, each form posting to Shopify's cart; uploaded in Online Store \u203A Themes. With no store data a Next.js Commerce component draws the design's samples." }
|
|
347664
|
+
agent: { name: "export_storefront", readOnly: false, description: "Download a storefront design (a page whose lists and pages read collections with a commerce schema) as a store's project, in a zip. target next-commerce-store: the whole Next.js Commerce app (Shopify client, cart, checkout, a page per screen), ready to deploy. target next-commerce: a React component per screen, each list a typed prop of Next.js Commerce's own types, each storefront form (addToCart, updateCartLine, removeCartLine, checkout, search) an action prop for a server action. target shopify-theme: an Online Store 2.0 theme, each screen a section and template (home: index, a page about a product: product, about a category: collection, search, cart), each list a Liquid loop over the store's products, cart or menu, each form posting to Shopify's cart; uploaded in Online Store \u203A Themes. With no store data a Next.js Commerce component draws the design's samples." }
|
|
347194
347665
|
},
|
|
347195
347666
|
run: async (args2, ctx2) => {
|
|
347196
347667
|
const doc2 = ctx2.doc();
|
|
347197
|
-
const [{
|
|
347198
|
-
Promise.resolve().then(() => (
|
|
347668
|
+
const [{ storefrontInputs: storefrontInputs2 }, { STOREFRONT_TARGETS: STOREFRONT_TARGETS2 }, { bytesOf: bytesOf2 }, { fileBase: fileBase2, flatten: flatten3 }, { zipFiles: zipFiles2 }, { downloadBlob: downloadBlob2 }] = await Promise.all([
|
|
347669
|
+
Promise.resolve().then(() => (init_storefront_prepare(), storefront_prepare_exports)),
|
|
347199
347670
|
Promise.resolve().then(() => (init_storefront(), storefront_exports)),
|
|
347200
347671
|
Promise.resolve().then(() => (init_project(), project_exports)),
|
|
347201
347672
|
Promise.resolve().then(() => (init_web(), web_exports)),
|
|
347202
|
-
Promise.resolve().then(() => (init_resolvers(), resolvers_exports)),
|
|
347203
347673
|
Promise.resolve().then(() => (init_zip_export(), zip_export_exports)),
|
|
347204
|
-
Promise.resolve().then(() => (init_download(), download_exports))
|
|
347205
|
-
Promise.resolve().then(() => (init_media_store(), media_store_exports))
|
|
347674
|
+
Promise.resolve().then(() => (init_download(), download_exports))
|
|
347206
347675
|
]);
|
|
347207
347676
|
const target2 = STOREFRONT_TARGETS2[args2.target];
|
|
347208
|
-
const screens =
|
|
347677
|
+
const { screens, media: media2 } = await storefrontInputs2(doc2, ctx2.pageId(), EXPORT_NODE_CAP);
|
|
347209
347678
|
if (!screens.length) throw new ToolError("This page has no screens: a top-level frame is a page of the store.");
|
|
347210
|
-
const resolvers = rendererAssetResolvers2(doc2);
|
|
347211
|
-
for (const s of screens) if (flatten3(s.tree.root).some((n) => n.asset)) await resolveWebAssets2(s.tree, resolvers);
|
|
347212
|
-
const media2 = {};
|
|
347213
|
-
await Promise.all(componentMedia2(doc2, screens).map(async (ref3) => {
|
|
347214
|
-
const blob = await loadMediaBlob2(ref3).catch(() => null);
|
|
347215
|
-
if (blob) media2[ref3] = { bytes: new Uint8Array(await blob.arrayBuffer()), type: blob.type };
|
|
347216
|
-
}));
|
|
347217
347679
|
const files = args2.target === "next-commerce" && args2.dir ? (await Promise.resolve().then(() => (init_components(), components_exports))).componentFiles(doc2, screens, { styling: args2.styling, dir: args2.dir, media: media2 }) : target2.files(doc2, screens, { media: media2, styling: args2.styling });
|
|
347218
347680
|
const base = `${fileBase2(doc2.name)}-${args2.target}`;
|
|
347219
347681
|
downloadBlob2(zipFiles2(files.map((f2) => ({ name: f2.path, bytes: bytesOf2(f2) }))), `${base}.zip`);
|
|
@@ -353017,6 +353479,7 @@ function publishSite(key5, files, opts = {}) {
|
|
|
353017
353479
|
var siteStatus, setSiteVersion, renameSite;
|
|
353018
353480
|
var init_sites = __esm({
|
|
353019
353481
|
"lib/client/cloud/sites.ts"() {
|
|
353482
|
+
"use strict";
|
|
353020
353483
|
init_files();
|
|
353021
353484
|
init_upload2();
|
|
353022
353485
|
init_api4();
|
|
@@ -353130,10 +353593,12 @@ __export(integrations_exports, {
|
|
|
353130
353593
|
attachDatabase: () => attachDatabase,
|
|
353131
353594
|
connectWithOAuth: () => connectWithOAuth,
|
|
353132
353595
|
connectWithToken: () => connectWithToken,
|
|
353596
|
+
connectedStore: () => connectedStore,
|
|
353133
353597
|
createDatabase: () => createDatabase,
|
|
353134
353598
|
createDestination: () => createDestination,
|
|
353135
353599
|
createResource: () => createResource,
|
|
353136
353600
|
deployDestination: () => deployDestination,
|
|
353601
|
+
deployStore: () => deployStore,
|
|
353137
353602
|
detachDatabase: () => detachDatabase,
|
|
353138
353603
|
disconnect: () => disconnect,
|
|
353139
353604
|
integrationsWorkspaceId: () => integrationsWorkspaceId,
|
|
@@ -353178,6 +353643,32 @@ function connectWithOAuth(workspaceId, provider2) {
|
|
|
353178
353643
|
});
|
|
353179
353644
|
});
|
|
353180
353645
|
}
|
|
353646
|
+
async function sha12(bytes2) {
|
|
353647
|
+
const digest = await crypto.subtle.digest("SHA-1", bytes2);
|
|
353648
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
353649
|
+
}
|
|
353650
|
+
async function deployStore(key5, id3, files, shopifyConnectionId, onProgress) {
|
|
353651
|
+
const listed = await Promise.all(files.map(async (f2) => ({ path: f2.path, sha: await sha12(f2.bytes), size: f2.bytes.length, bytes: f2.bytes })));
|
|
353652
|
+
const queue2 = [...new Map(listed.map((f2) => [f2.sha, f2])).values()];
|
|
353653
|
+
const total2 = queue2.length;
|
|
353654
|
+
let done = 0;
|
|
353655
|
+
await Promise.all(Array.from({ length: Math.min(4, queue2.length) }, async () => {
|
|
353656
|
+
for (let f2 = queue2.shift(); f2; f2 = queue2.shift()) {
|
|
353657
|
+
await api(`/api/sites/${key5}/destinations/${id3}/files`, { method: "POST", body: f2.bytes, headers: { "content-type": "application/octet-stream", "x-file-sha1": f2.sha } });
|
|
353658
|
+
onProgress?.(++done, total2);
|
|
353659
|
+
}
|
|
353660
|
+
}));
|
|
353661
|
+
return (await api(`/api/sites/${key5}/destinations/${id3}/store`, { method: "POST", json: { shopifyConnectionId, files: listed.map(({ path, sha, size: size6 }) => ({ path, sha, size: size6 })) } })).deploy;
|
|
353662
|
+
}
|
|
353663
|
+
async function connectedStore(connectionId) {
|
|
353664
|
+
if (connectionId) return connectionId;
|
|
353665
|
+
const workspaceId = integrationsWorkspaceId();
|
|
353666
|
+
if (!workspaceId) throw new Error("Your workspaces have not loaded yet. Try again in a moment.");
|
|
353667
|
+
const stores2 = (await listWorkspaceIntegrations(workspaceId)).integrations.filter((i2) => i2.manifest.capabilities.includes("commerce")).flatMap((i2) => i2.connections);
|
|
353668
|
+
if (!stores2.length) throw new Error("No store is connected. Connect Shopify in Integrations first.");
|
|
353669
|
+
if (stores2.length > 1) throw new Error(`Name the store: ${stores2.map((c2) => `${c2.account} (${c2.id})`).join(", ")}.`);
|
|
353670
|
+
return stores2[0].id;
|
|
353671
|
+
}
|
|
353181
353672
|
var integrationsWorkspaceId, listWorkspaceIntegrations, connectWithToken, disconnect, listResources, createResource, listDestinations, addDestination, removeDestination, deployDestination, siteBackend, attachDatabase, setSignIn, detachDatabase, listSubmissions, listCandidates, createDatabase, createDestination, storeProducts;
|
|
353182
353673
|
var init_integrations = __esm({
|
|
353183
353674
|
"lib/client/cloud/integrations.ts"() {
|
|
@@ -353217,7 +353708,7 @@ async function publishedFile() {
|
|
|
353217
353708
|
if (!key5) throw new CommandError("This file is not saved to the cloud yet, so it has no site. Publish it first.", "failed");
|
|
353218
353709
|
return key5;
|
|
353219
353710
|
}
|
|
353220
|
-
var statusData, publishSite2, publishSetVersion, publishStatus, publishCommands;
|
|
353711
|
+
var statusData, publishSite2, publishSetVersion, publishStatus, publishStore, publishCommands;
|
|
353221
353712
|
var init_publish = __esm({
|
|
353222
353713
|
"lib/commands/publish.ts"() {
|
|
353223
353714
|
"use strict";
|
|
@@ -353322,7 +353813,66 @@ var init_publish = __esm({
|
|
|
353322
353813
|
};
|
|
353323
353814
|
}
|
|
353324
353815
|
});
|
|
353325
|
-
|
|
353816
|
+
publishStore = defineCommand({
|
|
353817
|
+
id: "publish.store",
|
|
353818
|
+
title: "Publish as a store",
|
|
353819
|
+
domain: "publish",
|
|
353820
|
+
effect: "none",
|
|
353821
|
+
permission: null,
|
|
353822
|
+
needs: ["canvas"],
|
|
353823
|
+
params: external_exports.object({
|
|
353824
|
+
shopifyConnectionId: external_exports.string().optional().describe("The Shopify store it sells from, from list_integrations (capability commerce). Omit when one store is connected."),
|
|
353825
|
+
destinationId: external_exports.string().optional().describe("The Vercel project to deploy to (a deploy destination, from get_site_status). Omit: the file's deploy destination, or a new project named after the file.")
|
|
353826
|
+
}),
|
|
353827
|
+
confirm: {
|
|
353828
|
+
level: "confirm",
|
|
353829
|
+
summary: () => "Deploy this page as a store selling your Shopify products",
|
|
353830
|
+
detail: () => "The page becomes a Next.js Commerce store on Vercel, its products, cart and checkout your Shopify store's. A Vercel project is made for it when the file has none, and your store's address and Storefront token are set as that project's environment. Visitors can buy from it once it is live."
|
|
353831
|
+
},
|
|
353832
|
+
expose: {
|
|
353833
|
+
ui: { label: "Publish as a store\u2026", keywords: ["store", "shop", "shopify", "commerce", "next", "vercel", "deploy", "sell", "ecommerce", "checkout", "cart"] },
|
|
353834
|
+
agent: {
|
|
353835
|
+
name: "publish_store",
|
|
353836
|
+
description: "Deploy a storefront design (its lists and pages bound to collections with a commerce schema) as a working store: a Next.js Commerce app on Vercel selling the connected Shopify store's products, with its cart and checkout. Needs a Shopify connection (capability commerce) and a Vercel connection; uses the file's Vercel deploy destination or makes one. Returns the deployment URL (it builds for a minute or two). The user approves every call."
|
|
353837
|
+
}
|
|
353838
|
+
},
|
|
353839
|
+
run: async (p2, ctx2) => {
|
|
353840
|
+
await signedIn();
|
|
353841
|
+
const [{ openFileKey: openFileKey2 }, { uploadActiveDocument: uploadActiveDocument2 }, cloud, { storefrontInputs: storefrontInputs2 }, { nextCommerceFiles: nextCommerceFiles2 }, { bytesOf: bytesOf2 }, { EXPORT_NODE_CAP: EXPORT_NODE_CAP2 }] = await Promise.all([
|
|
353842
|
+
Promise.resolve().then(() => (init_publish_site(), publish_site_exports)),
|
|
353843
|
+
Promise.resolve().then(() => (init_sync2(), sync_exports2)),
|
|
353844
|
+
Promise.resolve().then(() => (init_integrations(), integrations_exports)),
|
|
353845
|
+
Promise.resolve().then(() => (init_storefront_prepare(), storefront_prepare_exports)),
|
|
353846
|
+
Promise.resolve().then(() => (init_next_commerce(), next_commerce_exports)),
|
|
353847
|
+
Promise.resolve().then(() => (init_project(), project_exports)),
|
|
353848
|
+
Promise.resolve().then(() => (init_export2(), export_exports2))
|
|
353849
|
+
]);
|
|
353850
|
+
const doc2 = ctx2.doc();
|
|
353851
|
+
if (!Object.values(doc2.collections ?? {}).some((c2) => c2.schema === "commerce.product")) throw new CommandError("This file is not a storefront: set a collection to Store products in Collections, or start from a Storefronts template.", "failed");
|
|
353852
|
+
const shop = await cloud.connectedStore(p2.shopifyConnectionId).catch((e) => {
|
|
353853
|
+
throw new CommandError(e instanceof Error ? e.message : String(e), "failed");
|
|
353854
|
+
});
|
|
353855
|
+
const { screens, media: media2 } = await storefrontInputs2(doc2, ctx2.pageId(), EXPORT_NODE_CAP2);
|
|
353856
|
+
if (!screens.length) throw new CommandError("This page has no screens: a top-level frame is a page of the store.", "failed");
|
|
353857
|
+
const key5 = openFileKey2() ?? await uploadActiveDocument2();
|
|
353858
|
+
const all4 = await cloud.listDestinations(key5);
|
|
353859
|
+
const deploys = all4.filter((d) => d.capability === "deploy");
|
|
353860
|
+
let dest = p2.destinationId ? all4.find((d) => d.id === p2.destinationId) : deploys[0];
|
|
353861
|
+
if (p2.destinationId && !dest) throw new CommandError(`This file has no destination ${p2.destinationId}.`, "invalid_params");
|
|
353862
|
+
if (!dest) {
|
|
353863
|
+
ctx2.notice("Making a Vercel project for the store\u2026");
|
|
353864
|
+
dest = await cloud.createDestination(key5, "deploy", "vercel");
|
|
353865
|
+
}
|
|
353866
|
+
const files = nextCommerceFiles2(doc2, screens, { media: media2 }).map((f2) => ({ path: f2.path, bytes: bytesOf2(f2) }));
|
|
353867
|
+
const where = dest.target.map((t3) => t3.name).join(" / ");
|
|
353868
|
+
const done = await cloud.deployStore(key5, dest.id, files, shop, (n, total2) => {
|
|
353869
|
+
if (n === total2) ctx2.notice(`Sent ${total2} files to ${where}: building the store`);
|
|
353870
|
+
});
|
|
353871
|
+
ctx2.notice(done.status === "failed" ? `The store did not deploy: ${done.error ?? "Vercel refused it"}` : `Store deploying: ${done.url}`);
|
|
353872
|
+
return { summary: `Store ${done.status === "ready" ? "is live" : done.status === "building" ? "is building" : "failed"} at ${done.url} (${where}, ${screens.length} ${screens.length === 1 ? "page" : "pages"}, ${files.length} files)`, data: { url: done.url, status: done.status, destinationId: dest.id, project: where, files: files.length, pages: screens.map((s) => s.name) } };
|
|
353873
|
+
}
|
|
353874
|
+
});
|
|
353875
|
+
publishCommands = [publishSite2, publishSetVersion, publishStore];
|
|
353326
353876
|
}
|
|
353327
353877
|
});
|
|
353328
353878
|
|
|
@@ -355724,7 +356274,7 @@ var init_storefront2 = __esm({
|
|
|
355724
356274
|
storefront = {
|
|
355725
356275
|
id: "storefront",
|
|
355726
356276
|
name: "Design a storefront",
|
|
355727
|
-
description: "Bind a shop to a store's data and export it for Next.js Commerce or Shopify.",
|
|
356277
|
+
description: "Bind a shop to a store's data and publish or export it for Next.js Commerce or Shopify.",
|
|
355728
356278
|
whenToUse: "the design should sell",
|
|
355729
356279
|
tools: ["get_document_outline", "edit_collection", "edit_records", "set_repeat", "set_route", "set_control", "export_storefront", "list_integrations", "open_integrations", "pull_store_products", "ask_user", "manage_todos"],
|
|
355730
356280
|
instructions: `# Design a storefront
|
|
@@ -355755,12 +356305,13 @@ are whole stores to begin from.
|
|
|
355755
356305
|
- \`removeCartLine\` or \`updateCartLine\` inside a cart line: \`merchandiseId\` is \`{{item.merchandise_id}}\`
|
|
355756
356306
|
- \`checkout\`
|
|
355757
356307
|
- \`search\`: a field named \`q\`
|
|
355758
|
-
6. **
|
|
355759
|
-
|
|
355760
|
-
|
|
355761
|
-
|
|
355762
|
-
- \`target: '
|
|
355763
|
-
on
|
|
356308
|
+
6. **Sell.** \`publish_store\` deploys it as a working store: a Next.js Commerce app on Vercel selling the connected
|
|
356309
|
+
Shopify store's products, with its cart and checkout (needs Shopify and Vercel connected: \`list_integrations\`).
|
|
356310
|
+
Or \`export_storefront\` writes the project for a store the user runs:
|
|
356311
|
+
- \`target: 'next-commerce-store'\`: the whole Next.js Commerce store, to deploy with Shopify's keys.
|
|
356312
|
+
- \`target: 'next-commerce'\`: components only, for a Next.js Commerce store that exists: a prop per list
|
|
356313
|
+
(\`products\`, \`cartLines\`, \`cart\`, \`menu\`), \`product\` on a product's page, \`onAddToCart\`, \`onCheckout\`\u2026 for the forms.
|
|
356314
|
+
- \`target: 'shopify-theme'\`: an Online Store 2.0 theme the user uploads in Shopify (Online Store \u203A Themes).
|
|
355764
356315
|
Say which, and where it goes (/docs/exporting/commerce).
|
|
355765
356316
|
|
|
355766
356317
|
## Rules
|
|
@@ -389159,7 +389710,7 @@ var init_integration = __esm({
|
|
|
389159
389710
|
}
|
|
389160
389711
|
},
|
|
389161
389712
|
run: async (args2, ctx2) => {
|
|
389162
|
-
const [{ cloudUser: cloudUser2 }, {
|
|
389713
|
+
const [{ cloudUser: cloudUser2 }, { connectedStore: connectedStore2, storeProducts: storeProducts2 }, { commerceItem: commerceItem2, COMMERCE_FIELDS: COMMERCE_FIELDS2 }, { fieldKey: fieldKey2, removeRecords: removeRecords2, upsertRecords: upsertRecords2 }] = await Promise.all([
|
|
389163
389714
|
Promise.resolve().then(() => (init_session(), session_exports)),
|
|
389164
389715
|
Promise.resolve().then(() => (init_integrations(), integrations_exports)),
|
|
389165
389716
|
Promise.resolve().then(() => (init_commerce(), commerce_exports)),
|
|
@@ -389171,15 +389722,9 @@ var init_integration = __esm({
|
|
|
389171
389722
|
if (!pick5.length) throw new CommandError(args2.collection ? `No store products collection "${args2.collection}".` : "This file has no collection that stands for store products: set one to Store products in Collections first.", "invalid_params");
|
|
389172
389723
|
if (pick5.length > 1) throw new CommandError(`Name the collection: ${pick5.map((c2) => c2.name).join(", ")}.`, "invalid_params");
|
|
389173
389724
|
const collection = pick5[0];
|
|
389174
|
-
|
|
389175
|
-
|
|
389176
|
-
|
|
389177
|
-
if (!workspaceId) throw new CommandError("Your workspaces have not loaded yet. Try again in a moment.", "failed");
|
|
389178
|
-
const stores2 = (await listWorkspaceIntegrations2(workspaceId)).integrations.filter((i2) => i2.manifest.capabilities.includes("commerce")).flatMap((i2) => i2.connections);
|
|
389179
|
-
if (!stores2.length) throw new CommandError("No store is connected. Connect Shopify in Integrations first.", "failed");
|
|
389180
|
-
if (stores2.length > 1) throw new CommandError(`Name the store: ${stores2.map((c2) => `${c2.account} (${c2.id})`).join(", ")}.`, "invalid_params");
|
|
389181
|
-
connectionId = stores2[0].id;
|
|
389182
|
-
}
|
|
389725
|
+
const connectionId = await connectedStore2(args2.connectionId).catch((e) => {
|
|
389726
|
+
throw new CommandError(e instanceof Error ? e.message : String(e), "failed");
|
|
389727
|
+
});
|
|
389183
389728
|
const products = await storeProducts2(connectionId, args2.limit);
|
|
389184
389729
|
if (!products.length) throw new CommandError("The store has no products to show.", "failed");
|
|
389185
389730
|
const keys4 = new Map(COMMERCE_FIELDS2["commerce.product"].map((f2) => [fieldKey2(f2.name), f2.name]));
|
|
@@ -402280,7 +402825,7 @@ init_resources();
|
|
|
402280
402825
|
init_desktop();
|
|
402281
402826
|
init_hypery();
|
|
402282
402827
|
import { createInterface } from "node:readline";
|
|
402283
|
-
var VERSION2 = "0.
|
|
402828
|
+
var VERSION2 = "0.16.0";
|
|
402284
402829
|
var HELP = `popcraft ${VERSION2} \u2014 the PopCraft command line
|
|
402285
402830
|
|
|
402286
402831
|
Files (headless, on .popcraft files):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@popcraft/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "The PopCraft command line: edit .popcraft files headlessly, call the REST API, publish to the marketplace and manage the desktop app's local plugins, themes, fonts, skills and agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://popcraft.app/docs/api/cli",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"esbuild": "0.28.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@popcraft/kit": "0.
|
|
37
|
+
"@popcraft/kit": "0.16.0"
|
|
38
38
|
},
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node build.mjs",
|