@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.
Files changed (2) hide show
  1. package/dist/popcraft.mjs +629 -84
  2. 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.15.0"}`, ...opts.headers };
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.15.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((s, i2) => {
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 propName2 = (base2) => {
346132
- let p2 = base2;
346133
- for (let n = 2; props.some((x2) => x2.name === p2) || p2 === "item"; n++) p2 = `${base2}${n}`;
346134
- return p2;
346135
- };
346136
- if (s.about) {
346137
- const schema = s.about.schema;
346138
- const p2 = propName2(camel3(singular(COMMERCE_PROP[schema].name)));
346139
- const type = COMMERCE_PROP[schema].type;
346140
- types.add(type);
346141
- props.push({ name: p2, type: `${type}`, doc: `The ${singular(s.about.name).toLowerCase()} this page is about; none: the design's first sample.` });
346142
- samples.push(sampleConst(`SAMPLE_${p2.toUpperCase()}`, doc2, s.about, 1));
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/shopify-theme.ts
346239
- function kindOf(s, doc2, route2) {
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 = kindOf(s, doc2, route2 ?? (i2 === 0 ? "/" : void 0));
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, every2;
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
- every2 = (n, out = []) => {
346437
- out.push(n);
346438
- n.children.forEach((c2) => every2(c2, out));
346439
- n.repeat?.template.forEach((c2) => every2(c2, out));
346440
- return out;
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 [{ componentMedia: componentMedia2, componentScreens: componentScreens2 }, { STOREFRONT_TARGETS: STOREFRONT_TARGETS2 }, { bytesOf: bytesOf2 }, { fileBase: fileBase2, flatten: flatten3, resolveWebAssets: resolveWebAssets2 }, { rendererAssetResolvers: rendererAssetResolvers2 }, { zipFiles: zipFiles2 }, { downloadBlob: downloadBlob2 }, { loadMediaBlob: loadMediaBlob2 }] = await Promise.all([
347198
- Promise.resolve().then(() => (init_components(), components_exports)),
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 = componentScreens2(doc2, ctx2.pageId(), EXPORT_NODE_CAP);
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
- publishCommands = [publishSite2, publishSetVersion];
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. **Export.** \`export_storefront\` writes the store's project, for the store the user runs:
355759
- - \`target: 'next-commerce'\`: components, a prop per list (\`products\`, \`cartLines\`, \`cart\`, \`menu\`), \`product\` on a
355760
- product's page, and \`onAddToCart\`, \`onCheckout\`\u2026 for the forms; the store passes its \`lib/shopify\` data and its
355761
- server actions.
355762
- - \`target: 'shopify-theme'\`: an Online Store 2.0 theme the user uploads in Shopify (Online Store \u203A Themes), running
355763
- on Shopify with its own cart and checkout.
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 }, { integrationsWorkspaceId: integrationsWorkspaceId2, listWorkspaceIntegrations: listWorkspaceIntegrations2, storeProducts: storeProducts2 }, { commerceItem: commerceItem2, COMMERCE_FIELDS: COMMERCE_FIELDS2 }, { fieldKey: fieldKey2, removeRecords: removeRecords2, upsertRecords: upsertRecords2 }] = await Promise.all([
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
- let connectionId = args2.connectionId;
389175
- if (!connectionId) {
389176
- const workspaceId = integrationsWorkspaceId2();
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.15.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.15.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.15.0"
37
+ "@popcraft/kit": "0.16.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "node build.mjs",