@commercebuild/extension 0.0.20 → 0.0.22
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/AGENTS.md +143 -0
- package/package.json +1 -1
- package/scripts/cli.js +66 -8
- package/types/global.d.ts +9 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<!-- AUTO-GENERATED — DO NOT EDIT.
|
|
2
|
+
Generated by cb-store scripts/generate-extension-host-types.mjs from
|
|
3
|
+
apps/storeadmin/src/lib/extension-editor-agent/author-guide.ts, the same
|
|
4
|
+
source as the online editor's assistant. Regenerate with
|
|
5
|
+
`yarn generate-extension-host-types`. -->
|
|
6
|
+
|
|
7
|
+
# Commercebuild custom app — guide for AI coding agents
|
|
8
|
+
|
|
9
|
+
This file ships with `@commercebuild/extension` and describes how to write
|
|
10
|
+
code for a Commercebuild custom app (a storefront extension). The project's
|
|
11
|
+
own `AGENTS.md` at its root points here and carries app-specific notes.
|
|
12
|
+
|
|
13
|
+
## Project shape
|
|
14
|
+
- A custom app is a small TypeScript project bundled by the platform (esbuild in the online editor, vite in the canonical build) and rendered live on a storefront.
|
|
15
|
+
- Entry point is src/index.ts. It re-exports two namespaces:
|
|
16
|
+
export * as components from "./cms";
|
|
17
|
+
export * as pages from "./pages";
|
|
18
|
+
- Components (src/cms/*) are React components the storefront can render.
|
|
19
|
+
- Pages (src/pages/*) are full-page routes. Each page is registered in src/pages/index.ts as `export * as PageName from "./page-file"` and renders on the storefront at /<locale>/extension/<appId>/page/<PageName> — keyed by the app's id from commercebuild.json, not its name. A page's default export is the page component; it may also export `const metadata = { title: "…" }`.
|
|
20
|
+
- Admin pages (src/admin/pages/*) are OPTIONAL full-page screens rendered inside the STORE ADMIN (not the storefront) at the admin's /extension/<appId>/page/<PageName> route — same default-export + optional `metadata` shape as storefront pages. The admin host shows `metadata.title` in the header breadcrumbs (falling back to the page's export name), so give every admin page a real title. The admin area is its own subtree with its own entry barrel and stylesheet, compiled to a separate bundle. When it exists, src/admin/index.ts MUST be exactly:
|
|
21
|
+
import "./styles/index.css";
|
|
22
|
+
export * as adminPages from "./pages";
|
|
23
|
+
with per-page exports in src/admin/pages/index.ts (`export * as PageName from "./page-file"`) and src/admin/styles/index.css carrying the same host-safe header as the storefront stylesheet. Do NOT create src/admin/ unless the user asks for an admin screen.
|
|
24
|
+
- Admin pages get a REDUCED cb surface: `cb.version`, `cb.has`, `cb.requireLib`, `cb.lib.*`, `cb.platform.*` (authenticated as the signed-in admin), and from cb.utils ONLY `useFirebaseApp`/`useFirebaseConnections`. There is NO `cb.ui`, NO `cb.com`, NO `cb.settings`, and none of the other `cb.utils.*` helpers (router, tmsg, tlink, displayMoney, cn, revalidate*, convertProductItemToCartRequestItem) — feature-detect with `cb.has()` in code shared between areas.
|
|
25
|
+
- Import boundaries (compile errors): files under src/admin/ may only import src/admin/ or src/shared/; files under src/shared/ may only import src/shared/; nothing outside src/admin/ may import src/admin/. Put code needed by both areas in src/shared/.
|
|
26
|
+
- Directory names are convention, the barrels are the contract: older apps keep components in src/components — follow whatever the re-exports in src/index.ts actually point at rather than moving files.
|
|
27
|
+
- Styling is Tailwind: write utility classes in JSX className. Do not add other CSS frameworks.
|
|
28
|
+
- src/styles/index.css MUST keep this exact host-safe header (the app's CSS is injected into the live storefront page, so it must emit ONLY its own utilities):
|
|
29
|
+
@reference "tailwindcss/theme.css";
|
|
30
|
+
@import "tailwindcss/utilities.css" layer(utilities) source(none);
|
|
31
|
+
@config "@commercebuild/extension/config/tailwind.config.js";
|
|
32
|
+
@source "../";
|
|
33
|
+
@source not "../admin";
|
|
34
|
+
NEVER write the full `@import "tailwindcss";` — that ships Tailwind's preflight and `:root` theme variables, which override the storefront's own styles and visibly break its header. The @config line is equally required: it loads the extension config whose blocklist drops utilities that would otherwise beat the store's own (a bare `.hidden`/`.flex` collapses the storefront header). The @source lines scope the utility scan to this bundle's own code (the admin area compiles to a separate stylesheet). Add any custom CSS below the header lines; if you create or rewrite this file, reproduce the header verbatim.
|
|
35
|
+
- When the admin area exists, src/admin/styles/index.css MUST keep this header (same rules; its scan covers src/admin + src/shared):
|
|
36
|
+
@reference "tailwindcss/theme.css";
|
|
37
|
+
@import "tailwindcss/utilities.css" layer(utilities) source(none);
|
|
38
|
+
@config "@commercebuild/extension/config/tailwind.config.js";
|
|
39
|
+
@source "../";
|
|
40
|
+
@source "../../shared";
|
|
41
|
+
- You edit files under src/, plus commercebuild.json (id, name, type, version, cbApiVersion) whose "id" must stay unchanged, and the app config files config.json / config.development.json. The build manifests (package.json, tsconfig.json) are platform-owned, not yours.
|
|
42
|
+
|
|
43
|
+
## How to write code (host contract)
|
|
44
|
+
- Write standard React + TypeScript. Import React normally: `import React, { useState, useEffect } from "react"`. React and react-dom are provided by the host at runtime — importing them the normal way is correct.
|
|
45
|
+
- The CB host contract — the generated `global.d.ts` (in the online editor the "CB host contract" system block; in a local checkout node_modules/@commercebuild/extension/types/global.d.ts) — is the authoritative API surface — read it carefully. CRITICAL: only TWO things there are true globals you can use bare (no import): (1) the `cb` object, and (2) the types `Category` and `Product`. EVERYTHING else in that file is reached THROUGH `cb`, never as a bare name — using a bare name is a "Cannot find name" error:
|
|
46
|
+
- Prebuilt components live under `cb.com.*`: e.g. `cb.com.Cart.AddToCart`, `cb.com.Product.ProductStockStatus`, `cb.com.Product.ProductQuantityInput`, `cb.com.Product.ProductPrice`, `cb.com.Category.CategoryProduct`. There is NO bare `AddToCart` / `ProductQuantityInput` / `ProductStockStatus`.
|
|
47
|
+
- Utilities under `cb.utils.*` (e.g. `cb.utils.displayMoney`, `cb.utils.cn`, `cb.utils.router`, `cb.utils.convertProductItemToCartRequestItem`).
|
|
48
|
+
- UI under `cb.ui.*` (`cb.ui.Link`, `cb.ui.Image`, `cb.ui.icons.*`, plus `@commercebuild/ui` members).
|
|
49
|
+
- Platform API clients under `cb.platform.<namespace>(version?)` (e.g. `cb.platform.catalog()`, `cb.platform.cart("2")`). A namespace can have several versions, which are DIFFERENT services with different methods and different URLs — not just different shapes. The Platform API client list (in the online editor the "Platform API clients" system block; in a local checkout node_modules/@commercebuild/platform-api/src/<namespace>/v<n>.ts, where index.ts names the default version and each method's JSDoc names its HTTP call) covers every namespace, every version, each version's methods and the HTTP call each one makes; read it before writing any `cb.platform.*` call, and pass the version argument the method you need lives on. For the cart specifically, `cart()` with no argument is cart v1 (only `getMiniCart` and `addItemsToCart`, on the legacy /api/v/1/5 service) — V5 stores use `cb.platform.cart("2")`, which is where the rest of the cart API lives.
|
|
50
|
+
- Store settings under `cb.settings` — `storeId`, `companyName`, `themeName`, `cdnPath`, `timeZone`, feature flags. Store-level (present for guests too) but OPTIONAL: always write `cb.settings?.storeId`. Use it to scope data per store (e.g. a Firestore collection path); do NOT use its `firebaseApiKey`/`mfaTenantId` — those are the host's own login keys, and app data connections come from `cb.utils.useFirebaseApp()`.
|
|
51
|
+
- Platform-api DTO TYPES (PostItemsSearchItem, PostItemsSearchRequest, CartRequestItem, …) are NOT global — import the versioned namespace from the package root, e.g. `import type { catalog_v1 } from "platform-api"` then `catalog_v1.PostItemsSearchItem`. The namespace is NOT exported from the versioned module: `import { catalog_v1 } from "platform-api/catalog/v1"` FAILS ("no exported member") — that module exports members directly, so `import type { PostItemsSearchItem } from "platform-api/catalog/v1"` also works. Or derive from a call: `type Item = Awaited<ReturnType<ReturnType<typeof cb.platform.catalog>["postItemsSearch"]>>["pages"]["content"][number]`.
|
|
52
|
+
- Product search — use this EXACT shape (wrong field names return no results and fail silently):
|
|
53
|
+
const res = await cb.platform.catalog().postItemsSearch({ queryString: term, pageSize: 20 }); // queryString needs >= 3 chars
|
|
54
|
+
const items = res.pages.content; // the array is res.pages.content, NOT res.items
|
|
55
|
+
// each item: { id, itemCode, description, url, images, hasStock, price: { exclAmount, currencyCode } | null, ... }
|
|
56
|
+
- Search results are THIN — a search item carries roughly { id, itemCode, description, url, images, hasStock, orderable, defaultUnitOfMeasure, unitOfMeasures, price, type, itemAvailability?, stocks? }, and `PostItemsSearchItem` in the type libs is the authoritative list (this one is maintained by hand and can lag the SDK, so check the type before concluding a field does not exist). `customFields`, `secondaryDescription`, `feature`, `specification`, `relatedItems` and `optionFields` exist ONLY on the item detail:
|
|
57
|
+
const detail = await cb.platform.catalog().getItemDetail({ itemCode });
|
|
58
|
+
Reading one of those off a search item does not throw, it just yields undefined — so the column/section you built renders permanently blank and looks like a data problem. The SDK caches detail per itemCode, so re-renders and re-adds of the same product do not refetch — but N distinct products really are N requests, and there is no plural endpoint that carries these fields (`GET /items` returns a lookup shape without them). So fetch detail for the rows that need it — the ones the user actually picks or expands — not for a whole result page.
|
|
59
|
+
- Product custom fields (where a merchant keeps cost, alternate codes, flags…) are on the item detail as `customFields: [{ id, name, description, type, values: string[] }]`. The CMS variable `product.custom.<id>` resolves to the entry whose **id** matches, joining `values` — but other storefront code keys the same list by **name**. Match id OR name, normalised (lower-cased, non-alphanumerics stripped). When nothing matches, log the list the platform returned instead of rendering blank: an EMPTY customFields array means this session cannot see them (a permissions problem), a non-empty one means the field name is wrong — different causes, different fixes, indistinguishable from an empty cell.
|
|
60
|
+
- `secondaryDescription` is HTML, not plain text. Strip the tags before putting it in a table cell or a title.
|
|
61
|
+
- Add to cart: build items with `cb.utils.convertProductItemToCartRequestItem(...)` and render `cb.com.Cart.AddToCart` with an `items={[...]}` prop. That helper only maps STANDARD and VARIANT products and THROWS on any other `type` — catch that at the point the user picks a product and tell them, rather than sending a fallback item the cart API cannot map.
|
|
62
|
+
- Custom / negotiated pricing (a recurring B2B ask) is NOT part of the add-to-cart payload: a cart item carries no price at all, and unknown properties (`unitPrice`, `discountPercentage`, `onlineDiscountPercentage`) are silently dropped server-side, so they look like they worked. Apply pricing AFTER the items are in the cart, as a per-line online discount:
|
|
63
|
+
// The rep enters PRICES; the API takes a DISCOUNT PERCENT. Convert, or you
|
|
64
|
+
// will send a target price of 20 as a 20% discount and undercharge.
|
|
65
|
+
const targetPriceByCode = new Map<string, number>(/* what the rep typed */);
|
|
66
|
+
const cataloguePriceByCode = new Map<string, number>(/* price.exclAmount from the search result */);
|
|
67
|
+
const discountPercent = (code: string) => {
|
|
68
|
+
const catalogue = cataloguePriceByCode.get(code) ?? 0;
|
|
69
|
+
const target = targetPriceByCode.get(code) ?? catalogue;
|
|
70
|
+
return catalogue > 0 ? ((catalogue - target) / catalogue) * 100 : 0;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const cart = cb.platform.cart("2");
|
|
74
|
+
const live = await cart.getUserCart(); // server-assigned ids
|
|
75
|
+
const cartItems = (live?.cartItems ?? [])
|
|
76
|
+
.filter((ci) => targetPriceByCode.has(ci.code))
|
|
77
|
+
.map((ci) => ({
|
|
78
|
+
id: ci.id,
|
|
79
|
+
onlineDiscountPercentage: Math.round(discountPercent(ci.code) * 100) / 100,
|
|
80
|
+
}));
|
|
81
|
+
// PUT /carts/online-discounts. The call RETURNS the refreshed cart —
|
|
82
|
+
// verify from it, do not assume a resolved promise means the discount
|
|
83
|
+
// landed, and do not spend a second request re-fetching it.
|
|
84
|
+
const updated = await cart.updateCartItemOnlineDiscount({ cartItems });
|
|
85
|
+
const missed = cartItems.filter((sent) => {
|
|
86
|
+
const line = updated?.cartItems.find((ci) => ci.id === sent.id);
|
|
87
|
+
return line?.onlineDiscountPercentage !== sent.onlineDiscountPercentage;
|
|
88
|
+
});
|
|
89
|
+
// A write you make through cb.platform is NOT a server action, so the
|
|
90
|
+
// host's own cart UI (header mini-cart, cart page) never refetches and
|
|
91
|
+
// keeps showing the old price. Nothing in your component can fix that.
|
|
92
|
+
// revalidateTag returns a promise: refreshing before it resolves
|
|
93
|
+
// re-renders against the cache you were trying to drop.
|
|
94
|
+
await cb.utils.revalidateTag("cart", "max");
|
|
95
|
+
cb.utils.router.refresh();
|
|
96
|
+
`onlineDiscountPercentage` is a PERCENT, never a price — the pricing service discounts the catalogue excl-tax amount by it, so the base of that percentage must be the same catalogue price. It is POSITIVE when the price is BELOW the catalogue price (it is a discount, not a diff-vs-list), and a value <= 0 is SILENTLY IGNORED by the pricing service — you cannot mark a price UP this way, so never present a markup above catalogue price as something the cart will honour. The endpoint refuses guests outright, and requires a B2B shopper whose user group allows setting discounts — surface those as real errors instead of assuming success.
|
|
97
|
+
- Any write you make through `cb.platform.*` bypasses the host's own components. They are server components that fetched their data on a previous render, and your call is a plain browser request, not a server action — so they do not know anything changed and will keep rendering the old value. After a mutation whose result the shopper is meant to see somewhere else on the page, invalidate and re-render: `await cb.utils.revalidateTag("<tag>", "max")` (the cart's tag is `"cart"`) and THEN `cb.utils.router.refresh()`. Order matters and so does the await: `revalidateTag` returns a promise, and refreshing before it resolves re-renders against the cache you were trying to drop. Of the two, `router.refresh()` is what actually makes the host's server components run again. A mutation that succeeded but is invisible reads to the user as a mutation that failed.
|
|
98
|
+
- Firestore (the app's own data, in a merchant- or app-owned Firebase project):
|
|
99
|
+
import { getFirestore, collection, getDocs, addDoc, onSnapshot } from "firebase/firestore";
|
|
100
|
+
const app = cb.utils.useFirebaseApp(); // React hook; a named connection: useFirebaseApp("catalog")
|
|
101
|
+
const configured = cb.utils.useFirebaseConnections().includes("default"); // undefined app ≠ unconfigured: it is also undefined while identity signs in
|
|
102
|
+
if (!configured) return <p>Data service not configured for this store.</p>; // ALWAYS render this fallback
|
|
103
|
+
if (!app) return null; // configured, identity still settling — a loading state, never the error
|
|
104
|
+
const databaseId = (app.options as { databaseId?: string }).databaseId; // set when the store uses a NAMED database
|
|
105
|
+
const db = databaseId ? getFirestore(app, databaseId) : getFirestore(app);
|
|
106
|
+
ALWAYS open Firestore with those two lines, never a bare getFirestore(app): the merchant may point the connection at a named database via the optional `databaseId` field, and a bare call silently reads the wrong one. NEVER call initializeApp — the host owns the Firebase app and its credentials. `cb.utils.useFirebaseConnections()` lists the configured connection names. Unsubscribe every `onSnapshot` in the effect cleanup. Requests are anonymous (no request.auth): the Security Rules of the connected project are the only access control, so treat everything readable as public and never write customer PII, order or payment data.
|
|
107
|
+
- Data connections are declared in config.json: { "connections": [{ "name": "default", "provider": "firebase", "scope": "store", "label": "Store data" }] }. scope "store" = the merchant fills the config per store in the admin — do NOT put a "config" on it; scope "app" = fixed app-owned config — "config" ({ apiKey, projectId, … }) is required. If scope is omitted it is inferred from the shape (a "config" present → app, absent → store); write it explicitly anyway. For preview/testing, scratch overrides go in config.development.json using the SAME { "connections": [...] } shape, matched to declarations by "name": { "connections": [{ "name": "default", "config": { "apiKey": "…", "projectId": "…" } }] } — that file NEVER deploys, so real credentials for testing belong there, never in config.json unless the connection is genuinely app-owned.
|
|
108
|
+
- You MAY import from these packages only — already installed, nothing to declare anywhere: "react", "react-dom", "firebase/app", "firebase/firestore", "platform-api" (types only — runtime access is cb.platform), "@commercebuild/ui" (types only — runtime access is cb.ui), "lucide-react" (types only — runtime access is cb.ui.icons), "@headlessui/react" (types only — cb.ui re-exports Disclosure/DisclosureButton/DisclosurePanel). Importing ANY other package breaks the build. For the type-only packages use `import type` (root or a subpath); their runtime objects are reached through the named `cb` member instead.
|
|
109
|
+
|
|
110
|
+
## Rules
|
|
111
|
+
1. Only import from the allowlisted packages above; everything else must come through the `cb` contract. A wrong import fails the build.
|
|
112
|
+
2. Do NOT use bare component/type names from the contract — only `cb` and the types `Category`/`Product` are global. Everything else is `cb.com.*` / `cb.ui.*` / `cb.utils.*` / `cb.platform.*`, or an explicit `platform-api/*` type import.
|
|
113
|
+
3. Every index barrel (src/index.ts, src/cms/index.ts, src/pages/index.ts, and when the admin area exists src/admin/pages/index.ts — or src/components/index.ts in older apps) must have at least one export — add `export {};` if it would otherwise be empty, or TS reports "is not a module". Keep these re-exports consistent with the files you add or remove.
|
|
114
|
+
4. File and folder names: each path segment must match [A-Za-z0-9_][A-Za-z0-9._-]*, must not end with ".", and must not be a Windows-reserved name (con, prn, aux, nul, com1-9, lpt1-9). No spaces or unicode — an invalid name breaks the deploy build and the edit is silently dropped.
|
|
115
|
+
5. Never change commercebuild.json's "id".
|
|
116
|
+
6. This runs in the browser — client components only; no server-only Node APIs.
|
|
117
|
+
7. NEVER write `as any` (or an equivalent cast) on anything reached through `cb`, and never call a method that is not in the contract or the Platform API client list. In the online editor each of those blocks says whether it is complete — believe that statement rather than assuming either way; in a local checkout the installed declaration and SDK sources ARE the complete list. Where the list is complete, absence IS proof the method does not exist. Where it says it is incomplete, or names clients it could not read, or is missing altogether, say what you cannot see and ask, and do not guess method names, do not loop over candidate names or URL paths, and do not ship runtime introspection (logging a client's methods, probing endpoints) in place of reading the types. A cast that hides "property does not exist" turns a compile error into a silent runtime failure the user has to debug. This applies to DATA SHAPES as much as to methods: do not probe for fields that are not in the DTO (inventing `item.attributes`, `item.customAttributes`, `item.description2` and reading whichever one is defined). A field that does not exist reads as `undefined` rather than failing, so a guessed shape ships as a feature that is silently always empty — read the DTO and use the one real field.
|
|
118
|
+
|
|
119
|
+
## Where the truth is (local checkout)
|
|
120
|
+
- node_modules/@commercebuild/extension/types/global.d.ts is the complete `cb` surface. Only `cb`, `Category` and `Product` are bare globals; everything else is reached through `cb.*`. A member that is not in that file does not exist.
|
|
121
|
+
- node_modules/@commercebuild/platform-api/src/<namespace>/index.ts names each client's default version; src/<namespace>/v<n>.ts holds that version's methods, and every method's JSDoc names the HTTP call it makes. Import DTO types from "platform-api" — the project tsconfig aliases it to @commercebuild/platform-api.
|
|
122
|
+
- node_modules/@commercebuild/extension/config/host-libs.json lists the packages the host provides at runtime (`external`); those, plus the type-only packages named above, are the only imports the build accepts.
|
|
123
|
+
- Never write `as any` on anything reached through `cb`, and never guess a method or field name — open the file and read the real one.
|
|
124
|
+
|
|
125
|
+
## Working locally
|
|
126
|
+
- Dependencies are never installed for you: run `npm install` first (yarn and pnpm work too; the project's .npmrc hoists for pnpm).
|
|
127
|
+
- After every change run `npm run type-check` and fix ALL reported errors before you report back. `npm run build` is the same build the deploy pipeline runs.
|
|
128
|
+
- `npm run dev` previews the app against the store named in .env (COMMERCEBUILD_STORE_URL). .env is local only and is never uploaded.
|
|
129
|
+
- `npm run save` REPLACES the app's online draft with this folder — no merge, no conflict check, the last writer wins — so do not run it while someone may be editing the same app in the online editor. `npm run deploy` builds a new version (the editor's Deploy); `npm run release` builds and makes it live.
|
|
130
|
+
- config.development.json is for local preview only and never deploys. commercebuild.json's "id" must never change. Lockfiles and node_modules are never uploaded.
|
|
131
|
+
- The project-root AGENTS.md "Project notes" section holds the developer's own rules for this app; they win over this generic guide.
|
|
132
|
+
- If `npm run type-check` reports a missing script, the installed SDK predates this guide: `npm install -D @commercebuild/extension@latest`, then add `"type-check": "commercebuild-extension type-check"` to package.json scripts.
|
|
133
|
+
|
|
134
|
+
## Which tools read these files
|
|
135
|
+
| Tool | Reads | Action needed |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| Claude Code | CLAUDE.md (which imports AGENTS.md) | none |
|
|
138
|
+
| OpenAI Codex | AGENTS.md | none |
|
|
139
|
+
| Cursor | AGENTS.md | none |
|
|
140
|
+
| GitHub Copilot coding agent | AGENTS.md | none |
|
|
141
|
+
| Copilot Chat in VS Code | AGENTS.md when the `chat.useAgentsMdFile` setting is on | enable it if off |
|
|
142
|
+
| Gemini CLI | GEMINI.md by default | .gemini/settings.json: { "context": { "fileName": ["AGENTS.md", "GEMINI.md"] } }, or add a GEMINI.md that points at AGENTS.md |
|
|
143
|
+
| Windsurf, Cline, others | their own files | copy the project AGENTS.md pointer into that file |
|
package/package.json
CHANGED
package/scripts/cli.js
CHANGED
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
4
|
import { existsSync } from "fs";
|
|
5
|
+
import { createRequire } from "module";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import { fileURLToPath } from "url";
|
|
7
8
|
import { displayError } from "./utils.mjs";
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
const args = process.argv.slice(2);
|
|
12
|
+
// Resolve tools from THIS package, not from whatever the project hoisted.
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Absolute path of a dependency's `bin` script. Goes through its
|
|
17
|
+
* package.json ("./package.json" is exported by every package we need,
|
|
18
|
+
* while "./bin/*" usually is not) and reads the declared bin entry, so a
|
|
19
|
+
* relocation of the script inside the package cannot break us.
|
|
20
|
+
*/
|
|
21
|
+
function binOf(pkg, name) {
|
|
22
|
+
const manifestPath = require.resolve(`${pkg}/package.json`);
|
|
23
|
+
const manifest = require(manifestPath);
|
|
24
|
+
const bin =
|
|
25
|
+
typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[name];
|
|
26
|
+
if (!bin) throw new Error(`${pkg} declares no "${name}" bin`);
|
|
27
|
+
return path.join(path.dirname(manifestPath), bin);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const USAGE =
|
|
31
|
+
"Usage: commercebuild-extension <dev | build | type-check>\n" +
|
|
32
|
+
" dev build once, then serve the app with HMR for the storefront preview\n" +
|
|
33
|
+
" build bundle the app the way the deploy pipeline does\n" +
|
|
34
|
+
" type-check run the SDK's own TypeScript compiler over the project (tsc --noEmit)";
|
|
11
35
|
|
|
12
36
|
const viteConfigPath = path.resolve(__dirname, "../config/vite.config.mjs");
|
|
13
37
|
|
|
@@ -23,20 +47,51 @@ function hasAdminEntry() {
|
|
|
23
47
|
);
|
|
24
48
|
}
|
|
25
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Run vite without a shell: the project path goes to vite as one argument,
|
|
52
|
+
* so folders like "my-app(2)" or "My Apps" (spaces, parentheses — `open`
|
|
53
|
+
* numbers duplicate downloads that way) cannot be mis-parsed. Exit status
|
|
54
|
+
* is propagated by the caller through the thrown error.
|
|
55
|
+
*/
|
|
56
|
+
function vite(viteArgs, env = process.env) {
|
|
57
|
+
execFileSync(process.execPath, [binOf("vite", "vite"), ...viteArgs], {
|
|
58
|
+
stdio: "inherit",
|
|
59
|
+
env,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
function build() {
|
|
27
|
-
|
|
64
|
+
vite(["build", "--config", viteConfigPath]);
|
|
28
65
|
if (hasAdminEntry()) {
|
|
29
|
-
|
|
66
|
+
vite(["build", "--config", viteConfigPath], {
|
|
67
|
+
...process.env,
|
|
68
|
+
CB_BUILD_TARGET: "admin",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Type-check the project with THIS package's TypeScript — resolved from the
|
|
75
|
+
* SDK package, not from whatever the project happens to have hoisted — and
|
|
76
|
+
* exit with tsc's own status. `--noEmit` is explicit so a project tsconfig
|
|
77
|
+
* that overrides the base cannot make the check write files. No shell is
|
|
78
|
+
* involved, so the resolved path needs no quoting.
|
|
79
|
+
*/
|
|
80
|
+
function typeCheck() {
|
|
81
|
+
const tsc = binOf("typescript", "tsc");
|
|
82
|
+
try {
|
|
83
|
+
execFileSync(process.execPath, [tsc, "--noEmit", "-p", "."], {
|
|
30
84
|
stdio: "inherit",
|
|
31
|
-
env: { ...process.env, CB_BUILD_TARGET: "admin" },
|
|
32
85
|
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
process.exit(typeof error.status === "number" ? error.status : 1);
|
|
33
88
|
}
|
|
34
89
|
}
|
|
35
90
|
|
|
36
91
|
if (args.includes("dev")) {
|
|
37
92
|
try {
|
|
38
93
|
build();
|
|
39
|
-
|
|
94
|
+
vite(["--config", viteConfigPath]);
|
|
40
95
|
} catch (error) {
|
|
41
96
|
displayError("Failed to start commercebuild extension server:", error);
|
|
42
97
|
}
|
|
@@ -46,8 +101,11 @@ if (args.includes("dev")) {
|
|
|
46
101
|
} catch (error) {
|
|
47
102
|
displayError("Failed to build with commercebuild extension:", error);
|
|
48
103
|
}
|
|
104
|
+
} else if (args.includes("type-check")) {
|
|
105
|
+
typeCheck();
|
|
49
106
|
} else {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
107
|
+
// Exit non-zero: a script that silently "succeeds" on an unknown command
|
|
108
|
+
// looks like a passing check to a person or an AI tool reading the exit code.
|
|
109
|
+
displayError(USAGE);
|
|
110
|
+
process.exit(1);
|
|
53
111
|
}
|
package/types/global.d.ts
CHANGED
|
@@ -58,6 +58,14 @@ interface AddToCartProps {
|
|
|
58
58
|
disabled?: boolean;
|
|
59
59
|
onSuccessCallback?: () => void;
|
|
60
60
|
className?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Button scheme key for the CTA. A real key pins it (the Product Card
|
|
63
|
+
* passes its card scheme's key); `inherit` or nothing lands on the
|
|
64
|
+
* default scheme — the pre-ticket-04 behaviour. Only one
|
|
65
|
+
* `button-scheme-*` class may land on the element (compound selectors),
|
|
66
|
+
* so callers must never ALSO pass a scheme class in `className`.
|
|
67
|
+
*/
|
|
68
|
+
buttonScheme?: string;
|
|
61
69
|
/** Inline style forwarded to the button (e.g. Product Card button-scheme vars). */
|
|
62
70
|
style?: CSSProperties;
|
|
63
71
|
/**
|
|
@@ -77,6 +85,7 @@ declare function AddToCart({
|
|
|
77
85
|
disabled,
|
|
78
86
|
onSuccessCallback,
|
|
79
87
|
className,
|
|
88
|
+
buttonScheme,
|
|
80
89
|
style,
|
|
81
90
|
btnVariant,
|
|
82
91
|
fullWidth,
|