@commercebuild/extension 0.0.17 → 0.0.19
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/config/host-libs.json +2 -9
- package/config/tsconfig.extension.json +1 -1
- package/config/vite.config.mjs +130 -6
- package/package.json +6 -2
- package/schemas/config.schema.json +16 -26
- package/scripts/cli.js +27 -4
- package/scripts/test-build.mjs +223 -0
- package/types/css.d.ts +7 -0
- package/types/global.d.ts +254 -24
- package/types/index.d.ts +1 -0
package/config/host-libs.json
CHANGED
|
@@ -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
|
}
|
package/config/vite.config.mjs
CHANGED
|
@@ -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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"types": "./types/index.d.ts",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -34,5 +34,9 @@
|
|
|
34
34
|
},
|
|
35
35
|
"bin": {
|
|
36
36
|
"commercebuild-extension": "./scripts/cli.js"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node scripts/test-build.mjs",
|
|
40
|
+
"release": "npm whoami || npm login && pnpm publish --no-git-checks"
|
|
37
41
|
}
|
|
38
|
-
}
|
|
42
|
+
}
|
|
@@ -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,17 +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
|
-
"description": "\"store\"
|
|
39
|
-
"enum": [
|
|
40
|
-
"store",
|
|
41
|
-
"app"
|
|
42
|
-
],
|
|
43
|
-
"default": "store"
|
|
34
|
+
"description": "\"store\": 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. Omitted: inferred — a declaration with a \"config\" is app-scoped, one without is store-scoped.",
|
|
35
|
+
"enum": ["store", "app"]
|
|
44
36
|
},
|
|
45
37
|
"label": {
|
|
46
38
|
"type": "string",
|
|
@@ -52,6 +44,10 @@
|
|
|
52
44
|
},
|
|
53
45
|
"config": {
|
|
54
46
|
"$ref": "#/$defs/firebaseConfig"
|
|
47
|
+
},
|
|
48
|
+
"oidcEnabled": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"description": "Federate the signed-in shopper's CB Store identity into this connection's Firebase Auth (Firestore Rules then read the cb_* claims). App-scoped declarations only — the merchant accepts an identity-sharing notice once at install/first configuration. On a store-scoped connection the merchant controls this from Installed Apps → Configure data instead, and a flag here is ignored."
|
|
55
51
|
}
|
|
56
52
|
},
|
|
57
53
|
"additionalProperties": false,
|
|
@@ -63,14 +59,10 @@
|
|
|
63
59
|
"const": "app"
|
|
64
60
|
}
|
|
65
61
|
},
|
|
66
|
-
"required": [
|
|
67
|
-
"scope"
|
|
68
|
-
]
|
|
62
|
+
"required": ["scope"]
|
|
69
63
|
},
|
|
70
64
|
"then": {
|
|
71
|
-
"required": [
|
|
72
|
-
"config"
|
|
73
|
-
]
|
|
65
|
+
"required": ["config"]
|
|
74
66
|
}
|
|
75
67
|
}
|
|
76
68
|
]
|
|
@@ -78,18 +70,13 @@
|
|
|
78
70
|
"firebaseConfig": {
|
|
79
71
|
"type": "object",
|
|
80
72
|
"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
|
-
],
|
|
73
|
+
"required": ["apiKey", "projectId"],
|
|
85
74
|
"properties": {
|
|
86
75
|
"apiKey": {
|
|
87
|
-
"type": "string"
|
|
88
|
-
"minLength": 1
|
|
76
|
+
"type": "string"
|
|
89
77
|
},
|
|
90
78
|
"projectId": {
|
|
91
|
-
"type": "string"
|
|
92
|
-
"minLength": 1
|
|
79
|
+
"type": "string"
|
|
93
80
|
},
|
|
94
81
|
"authDomain": {
|
|
95
82
|
"type": "string"
|
|
@@ -105,6 +92,9 @@
|
|
|
105
92
|
},
|
|
106
93
|
"measurementId": {
|
|
107
94
|
"type": "string"
|
|
95
|
+
},
|
|
96
|
+
"databaseId": {
|
|
97
|
+
"type": "string"
|
|
108
98
|
}
|
|
109
99
|
},
|
|
110
100
|
"additionalProperties": true
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
@@ -23,7 +28,7 @@ declare function useLanagueTranslater(): {
|
|
|
23
28
|
_t: (k: string, obj?: any) => string | undefined;
|
|
24
29
|
t: (k: string) => string;
|
|
25
30
|
tt: (k: string, obj?: any) => (kk: string) => string;
|
|
26
|
-
tmsg: (
|
|
31
|
+
tmsg: (kk: string) => string;
|
|
27
32
|
tlink: (uri: string) => string;
|
|
28
33
|
talink: (uri: string) => string;
|
|
29
34
|
trlink: (uri: string) => string;
|
|
@@ -43,6 +48,7 @@ type CartRequestItem = Omit<
|
|
|
43
48
|
unitPrice?: number;
|
|
44
49
|
detailPageUrl?: string;
|
|
45
50
|
};
|
|
51
|
+
type PostItemsSearchRequest = catalog_v1.PostItemsSearchRequest;
|
|
46
52
|
type PostItemsSearchItem = catalog_v1.PostItemsSearchItem;
|
|
47
53
|
type PostItemsSearchResponse = catalog_v1.PostItemsSearchResponse;
|
|
48
54
|
type GetItemDetailResponse = catalog_v1.GetItemDetailResponse;
|
|
@@ -76,12 +82,95 @@ declare function AddToCart({
|
|
|
76
82
|
fullWidth,
|
|
77
83
|
}: AddToCartProps): React.JSX.Element;
|
|
78
84
|
|
|
85
|
+
interface GroupSectionItem {
|
|
86
|
+
sectionKey: "group";
|
|
87
|
+
id?: string;
|
|
88
|
+
visible?:
|
|
89
|
+
| boolean
|
|
90
|
+
| {
|
|
91
|
+
value?: boolean;
|
|
92
|
+
default?: boolean;
|
|
93
|
+
};
|
|
94
|
+
gap?: string;
|
|
95
|
+
justify?: string;
|
|
96
|
+
align?: string;
|
|
97
|
+
items?: SectionItem[];
|
|
98
|
+
}
|
|
99
|
+
interface LeafSectionItem {
|
|
100
|
+
sectionKey: string;
|
|
101
|
+
id?: string;
|
|
102
|
+
visible?:
|
|
103
|
+
| boolean
|
|
104
|
+
| {
|
|
105
|
+
value?: boolean;
|
|
106
|
+
default?: boolean;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* A leaf section that renders authored rich-text using the variable
|
|
111
|
+
* resolution pipeline. Carried by the `customText` discriminator so the
|
|
112
|
+
* variant-specific fields stay off the shared base.
|
|
113
|
+
*/
|
|
114
|
+
interface CustomTextLeafSectionItem extends LeafSectionItem {
|
|
115
|
+
sectionKey: "customText";
|
|
116
|
+
customLabel?: string;
|
|
117
|
+
customLabelVisible?: boolean;
|
|
118
|
+
customValue?: string;
|
|
119
|
+
}
|
|
120
|
+
type SectionItem =
|
|
121
|
+
| GroupSectionItem
|
|
122
|
+
| CustomTextLeafSectionItem
|
|
123
|
+
| LeafSectionItem;
|
|
124
|
+
|
|
125
|
+
interface StandardCardLeafField extends LeafSectionItem {
|
|
126
|
+
sectionKey:
|
|
127
|
+
| "image"
|
|
128
|
+
| "item_code"
|
|
129
|
+
| "title"
|
|
130
|
+
| "stock"
|
|
131
|
+
| "price"
|
|
132
|
+
| "add_to_cart";
|
|
133
|
+
}
|
|
134
|
+
type CardLeafField = StandardCardLeafField | CustomTextLeafSectionItem;
|
|
135
|
+
type CardField = CardLeafField | GroupSectionItem;
|
|
136
|
+
/**
|
|
137
|
+
* One column of a list-view row. Columns sit left-to-right; the entries inside
|
|
138
|
+
* a column stack vertically, and a `group` entry puts several of them on one
|
|
139
|
+
* row (the same primitive the tile card uses).
|
|
140
|
+
*/
|
|
141
|
+
interface CardColumn {
|
|
142
|
+
id?: string;
|
|
143
|
+
visible?:
|
|
144
|
+
| boolean
|
|
145
|
+
| {
|
|
146
|
+
value?: boolean;
|
|
147
|
+
default?: boolean;
|
|
148
|
+
};
|
|
149
|
+
/** Key into LIST_COLUMN_WIDTH. */
|
|
150
|
+
width?: string;
|
|
151
|
+
/** Key into LIST_COLUMN_ALIGN. */
|
|
152
|
+
align?: string;
|
|
153
|
+
items?: CardField[];
|
|
154
|
+
}
|
|
155
|
+
|
|
79
156
|
declare function CategoryProduct({
|
|
80
157
|
product,
|
|
81
158
|
imagePriority,
|
|
159
|
+
schemeKey,
|
|
160
|
+
fields,
|
|
161
|
+
listColumns,
|
|
162
|
+
imageSizes,
|
|
82
163
|
}: {
|
|
83
164
|
product: PostItemsSearchItem;
|
|
84
165
|
imagePriority?: boolean;
|
|
166
|
+
/** Product Card theme scheme chosen on the listing block. */
|
|
167
|
+
schemeKey?: string;
|
|
168
|
+
/** Per-block field override; undefined leaves the scheme in charge. */
|
|
169
|
+
fields?: CardField[];
|
|
170
|
+
/** List-view columns; undefined renders the stacked tile card. */
|
|
171
|
+
listColumns?: CardColumn[];
|
|
172
|
+
/** Overrides the card's default `sizes` (list thumbnails are much smaller). */
|
|
173
|
+
imageSizes?: string;
|
|
85
174
|
}): React.JSX.Element;
|
|
86
175
|
|
|
87
176
|
declare function CategoryBreadcrumb({
|
|
@@ -102,13 +191,22 @@ declare function CategorySidebar({
|
|
|
102
191
|
aggregation,
|
|
103
192
|
currentFilters,
|
|
104
193
|
isDrawer,
|
|
194
|
+
sections,
|
|
195
|
+
showCounts,
|
|
105
196
|
}: {
|
|
106
197
|
category: Category;
|
|
107
198
|
url: string;
|
|
108
199
|
aggregation?: PostItemsSearchResponse["aggregation"];
|
|
109
200
|
currentFilters: Record<string, string[]>;
|
|
110
201
|
isDrawer?: boolean;
|
|
111
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Structural elements to render, in order, from the listing block's
|
|
204
|
+
* `filterSections` repeater. Undefined keeps the historic order.
|
|
205
|
+
*/
|
|
206
|
+
sections?: string[];
|
|
207
|
+
/** Show the match count beside each facet name and value. */
|
|
208
|
+
showCounts?: boolean;
|
|
209
|
+
}): React__default.JSX.Element | null;
|
|
112
210
|
|
|
113
211
|
declare function CategorySort({
|
|
114
212
|
url,
|
|
@@ -125,9 +223,36 @@ declare function CategorySort({
|
|
|
125
223
|
declare function CategoryProducts({
|
|
126
224
|
hasSiderbar,
|
|
127
225
|
items,
|
|
226
|
+
columnsClass,
|
|
227
|
+
schemeKey,
|
|
228
|
+
fields,
|
|
229
|
+
listColumns,
|
|
230
|
+
autoScroll,
|
|
231
|
+
total,
|
|
128
232
|
}: {
|
|
129
233
|
hasSiderbar?: boolean;
|
|
130
234
|
items: PostItemsSearchItem[];
|
|
235
|
+
/**
|
|
236
|
+
* Literal Tailwind column classes from the listing block's frozen map.
|
|
237
|
+
* Never interpolated — `grid-cols-${n}` compiles to nothing.
|
|
238
|
+
* The default reproduces the pre-CMS grid exactly.
|
|
239
|
+
*/
|
|
240
|
+
columnsClass?: string;
|
|
241
|
+
schemeKey?: string;
|
|
242
|
+
fields?: CardField[];
|
|
243
|
+
/**
|
|
244
|
+
* List-view columns. Non-empty switches the grid to a single column of rows
|
|
245
|
+
* and hands each card its own layout — `columnsClass` is then unused.
|
|
246
|
+
*/
|
|
247
|
+
listColumns?: CardColumn[];
|
|
248
|
+
/**
|
|
249
|
+
* The route's own search request, replayed a page at a time as the shopper
|
|
250
|
+
* scrolls. `null` keeps the grid to the server-rendered page, which is what
|
|
251
|
+
* page-number pagination wants.
|
|
252
|
+
*/
|
|
253
|
+
autoScroll?: PostItemsSearchRequest | null;
|
|
254
|
+
/** Total matches, so auto-scroll knows when to stop. */
|
|
255
|
+
total?: number;
|
|
131
256
|
}): React.JSX.Element;
|
|
132
257
|
|
|
133
258
|
declare function CategoryProductsPageSize({
|
|
@@ -149,13 +274,120 @@ declare function ProductTitle({
|
|
|
149
274
|
product: GetItemDetailResponse;
|
|
150
275
|
}): React.JSX.Element;
|
|
151
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Shared Constants Types
|
|
279
|
+
*
|
|
280
|
+
* Type definitions for constants used across the theme editor.
|
|
281
|
+
* These establish common interfaces for Tailwind CSS constants and configuration.
|
|
282
|
+
*/
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Box Shadow Config Interface
|
|
286
|
+
* Configuration for box shadow utilities
|
|
287
|
+
* Consolidated from cms/utils/tailwind-utils.ts
|
|
288
|
+
*/
|
|
289
|
+
interface BoxShadowConfig {
|
|
290
|
+
preset?: string;
|
|
291
|
+
custom?: {
|
|
292
|
+
offsetX: string;
|
|
293
|
+
offsetY: string;
|
|
294
|
+
blurRadius: string;
|
|
295
|
+
spreadRadius?: string;
|
|
296
|
+
color: string;
|
|
297
|
+
opacity?: string;
|
|
298
|
+
inset?: boolean;
|
|
299
|
+
};
|
|
300
|
+
useCustom?: boolean;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Border Width Interface
|
|
305
|
+
* Interface for border width configuration
|
|
306
|
+
* Consolidated from cms/components/property/border-controller.tsx
|
|
307
|
+
*/
|
|
308
|
+
interface BorderWidth {
|
|
309
|
+
top: string;
|
|
310
|
+
right: string;
|
|
311
|
+
bottom: string;
|
|
312
|
+
left: string;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Border Radius Interface
|
|
316
|
+
* Interface for border radius configuration
|
|
317
|
+
* Consolidated from cms/components/property/border-controller.tsx
|
|
318
|
+
*/
|
|
319
|
+
interface BorderRadius {
|
|
320
|
+
topLeft: string;
|
|
321
|
+
topRight: string;
|
|
322
|
+
bottomRight: string;
|
|
323
|
+
bottomLeft: string;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Border Interface
|
|
327
|
+
* Interface for border configuration
|
|
328
|
+
* Consolidated from cms/components/property/border-controller.tsx
|
|
329
|
+
*/
|
|
330
|
+
interface Border {
|
|
331
|
+
width: BorderWidth;
|
|
332
|
+
style: string;
|
|
333
|
+
color: string;
|
|
334
|
+
/** Border color opacity 0–100 (UN-3049). Optional; undefined ⇒ fully opaque. */
|
|
335
|
+
opacity?: string;
|
|
336
|
+
radius: BorderRadius;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
type ImageFit = "cover" | "contain" | "fill" | "none";
|
|
340
|
+
/** Aspect vocabulary shared with the Product Card scheme (UN-3049). */
|
|
341
|
+
type ImageAspect = "square" | "landscape" | "portrait" | "wide";
|
|
342
|
+
interface ImageSurfaceSettings {
|
|
343
|
+
border?: Border;
|
|
344
|
+
shadow?: BoxShadowConfig;
|
|
345
|
+
bgColor?: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
152
348
|
type ThumbnailPosition = "top" | "right" | "bottom" | "left";
|
|
349
|
+
/**
|
|
350
|
+
* How the desktop thumbnail strip presents itself (UN-3295).
|
|
351
|
+
*
|
|
352
|
+
* `carousel` is the shipped behaviour: one scrollable rail with prev/next.
|
|
353
|
+
* `stack` shows a fixed number of thumbnails and expands the rest in place.
|
|
354
|
+
* Stack applies to `top` and `bottom` only — a `left`/`right` strip is one
|
|
355
|
+
* `--thumb-w` column wide (80px by default), too narrow for the control.
|
|
356
|
+
*/
|
|
357
|
+
type ThumbnailLayout = "carousel" | "stack";
|
|
358
|
+
/** Main-image controls (UN-3285): presentation + its own Border/Shadow. */
|
|
359
|
+
interface ProductImageDisplaySettings extends ImageSurfaceSettings {
|
|
360
|
+
aspect?: ImageAspect;
|
|
361
|
+
fit?: ImageFit;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Thumbnail controls (UN-3285). Width and height are independent px values;
|
|
365
|
+
* both default to the legacy 80px box. Desktop-only (sm+) — on mobile the
|
|
366
|
+
* carousel IS the gallery and its slides stay full-width.
|
|
367
|
+
*/
|
|
368
|
+
interface ProductThumbnailDisplaySettings extends ImageSurfaceSettings {
|
|
369
|
+
width?: number | "";
|
|
370
|
+
height?: number | "";
|
|
371
|
+
/** Gap between thumbnails, px. Unset = the 10px default; explicit 0 holds. */
|
|
372
|
+
gap?: number | "";
|
|
373
|
+
/** Gap between the strip and the main image, px. Unset = the 10px default. */
|
|
374
|
+
imageGap?: number | "";
|
|
375
|
+
fit?: ImageFit;
|
|
376
|
+
/** Desktop strip presentation (UN-3295). Unset = the shipped carousel. */
|
|
377
|
+
layout?: ThumbnailLayout;
|
|
378
|
+
/** Thumbnails shown before "Show more", stack layout only. Unset = 4. */
|
|
379
|
+
showMoreAfter?: number | "";
|
|
380
|
+
}
|
|
153
381
|
declare const ProductImages: ({
|
|
154
382
|
product,
|
|
155
383
|
thumbnailPosition,
|
|
384
|
+
imageSettings,
|
|
385
|
+
thumbnailSettings,
|
|
156
386
|
}: {
|
|
157
387
|
product: GetItemDetailResponse;
|
|
158
388
|
thumbnailPosition?: ThumbnailPosition;
|
|
389
|
+
imageSettings?: ProductImageDisplaySettings;
|
|
390
|
+
thumbnailSettings?: ProductThumbnailDisplaySettings;
|
|
159
391
|
}) => React.JSX.Element;
|
|
160
392
|
|
|
161
393
|
declare function ProductPrice({
|
|
@@ -306,31 +538,27 @@ interface FirebaseConnectionConfig {
|
|
|
306
538
|
messagingSenderId?: string;
|
|
307
539
|
appId?: string;
|
|
308
540
|
measurementId?: string;
|
|
541
|
+
/**
|
|
542
|
+
* Firestore database to use when the project has more than one. NOT part
|
|
543
|
+
* of the console's firebaseConfig snippet — the SDK has no notion of a
|
|
544
|
+
* default database in its options, so this rides along as an extra key
|
|
545
|
+
* (initializeApp keeps unknown keys on `app.options`) and the app passes
|
|
546
|
+
* it to `getFirestore(app, databaseId)`. Empty/absent = `(default)`.
|
|
547
|
+
*/
|
|
548
|
+
databaseId?: string;
|
|
309
549
|
}
|
|
310
550
|
|
|
311
551
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* and the extension's resolved per-store configuration. Contract hooks
|
|
321
|
-
* like cb.utils.useFirebaseApp() read it, so the same component gets
|
|
322
|
-
* its own extension's connections wherever it renders — including the
|
|
323
|
-
* synthetic "preview" / "local" registrations, which just carry their
|
|
324
|
-
* own scope value.
|
|
552
|
+
* Shared implementation — see @commercebuild/extension-host-libs
|
|
553
|
+
* (extension-scope.tsx). Only the TYPE re-exports below stay special:
|
|
554
|
+
* they are re-exported into the generated author-facing declaration,
|
|
555
|
+
* and tsup's dts build force-externalizes anything imported by package
|
|
556
|
+
* name (a storefront dependency) — authors can't install the workspace
|
|
557
|
+
* package, so its types must inline via a RELATIVE path. A type-only
|
|
558
|
+
* import is erased at runtime, so the bundler never sees this path;
|
|
559
|
+
* runtime members keep importing the package normally.
|
|
325
560
|
*/
|
|
326
561
|
|
|
327
|
-
/**
|
|
328
|
-
* The provider type family (config shapes, the ResolvedConnection
|
|
329
|
-
* discriminated union) is owned by the provider registry in
|
|
330
|
-
* @commercebuild/extension-host-libs — one place declares them for the
|
|
331
|
-
* storefront, storeadmin and the config JSON Schema alike. This module
|
|
332
|
-
* re-exports the contract-facing names.
|
|
333
|
-
*/
|
|
334
562
|
type CbFirebaseConfig = FirebaseConnectionConfig;
|
|
335
563
|
|
|
336
564
|
/**
|
|
@@ -567,7 +795,9 @@ interface CbHostApi {
|
|
|
567
795
|
* render a fallback for that case. With no argument: the store's
|
|
568
796
|
* only connection when exactly one is configured, else the one
|
|
569
797
|
* named "default". Never call initializeApp yourself; pass this app
|
|
570
|
-
* to the product entry points
|
|
798
|
+
* to the product entry points. Honour a named database:
|
|
799
|
+
* `const id = (app.options as { databaseId?: string }).databaseId;`
|
|
800
|
+
* `const db = id ? getFirestore(app, id) : getFirestore(app);`
|
|
571
801
|
*/
|
|
572
802
|
useFirebaseApp: (
|
|
573
803
|
connection?: string,
|
package/types/index.d.ts
CHANGED