@commercebuild/extension 0.0.16 → 0.0.18

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.
@@ -1,18 +1,11 @@
1
1
  {
2
2
  "//": "AUTO-GENERATED from packages/extension-host-libs — do not edit. Regenerate with `yarn generate-extension-host-types`.",
3
- "external": [
4
- "react",
5
- "react-dom",
6
- "firebase/app",
7
- "firebase/firestore"
8
- ],
3
+ "external": ["react", "react-dom", "firebase/app", "firebase/firestore"],
9
4
  "globals": {
10
5
  "react": "cb.lib.React",
11
6
  "react-dom": "cb.lib.ReactDOM",
12
7
  "firebase/app": "cb.lib.firebase.app",
13
8
  "firebase/firestore": "cb.lib.firebase.firestore"
14
9
  },
15
- "forbidden": [
16
- "firebase/firestore/lite"
17
- ]
10
+ "forbidden": ["firebase/firestore/lite"]
18
11
  }
@@ -5,7 +5,7 @@
5
5
  "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
6
  "module": "ESNext",
7
7
  "skipLibCheck": true,
8
- "moduleResolution": "node",
8
+ "moduleResolution": "bundler",
9
9
  "allowImportingTsExtensions": true,
10
10
  "resolveJsonModule": true,
11
11
  "isolatedModules": true,
@@ -66,6 +66,32 @@ function cbManifestPlugin() {
66
66
  used.add(spec);
67
67
  }
68
68
  }
69
+
70
+ // The admin invocation MERGES its per-bundle hostLibs into the
71
+ // manifest the storefront build just emitted — never a union
72
+ // list (a union would make every shopper preload admin-only host
73
+ // libs). The `admin` key's presence doubles as the "this upload
74
+ // has an admin bundle" signal for the store admin's loader.
75
+ if (buildTarget === "admin") {
76
+ const manifestPath = path.resolve(
77
+ process.cwd(),
78
+ "dist/cb-manifest.json",
79
+ );
80
+ if (!existsSync(manifestPath)) {
81
+ throw new Error(
82
+ "dist/cb-manifest.json is missing — the storefront build must run before the admin build",
83
+ );
84
+ }
85
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
86
+ manifest.admin = { hostLibs: [...used].sort() };
87
+ this.emitFile({
88
+ type: "asset",
89
+ fileName: "cb-manifest.json",
90
+ source: JSON.stringify(manifest, null, 2) + "\n",
91
+ });
92
+ return;
93
+ }
94
+
69
95
  let connections = [];
70
96
  let declaredHostLibs = [];
71
97
  try {
@@ -132,20 +158,114 @@ function forbiddenImportsPlugin() {
132
158
  };
133
159
  }
134
160
 
