@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.
- package/config/host-libs.json +2 -9
- package/config/tsconfig.extension.json +1 -1
- package/config/vite.config.mjs +130 -6
- package/package.json +7 -4
- package/schemas/config.schema.json +8 -24
- 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 +13 -1
- package/types/index.d.ts +1 -2
- package/types/category.d.ts +0 -7
- package/types/product.d.ts +0 -14
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.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": "
|
|
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
|
-
|
|
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 {
|
|
@@ -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
package/types/category.d.ts
DELETED
package/types/product.d.ts
DELETED
|
@@ -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 {};
|