@commercebuild/extension 0.0.14 → 0.0.16

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.
@@ -0,0 +1,18 @@
1
+ {
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
+ ],
9
+ "globals": {
10
+ "react": "cb.lib.React",
11
+ "react-dom": "cb.lib.ReactDOM",
12
+ "firebase/app": "cb.lib.firebase.app",
13
+ "firebase/firestore": "cb.lib.firebase.firestore"
14
+ },
15
+ "forbidden": [
16
+ "firebase/firestore/lite"
17
+ ]
18
+ }
@@ -3,7 +3,7 @@ import ViteHMRNotifierPlugin from "../scripts/vite-plugin-hmr-notifier.mjs";
3
3
  import react from "@vitejs/plugin-react";
4
4
  import tailwindcss from "@tailwindcss/vite";
5
5
  // import dts from "vite-plugin-dts";
6
- // import { fileURLToPath } from "url";
6
+ import { fileURLToPath } from "url";
7
7
  import path from "path";
8
8
  import { existsSync, readFileSync } from "fs";
9
9
  import chalk from "chalk";
@@ -30,8 +30,107 @@ const cbMetaBanner = cbApiVersion
30
30
  cbApiVersion,
31
31
  )}});}}catch(e){}})();`
32
32
  : "";
33
- // const __filename = fileURLToPath(import.meta.url);
34
- // const __dirname = dirname(__filename);
33
+
34
+ // Host-provided libraries (import specifier → cb.* global), generated
35
+ // from the cb-store host-lib registry so this build externalizes
36
+ // exactly what the storefront provides. Regenerated by cb-store's
37
+ // `yarn generate-extension-host-types`; do not edit host-libs.json.
38
+ const hostLibs = JSON.parse(
39
+ readFileSync(
40
+ path.join(path.dirname(fileURLToPath(import.meta.url)), "host-libs.json"),
41
+ "utf8",
42
+ ),
43
+ );
44
+
45
+ /**
46
+ * Emit dist/cb-manifest.json alongside the bundle: the host libs this
47
+ * build actually externalized (the storefront preloads the lazy ones
48
+ * BEFORE injecting index.js) plus the app's data-connection
49
+ * declarations copied from config.json. The manifest travels with the
50
+ * artifact — versioned by uploadId like index.js itself — so nothing
51
+ * app-side needs to be written to Firestore, and a store pinned to an
52
+ * old upload gets exactly that upload's declarations.
53
+ */
54
+ function cbManifestPlugin() {
55
+ return {
56
+ name: "cb-emit-manifest",
57
+ generateBundle(_options, bundle) {
58
+ // The chunk's own `imports` lists exactly the modules rollup left
59
+ // external — the ground truth of what the artifact expects the
60
+ // host to provide (a resolveId hook can't see them: rollup
61
+ // matches the `external` option before plugins run).
62
+ const used = new Set();
63
+ for (const output of Object.values(bundle)) {
64
+ if (output.type !== "chunk") continue;
65
+ for (const spec of [...output.imports, ...output.dynamicImports]) {
66
+ used.add(spec);
67
+ }
68
+ }
69
+ let connections = [];
70
+ let declaredHostLibs = [];
71
+ try {
72
+ const configPath = path.resolve(process.cwd(), "config.json");
73
+ if (existsSync(configPath)) {
74
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
75
+ if (Array.isArray(config.connections)) {
76
+ connections = config.connections.filter(
77
+ (c) => c && typeof c === "object" && typeof c.name === "string",
78
+ );
79
+ }
80
+ }
81
+ const manifestPath = path.resolve(process.cwd(), "commercebuild.json");
82
+ if (existsSync(manifestPath)) {
83
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
84
+ // Optional explicit override for anything the resolve hook
85
+ // can't see (kept for parity with the editor compiler).
86
+ if (Array.isArray(manifest.hostLibs)) {
87
+ declaredHostLibs = manifest.hostLibs.filter(
88
+ (s) => typeof s === "string",
89
+ );
90
+ }
91
+ }
92
+ } catch {
93
+ // A broken config fails the type-check/build on its own terms.
94
+ }
95
+ this.emitFile({
96
+ type: "asset",
97
+ fileName: "cb-manifest.json",
98
+ source:
99
+ JSON.stringify(
100
+ {
101
+ cbApiVersion,
102
+ hostLibs: [...new Set([...used, ...declaredHostLibs])].sort(),
103
+ connections,
104
+ },
105
+ null,
106
+ 2,
107
+ ) + "\n",
108
+ });
109
+ },
110
+ };
111
+ }
112
+
113
+ // Packages that must never be bundled OR externalized — each would
114
+ // silently conflict with a host lib at runtime (e.g. firestore lite
115
+ // registers the same 'firestore' component as the full SDK, and
116
+ // whichever loads first wins with no error).
117
+ function forbiddenImportsPlugin() {
118
+ const forbidden = new Set(hostLibs.forbidden ?? []);
119
+ return {
120
+ name: "cb-forbidden-imports",
121
+ enforce: "pre",
122
+ resolveId(source) {
123
+ if (forbidden.has(source)) {
124
+ throw new Error(
125
+ `"${source}" cannot be used in a commercebuild extension — ` +
126
+ `the host provides a conflicting implementation. ` +
127
+ `Import the host-provided module instead (see host-libs.json).`,
128
+ );
129
+ }
130
+ return null;
131
+ },
132
+ };
133
+ }
35
134
 
36
135
  const jsEntry = path.resolve(process.cwd(), "src/index.js");
37
136
  const tsEntry = path.resolve(process.cwd(), "src/index.ts");
@@ -48,13 +147,10 @@ const baseBuildConfig = {
48
147
  minify: "esbuild",
49
148
  sourcemap: false,
50
149
  rollupOptions: {
51
- external: ["react", "react-dom"],
150
+ external: hostLibs.external,
52
151
  output: {
53
152
  banner: cbMetaBanner,
54
- globals: {
55
- react: "cb.lib.React",
56
- "react-dom": "cb.lib.ReactDom",
57
- },
153
+ globals: hostLibs.globals,
58
154
  assetFileNames: (assetInfo) => {
59
155
  if (assetInfo.name.endsWith(".css")) {
60
156
  return assetInfo.originalFileName || assetInfo.name || "asset.css";
@@ -67,6 +163,8 @@ const baseBuildConfig = {
67
163
 
68
164
  const prodConfig = {
69
165
  plugins: [
166
+ forbiddenImportsPlugin(),
167
+ cbManifestPlugin(),
70
168
  react({
71
169
  jsxRuntime: "classic",
72
170
  }),
@@ -106,6 +204,8 @@ export default defineConfig(({ mode, command }) => {
106
204
  return {
107
205
  root: ".",
108
206
  plugins: [
207
+ forbiddenImportsPlugin(),
208
+ cbManifestPlugin(),
109
209
  ViteHMRNotifierPlugin(),
110
210
  react({
111
211
  jsxRuntime: "classic",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commercebuild/extension",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "types": "./types/index.d.ts",
5
5
  "exports": {
6
6
  ".": {
@@ -22,6 +22,7 @@
22
22
  "@vitejs/plugin-react": "^4.5.2",
23
23
  "chalk": "^5.4.1",
24
24
  "esbuild": "^0.25.6",
25
+ "firebase": "^12.2.1",
25
26
  "lucide-react": "^0.525.0",
26
27
  "react": "^19.1.0",
27
28
  "react-dom": "^19.1.0",
@@ -34,4 +35,4 @@
34
35
  "bin": {
35
36
  "commercebuild-extension": "./scripts/cli.js"
36
37
  }
37
- }
38
+ }
@@ -0,0 +1,113 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "commercebuild extension configuration",
4
+ "description": "Data-connection declarations (config.json) or development overrides (config.development.json — never deployed, matched to declarations by name).",
5
+ "type": "object",
6
+ "properties": {
7
+ "$schema": {
8
+ "type": "string"
9
+ },
10
+ "connections": {
11
+ "type": "array",
12
+ "description": "Named data connections. In config.json each entry declares a connection; in config.development.json each entry overrides the declaration with the same name (scratch names may be introduced for testing).",
13
+ "items": {
14
+ "$ref": "#/$defs/connection"
15
+ }
16
+ }
17
+ },
18
+ "additionalProperties": false,
19
+ "$defs": {
20
+ "connection": {
21
+ "type": "object",
22
+ "required": [
23
+ "name"
24
+ ],
25
+ "properties": {
26
+ "name": {
27
+ "type": "string",
28
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]*$",
29
+ "description": "Connection name the app code passes to cb.utils.useFirebaseApp(name). With exactly one connection configured, useFirebaseApp() with no argument resolves it; otherwise the no-argument form looks for \"default\"."
30
+ },
31
+ "provider": {
32
+ "description": "Backing service. Absent means \"firebase\".",
33
+ "enum": [
34
+ "firebase"
35
+ ]
36
+ },
37
+ "scope": {
38
+ "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
+ ],
43
+ "default": "store"
44
+ },
45
+ "label": {
46
+ "type": "string",
47
+ "description": "Shown as the connection's title in the merchant's config form."
48
+ },
49
+ "description": {
50
+ "type": "string",
51
+ "description": "Shown under the label in the merchant's config form."
52
+ },
53
+ "config": {
54
+ "$ref": "#/$defs/firebaseConfig"
55
+ }
56
+ },
57
+ "additionalProperties": false,
58
+ "allOf": [
59
+ {
60
+ "if": {
61
+ "properties": {
62
+ "scope": {
63
+ "const": "app"
64
+ }
65
+ },
66
+ "required": [
67
+ "scope"
68
+ ]
69
+ },
70
+ "then": {
71
+ "required": [
72
+ "config"
73
+ ]
74
+ }
75
+ }
76
+ ]
77
+ },
78
+ "firebaseConfig": {
79
+ "type": "object",
80
+ "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
+ ],
85
+ "properties": {
86
+ "apiKey": {
87
+ "type": "string",
88
+ "minLength": 1
89
+ },
90
+ "projectId": {
91
+ "type": "string",
92
+ "minLength": 1
93
+ },
94
+ "authDomain": {
95
+ "type": "string"
96
+ },
97
+ "storageBucket": {
98
+ "type": "string"
99
+ },
100
+ "messagingSenderId": {
101
+ "type": "string"
102
+ },
103
+ "appId": {
104
+ "type": "string"
105
+ },
106
+ "measurementId": {
107
+ "type": "string"
108
+ }
109
+ },
110
+ "additionalProperties": true
111
+ }
112
+ }
113
+ }
package/types/global.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  import * as React from "react";
7
7
  import React__default, { CSSProperties } from "react";
8
8
  import * as ReactDOM from "react-dom";
9
- import { cart_v2, catalog_v1, Platform } from "@commercebuild/platform-api";
9
+ import * as FirebaseAppNs from "firebase/app";
10
+ import * as FirebaseFirestoreNs from "firebase/firestore";
11
+ import { cart_v2, catalog_v1, Platform, customer_v1 } from "@commercebuild/platform-api";
10
12
  import * as icons from "lucide-react";
11
13
  import * as uis from "@commercebuild/ui";
12
14
  import {
@@ -283,6 +285,47 @@ declare function revalidatePath(
283
285
  ): Promise<void>;
284
286
  declare function revalidate(): Promise<void>;
285
287
 
288
+ /**
289
+ * Firebase web config (the firebaseConfig snippet from the Firebase
290
+ * console). Not a secret — it ships to every browser by design; access
291
+ * control lives entirely in the project's Security Rules. Keys must
292
+ * stay in sync with PROVIDER_FIELDS.firebase below.
293
+ */
294
+ interface FirebaseConnectionConfig {
295
+ apiKey: string;
296
+ projectId: string;
297
+ authDomain?: string;
298
+ storageBucket?: string;
299
+ messagingSenderId?: string;
300
+ appId?: string;
301
+ measurementId?: string;
302
+ }
303
+
304
+ /**
305
+ * Per-extension render scope.
306
+ *
307
+ * Extension code has no runtime identity of its own — components are
308
+ * bare functions and `cb` is one page-global object, while a single
309
+ * React tree can hold components from several different extensions at
310
+ * once. This context supplies that identity lexically: the host wraps
311
+ * every extension render site (renderExtensionComponent's boundary and
312
+ * the extension page route) in a provider carrying the registration id
313
+ * and the extension's resolved per-store configuration. Contract hooks
314
+ * like cb.utils.useFirebaseApp() read it, so the same component gets
315
+ * its own extension's connections wherever it renders — including the
316
+ * synthetic "preview" / "local" registrations, which just carry their
317
+ * own scope value.
318
+ */
319
+
320
+ /**
321
+ * The provider type family (config shapes, the ResolvedConnection
322
+ * discriminated union) is owned by the provider registry in
323
+ * @commercebuild/extension-host-libs — one place declares them for the
324
+ * storefront, storeadmin and the config JSON Schema alike. This module
325
+ * re-exports the contract-facing names.
326
+ */
327
+ type CbFirebaseConfig = FirebaseConnectionConfig;
328
+
286
329
  /**
287
330
  * The extension host API contract (`window.cb`).
288
331
  *
@@ -333,6 +376,8 @@ interface CbHostDeps {
333
376
  router: CbRouter;
334
377
  tmsg: TranslationHelpers["tmsg"];
335
378
  tlink: TranslationHelpers["tlink"];
379
+ /** Session store settings; null before/without a platform session. */
380
+ settings?: customer_v1.UserSelfSettingsInterface | null;
336
381
  }
337
382
  type CbClassValue =
338
383
  | string
@@ -427,6 +472,17 @@ type CbImage = React.ForwardRefExoticComponent<
427
472
  lazyRoot?: string;
428
473
  } & React.RefAttributes<HTMLImageElement | null>
429
474
  >;
475
+
476
+ /**
477
+ * Import specifier → module namespace for every runtime host lib, so
478
+ * `cb.requireLib("firebase/firestore")` returns the fully typed module.
479
+ */
480
+ interface CbHostLibs {
481
+ react: typeof React;
482
+ "react-dom": typeof ReactDOM;
483
+ "firebase/app": typeof FirebaseAppNs;
484
+ "firebase/firestore": typeof FirebaseFirestoreNs;
485
+ }
430
486
  interface CbHostApi {
431
487
  /** Host-contract semver — see docs/extension-host-api/versioning.md. */
432
488
  version: string;
@@ -435,14 +491,47 @@ interface CbHostApi {
435
491
  * shares the host's single instance instead of bundling its own (critical for
436
492
  * React — two instances break hooks). The extension build maps
437
493
  * `import ... from "react"` → `cb.lib.React` and `"react-dom"` →
438
- * `cb.lib.ReactDom`.
494
+ * `cb.lib.ReactDOM`.
439
495
  */
440
496
  lib: {
441
497
  React: typeof React;
442
- ReactDom: typeof ReactDOM;
498
+ ReactDOM: typeof ReactDOM;
499
+ /**
500
+ * Lazy host libs, bound by the extension build — do not read
501
+ * directly. The host import()s each one before injecting a bundle
502
+ * that declared it; until then a member read throws a descriptive
503
+ * error naming the missing lib. Use `cb.requireLib(...)` for an
504
+ * explicit, typed lookup.
505
+ */
506
+ firebase: {
507
+ app: typeof FirebaseAppNs;
508
+ firestore: typeof FirebaseFirestoreNs;
509
+ };
443
510
  };
511
+ /**
512
+ * The module namespace for a runtime host lib, throwing a descriptive
513
+ * error when it is not loaded (the extension didn't declare it).
514
+ */
515
+ requireLib: <S extends keyof CbHostLibs>(spec: S) => CbHostLibs[S];
444
516
  /** The platform (commerce backend) API SDK client. */
445
517
  platform: Platform;
518
+ /**
519
+ * Store settings from the current platform session — `storeId`,
520
+ * `companyName`, `themeName`, `cdnPath`, `timeZone`, feature flags…
521
+ * Store-level (not user-level), so it is populated for guests too.
522
+ *
523
+ * OPTIONAL on purpose: it is absent while no platform session exists
524
+ * (a failed session fetch, or after `signOut`). Guard it —
525
+ * `cb.settings?.storeId` — or feature-detect with
526
+ * `cb.has("settings")`.
527
+ *
528
+ * The same object `cb.platform.auth().getUserSelf()` returns, exposed
529
+ * synchronously. Its `firebaseApiKey` / `mfaTenantId` / `googleApiKey`
530
+ * are the HOST's own integration keys (login MFA, address
531
+ * autocomplete) — read them for display/diagnostics only; an app's own
532
+ * data connections come from `cb.utils.useFirebaseApp()`.
533
+ */
534
+ settings?: customer_v1.UserSelfSettingsInterface;
446
535
  /**
447
536
  * Feature-detect a member by dotted path, e.g.
448
537
  * `cb.has("com.Product.ProductMakeOffer")`. Lets an extension degrade
@@ -459,6 +548,23 @@ interface CbHostApi {
459
548
  revalidate: typeof revalidate;
460
549
  revalidateTag: typeof revalidateTag;
461
550
  revalidatePath: typeof revalidatePath;
551
+ /**
552
+ * React hook: connection names the store configured for the current
553
+ * extension (via storeadmin → Installed Apps → Configure data).
554
+ * Empty outside an extension subtree.
555
+ */
556
+ useFirebaseConnections: () => string[];
557
+ /**
558
+ * React hook: the host-initialized FirebaseApp for a named
559
+ * connection, or undefined when it is not configured — always
560
+ * render a fallback for that case. With no argument: the store's
561
+ * only connection when exactly one is configured, else the one
562
+ * named "default". Never call initializeApp yourself; pass this app
563
+ * to the product entry points (e.g. getFirestore(app)).
564
+ */
565
+ useFirebaseApp: (
566
+ connection?: string,
567
+ ) => FirebaseAppNs.FirebaseApp | undefined;
462
568
  };
463
569
  com: {
464
570
  Cart: {
@@ -521,4 +627,4 @@ declare global {
521
627
  }
522
628
  }
523
629
 
524
- export type { CbHostApi, CbHostDeps, CbRouter };
630
+ export type { CbFirebaseConfig, CbHostApi, CbHostDeps, CbHostLibs, CbRouter };