135
- const jsEntry = path.resolve(process.cwd(), "src/index.js");
136
- const tsEntry = path.resolve(process.cwd(), "src/index.ts");
137
- const entryFile = existsSync(tsEntry) ? tsEntry : jsEntry;
161
+ // UN-3206: the OPTIONAL admin area (src/admin/) compiles to a second,
162
+ // self-contained bundle — dist/admin.js + admin.css — rendered inside
163
+ // the store admin. CB_BUILD_TARGET=admin selects it; the CLI always
164
+ // runs the storefront build first and the admin one only when the
165
+ // admin entry exists (see scripts/cli.js). Both bundles share the
166
+ // CBExtension global/banner protocol: they execute in different
167
+ // documents and never collide.
168
+ const buildTarget =
169
+ process.env.CB_BUILD_TARGET === "admin" ? "admin" : "storefront";
170
+
171
+ function resolveEntry(dir) {
172
+ const ts = path.resolve(process.cwd(), dir, "index.ts");
173
+ const js = path.resolve(process.cwd(), dir, "index.js");
174
+ return existsSync(ts) ? ts : existsSync(js) ? js : null;
175
+ }
176
+
177
+ const storefrontEntry =
178
+ resolveEntry("src") ?? path.resolve(process.cwd(), "src/index.js");
179
+ const adminEntry = resolveEntry("src/admin");
180
+
181
+ if (buildTarget === "admin") {
182
+ if (!adminEntry) {
183
+ throw new Error(
184
+ "CB_BUILD_TARGET=admin but src/admin/index.ts does not exist",
185
+ );
186
+ }
187
+ if (!existsSync(path.resolve(process.cwd(), "src/admin/styles/index.css"))) {
188
+ throw new Error(
189
+ "src/admin/index.ts requires src/admin/styles/index.css (the admin stylesheet entry)",
190
+ );
191
+ }
192
+ }
193
+
194
+ const entryFile = buildTarget === "admin" ? adminEntry : storefrontEntry;
195
+
196
+ // Import boundary rules, mirroring the in-browser editor compiler in
197
+ // cb-store (apps/storeadmin extension-editor esbuild plugin) — the two
198
+ // pipelines MUST agree, or code that fails in editor preview would
199
+ // still publish:
200
+ // - src/admin/** may import only src/admin/** and src/shared/**;
201
+ // - src/shared/** may import only src/shared/**;
202
+ // - nothing outside src/admin/** may import src/admin/**.
203
+ // These keep each bundle's module set structurally clean, which is what
204
+ // makes the per-target Tailwind source scopes exact.
205
+ function projectRelative(file) {
206
+ const abs = path.resolve(file.split("?")[0]);
207
+ const rel = path.relative(process.cwd(), abs).split(path.sep).join("/");
208
+ return rel.startsWith("..") ? null : rel;
209
+ }
210
+
211
+ export function importBoundaryViolation(importer, resolved) {
212
+ const fromAdmin = importer.startsWith("src/admin/");
213
+ const fromShared = importer.startsWith("src/shared/");
214
+ const toAdmin = resolved.startsWith("src/admin/");
215
+ const toShared = resolved.startsWith("src/shared/");
216
+ if (fromAdmin && !toAdmin && !toShared) {
217
+ return "files under src/admin/ may only import src/admin/ or src/shared/ (the admin bundle must stay independent of storefront code)";
218
+ }
219
+ if (fromShared && !toShared) {
220
+ return "files under src/shared/ may only import src/shared/ (shared code is part of both bundles)";
221
+ }
222
+ if (!fromAdmin && toAdmin) {
223
+ return "only files under src/admin/ may import src/admin/ (admin code never ships in the storefront bundle)";
224
+ }
225
+ return null;
226
+ }
227
+
228
+ function importBoundaryPlugin() {
229
+ return {
230
+ name: "cb-import-boundaries",
231
+ enforce: "pre",
232
+ resolveId(source, importer) {
233
+ if (!importer || !source.startsWith(".")) return null;
234
+ const from = projectRelative(importer);
235
+ // Directory prefixes are all the rules need — a missing extension
236
+ // or /index resolution never changes which subtree a path is in.
237
+ const to = projectRelative(
238
+ path.resolve(path.dirname(importer.split("?")[0]), source),
239
+ );
240
+ if (!from || !to) return null;
241
+ const violation = importBoundaryViolation(from, to);
242
+ if (violation) {
243
+ throw new Error(
244
+ `Forbidden import of "${source}" from "${from}": ${violation}`,
245
+ );
246
+ }
247
+ return null;
248
+ },
249
+ };
250
+ }
138
251
 
