@cimplify/cli 0.7.4 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-4PBCFGM3.mjs → add-L36PRZQO.mjs} +1 -1
- package/dist/{chunk-NTY7JESF.mjs → chunk-CIRUNSIM.mjs} +2 -2
- package/dist/{chunk-LRTPNNQG.mjs → chunk-Q4H7IMO5.mjs} +1 -1
- package/dist/{chunk-QOPMMTVI.mjs → chunk-Z4OBZRGQ.mjs} +10 -10
- package/dist/dispatcher.mjs +9 -9
- package/dist/{doctor-ZVYWEE53.mjs → doctor-FXZMYCL5.mjs} +2 -2
- package/dist/{explain-S6S33HPN.mjs → explain-AJEHO5D5.mjs} +5 -5
- package/dist/{introspect-FFB7TA2E.mjs → introspect-FK4XSDDL.mjs} +2 -2
- package/dist/{list-RLWFTEP4.mjs → list-T2QQBM2X.mjs} +1 -1
- package/dist/{update-WH6NDWIM.mjs → update-3SQQSO5C.mjs} +1 -1
- package/package.json +2 -2
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/components/signed-out-prompt.tsx +32 -0
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/package.json +1 -1
package/dist/dispatcher.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { TEMPLATES } from './chunk-
|
|
3
|
-
import { package_default } from './chunk-
|
|
2
|
+
import { TEMPLATES } from './chunk-Z4OBZRGQ.mjs';
|
|
3
|
+
import { package_default } from './chunk-CIRUNSIM.mjs';
|
|
4
4
|
|
|
5
5
|
// src/dispatcher.ts
|
|
6
6
|
var VERSION = package_default.version ?? "unknown";
|
|
@@ -138,16 +138,16 @@ var COMMANDS = {
|
|
|
138
138
|
logs: () => import('./logs-YNN2PQ24.mjs'),
|
|
139
139
|
status: () => import('./status-JSYXM5RT.mjs'),
|
|
140
140
|
dev: () => import('./dev-ONW2S77K.mjs'),
|
|
141
|
-
introspect: () => import('./introspect-
|
|
141
|
+
introspect: () => import('./introspect-FK4XSDDL.mjs'),
|
|
142
142
|
inspect: () => import('./inspect-CGYX4DDF.mjs'),
|
|
143
|
-
doctor: () => import('./doctor-
|
|
144
|
-
explain: () => import('./explain-
|
|
143
|
+
doctor: () => import('./doctor-FXZMYCL5.mjs'),
|
|
144
|
+
explain: () => import('./explain-AJEHO5D5.mjs'),
|
|
145
145
|
assets: () => import('./assets-74SK63TR.mjs'),
|
|
146
146
|
repo: () => import('./repo-KNQMSPVV.mjs'),
|
|
147
|
-
list: () => import('./list-
|
|
148
|
-
add: () => import('./add-
|
|
149
|
-
update: () => import('./update-
|
|
150
|
-
upgrade: () => import('./update-
|
|
147
|
+
list: () => import('./list-T2QQBM2X.mjs'),
|
|
148
|
+
add: () => import('./add-L36PRZQO.mjs'),
|
|
149
|
+
update: () => import('./update-3SQQSO5C.mjs'),
|
|
150
|
+
upgrade: () => import('./update-3SQQSO5C.mjs'),
|
|
151
151
|
"auth-step-up": () => import('./auth-step-up-BIUYQJP6.mjs')
|
|
152
152
|
};
|
|
153
153
|
var COMMAND_PREFIXES = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { gatherIntrospection } from './chunk-
|
|
2
|
+
import { gatherIntrospection } from './chunk-Q4H7IMO5.mjs';
|
|
3
3
|
import './chunk-K5464A3L.mjs';
|
|
4
4
|
import './chunk-DBZ3UOQ2.mjs';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-CIRUNSIM.mjs';
|
|
6
6
|
import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
|
|
7
7
|
import { ApiClient } from './chunk-MAOO6ZZ5.mjs';
|
|
8
8
|
import { readAuthOrNull } from './chunk-UBAI443T.mjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default } from './chunk-
|
|
2
|
+
import { package_default } from './chunk-CIRUNSIM.mjs';
|
|
3
3
|
import { parseArgs } from './chunk-C4M3DXKC.mjs';
|
|
4
4
|
import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
|
|
5
5
|
|
|
@@ -17,7 +17,7 @@ var TOPICS = [
|
|
|
17
17
|
"title": "Customizing a template",
|
|
18
18
|
"description": "When and how to eject SDK components into your project",
|
|
19
19
|
"source_url": "https://cimplify.dev/docs/templates/customizing",
|
|
20
|
-
"body": '## The customisation tiers\n\n \n \n\n| Tier | When | Cost |\n| --- | --- | --- |\n| Edit `brand.ts` | Copy, links, contact, hero, FAQ, policies | Free updates from the SDK forever |\n| Edit `globals.css @theme` | Palette, radius, fonts | Free updates from the SDK forever |\n| `classNames` overrides | Restyle a slot inside an SDK component | Free updates; no fork |\n| Eject a component | Restructure JSX, change behaviour, add merchant logic | You own the file; no auto-updates |\n| Extend `BrandSchema` | Industry-specific fields (lookbook, booking policy) | Type-checked, validated, schema co-evolves |\n\n \n\n \n\n## Edit content via `brand.ts`\n\n \n\nHardcoding a string in a page or component is the wrong move. Hoist it into the brand object first; everything that reads from `brand` stays consistent across pages, sitemap, and metadata.\n\n \n \n\n```tsx\n// \u274C wrong: hardcoded in a component\n<h1>Akua\'s Bakery</h1>\n\n// \u2705 right: sourced from the brand contract\n<h1>{brand.hero.title}</h1>\n```\n\n \n\n \n\n## Eject a component\n\n \n\nThe SDK ships ~67 ejectable components in its registry (cards, selectors, full pages, primitives). Eject when a `classNames` override can\'t reach the part you need to change, or when you\'re restructuring beyond what the component\'s props support.\n\n \n \n\n```bash\n# Browse the registry\ncimplify list\n\n# Eject one\ncimplify add cart-summary\n# \u2192 writes ./components/cart-summary.tsx, owned by you\n\n# Replace the SDK import in your page\n# import { CartSummary } from "@cimplify/sdk/react"\n# import { CartSummary } from "@/components/cart-summary"\n```\n\n \n \n\nOnce ejected, the file is yours. SDK upgrades won\'t touch it; you trade off auto-bugfixes for full control.\n\n \n \n\n**Don\'t reinvent the customizer.** Variant selection, add-on math, bundle pricing, and composite mode-switching took many iterations to get right. Eject `variant-selector`, `composite-selector`, `bundle-selector`, or `add-on-selector` and re-style; don\'t rewrite the cart payload contract. See `AGENTS.md` for the full doctrine.\n\n \n\n \n\n## Extend the brand schema\n\n \n\nIndustry-specific fields belong on the schema, not in scattered component props. Use `BrandSchema.extend` so your additions get the same boot-time validation as the canonical fields.\n\n \n \n\n```ts title="lib/schema.ts (services template)"\n\nexport const ServicesBrandSchema = BrandSchema.extend({\n bookingPolicy: z.object({\n cancellationWindowHours: z.number().int().positive(),\n depositPercent: z.number().int().min(0).max(100),\n rescheduleNoticeHours: z.number().int().positive(),\n }),\n});\n\nexport type ServicesBrand = z.infer<typeof ServicesBrandSchema>;\n```\n\n \n \n \n\n```ts title="lib/brand.ts"\n\nexport const brand: ServicesBrand = {\n // \u2026all the standard Brand fields\u2026\n bookingPolicy: {\n cancellationWindowHours: 24,\n depositPercent: 25,\n rescheduleNoticeHours: 12,\n },\n};\n```\n\n \n \n \n\n```ts title="__tests__/brand.test.ts: assert against the extended schema"\n\ndescribe("brand", () => {\n it("conforms to the services brand contract", () => {\n expect(ServicesBrandSchema.safeParse(brand).success).toBe(true);\n });\n});\n```\n\n \n\n \n\n## Add a new section\n\n \n\n1. Build the section as a Server Component in `components/`.\n2. Read merchant-specific copy from `brand`; extend the schema if the field isn\'t there.\n3. Wrap any client interactivity in a `*-client.tsx` island behind `<Suspense>` to keep the chrome cached.\n4. Compose into the page (`app/page.tsx`, `app/products/[slug]/page.tsx`, \u2026).\n5. Run `bun run check`.\n\n \n\n## Wire a Server Action\n\n \n \n\n```ts\n"use server";\n\nexport async function cancelMyOrder(orderId: string) {\n const r = await getServerClient().orders.cancel(orderId, "customer requested");\n if (!r.ok) return { ok: false as const, message: r.error.message };\n\n await revalidateOrders();\n return { ok: true as const };\n}\n```\n\n \n\n \n\n## Don\'ts\n\n \n\n- Hardcode strings in pages or components.\n- Enable `cacheComponents: true` in `next.config.ts`.
|
|
20
|
+
"body": '## The customisation tiers\n\n \n \n\n| Tier | When | Cost |\n| --- | --- | --- |\n| Edit `brand.ts` | Copy, links, contact, hero, FAQ, policies | Free updates from the SDK forever |\n| Edit `globals.css @theme` | Palette, radius, fonts | Free updates from the SDK forever |\n| `classNames` overrides | Restyle a slot inside an SDK component | Free updates; no fork |\n| Eject a component | Restructure JSX, change behaviour, add merchant logic | You own the file; no auto-updates |\n| Extend `BrandSchema` | Industry-specific fields (lookbook, booking policy) | Type-checked, validated, schema co-evolves |\n\n \n\n \n\n## Edit content via `brand.ts`\n\n \n\nHardcoding a string in a page or component is the wrong move. Hoist it into the brand object first; everything that reads from `brand` stays consistent across pages, sitemap, and metadata.\n\n \n \n\n```tsx\n// \u274C wrong: hardcoded in a component\n<h1>Akua\'s Bakery</h1>\n\n// \u2705 right: sourced from the brand contract\n<h1>{brand.hero.title}</h1>\n```\n\n \n\n \n\n## Eject a component\n\n \n\nThe SDK ships ~67 ejectable components in its registry (cards, selectors, full pages, primitives). Eject when a `classNames` override can\'t reach the part you need to change, or when you\'re restructuring beyond what the component\'s props support.\n\n \n \n\n```bash\n# Browse the registry\ncimplify list\n\n# Eject one\ncimplify add cart-summary\n# \u2192 writes ./components/cart-summary.tsx, owned by you\n\n# Replace the SDK import in your page\n# import { CartSummary } from "@cimplify/sdk/react"\n# import { CartSummary } from "@/components/cart-summary"\n```\n\n \n \n\nOnce ejected, the file is yours. SDK upgrades won\'t touch it; you trade off auto-bugfixes for full control.\n\n \n \n\n**Don\'t reinvent the customizer.** Variant selection, add-on math, bundle pricing, and composite mode-switching took many iterations to get right. Eject `variant-selector`, `composite-selector`, `bundle-selector`, or `add-on-selector` and re-style; don\'t rewrite the cart payload contract. See `AGENTS.md` for the full doctrine.\n\n \n\n \n\n## Extend the brand schema\n\n \n\nIndustry-specific fields belong on the schema, not in scattered component props. Use `BrandSchema.extend` so your additions get the same boot-time validation as the canonical fields.\n\n \n \n\n```ts title="lib/schema.ts (services template)"\n\nexport const ServicesBrandSchema = BrandSchema.extend({\n bookingPolicy: z.object({\n cancellationWindowHours: z.number().int().positive(),\n depositPercent: z.number().int().min(0).max(100),\n rescheduleNoticeHours: z.number().int().positive(),\n }),\n});\n\nexport type ServicesBrand = z.infer<typeof ServicesBrandSchema>;\n```\n\n \n \n \n\n```ts title="lib/brand.ts"\n\nexport const brand: ServicesBrand = {\n // \u2026all the standard Brand fields\u2026\n bookingPolicy: {\n cancellationWindowHours: 24,\n depositPercent: 25,\n rescheduleNoticeHours: 12,\n },\n};\n```\n\n \n \n \n\n```ts title="__tests__/brand.test.ts: assert against the extended schema"\n\ndescribe("brand", () => {\n it("conforms to the services brand contract", () => {\n expect(ServicesBrandSchema.safeParse(brand).success).toBe(true);\n });\n});\n```\n\n \n\n \n\n## Add a new section\n\n \n\n1. Build the section as a Server Component in `components/`.\n2. Read merchant-specific copy from `brand`; extend the schema if the field isn\'t there.\n3. Wrap any client interactivity in a `*-client.tsx` island behind `<Suspense>` to keep the chrome cached.\n4. Compose into the page (`app/page.tsx`, `app/products/[slug]/page.tsx`, \u2026).\n5. Run `bun run check`.\n\n \n\n## Wire a Server Action\n\n \n \n\n```ts\n"use server";\n\nexport async function cancelMyOrder(orderId: string) {\n const r = await getServerClient().orders.cancel(orderId, "customer requested");\n if (!r.ok) return { ok: false as const, message: r.error.message };\n\n await revalidateOrders();\n return { ok: true as const };\n}\n```\n\n \n\n \n\n## Don\'ts\n\n \n\n- Hardcode strings in pages or components.\n- Enable `cacheComponents: true` in `next.config.ts`. Cache Components (the `\'use cache\'` directive) is still experimental and its runtime constraints don\'t suit hosted storefronts. Stay on the ISR Previous Model the templates ship with.\n- Use `\'use cache\'` / `cacheTag` / `cacheLife` directives. Same reason \u2014 they require Cache Components, which we don\'t enable. Use `export const revalidate` per page + `cacheOptions: { revalidate, tags }` on SDK reads.\n- Bypass `getServerClient()` by instantiating `createCimplifyClient` directly in a Server Component; you\'ll lose per-request memoisation.\n\n \n\n## Next\n\n \n \n- [**Brand schema**](/docs/templates/brand)\n Field reference and a full example\n\n \n- [**Testing**](/docs/testing)\n Catch ejections that broke the contract\n'
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"name": "products",
|
|
@@ -87,7 +87,7 @@ var TOPICS = [
|
|
|
87
87
|
"title": "Orders",
|
|
88
88
|
"description": "Order shape and status flow",
|
|
89
89
|
"source_url": "https://cimplify.dev/docs/sdk/orders",
|
|
90
|
-
"body": "## List orders\n\n \n \n\n```ts\nconst r = await client.orders.list({ limit: 20 })\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nconsole.log(r.value.length)\nfor (const order of r.value) {\n console.log(order.id, order.order_number, order.status)\n}\n```\n\n \n\n \n\n## Filter by status\n\n \n \n\n```ts\nconst pending = await client.orders.list({ status: 'pending' })\nconst completed = await client.orders.list({ status: 'completed', limit: 50 })\n\n// Convenience wrappers\nconst recent = await client.orders.getRecent(5) // last 5\nconst cancelled = await client.orders.getByStatus('cancelled')\n```\n\n \n\n \n\n## Get a single order\n\n \n\nFor guest orders the SDK appends the cached `bill_token` automatically; the lookup works in the same browser session that placed the order, with no auth.\n\n \n \n\n```ts\n\nconst r = await client.orders.get('ord_xxx')\nif (!r.ok) return\n\nconst order = r.value\nconsole.log(order.status)\nconsole.log(order.items.length)\nconsole.log(order.total_price) // Money string\nconsole.log(formatPrice(parsePrice(order.total_price), order.currency))\n```\n\n \n\n \n\n## Cancel an order\n\n \n \n\n```ts\nconst cancelled = await client.orders.cancel('ord_xxx', 'changed_mind')\nif (!cancelled.ok) {\n // Codes you'll see: ORDER_NOT_CANCELLABLE, ORDER_ALREADY_FULFILLED\n console.error(cancelled.error.code, cancelled.error.message)\n}\n\n// Idempotent retry: same key, same outcome on the server\nawait client.orders.cancel('ord_xxx', 'changed_mind', {\n idempotencyKey: 'cancel-ord_xxx-once',\n})\n```\n\n \n\n \n\n## Order status values\n\n \n \n\n| Status | Meaning |\n| --- | --- |\n| `pending` | Created, awaiting payment confirmation |\n| `confirmed` | Accepted by the business |\n| `preparing` | Being assembled or fulfilled |\n| `ready` | Ready for pickup or delivery dispatch |\n| `completed` | Fulfilled; terminal state |\n| `cancelled` | Cancelled; terminal state |\n| `refunded` | Payment refunded |\n\n \n\n \n\n## Poll until terminal\n\n \n \n\n```ts\nasync function pollUntilTerminal(orderId: string, intervalMs = 5000) {\n for (;;) {\n const r = await client.orders.get(orderId)\n if (!r.ok) return r\n\n const status = r.value.status\n if (status === 'completed' || status === 'cancelled' || status === 'refunded') {\n return r\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n}\n```\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `list({ status?, limit?, offset? })` | `Result<Order[]>` |\n| `get(orderId)` | `Result<Order>` |\n| `cancel(orderId, reason?, opts?)` | `Result<Order>` |\n| `getRecent(limit?)` | `Result<Order[]>` |\n| `getByStatus(status)` | `Result<Order[]>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Checkout**](/docs/sdk/checkout)\n Create the orders that this page lists\n\n \n- [**Subscriptions**](/docs/sdk/subscriptions)\n For recurring orders, see the subscription surface\n\n \n- [**Scheduling**](/docs/sdk/scheduling)\n Reschedule or cancel a booking line item\n\n \n- [**Error handling**](/docs/sdk/errors)\n Cancellation guards and retryable errors\n"
|
|
90
|
+
"body": "`client.orders.list()` returns different results depending on how the\nclient was constructed:\n\n- **Guest client** (`getServerClient()` with no `accessToken`, or the\n browser `cimplify` client with no signed-in shopper): returns nothing\n by default \u2014 the API has no customer to scope to. `client.orders.get()`\n still works on the same browser session that placed the order via the\n cached `bill_token`.\n- **Authenticated client** (`getAuthenticatedServerClient()` from the\n templates, or `getServerClient({ accessToken })` from a custom integration):\n returns *that customer's* orders at *this merchant*.\n\nSee [Sign in with Cimplify \u2192 Personalized server-side reads](/docs/sdk/auth#personalized-server-side-reads)\nfor the full setup.\n\n## Render orders for the signed-in shopper\n\n```tsx title=\"app/account/orders/page.tsx\"\n\nexport const revalidate = 0; // personalized; do not ISR-cache\n\nexport default async function OrdersPage() {\n const client = await getAuthenticatedServerClient();\n const r = await client.orders.list({ limit: 20 });\n if (!r.ok) throw new Error(r.error.message);\n\n if (r.value.length === 0) {\n return <p>No orders yet.</p>;\n }\n return (\n <ul>\n {r.value.map((o) => (\n <li key={o.id}>#{o.order_number} \u2014 {o.status} \u2014 {o.total_price}</li>\n ))}\n </ul>\n );\n}\n```\n\nIf `cimplify_access` is missing (signed-out shopper), `client.orders.list()`\nreturns the empty guest view. Branch on `getSession()` from `lib/auth.ts`\nif you want to redirect to sign-in instead.\n\n## List orders\n\n \n \n\n```ts\nconst r = await client.orders.list({ limit: 20 })\nif (!r.ok) {\n console.error(r.error.code, r.error.message)\n return\n}\n\nconsole.log(r.value.length)\nfor (const order of r.value) {\n console.log(order.id, order.order_number, order.status)\n}\n```\n\n \n\n \n\n## Filter by status\n\n \n \n\n```ts\nconst pending = await client.orders.list({ status: 'pending' })\nconst completed = await client.orders.list({ status: 'completed', limit: 50 })\n\n// Convenience wrappers\nconst recent = await client.orders.getRecent(5) // last 5\nconst cancelled = await client.orders.getByStatus('cancelled')\n```\n\n \n\n \n\n## Get a single order\n\n \n\nFor guest orders the SDK appends the cached `bill_token` automatically; the lookup works in the same browser session that placed the order, with no auth.\n\n \n \n\n```ts\n\nconst r = await client.orders.get('ord_xxx')\nif (!r.ok) return\n\nconst order = r.value\nconsole.log(order.status)\nconsole.log(order.items.length)\nconsole.log(order.total_price) // Money string\nconsole.log(formatPrice(parsePrice(order.total_price), order.currency))\n```\n\n \n\n \n\n## Cancel an order\n\n \n \n\n```ts\nconst cancelled = await client.orders.cancel('ord_xxx', 'changed_mind')\nif (!cancelled.ok) {\n // Codes you'll see: ORDER_NOT_CANCELLABLE, ORDER_ALREADY_FULFILLED\n console.error(cancelled.error.code, cancelled.error.message)\n}\n\n// Idempotent retry: same key, same outcome on the server\nawait client.orders.cancel('ord_xxx', 'changed_mind', {\n idempotencyKey: 'cancel-ord_xxx-once',\n})\n```\n\n \n\n \n\n## Order status values\n\n \n \n\n| Status | Meaning |\n| --- | --- |\n| `pending` | Created, awaiting payment confirmation |\n| `confirmed` | Accepted by the business |\n| `preparing` | Being assembled or fulfilled |\n| `ready` | Ready for pickup or delivery dispatch |\n| `completed` | Fulfilled; terminal state |\n| `cancelled` | Cancelled; terminal state |\n| `refunded` | Payment refunded |\n\n \n\n \n\n## Poll until terminal\n\n \n \n\n```ts\nasync function pollUntilTerminal(orderId: string, intervalMs = 5000) {\n for (;;) {\n const r = await client.orders.get(orderId)\n if (!r.ok) return r\n\n const status = r.value.status\n if (status === 'completed' || status === 'cancelled' || status === 'refunded') {\n return r\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs))\n }\n}\n```\n\n \n\n \n\n## Method reference\n\n \n \n\n| Method | Returns |\n| --- | --- |\n| `list({ status?, limit?, offset? })` | `Result<Order[]>` |\n| `get(orderId)` | `Result<Order>` |\n| `cancel(orderId, reason?, opts?)` | `Result<Order>` |\n| `getRecent(limit?)` | `Result<Order[]>` |\n| `getByStatus(status)` | `Result<Order[]>` |\n\n \n\n \n\n## Related\n\n \n \n- [**Checkout**](/docs/sdk/checkout)\n Create the orders that this page lists\n\n \n- [**Subscriptions**](/docs/sdk/subscriptions)\n For recurring orders, see the subscription surface\n\n \n- [**Scheduling**](/docs/sdk/scheduling)\n Reschedule or cancel a booking line item\n\n \n- [**Error handling**](/docs/sdk/errors)\n Cancellation guards and retryable errors\n"
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
"name": "subscriptions",
|
|
@@ -129,7 +129,7 @@ var TOPICS = [
|
|
|
129
129
|
"title": "Assets",
|
|
130
130
|
"description": "Storefront brand assets \u2014 upload to CDN, build URLs, render through next/image",
|
|
131
131
|
"source_url": "https://cimplify.dev/docs/sdk/assets",
|
|
132
|
-
"body": 'The asset surface on the SDK is **read-side only** \u2014 building URLs for images you\'ve already uploaded via [`cimplify assets upload`](/docs/cli/assets). Customer-runtime uploads still go through `client.uploads.upload(file)`.\n\n## `assetUrl(path, opts?)`\n\nPure URL builder. Prepends the CDN base for relative paths, passes absolute URLs through with the transform query contract appended.\n\n```ts\n\nassetUrl("hero/main.jpg")\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg"\n\nassetUrl("hero/main.jpg", { w: 1200, format: "webp", quality: 80 })\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg?w=1200&format=webp&quality=80"\n\nassetUrl("hero/main.jpg", { base: "http://localhost:8787/storage" })\n// "http://localhost:8787/storage/hero/main.jpg"\n\nassetUrl("https://res.cloudinary.com/foo/bar.jpg")\n// "https://res.cloudinary.com/foo/bar.jpg" (absolute \u2192 passthrough)\n```\n\n### Options\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `base` | `string` | Override the CDN base. Defaults to `DEFAULT_CDN_BASE_URL`. |\n| `w` / `h` | `number` | Width / height in pixels (
|
|
132
|
+
"body": 'The asset surface on the SDK is **read-side only** \u2014 building URLs for images you\'ve already uploaded via [`cimplify assets upload`](/docs/cli/assets). Customer-runtime uploads still go through `client.uploads.upload(file)`.\n\n## `assetUrl(path, opts?)`\n\nPure URL builder. Prepends the CDN base for relative paths, passes absolute URLs through with the transform query contract appended.\n\n```ts\n\nassetUrl("hero/main.jpg")\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg"\n\nassetUrl("hero/main.jpg", { w: 1200, format: "webp", quality: 80 })\n// "https://storefrontassetscdn.cimplify.io/hero/main.jpg?w=1200&format=webp&quality=80"\n\nassetUrl("hero/main.jpg", { base: "http://localhost:8787/storage" })\n// "http://localhost:8787/storage/hero/main.jpg"\n\nassetUrl("https://res.cloudinary.com/foo/bar.jpg")\n// "https://res.cloudinary.com/foo/bar.jpg" (absolute \u2192 passthrough)\n```\n\n### Options\n\n| Field | Type | Notes |\n| --- | --- | --- |\n| `base` | `string` | Override the CDN base. Defaults to `DEFAULT_CDN_BASE_URL`. |\n| `w` / `h` | `number` | Width / height in pixels (on-the-fly resizing) |\n| `quality` | `number` | 0\u2013100 |\n| `format` | `"auto" \\| "webp" \\| "avif" \\| "jpeg"` | `"auto"` lets the CDN pick by `Accept` header |\n\nThe transform query is only meaningful when the URL terminates on the Cimplify storefront CDN (which supports on-the-fly resizing). For external hosts, unknown params are typically ignored \u2014 the image still loads at its original size.\n\n## `isCimplifyAsset(src, base?)`\n\nDecides whether `src` is hosted on a Cimplify CDN. Used by the storefront\'s `next/image` loader to skip transforms for external URLs.\n\n```ts\n\nisCimplifyAsset("hero/main.jpg") // true (relative \u2192 ours)\nisCimplifyAsset("/img/seed/bakery/x.jpg") // true (dev mock passthrough)\nisCimplifyAsset("https://storefrontassetscdn.cimplify.io/x.jpg") // true\nisCimplifyAsset("https://cdn.cimplify.io/x.jpg") // true\nisCimplifyAsset("https://static-tmp.cimplify.io/seed/x.jpg") // true\nisCimplifyAsset("https://res.cloudinary.com/demo/upload/x.jpg") // false\nisCimplifyAsset("https://images.unsplash.com/photo-xyz") // false\n```\n\n## Built-in `next/image` loader\n\nEvery storefront template scaffolded by `cimplify init` ships `lib/cimplify-loader.ts` and points `images.loaderFile` at it. The loader:\n\n```ts\n\nconst cdnBase = process.env.NEXT_PUBLIC_CIMPLIFY_CDN_URL?.trim() || undefined;\n\nconst cimplifyImageLoader: ImageLoader = ({ src, width, quality }) => {\n if (isCimplifyAsset(src, cdnBase)) {\n return assetUrl(src, { base: cdnBase, w: width, quality, format: "auto" });\n }\n return src;\n};\n\nexport default cimplifyImageLoader;\n```\n\n**Cloudinary URLs from `client.catalogue.getProducts()` flow through untouched.** Cimplify-hosted URLs get the transform query. The result: every `<Image>` works regardless of where its asset lives, and Cimplify-hosted ones get edge-optimized.\n\nTo point the loader at a different CDN (for testing, multi-CDN setups, etc.) set `NEXT_PUBLIC_CIMPLIFY_CDN_URL` in your `.env.local`:\n\n```\nNEXT_PUBLIC_CIMPLIFY_CDN_URL=http://localhost:8787/storage\n```\n\n## `useImage(path, opts?)` \u2014 React\n\nThin hook that returns `{ src, loader }` for `<Image>`:\n\n```tsx\n\nexport function Hero() {\n const { src, loader } = useImage("hero/main.jpg", { format: "webp", quality: 85 });\n return <Image src={src} loader={loader} alt="..." width={1200} height={800} />;\n}\n```\n\nIf `images.loaderFile` is already set globally in your `next.config.ts`, you typically don\'t need `useImage` \u2014 just `<Image src={assetUrl("hero/main.jpg")} />` works. `useImage` is useful when you want per-component transform overrides on a project that doesn\'t use a global loader.\n\n### Options\n\n| Field | Notes |\n| --- | --- |\n| `base` | Per-call CDN base override |\n| `w`, `h` | Default transform values; per-image `width` from `<Image>` still wins |\n| `format`, `quality` | Default transform values |\n\n`useImage` is `useMemo`-stable across renders when the path + opts don\'t change.\n\n## Constants\n\n```ts\n\nDEFAULT_CDN_BASE_URL // "https://storefrontassetscdn.cimplify.io"\nCIMPLIFY_CDN_HOSTS // readonly ["storefrontassetscdn.cimplify.io", "cdn.cimplify.io", "static-tmp.cimplify.io"]\n```\n\n## What\'s not in v1\n\n- **`useUpload` hook** \u2014 runtime customer-facing uploads with progress + cancel. Deferred to v1.1. Customer uploads today go through `client.uploads.upload(file)` (no progress, no cancel \u2014 just `await`).\n- **`SEED_IDS` typed const tree** \u2014 demo-only convenience for referencing seed-image slugs in code. Generate-from-seed-source step deferred to v1.1; today demos use `seedImage(industry, slug)` from the SDK\'s mock module.\n- **Cloudinary / S3 path-transform support** \u2014 the loader passes external hosts through unchanged; Cloudinary\'s `c_fill,w_500/` path transforms aren\'t synthesized.\n- **`assets rm` server-side delete** \u2014 CLI removes manifest entries; the remote blob stays until v1.1\'s media-manager `DELETE` wiring.\n\n## Where it lives in the platform\n\n- Wire format: `POST /v1/businesses/{id}/assets/{init,confirm}` \u2014 see the management API reference\n- Backend service: `crate::uploads::service::UploadService::init_storefront_asset`\n- Served from the Cimplify asset CDN at `storefrontassetscdn.cimplify.io`\n- Storage key: `assets/{business_id}/{folder}/{filename}` \u2014 deterministic, idempotent, agent-predictable\n\n## Related\n\n- [`cimplify assets`](/docs/cli/assets) \u2014 CLI surface for upload/ls/rm\n- [Uploads (customer-runtime)](/docs/sdk/uploads) \u2014 `client.uploads.upload(file)` for private 7-day-presigned uploads\n'
|
|
133
133
|
},
|
|
134
134
|
{
|
|
135
135
|
"name": "exit-codes",
|
|
@@ -157,7 +157,7 @@ var TOPICS = [
|
|
|
157
157
|
"title": "Revalidation",
|
|
158
158
|
"description": "Canonical cimplify:* cache-tag vocabulary + revalidate helpers",
|
|
159
159
|
"source_url": "https://cimplify.dev/docs/sdk/revalidation",
|
|
160
|
-
"body": "Cimplify storefronts cache catalogue reads with Next 16's ISR (`export const revalidate = N` + `next.tags` via the SDK's `cacheOptions`). When a merchant edits a product, collection, or brand asset, the Cimplify eviction pipeline directly purges the matching R2 entries on your storefront's worker and the corresponding URLs from the Cloudflare edge \u2014 no `/api/revalidate` webhook round-trip required.\n\nThis page is the canonical contract between Cimplify and your storefront. Use the typed helpers \u2014 never hand-write tag strings \u2014 so the scheme stays in one place.\n\n## Why ISR, not Cache Components\n\nCimplify storefronts deploy to Cloudflare Workers (via opennext-cloudflare). Next 16's Cache Components feature (the `'use cache'` directive) requires Node-specific event-loop atomicity and serializes a postponed state that routinely exceeds CF Workers' 128MB zlib limit \u2014 pages 5xx with `RangeError: maxOutputLength`. We use Next 16's [Previous Model](https://nextjs.org/docs/app/guides/caching-without-cache-components): `export const revalidate` at the page level, `cacheOptions: { revalidate, tags }` on SDK reads (forwarded as `fetch().next.{revalidate,tags}`), and `revalidateTag` for invalidation. Stable on every runtime; what every Cimplify template ships with. Keep `cacheComponents` **off** in `next.config.ts`.\n\n## Tags\n\nAll tags are namespaced under `cimplify:` so they can't collide with consumer tags. Build them via `tags` from `@cimplify/sdk/server`:\n\n```ts\n\nexport const revalidate = 3600;\n\nasync function getProducts() {\n const r = await getServerClient().catalogue.getProducts(\n { limit: 24 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n return r.ok ? r.value.items : [];\n}\n```\n\n### Catalogue\n\n| Helper | Tag |\n| --- | --- |\n| `tags.products()` | `cimplify:products` |\n| `tags.product(id)` | `cimplify:product:{id}` |\n| `tags.categories()` | `cimplify:categories` |\n| `tags.category(id)` | `cimplify:category:{id}` |\n| `tags.categoryProducts(id)` | `cimplify:category:{id}:products` |\n| `tags.collections()` | `cimplify:collections` |\n| `tags.collection(id)` | `cimplify:collection:{id}` |\n| `tags.collectionProducts(id)` | `cimplify:collection:{id}:products` |\n| `tags.tag(name)` | `cimplify:tag:{name}` (product tag, e.g. \"vegan\") |\n| `tags.addons()` | `cimplify:addons` |\n| `tags.addon(id)` | `cimplify:addon:{id}` |\n| `tags.stock()` | `cimplify:stock` |\n| `tags.stockFor(productId)` | `cimplify:stock:{productId}` |\n\n### Brand / business\n\n| Helper | Tag |\n| --- | --- |\n| `tags.business()` | `cimplify:business` |\n| `tags.brand()` | `cimplify:brand` |\n| `tags.locations()` | `cimplify:locations` |\n| `tags.location(id)` | `cimplify:location:{id}` |\n| `tags.locale()` | `cimplify:locale` |\n\n### Pricing / subscriptions\n\n| Helper | Tag |\n| --- | --- |\n| `tags.pricing()` | `cimplify:pricing` |\n| `tags.subscriptions()` | `cimplify:subscriptions` |\n| `tags.subscription(id)` | `cimplify:subscription:{id}` |\n\n### Customer-scoped (Server Actions only)\n\n| Helper | Tag |\n| --- | --- |\n| `tags.orders(customerId)` | `cimplify:orders:{customerId}` |\n| `tags.order(id)` | `cimplify:order:{id}` |\n\n## Granularity\n\nTag with **both** a broad and a precise tag on every read so either-flavour invalidation works:\n\n```ts\ncacheOptions: { revalidate: 3600, tags: [tags.products(), tags.product(id)] }\n```\n\nThe broad tag (`products`) catches \"I changed something product-related, invalidate everything.\" The precise tag (`product:{id}`) catches \"I changed exactly this one product, invalidate only its entries.\"\n\n## Revalidating from a Server Action\n\n```ts\n\nexport async function saveProduct(id: string, data: ProductInput) {\n await client.catalogue.updateProduct(id, data);\n await revalidateProduct(id);\n}\n```\n\nHelpers:\n\n| Helper | Invalidates |\n| --- | --- |\n| `revalidateProducts()` | `products` |\n| `revalidateProduct(id)` | `product:{id}` + `products` |\n| `revalidateCategories()` | `categories` |\n| `revalidateCategory(id)` | `category:{id}` + `category:{id}:products` + `categories` |\n| `revalidateCollections()` | `collections` |\n| `revalidateCollection(id)` | `collection:{id}` + `collection:{id}:products` + `collections` |\n| `revalidateBusiness()` | `business` |\n| `revalidateBrand()` | `brand` |\n| `revalidateLocations()` | `locations` |\n| `revalidateLocation(id)` | `location:{id}` + `locations` |\n| `revalidatePricing()` | `pricing` + `products` |\n| `revalidateAddOns()` | `addons` |\n| `revalidateAddOn(id)` | `addon:{id}` + `addons` |\n| `revalidateSubscriptions()` | `subscriptions` |\n| `revalidateSubscription(id)` | `subscription:{id}` + `subscriptions` |\n| `revalidateStock(productId?)` | `stock:{productId}` + `stock` (or just `stock` if no id) |\n| `revalidateByTag(tag)` | escape hatch for a raw tag |\n\n## The Cimplify \u2192 storefront eviction contract\n\nWhen a merchant edits something in the dashboard, Cimplify's bus emits an event (`ProductUpdated`, `CategoryChanged`, etc.). The Rust EvictionDispatcher resolves which storefronts serve the affected business, derives the resource tags, and HMAC-POSTs to a central tag-cache worker on `*.workers.dev`. That worker:\n\n1. Looks up the matching cache entries in a `PathIndex` Durable Object (populated as your storefront writes ISR caches)\n2. Deletes the R2 entries directly so opennext can't serve from them\n3. Calls Cloudflare's `cache_purge` API for the corresponding URLs on the merchant's hostname\n4. Marks the tags stale in the shared `DOShardedTagCache` so any in-flight read sees the invalidation\n\nNet effect: an edit in the dashboard purges your storefront's caches **globally** in ~1\u20133 seconds, with no webhook round-trip into your worker. The R2 + edge purge happens entirely on Cimplify-owned infrastructure that has bindings to your storefront's R2 bucket and to the CF zone.\n\nThe tags Cimplify dispatches use the **exact strings** from the tables above. If you've tagged your `cacheOptions.tags` with the helpers above, your storefront stays in sync automatically.\n\n## Dynamic routes \u2014 tag by **ID**, never by slug\n\nCimplify dispatches revalidation tags **keyed by database ID**, not by URL slug. A `/products/[slug]` page that caches itself under `tags.product(slug)` will never be invalidated by an edit \u2014 the tag the storefront wrote doesn't match the tag Cimplify fires.\n\nResolve the slug to a record first, then tag with `record.id`:\n\n```ts title=\"app/products/[slug]/page.tsx \u2014 correct\"\n\nexport const revalidate = 3600;\n\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) return [{ slug: \"__placeholder__\" }];\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n const { slug } = await params;\n\n // First resolve slug \u2192 product. Tag the resolution read with the\n // collection-level tag so adds/removes invalidate it. This call hits the\n // SDK with a fallback-broad tag.\n const r = await getServerClient().catalogue.getProductBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) notFound();\n\n // Now re-fetch with the resolved ID so the per-product tag is keyed on the\n // stable identifier \u2014 revalidateProduct(id) from Cimplify will hit this\n // entry on the next mutation.\n const detailed = await getServerClient().catalogue.getProduct(\n r.value.id,\n { cacheOptions: { revalidate: 3600, tags: [tags.product(r.value.id), tags.products()] } },\n );\n\n if (!detailed.ok) notFound();\n return <ProductDetail product={detailed.value} />;\n}\n```\n\nThe same pattern applies to `[slug]` routes for categories, collections, addons, locations, and so on \u2014 always tag with the resolved `record.id`, never with the URL slug.\n\n### Why this matters\n\n- **Renames don't orphan.** The cache key includes the slug (path-based), so a renamed product gets a fresh cache entry under the new slug. The old slug-keyed entry is invalidated via the `tags.products()` collection tag and will 404 naturally.\n- **IDs are stable.** Cimplify's bus emits `ProductUpdated { id }` events; the slug isn't on the event payload, and adding it would create races with concurrent rename. Keeping the tag scheme ID-only keeps the contract simple.\n- **You get instant freshness.** With Cimplify's eviction pipeline (R2 delete + CDN purge), a 1-hour `revalidate` entry can sit cached and still flip fresh within ~3s of an edit \u2014 but only if the tag actually matches.\n\n## SDK timeout\n\nThe SDK's default per-call timeout is **5 seconds**. Picked so a single slow upstream call can't exhaust the 30s render budget \u2014 a hung `getCollections()` used to take the entire page render with it. With the 5s default, a slow call fails fast, `Result.ok` becomes `false`, the page renders with empty data, and opennext caches that partial render so subsequent hits are instant.\n\nOverride only for genuinely-large bulk reads (admin tooling, batch jobs):\n\n```ts\n\nconst adminClient = createCimplifyClient({\n secretKey: process.env.CIMPLIFY_SECRET_KEY,\n timeout: 30_000, // 30s for a 10k-row export\n});\n```\n\n## Don't cache empty responses as if they were real\n\nIf a render produces empty results because of a transient backend hiccup, that empty render gets cached for the full `revalidate` window \u2014 and every visitor sees an empty storefront until someone manually invalidates.\n\nTwo defences:\n\n**1.** The SDK's 5s timeout above stops a slow call from hanging the whole render, but the result is still `Result.ok = false`. Branch on that and short-circuit to a not-found / soft state rather than committing the empty render to cache:\n\n```ts title=\"app/categories/[slug]/page.tsx\"\nasync function getCategory(slug: string) {\n const r = await getServerClient().catalogue.getCategoryBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.categories()] } },\n );\n // Distinguish a real 404 from a transient SDK error. Only `notFound()` on\n // the real 404; for transient errors, render a soft loading state so the\n // cache doesn't lock in an incorrect \"page couldn't load\" for an hour.\n if (!r.ok) {\n if (r.error.code === \"NOT_FOUND\") notFound();\n return { soft: true as const };\n }\n return { soft: false as const, category: r.value };\n}\n```\n\n**2.** Use the eviction-pipeline path for catalogue invalidation rather than relying on `revalidate` window expiry. A real edit in the dashboard purges the cache within seconds; the `revalidate` value is a backstop, not the primary refresh mechanism.\n\n## Tag taxonomy is locked\n\nThe vocabulary above is the v1 contract. Adding new tag helpers is non-breaking; removing or renaming any of them bumps the SDK major version. Agents that bake these strings into prompts can pin to v0.x of `@cimplify/sdk`.\n"
|
|
160
|
+
"body": "Cimplify storefronts cache catalogue reads with Next 16's ISR (`export const revalidate = N` + `next.tags` via the SDK's `cacheOptions`). When a merchant edits a product, collection, or brand asset, Cimplify invalidates the matching caches for your storefront automatically \u2014 no `/api/revalidate` webhook round-trip required.\n\nThis page is the canonical contract between Cimplify and your storefront. Use the typed helpers \u2014 never hand-write tag strings \u2014 so the scheme stays in one place.\n\n## Why ISR, not Cache Components\n\nCimplify templates use Next 16's [Previous Model](https://nextjs.org/docs/app/guides/caching-without-cache-components): `export const revalidate` at the page level, `cacheOptions: { revalidate, tags }` on SDK reads (forwarded as `fetch().next.{revalidate,tags}`), and `revalidateTag` for invalidation. The `'use cache'` directive (Cache Components) is still experimental and its runtime constraints don't suit hosted storefronts. The Previous Model is stable, fully supported, and what every Cimplify template ships with. Keep `cacheComponents` **off** in `next.config.ts`.\n\n## Tags\n\nAll tags are namespaced under `cimplify:` so they can't collide with consumer tags. Build them via `tags` from `@cimplify/sdk/server`:\n\n```ts\n\nexport const revalidate = 3600;\n\nasync function getProducts() {\n const r = await getServerClient().catalogue.getProducts(\n { limit: 24 },\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n return r.ok ? r.value.items : [];\n}\n```\n\n### Catalogue\n\n| Helper | Tag |\n| --- | --- |\n| `tags.products()` | `cimplify:products` |\n| `tags.product(id)` | `cimplify:product:{id}` |\n| `tags.categories()` | `cimplify:categories` |\n| `tags.category(id)` | `cimplify:category:{id}` |\n| `tags.categoryProducts(id)` | `cimplify:category:{id}:products` |\n| `tags.collections()` | `cimplify:collections` |\n| `tags.collection(id)` | `cimplify:collection:{id}` |\n| `tags.collectionProducts(id)` | `cimplify:collection:{id}:products` |\n| `tags.tag(name)` | `cimplify:tag:{name}` (product tag, e.g. \"vegan\") |\n| `tags.addons()` | `cimplify:addons` |\n| `tags.addon(id)` | `cimplify:addon:{id}` |\n| `tags.stock()` | `cimplify:stock` |\n| `tags.stockFor(productId)` | `cimplify:stock:{productId}` |\n\n### Brand / business\n\n| Helper | Tag |\n| --- | --- |\n| `tags.business()` | `cimplify:business` |\n| `tags.brand()` | `cimplify:brand` |\n| `tags.locations()` | `cimplify:locations` |\n| `tags.location(id)` | `cimplify:location:{id}` |\n| `tags.locale()` | `cimplify:locale` |\n\n### Pricing / subscriptions\n\n| Helper | Tag |\n| --- | --- |\n| `tags.pricing()` | `cimplify:pricing` |\n| `tags.subscriptions()` | `cimplify:subscriptions` |\n| `tags.subscription(id)` | `cimplify:subscription:{id}` |\n\n### Customer-scoped (Server Actions only)\n\n| Helper | Tag |\n| --- | --- |\n| `tags.orders(customerId)` | `cimplify:orders:{customerId}` |\n| `tags.order(id)` | `cimplify:order:{id}` |\n\n## Granularity\n\nTag with **both** a broad and a precise tag on every read so either-flavour invalidation works:\n\n```ts\ncacheOptions: { revalidate: 3600, tags: [tags.products(), tags.product(id)] }\n```\n\nThe broad tag (`products`) catches \"I changed something product-related, invalidate everything.\" The precise tag (`product:{id}`) catches \"I changed exactly this one product, invalidate only its entries.\"\n\n## Revalidating from a Server Action\n\n```ts\n\nexport async function saveProduct(id: string, data: ProductInput) {\n await client.catalogue.updateProduct(id, data);\n await revalidateProduct(id);\n}\n```\n\nHelpers:\n\n| Helper | Invalidates |\n| --- | --- |\n| `revalidateProducts()` | `products` |\n| `revalidateProduct(id)` | `product:{id}` + `products` |\n| `revalidateCategories()` | `categories` |\n| `revalidateCategory(id)` | `category:{id}` + `category:{id}:products` + `categories` |\n| `revalidateCollections()` | `collections` |\n| `revalidateCollection(id)` | `collection:{id}` + `collection:{id}:products` + `collections` |\n| `revalidateBusiness()` | `business` |\n| `revalidateBrand()` | `brand` |\n| `revalidateLocations()` | `locations` |\n| `revalidateLocation(id)` | `location:{id}` + `locations` |\n| `revalidatePricing()` | `pricing` + `products` |\n| `revalidateAddOns()` | `addons` |\n| `revalidateAddOn(id)` | `addon:{id}` + `addons` |\n| `revalidateSubscriptions()` | `subscriptions` |\n| `revalidateSubscription(id)` | `subscription:{id}` + `subscriptions` |\n| `revalidateStock(productId?)` | `stock:{productId}` + `stock` (or just `stock` if no id) |\n| `revalidateByTag(tag)` | escape hatch for a raw tag |\n\n## The Cimplify \u2192 storefront eviction contract\n\nWhen a merchant edits something in the dashboard, Cimplify automatically invalidates the matching caches for every storefront serving that business \u2014 **globally, in ~1\u20133 seconds**, with no webhook round-trip into your storefront. You don't wire anything up; tagging your reads correctly is the whole contract.\n\nThe tags Cimplify dispatches use the **exact strings** from the tables above, keyed by database ID. If you've tagged your `cacheOptions.tags` with the helpers above, your storefront stays in sync automatically.\n\n## Dynamic routes \u2014 tag by **ID**, never by slug\n\nCimplify dispatches revalidation tags **keyed by database ID**, not by URL slug. A `/products/[slug]` page that caches itself under `tags.product(slug)` will never be invalidated by an edit \u2014 the tag the storefront wrote doesn't match the tag Cimplify fires.\n\nResolve the slug to a record first, then tag with `record.id`:\n\n```ts title=\"app/products/[slug]/page.tsx \u2014 correct\"\n\nexport const revalidate = 3600;\n\nexport async function generateStaticParams() {\n const r = await getServerClient().catalogue.getProducts({ limit: 10_000 });\n if (!r.ok || r.value.items.length === 0) return [{ slug: \"__placeholder__\" }];\n return r.value.items.map((p) => ({ slug: p.slug ?? p.id }));\n}\n\nexport default async function Page({ params }: { params: Promise<{ slug: string }> }) {\n const { slug } = await params;\n\n // First resolve slug \u2192 product. Tag the resolution read with the\n // collection-level tag so adds/removes invalidate it. This call hits the\n // SDK with a fallback-broad tag.\n const r = await getServerClient().catalogue.getProductBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.products()] } },\n );\n if (!r.ok) notFound();\n\n // Now re-fetch with the resolved ID so the per-product tag is keyed on the\n // stable identifier \u2014 revalidateProduct(id) from Cimplify will hit this\n // entry on the next mutation.\n const detailed = await getServerClient().catalogue.getProduct(\n r.value.id,\n { cacheOptions: { revalidate: 3600, tags: [tags.product(r.value.id), tags.products()] } },\n );\n\n if (!detailed.ok) notFound();\n return <ProductDetail product={detailed.value} />;\n}\n```\n\nThe same pattern applies to `[slug]` routes for categories, collections, addons, locations, and so on \u2014 always tag with the resolved `record.id`, never with the URL slug.\n\n### Why this matters\n\n- **Renames don't orphan.** The cache key includes the slug (path-based), so a renamed product gets a fresh cache entry under the new slug. The old slug-keyed entry is invalidated via the `tags.products()` collection tag and will 404 naturally.\n- **IDs are stable.** Cimplify's bus emits `ProductUpdated { id }` events; the slug isn't on the event payload, and adding it would create races with concurrent rename. Keeping the tag scheme ID-only keeps the contract simple.\n- **You get instant freshness.** With Cimplify's automatic eviction, a 1-hour `revalidate` entry can sit cached and still flip fresh within ~3s of an edit \u2014 but only if the tag actually matches.\n\n## SDK timeout\n\nThe SDK's default per-call timeout is **5 seconds**. Picked so a single slow upstream call can't exhaust the page render budget \u2014 a hung `getCollections()` would otherwise take the entire page render with it. With the 5s default, a slow call fails fast, `Result.ok` becomes `false`, the page renders with empty data, and the result is cached so subsequent hits are instant.\n\nOverride only for genuinely-large bulk reads (admin tooling, batch jobs):\n\n```ts\n\nconst adminClient = createCimplifyClient({\n secretKey: process.env.CIMPLIFY_SECRET_KEY,\n timeout: 30_000, // 30s for a 10k-row export\n});\n```\n\n## Don't cache empty responses as if they were real\n\nIf a render produces empty results because of a transient backend hiccup, that empty render gets cached for the full `revalidate` window \u2014 and every visitor sees an empty storefront until someone manually invalidates.\n\nTwo defences:\n\n**1.** The SDK's 5s timeout above stops a slow call from hanging the whole render, but the result is still `Result.ok = false`. Branch on that and short-circuit to a not-found / soft state rather than committing the empty render to cache:\n\n```ts title=\"app/categories/[slug]/page.tsx\"\nasync function getCategory(slug: string) {\n const r = await getServerClient().catalogue.getCategoryBySlug(\n slug,\n { cacheOptions: { revalidate: 3600, tags: [tags.categories()] } },\n );\n // Distinguish a real 404 from a transient SDK error. Only `notFound()` on\n // the real 404; for transient errors, render a soft loading state so the\n // cache doesn't lock in an incorrect \"page couldn't load\" for an hour.\n if (!r.ok) {\n if (r.error.code === \"NOT_FOUND\") notFound();\n return { soft: true as const };\n }\n return { soft: false as const, category: r.value };\n}\n```\n\n**2.** Use the eviction-pipeline path for catalogue invalidation rather than relying on `revalidate` window expiry. A real edit in the dashboard purges the cache within seconds; the `revalidate` value is a backstop, not the primary refresh mechanism.\n\n## Tag taxonomy is locked\n\nThe vocabulary above is the v1 contract. Adding new tag helpers is non-breaking; removing or renaming any of them bumps the SDK major version. Agents that bake these strings into prompts can pin to v0.x of `@cimplify/sdk`.\n"
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
"name": "inspect-suggestions",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-
|
|
2
|
+
export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-Q4H7IMO5.mjs';
|
|
3
3
|
import './chunk-K5464A3L.mjs';
|
|
4
4
|
import './chunk-DBZ3UOQ2.mjs';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-CIRUNSIM.mjs';
|
|
6
6
|
import './chunk-C4M3DXKC.mjs';
|
|
7
7
|
import './chunk-UBAI443T.mjs';
|
|
8
8
|
import './chunk-E2T2SBP5.mjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { REGISTRY_INDEX } from './chunk-
|
|
2
|
+
import { REGISTRY_INDEX } from './chunk-Z4OBZRGQ.mjs';
|
|
3
3
|
import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
|
|
4
4
|
import { CliError, CLI_ERROR_CODE, info, bold, dim, green, result } from './chunk-E2T2SBP5.mjs';
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default } from './chunk-
|
|
2
|
+
import { package_default } from './chunk-CIRUNSIM.mjs';
|
|
3
3
|
import { promptYesNo } from './chunk-ITAFAORS.mjs';
|
|
4
4
|
import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
|
|
5
5
|
import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"vitest": "^4.1.5"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@cimplify/sdk": "^0.
|
|
48
|
+
"@cimplify/sdk": "^0.58.0"
|
|
49
49
|
}
|
|
50
50
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { CimplifySignInButton } from "@cimplify/sdk/react";
|
|
2
|
+
import { brand } from "@/lib/brand";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
heading?: string;
|
|
6
|
+
subtitle?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SignedOutPrompt({ heading, subtitle }: Props) {
|
|
10
|
+
const clientId = process.env.NEXT_PUBLIC_CIMPLIFY_CLIENT_ID ?? "";
|
|
11
|
+
const redirectUri = process.env.NEXT_PUBLIC_CIMPLIFY_REDIRECT_URI ?? "";
|
|
12
|
+
return (
|
|
13
|
+
<div className="max-w-md mx-auto bg-card border border-border rounded-2xl p-8 sm:p-10 text-center">
|
|
14
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.16em] text-primary mb-2">
|
|
15
|
+
{brand.account.loginEyebrow}
|
|
16
|
+
</p>
|
|
17
|
+
<h2 className="font-serif text-2xl sm:text-3xl font-semibold mb-3 -tracking-[0.02em]">
|
|
18
|
+
{heading ?? brand.account.loginTitle}
|
|
19
|
+
</h2>
|
|
20
|
+
<p className="text-sm text-muted-foreground mb-7 leading-relaxed">
|
|
21
|
+
{subtitle ?? brand.account.loginSubtitle}
|
|
22
|
+
</p>
|
|
23
|
+
<CimplifySignInButton
|
|
24
|
+
clientId={clientId}
|
|
25
|
+
redirectUri={redirectUri}
|
|
26
|
+
variant="primary"
|
|
27
|
+
fullWidth
|
|
28
|
+
onSuccess={() => window.location.reload()}
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|