139
252
  const baseBuildConfig = {
140
253
  lib: {
141
254
  entry: entryFile,
142
255
  name: "CBExtension",
143
256
  formats: ["umd"],
144
- fileName: (format, entryName) =>
145
- `${entryName}.${format === "es" ? "mjs" : "js"}`,
257
+ // Both entries are index.ts, so the entry name can't distinguish
258
+ // the bundles name the artifacts by target explicitly.
259
+ fileName: () => (buildTarget === "admin" ? "admin.js" : "index.js"),
260
+ cssFileName: buildTarget === "admin" ? "admin" : "style",
146
261
  },
147
262
  minify: "esbuild",
148
263
  sourcemap: false,
264
+ // The admin invocation writes into the dist/ the storefront build
265
+ // just produced — vite's default would empty it and delete
266
+ // index.js/style.css/cb-manifest.json (which the manifest merge
267
+ // below depends on).
268
+ emptyOutDir: buildTarget !== "admin",
149
269
  rollupOptions: {
150
270
  external: hostLibs.external,
151
271
  output: {
@@ -153,7 +273,9 @@ const baseBuildConfig = {
153
273
  globals: hostLibs.globals,
154
274
  assetFileNames: (assetInfo) => {
155
275
  if (assetInfo.name.endsWith(".css")) {
156
- return assetInfo.originalFileName || assetInfo.name || "asset.css";
276
+ // The lib css asset is named per target here — both entries
277
+ // are index.ts, so nothing upstream distinguishes them.
278
+ return buildTarget === "admin" ? "admin.css" : "style.css";
157
279
  }
158
280
  return assetInfo.name;
159
281
  },
@@ -164,6 +286,7 @@ const baseBuildConfig = {
164
286
  const prodConfig = {
165
287
  plugins: [
166
288
  forbiddenImportsPlugin(),
289
+ importBoundaryPlugin(),
167
290
  cbManifestPlugin(),
168
291
  react({
169
292
  jsxRuntime: "classic",
@@ -205,6 +328,7 @@ export default defineConfig(({ mode, command }) => {
205
328
  root: ".",
206
329
  plugins: [
207
330
  forbiddenImportsPlugin(),
331
+ importBoundaryPlugin(),
208
332
  cbManifestPlugin(),
209
333
  ViteHMRNotifierPlugin(),
210
334
  react({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercebuild/extension",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "types": "./types/index.d.ts",
5
5
  "exports": {
6
6
  ".": {
@@ -10,8 +10,8 @@
10
10
  "./config/tsconfig.extension.json": "./config/tsconfig.extension.json"
11
11
  },
12
12
  "dependencies": {
13
- "@commercebuild/platform-api": "^0.*",
14
- "@commercebuild/ui": "^0.*",
13
+ "@commercebuild/platform-api": "^0.11.33",
14
+ "@commercebuild/ui": "~0.0.9",
15
15
  "@headlessui/react": "^2.2.4",
16
16
  "@tailwindcss/postcss": "^4.1.10",
17
17
  "@tailwindcss/vite": "^4.1.11",
@@ -34,5 +34,8 @@
34
34
  },
35
35
  "bin": {
36
36
  "commercebuild-extension": "./scripts/cli.js"
37
+ },
38
+ "scripts": {
39
+ "test": "node scripts/test-build.mjs"
37
40
  }
38
- }
41
+ }
@@ -19,9 +19,7 @@
19
19
  "$defs": {
20
20
  "connection": {
21
21
  "type": "object",
22
- "required": [
23
- "name"
24
- ],
22
+ "required": ["name"],
25
23
  "properties": {
26
24
  "name": {
27
25
  "type": "string",
@@ -30,16 +28,11 @@
30
28
  },
31
29
  "provider": {
32
30
  "description": "Backing service. Absent means \"firebase\".",
33
- "enum": [
34
- "firebase"
35
- ]
31
+ "enum": ["firebase"]
36
32
  },
37
33
  "scope": {
38
34
  "description": "\"store\" (the default): the merchant supplies the config per store under Installed Apps → Configure data — do not put a config here. \"app\": the config is fixed in this declaration (required below), owned by the app and not merchant-overridable.",
39
- "enum": [
40
- "store",
41
- "app"
42
- ],
35
+ "enum": ["store", "app"],
43
36
  "default": "store"
44
37
  },
45
38
  "label": {
@@ -63,14 +56,10 @@
63
56
  "const": "app"
64
57
  }
65
58
  },
66
- "required": [
67
- "scope"
68
- ]
59
+ "required": ["scope"]
69
60
  },
70
61
  "then": {
71
- "required": [
72
- "config"
73
- ]
62
+ "required": ["config"]
74
63
  }
75
64
  }
76
65
  ]
@@ -78,18 +67,13 @@
78
67
  "firebaseConfig": {
79
68
  "type": "object",
80
69
  "description": "Firebase web config (the firebaseConfig snippet from the Firebase console). Public by design — access control lives in the project's Security Rules.",
81
- "required": [
82
- "apiKey",
83
- "projectId"
84
- ],
70
+ "required": ["apiKey", "projectId"],
85
71
  "properties": {
86
72
  "apiKey": {
87
- "type": "string",
88
- "minLength": 1
73
+ "type": "string"
89
74
  },
90
75
  "projectId": {
91
- "type": "string",
92
- "minLength": 1
76
+ "type": "string"
93
77
  },
94
78
  "authDomain": {
95
79
  "type": "string"
package/scripts/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { execSync } from "child_process";
4
+ import { existsSync } from "fs";
4
5
  import path from "path";
5
6
  import { fileURLToPath } from "url";
6
7
  import { displayError } from "./utils.mjs";
@@ -8,18 +9,40 @@ const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const args = process.argv.slice(2);
10
11
 
12
+ const viteConfigPath = path.resolve(__dirname, "../config/vite.config.mjs");
13
+
14
+ // Two independent bundles from one project: the storefront build always
15
+ // runs; the admin build runs only when the OPTIONAL admin area exists,
16
+ // and always AFTER the storefront one — its cb-manifest plugin merges
17
+ // an `admin` section into the manifest the storefront build emitted,
18
+ // and it keeps dist/ intact (emptyOutDir: false in the vite config).
19
+ function hasAdminEntry() {
20
+ return (
21
+ existsSync(path.resolve(process.cwd(), "src/admin/index.ts")) ||
22
+ existsSync(path.resolve(process.cwd(), "src/admin/index.js"))
23
+ );
24
+ }
25
+
26
+ function build() {
27
+ execSync(`vite build --config ${viteConfigPath}`, { stdio: "inherit" });
28
+ if (hasAdminEntry()) {
29
+ execSync(`vite build --config ${viteConfigPath}`, {
30
+ stdio: "inherit",
31
+ env: { ...process.env, CB_BUILD_TARGET: "admin" },
32
+ });
33
+ }
34
+ }
35
+
11
36
  if (args.includes("dev")) {
12
37
  try {
13
- const viteConfigPath = path.resolve(__dirname, "../config/vite.config.mjs");
14
- execSync(`vite build --config ${viteConfigPath}`, { stdio: "inherit" });
38
+ build();
15
39
  execSync(`vite --config ${viteConfigPath}`, { stdio: "inherit" });
16
40
  } catch (error) {
17
41
  displayError("Failed to start commercebuild extension server:", error);
18
42
  }
19
43
  } else if (args.includes("build")) {
20
44
  try {
21
- const viteConfigPath = path.resolve(__dirname, "../config/vite.config.mjs");
22
- execSync(`vite build --config ${viteConfigPath}`, { stdio: "inherit" });
45
+ build();
23
46
  } catch (error) {
24
47
  displayError("Failed to build with commercebuild extension:", error);
25
48
  }
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Integration test for the dual-bundle build (UN-3206): builds a scratch
4
+ * fixture project with the real vite pipeline and asserts the artifact
5
+ * contract the hosts depend on — storefront trio untouched by the admin
6
+ * invocation, per-target CSS scan scopes, cb-manifest merge semantics,
7
+ * and the import boundary rules. Run with `node scripts/test-build.mjs`.
8
+ *
9
+ * These scopes/rules MUST stay in lockstep with the in-browser editor
10
+ * compiler in cb-store (apps/storeadmin extension-editor) — this test is
11
+ * the canonical side of that parity contract.
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import { execFileSync } from "node:child_process";
15
+ import {
16
+ cpSync,
17
+ existsSync,
18
+ mkdirSync,
19
+ mkdtempSync,
20
+ readFileSync,
21
+ rmSync,
22
+ symlinkSync,
23
+ writeFileSync,
24
+ } from "node:fs";
25
+ import { tmpdir } from "node:os";
26
+ import path from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+
29
+ const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
30
+ const viteBin = path.join(pkgRoot, "node_modules", ".bin", "vite");
31
+ const viteConfig = path.join(pkgRoot, "config", "vite.config.mjs");
32
+
33
+ const fixture = mkdtempSync(path.join(tmpdir(), "cb-ext-build-"));
34
+
35
+ function write(rel, content) {
36
+ const target = path.join(fixture, rel);
37
+ mkdirSync(path.dirname(target), { recursive: true });
38
+ writeFileSync(target, content);
39
+ }
40
+
41
+ const STOREFRONT_CSS = `@reference "tailwindcss/theme.css";
42
+ @import "tailwindcss/utilities.css" layer(utilities) source(none);
43
+ @config "@commercebuild/extension/config/tailwind.config.js";
44
+ @source "../";
45
+ @source not "../admin";
46
+ `;
47
+
48
+ const ADMIN_CSS = `@reference "tailwindcss/theme.css";
49
+ @import "tailwindcss/utilities.css" layer(utilities) source(none);
50
+ @config "@commercebuild/extension/config/tailwind.config.js";
51
+ @source "../";
52
+ @source "../../shared";
53
+ `;
54
+
55
+ function scaffold() {
56
+ write(
57
+ "commercebuild.json",
58
+ JSON.stringify({
59
+ id: "fixture",
60
+ name: "Fixture",
61
+ type: "extension",
62
+ description: "",
63
+ version: "0.0.1",
64
+ cbApiVersion: "^0.6",
65
+ }),
66
+ );
67
+ write(
68
+ "src/index.ts",
69
+ 'import "./styles/index.css";\nexport * as components from "./cms";\nexport * as pages from "./pages";\n',
70
+ );
71
+ write("src/cms/index.ts", "export {};\n");
72
+ write("src/pages/index.ts", 'export * as Shop from "./shop";\n');
73
+ write(
74
+ "src/pages/shop.tsx",
75
+ 'import React from "react";\nimport { shared } from "../shared/util";\nexport default function Shop() {\n const [n] = React.useState(0);\n return <div className="p-1">{shared()}{n}</div>;\n}\n',
76
+ );
77
+ write(
78
+ "src/shared/util.tsx",
79
+ 'import React from "react";\nexport function shared() {\n return <span className="p-2">shared</span>;\n}\n',
80
+ );
81
+ write("src/styles/index.css", STOREFRONT_CSS);
82
+ write(
83
+ "src/admin/index.ts",
84
+ 'import "./styles/index.css";\nexport * as adminPages from "./pages";\n',
85
+ );
86
+ write("src/admin/pages/index.ts", 'export * as Dashboard from "./dashboard";\n');
87
+ write(
88
+ "src/admin/pages/dashboard.tsx",
89
+ 'import React from "react";\nimport { getFirestore } from "firebase/firestore";\nimport { shared } from "../../shared/util";\nexport default function Dashboard() {\n const [n] = React.useState(0);\n return <div className="p-3">{String(!!getFirestore)}{shared()}{n}</div>;\n}\n',
90
+ );
91
+ write("src/admin/styles/index.css", ADMIN_CSS);
92
+
93
+ // The fixture resolves the package (for @config) and tailwindcss (for
94
+ // the layer stylesheets) through symlinks into this repo's own
95
+ // installation — no network install.
96
+ const nm = path.join(fixture, "node_modules");
97
+ mkdirSync(path.join(nm, "@commercebuild"), { recursive: true });
98
+ symlinkSync(pkgRoot, path.join(nm, "@commercebuild", "extension"));
99
+ // pnpm layout: this entry is itself a symlink into the store — a
100
+ // symlink to a symlink resolves fine.
101
+ symlinkSync(
102
+ path.join(pkgRoot, "node_modules", "tailwindcss"),
103
+ path.join(nm, "tailwindcss"),
104
+ );
105
+ }
106
+
107
+ function build(env = {}) {
108
+ execFileSync(viteBin, ["build", "--config", viteConfig], {
109
+ cwd: fixture,
110
+ stdio: "pipe",
111
+ env: { ...process.env, ...env },
112
+ });
113
+ }
114
+
115
+ function dist(name) {
116
+ return readFileSync(path.join(fixture, "dist", name), "utf8");
117
+ }
118
+
119
+ try {
120
+ scaffold();
121
+
122
+ // --- Dual build: storefront first, then admin (matching cli.js). ---
123
+ build();
124
+ build({ CB_BUILD_TARGET: "admin" });
125
+
126
+ for (const file of [
127
+ "index.js",
128
+ "style.css",
129
+ "admin.js",
130
+ "admin.css",
131
+ "cb-manifest.json",
132
+ ]) {
133
+ assert.ok(
134
+ existsSync(path.join(fixture, "dist", file)),
135
+ `dist/${file} exists after the dual build`,
136
+ );
137
+ }
138
+
139
+ // Per-target CSS scan scopes (p-1=storefront page, p-2=shared, p-3=admin).
140
+ const style = dist("style.css");
141
+ assert.match(style, /\.p-1\b/, "style.css has storefront utilities");
142
+ assert.match(style, /\.p-2\b/, "style.css has shared utilities");
143
+ assert.doesNotMatch(style, /\.p-3\b/, "style.css has NO admin utilities");
144
+ const adminCss = dist("admin.css");
145
+ assert.doesNotMatch(adminCss, /\.p-1\b/, "admin.css has NO storefront utilities");
146
+ assert.match(adminCss, /\.p-2\b/, "admin.css has shared utilities");
147
+ assert.match(adminCss, /\.p-3\b/, "admin.css has admin utilities");
148
+
149
+ // Manifest merge: storefront fields retained, admin.hostLibs per-bundle.
150
+ const manifest = JSON.parse(dist("cb-manifest.json"));
151
+ assert.equal(manifest.cbApiVersion, "^0.6");
152
+ assert.deepEqual(manifest.hostLibs, ["react"]);
153
+ assert.deepEqual(manifest.admin, {
154
+ hostLibs: ["firebase/firestore", "react"],
155
+ });
156
+
157
+ // Both bundles carry the meta banner and the shared global protocol.
158
+ assert.match(dist("admin.js"), /__CBExtensionMeta/);
159
+ assert.match(dist("admin.js"), /CBExtension/);
160
+ assert.match(dist("index.js"), /__CBExtensionMeta/);
161
+
162
+ // --- Boundary rules fail the build, all three directions. ---
163
+ const violations = [
164
+ [
165
+ "src/admin/pages/bad.ts",
166
+ 'export { x } from "../../cms/bad-target";\n',
167
+ "src/admin/pages/index.ts",
168
+ './bad',
169
+ ],
170
+ [
171
+ "src/pages/bad.ts",
172
+ 'export { helper } from "../admin/helper";\n',
173
+ "src/pages/index.ts",
174
+ './bad',
175
+ ],
176
+ [
177
+ "src/shared/bad.ts",
178
+ 'export { helper } from "../admin/helper";\n',
179
+ "src/pages/index.ts",
180
+ '../shared/bad',
181
+ ],
182
+ ];
183
+ write("src/cms/bad-target.ts", "export const x = 1;\n");
184
+ write("src/admin/helper.ts", "export const helper = 1;\n");
185
+ for (const [file, source, barrel, specifier] of violations) {
186
+ write(file, source);
187
+ const original = readFileSync(path.join(fixture, barrel), "utf8");
188
+ write(barrel, `${original}export * from "${specifier}";\n`);
189
+ let failed = false;
190
+ try {
191
+ build(file.startsWith("src/admin/") ? { CB_BUILD_TARGET: "admin" } : {});
192
+ } catch (err) {
193
+ failed = true;
194
+ assert.match(
195
+ String(err.stderr ?? err),
196
+ /Forbidden import/,
197
+ `${file} violation names the rule`,
198
+ );
199
+ }
200
+ assert.ok(failed, `${file} boundary violation fails the build`);
201
+ rmSync(path.join(fixture, file));
202
+ write(barrel, original);
203
+ }
204
+
205
+ // --- Admin-less project: classic three artifacts, nothing extra. ---
206
+ rmSync(path.join(fixture, "src/admin"), { recursive: true });
207
+ rmSync(path.join(fixture, "dist"), { recursive: true });
208
+ build();
209
+ assert.ok(existsSync(path.join(fixture, "dist", "index.js")));
210
+ assert.ok(existsSync(path.join(fixture, "dist", "style.css")));
211
+ assert.ok(!existsSync(path.join(fixture, "dist", "admin.js")));
212
+ assert.ok(!existsSync(path.join(fixture, "dist", "admin.css")));
213
+ assert.equal(JSON.parse(dist("cb-manifest.json")).admin, undefined);
214
+ // Admin invocation without an admin entry is an explicit error.
215
+ assert.throws(
216
+ () => build({ CB_BUILD_TARGET: "admin" }),
217
+ /src\/admin\/index\.ts/,
218
+ );
219
+
220
+ console.log("extension dual-build integration test: OK");
221
+ } finally {
222
+ rmSync(fixture, { recursive: true, force: true });
223
+ }
package/types/css.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ // Stylesheet imports are side-effect only: the build extracts them into
2
+ // the bundle's CSS artifact (style.css / admin.css) and they resolve to
3
+ // no JS value. This ambient declaration is what lets the entry barrels'
4
+ // `import "./styles/index.css"` type-check — newer TypeScript
5
+ // (noUncheckedSideEffectImports) errors on side-effect imports that
6
+ // resolve to neither a module nor a type declaration.
7
+ declare module "*.css";
package/types/global.d.ts CHANGED
@@ -8,7 +8,12 @@ import React__default, { CSSProperties } from "react";
8
8
  import * as ReactDOM from "react-dom";
9
9
  import * as FirebaseAppNs from "firebase/app";
10
10
  import * as FirebaseFirestoreNs from "firebase/firestore";
11
- import { cart_v2, catalog_v1, Platform, customer_v1 } from "@commercebuild/platform-api";
11
+ import {
12
+ cart_v2,
13
+ catalog_v1,
14
+ Platform,
15
+ customer_v1,
16
+ } from "@commercebuild/platform-api";
12
17
  import * as icons from "lucide-react";
13
18
  import * as uis from "@commercebuild/ui";
14
19
  import {
@@ -54,6 +59,12 @@ interface AddToCartProps {
54
59
  className?: string;
55
60
  /** Inline style forwarded to the button (e.g. Product Card button-scheme vars). */
56
61
  style?: CSSProperties;
62
+ /**
63
+ * Button-scheme variant forwarded to the button, which opts it into the
64
+ * `data-btn-variant` colour and hover-animation rules in globals.css. Only
65
+ * set by callers that also apply a `button-scheme-{key}` class.
66
+ */
67
+ btnVariant?: "primary" | "secondary" | "outline";
57
68
  /**
58
69
  * Width control (opt-in). Omit for the default responsive width; `true` = full
59
70
  * width at all breakpoints; `false` = content width (auto).
@@ -66,6 +77,7 @@ declare function AddToCart({
66
77
  onSuccessCallback,
67
78
  className,
68
79
  style,
80
+ btnVariant,
69
81
  fullWidth,
70
82
  }: AddToCartProps): React.JSX.Element;
71
83
 
package/types/index.d.ts CHANGED
@@ -1,3 +1,2 @@
1
1
  import "./global";
2
- import "./product";
3
- import "./category";
2
+ import "./css";
@@ -1,7 +0,0 @@
1
- import { catalog_v1 } from "@commercebuild/platform-api";
2
-
3
- declare global {
4
- type Category = catalog_v1.CatalogHierarchyBrandCategoryDto;
5
- }
6
-
7
- export {};
@@ -1,14 +0,0 @@
1
- import { catalog_v1 } from "@commercebuild/platform-api";
2
-
3
- declare global {
4
- interface Product extends catalog_v1.CatalogProductSearchProduct {
5
- image: Required<
6
- Pick<catalog_v1.CatalogProductSearchProduct["details"]["images"], "main">
7
- >["main"];
8
- originalPrice?: catalog_v1.CatalogPriceItemDto;
9
- formattedPrice: string;
10
- href: string;
11
- }
12
- }
13
-
14
- export {};