From 9e6f6c659e8f2e4bb8da2fe0a3dd4abcd4a785ee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 8 Oct 2026 19:14:44 +0000 Subject: [PATCH] Add --bitgate-* theme tokens and a build-injected VERSION. Widget styles now prefer --bitgate-* custom properties, falling back to the deprecated --gov-* names and then the existing defaults so current pages and overrides keep working. Browser bundles and ESM entry points expose VERSION (and window.bitgate.version) from package.json. Document allowlist on the widget policy preset list. Sync the lockfile so npm ci sees @bitgate/standalone. Co-authored-by: thePR0M3TH3AN --- README.md | 5 ++ docs/TODO.md | 4 +- package-lock.json | 16 +++++ packages/core/src/index.js | 2 + packages/core/src/version.js | 9 +++ packages/core/test/version.test.js | 17 ++++++ packages/site/public/docs.html | 1 + packages/standalone/build.mjs | 3 + packages/standalone/src/expose-version.js | 29 +++++++++ packages/standalone/src/index.js | 5 ++ packages/standalone/test/standalone.test.js | 18 ++++++ packages/widget/README.md | 41 ++++++++++++- packages/widget/build.mjs | 3 + packages/widget/src/admin.js | 6 +- packages/widget/src/base.js | 67 +++++++++++++++++---- packages/widget/src/expose-version.js | 29 +++++++++ packages/widget/src/index.js | 9 +++ packages/widget/src/provider.js | 2 +- packages/widget/src/viewer.js | 3 +- packages/widget/test/build-version.test.js | 27 +++++++++ packages/widget/test/provider.test.js | 6 ++ packages/widget/test/theme.test.js | 31 ++++++++++ packages/widget/test/version.test.js | 28 +++++++++ scripts/esbuild-bitgate-version.mjs | 30 +++++++++ 24 files changed, 369 insertions(+), 22 deletions(-) create mode 100644 packages/core/src/version.js create mode 100644 packages/core/test/version.test.js create mode 100644 packages/standalone/src/expose-version.js create mode 100644 packages/widget/src/expose-version.js create mode 100644 packages/widget/test/build-version.test.js create mode 100644 packages/widget/test/theme.test.js create mode 100644 packages/widget/test/version.test.js create mode 100644 scripts/esbuild-bitgate-version.mjs diff --git a/README.md b/README.md index 0559312..65470a5 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,11 @@ allowlisted publishers are shown — the symmetric twin of a denylist, for invite-only or curated surfaces). Every number in them is a starting point — see the [integration guide](docs/integration-guide.md) to write your own. +The package version is exported as `VERSION` and, in browser bundles, as +`window.bitgate.version`. Both are injected from `package.json` at build time. +Widget theme tokens use `--bitgate-*` custom properties; the older `--gov-*` +names remain as fallbacks (see [`packages/widget/README.md`](packages/widget/README.md)). + ## Quick start — JavaScript ```js diff --git a/docs/TODO.md b/docs/TODO.md index 3c4a5d2..7849a03 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -175,7 +175,9 @@ unchanged. authority or leaking private application context. - [ ] Decide whether standalone output is generated from the canonical engine directly or from a separately maintained browser entry point. -- [ ] Decide how browser builds expose version and integrity metadata. +- [x] Decide how browser builds expose version and integrity metadata. + `VERSION` + `window.bitgate.version` equal `package.json`, injected at + build time. Integrity manifests remain open (distribution / D7). ## BitBlocks App Kit compatibility layer diff --git a/package-lock.json b/package-lock.json index 2146f08..8f34df3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,10 @@ "resolved": "packages/site", "link": true }, + "node_modules/@bitgate/standalone": { + "resolved": "packages/standalone", + "link": true + }, "node_modules/@bitgate/testing": { "resolved": "packages/testing", "link": true @@ -1837,6 +1841,18 @@ "name": "@bitgate/site", "version": "0.1.0" }, + "packages/standalone": { + "name": "@bitgate/standalone", + "version": "0.1.0", + "license": "GPL-3.0-or-later", + "dependencies": { + "@bitgate/core": "*", + "@bitgate/nostr": "*" + }, + "devDependencies": { + "esbuild": "^0.28.2" + } + }, "packages/testing": { "name": "@bitgate/testing", "version": "0.1.0", diff --git a/packages/core/src/index.js b/packages/core/src/index.js index a146467..c6b3d24 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -111,6 +111,8 @@ export { aggregateMutes, } from "./evaluator.js"; +export { VERSION } from "./version.js"; + /** * @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget * @typedef {import('./identifiers.js').UserTarget} UserTarget diff --git a/packages/core/src/version.js b/packages/core/src/version.js new file mode 100644 index 0000000..e9db93c --- /dev/null +++ b/packages/core/src/version.js @@ -0,0 +1,9 @@ +/** + * BitGate library version. + * + * Keep this string in lockstep with the root and `@bitgate/core` `package.json` + * versions. `packages/core/test/version.test.js` asserts they match. Browser + * bundles replace this module at build time with the same value read from + * `package.json`, so the shipped file cannot drift. + */ +export const VERSION = "0.1.0"; diff --git a/packages/core/test/version.test.js b/packages/core/test/version.test.js new file mode 100644 index 0000000..7bb4a28 --- /dev/null +++ b/packages/core/test/version.test.js @@ -0,0 +1,17 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +import { VERSION } from "../src/index.js"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const rootPkg = JSON.parse(readFileSync(join(HERE, "..", "..", "..", "package.json"), "utf8")); +const corePkg = JSON.parse(readFileSync(join(HERE, "..", "package.json"), "utf8")); + +describe("VERSION", () => { + it("matches package.json (root and @bitgate/core)", () => { + expect(VERSION).toBe(rootPkg.version); + expect(VERSION).toBe(corePkg.version); + }); +}); diff --git a/packages/site/public/docs.html b/packages/site/public/docs.html index b395fe9..1af9b36 100644 --- a/packages/site/public/docs.html +++ b/packages/site/public/docs.html @@ -181,6 +181,7 @@ provider.runtime.policies.setLocalPolicy(policy); socialfeed, profile, playbackTimelines and posts. Discovery declines to hard-hide. commercebrowse, detail, checkout, seller-dashboardMarketplaces. Checkout decides transactions only. admin-onlydefaultOperator deny lists, with no trust-graph signals at all. + allowlistdefaultOnly allowlisted publishers are shown; unlisted ones are hidden. Symmetric twin of a denylist, for invite-only or curated surfaces. diff --git a/packages/standalone/build.mjs b/packages/standalone/build.mjs index 58a3bb9..1e96297 100644 --- a/packages/standalone/build.mjs +++ b/packages/standalone/build.mjs @@ -15,6 +15,8 @@ import { build, context } from "esbuild"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { bitgateVersionPlugin } from "../../scripts/esbuild-bitgate-version.mjs"; + const HERE = dirname(fileURLToPath(import.meta.url)); /** @type {import("esbuild").BuildOptions} */ @@ -31,6 +33,7 @@ const options = { banner: { js: "/*! BitGate (headless standalone) — GPL-3.0-or-later — https://github.com/PR0M3TH3AN/Nostr-Governance */", }, + plugins: [bitgateVersionPlugin()], }; const watch = process.argv.includes("--watch"); diff --git a/packages/standalone/src/expose-version.js b/packages/standalone/src/expose-version.js new file mode 100644 index 0000000..7c5bef7 --- /dev/null +++ b/packages/standalone/src/expose-version.js @@ -0,0 +1,29 @@ +import { VERSION } from "@bitgate/core"; + +/** + * Publish `window.bitgate.version` for script-tag integrators. + * + * Additive: creates `window.bitgate` when missing, otherwise writes `version` + * onto the existing object so a host-provided namespace is not replaced. + * No-op in Node (no `window`). + * + * @param {Record} [target] - Defaults to `globalThis.window` + * @returns {Record|null} The namespace that received `version` + */ +export function exposeBitGateVersion(target) { + const root = + target ?? + (typeof globalThis.window !== "undefined" + ? /** @type {Record} */ (globalThis.window) + : null); + if (!root || typeof root !== "object") { + return null; + } + + const existing = root.bitgate; + const ns = + existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}; + ns.version = VERSION; + root.bitgate = ns; + return ns; +} diff --git a/packages/standalone/src/index.js b/packages/standalone/src/index.js index 1bebdb3..11cf25b 100644 --- a/packages/standalone/src/index.js +++ b/packages/standalone/src/index.js @@ -11,5 +11,10 @@ // Both re-exported packages are dependency-free (@bitgate/verify, the only // package with third-party deps, is intentionally not pulled in), so the bundle // carries no external code. +import { exposeBitGateVersion } from "./expose-version.js"; + export * from "@bitgate/core"; export * from "@bitgate/nostr"; +export { exposeBitGateVersion }; + +exposeBitGateVersion(); diff --git a/packages/standalone/test/standalone.test.js b/packages/standalone/test/standalone.test.js index 019725a..9134058 100644 --- a/packages/standalone/test/standalone.test.js +++ b/packages/standalone/test/standalone.test.js @@ -2,10 +2,16 @@ // engine (core) + label codec (nostr) — so a no-build site gets everything from // the single bundled file. Tests the entry, not the built artifact (dist is // gitignored); the build itself is exercised by `npm run build:standalone`. +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import * as bitgate from "../src/index.js"; +const HERE = dirname(fileURLToPath(import.meta.url)); +const rootPkg = JSON.parse(readFileSync(join(HERE, "..", "..", "..", "package.json"), "utf8")); + describe("standalone entry surface", () => { it("re-exports the core engine and the Nostr label codec together", () => { for (const name of [ @@ -17,6 +23,7 @@ describe("standalone entry surface", () => { "createPolicyDefinition", "createEventTarget", "ALLOWLIST_POLICY", + "VERSION", // nostr label codec "decodeLabels", "encodeLabel", @@ -67,4 +74,15 @@ describe("standalone entry surface", () => { expect(decision.transaction?.effect).toBe("deny"); }); + + it("exports VERSION matching package.json", () => { + expect(bitgate.VERSION).toBe(rootPkg.version); + }); + + it("publishes version onto a window-like global without replacing other keys", () => { + const target = { bitgate: { extra: true } }; + bitgate.exposeBitGateVersion(target); + expect(target.bitgate.version).toBe(bitgate.VERSION); + expect(target.bitgate.extra).toBe(true); + }); }); diff --git a/packages/widget/README.md b/packages/widget/README.md index c824642..5b42ff2 100644 --- a/packages/widget/README.md +++ b/packages/widget/README.md @@ -34,10 +34,14 @@ both with every descendant. Nothing else to wire. | --- | --- | | `relays` | Comma-separated relay URLs (required) | | `root` | Root administrator pubkey, from your deployment config | -| `policy` | `social`, `commerce`, or `admin-only` (default) | +| `policy` | `social`, `commerce`, `admin-only` (default), or `allowlist` | | `viewer` | Viewer pubkey, if known at render time | | `application` / `namespace` | Storage and event namespacing | +`allowlist` is the symmetric twin of a denylist: only publishers with a +`user-allow` contribution are shown, unlisted ones are hidden across all four +dimensions, and protected actors (root, moderators) are never gated. + ### Target attributes Any target-bearing element reads its target from markup: @@ -161,13 +165,44 @@ button teaches nothing about the authority model. The command layer enforces the same check independently — the UI is an explanation, never the gate. **Styling is yours.** Elements expose `part` attributes and inherit `color` and -`font` from the host. Override the CSS custom properties (`--gov-fg`, -`--gov-surface`, `--gov-warn`, `--gov-danger`, `--gov-radius`) or target parts: +`font` from the host. Override the CSS custom properties or target parts: + +| Token | Purpose | +| --- | --- | +| `--bitgate-fg` | Foreground / text (`currentColor`) | +| `--bitgate-muted` | Secondary text | +| `--bitgate-border` | Control borders | +| `--bitgate-surface` | Button / panel background | +| `--bitgate-warn` | Warning copy | +| `--bitgate-danger` | Danger / destructive accent | +| `--bitgate-radius` | Control corner radius | + +Each token prefers `--bitgate-*`, then the deprecated `--gov-*` name +(`--gov-fg`, `--gov-muted`, `--gov-border`, `--gov-surface`, `--gov-warn`, +`--gov-danger`, `--gov-radius`), then the built-in default. Existing `--gov-*` +overrides keep working; new pages should set `--bitgate-*`. `::part()` names +are unchanged. ```css +bitgate-veil { --bitgate-warn: #c2410c; } bitgate-veil::part(veil) { border: 1px solid red; } ``` +## Version + +The library version is the `package.json` version, injected into browser +bundles at build time: + +```js +import { VERSION } from "@bitgate/widget"; +console.log(VERSION); // e.g. "0.1.0" +console.log(window.bitgate.version); // same string, for script-tag hosts +``` + +`window.bitgate` is created if missing; if a host already put an object there, +only `.version` is written. Spec / wire versions (`bitgate/1`, policy +`schemaVersion`) are independent of this package version. + ## Environment `defineBitGateElements()` is safe to call repeatedly and safe to import in diff --git a/packages/widget/build.mjs b/packages/widget/build.mjs index d49ed86..4ceea9e 100644 --- a/packages/widget/build.mjs +++ b/packages/widget/build.mjs @@ -19,6 +19,8 @@ import { build, context } from "esbuild"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { bitgateVersionPlugin } from "../../scripts/esbuild-bitgate-version.mjs"; + const HERE = dirname(fileURLToPath(import.meta.url)); /** @type {import("esbuild").BuildOptions} */ @@ -37,6 +39,7 @@ const options = { banner: { js: "/*! BitGate widget — GPL-3.0-or-later — https://github.com/PR0M3TH3AN/Nostr-Governance */", }, + plugins: [bitgateVersionPlugin()], }; const watch = process.argv.includes("--watch"); diff --git a/packages/widget/src/admin.js b/packages/widget/src/admin.js index 3a347ea..ab27aa3 100644 --- a/packages/widget/src/admin.js +++ b/packages/widget/src/admin.js @@ -11,7 +11,7 @@ // Second, nothing here computes policy. The console displays effective state // and issues commands; every accept/reject decision belongs to the runtime. -import { GovernanceElement, defineElement, escapeHtml, shortenKey } from "./base.js"; +import { GovernanceElement, defineElement, escapeHtml, shortenKey, themeVar } from "./base.js"; /** * @typedef {import('@bitgate/runtime').GovernanceCommands} GovernanceCommands @@ -213,10 +213,10 @@ export class GovernanceAdminPanel extends GovernanceElement { this.renderHtml(`
${ diff --git a/packages/widget/src/base.js b/packages/widget/src/base.js index 485f18a..8452f44 100644 --- a/packages/widget/src/base.js +++ b/packages/widget/src/base.js @@ -27,34 +27,75 @@ export function canRegisterElements() { return typeof globalThis.HTMLElement === "function" && typeof globalThis.customElements === "object"; } +/** + * Default values for widget theme tokens. + * + * `--gov-*` names are the historical prefix (from Nostr-Governance). `--bitgate-*` + * is the agreed prefix. Usage sites prefer the new name, then the old one, then + * this default, so existing pages and `--gov-*` overrides keep working. + * + * @type {Readonly<{ + * fg: string, + * muted: string, + * border: string, + * surface: string, + * warn: string, + * danger: string, + * radius: string, + * }>} + */ +export const THEME_DEFAULTS = Object.freeze({ + fg: "currentColor", + muted: "color-mix(in srgb, currentColor 60%, transparent)", + border: "color-mix(in srgb, currentColor 20%, transparent)", + surface: "color-mix(in srgb, canvas 96%, currentColor)", + warn: "#b45309", + danger: "#b91c1c", + radius: "8px", +}); + +/** + * CSS value for a theme token: `--bitgate-*` wins, then deprecated `--gov-*`, + * then the existing default. + * + * `--bitgate-*` is deliberately not assigned on `:host`. Assigning it would + * make `var(--bitgate-*, …)` always hit and ignore `--gov-*` overrides. + * + * @param {keyof typeof THEME_DEFAULTS} name + * @returns {string} + */ +export function themeVar(name) { + return `var(--bitgate-${name}, var(--gov-${name}, ${THEME_DEFAULTS[name]}))`; +} + /** Base styles shared by every element, scoped inside each shadow root. */ export const BASE_STYLES = ` :host { - --gov-fg: currentColor; - --gov-muted: color-mix(in srgb, currentColor 60%, transparent); - --gov-border: color-mix(in srgb, currentColor 20%, transparent); - --gov-surface: color-mix(in srgb, canvas 96%, currentColor); - --gov-warn: #b45309; - --gov-danger: #b91c1c; - --gov-radius: 8px; + --gov-fg: ${THEME_DEFAULTS.fg}; + --gov-muted: ${THEME_DEFAULTS.muted}; + --gov-border: ${THEME_DEFAULTS.border}; + --gov-surface: ${THEME_DEFAULTS.surface}; + --gov-warn: ${THEME_DEFAULTS.warn}; + --gov-danger: ${THEME_DEFAULTS.danger}; + --gov-radius: ${THEME_DEFAULTS.radius}; display: block; - color: var(--gov-fg); + color: ${themeVar("fg")}; font: inherit; } :host([hidden]) { display: none; } button { font: inherit; color: inherit; - background: var(--gov-surface); - border: 1px solid var(--gov-border); - border-radius: var(--gov-radius); + background: ${themeVar("surface")}; + border: 1px solid ${themeVar("border")}; + border-radius: ${themeVar("radius")}; padding: 0.4em 0.8em; cursor: pointer; } - button:hover:not(:disabled) { border-color: var(--gov-fg); } + button:hover:not(:disabled) { border-color: ${themeVar("fg")}; } button:disabled { opacity: 0.5; cursor: not-allowed; } .reason { - color: var(--gov-muted); + color: ${themeVar("muted")}; font-size: 0.875em; } .row { display: flex; gap: 0.5em; align-items: center; flex-wrap: wrap; } diff --git a/packages/widget/src/expose-version.js b/packages/widget/src/expose-version.js new file mode 100644 index 0000000..7c5bef7 --- /dev/null +++ b/packages/widget/src/expose-version.js @@ -0,0 +1,29 @@ +import { VERSION } from "@bitgate/core"; + +/** + * Publish `window.bitgate.version` for script-tag integrators. + * + * Additive: creates `window.bitgate` when missing, otherwise writes `version` + * onto the existing object so a host-provided namespace is not replaced. + * No-op in Node (no `window`). + * + * @param {Record} [target] - Defaults to `globalThis.window` + * @returns {Record|null} The namespace that received `version` + */ +export function exposeBitGateVersion(target) { + const root = + target ?? + (typeof globalThis.window !== "undefined" + ? /** @type {Record} */ (globalThis.window) + : null); + if (!root || typeof root !== "object") { + return null; + } + + const existing = root.bitgate; + const ns = + existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {}; + ns.version = VERSION; + root.bitgate = ns; + return ns; +} diff --git a/packages/widget/src/index.js b/packages/widget/src/index.js index 0e84329..de3da41 100644 --- a/packages/widget/src/index.js +++ b/packages/widget/src/index.js @@ -21,9 +21,11 @@ export { } from "@bitgate/runtime"; export { + VERSION, SOCIAL_POLICY, COMMERCE_POLICY, ADMIN_ONLY_POLICY, + ALLOWLIST_POLICY, POLICY_PRESETS, getPolicyPreset, createPolicyDefinition, @@ -42,6 +44,7 @@ export { export { BASE_STYLES, + THEME_DEFAULTS, GovernanceElement, canRegisterElements, defineElement, @@ -49,8 +52,11 @@ export { requestContext, shortenKey, targetFromAttributes, + themeVar, } from "./base.js"; +export { exposeBitGateVersion } from "./expose-version.js"; + export { BitGateError, BitGateProvider, @@ -76,9 +82,12 @@ export { } from "./admin.js"; import { defineAdminElements } from "./admin.js"; +import { exposeBitGateVersion } from "./expose-version.js"; import { defineProviderElements } from "./provider.js"; import { defineViewerElements } from "./viewer.js"; +exposeBitGateVersion(); + /** * Register every governance element. * diff --git a/packages/widget/src/provider.js b/packages/widget/src/provider.js index 0550a07..18a8917 100644 --- a/packages/widget/src/provider.js +++ b/packages/widget/src/provider.js @@ -151,7 +151,7 @@ export class BitGateProvider extends ProviderBase { if (!policy) { throw new Error( - `Unknown policy preset "${policyName}". Use social, commerce, admin-only, or set .policy directly.`, + `Unknown policy preset "${policyName}". Use social, commerce, admin-only, allowlist, or set .policy directly.`, ); } if (relays.length === 0) { diff --git a/packages/widget/src/viewer.js b/packages/widget/src/viewer.js index 5e5b74a..d558f8c 100644 --- a/packages/widget/src/viewer.js +++ b/packages/widget/src/viewer.js @@ -10,6 +10,7 @@ import { escapeHtml, shortenKey, targetFromAttributes, + themeVar, } from "./base.js"; /** @@ -161,7 +162,7 @@ export class GovernanceVeil extends GovernanceElement { position: absolute; inset: 0; display: grid; place-content: center; gap: 0.5em; text-align: center; padding: 1em; } - .warn { color: var(--gov-warn); } + .warn { color: ${themeVar("warn")}; }
diff --git a/packages/widget/test/build-version.test.js b/packages/widget/test/build-version.test.js new file mode 100644 index 0000000..fae137b --- /dev/null +++ b/packages/widget/test/build-version.test.js @@ -0,0 +1,27 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { build } from "esbuild"; +import { describe, expect, it } from "vitest"; + +import { bitgateVersionPlugin, readLibraryVersion } from "../../../scripts/esbuild-bitgate-version.mjs"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const versionJs = join(HERE, "..", "..", "core", "src", "version.js"); + +describe("build-time version injection", () => { + it("replaces the version module with package.json's version", async () => { + const version = readLibraryVersion(); + const result = await build({ + entryPoints: [versionJs], + bundle: true, + write: false, + format: "esm", + platform: "neutral", + plugins: [bitgateVersionPlugin(version)], + }); + + const bundled = result.outputFiles[0].text; + expect(bundled).toContain(`VERSION = ${JSON.stringify(version)}`); + expect(bundled).not.toContain("Keep this string in lockstep"); + }); +}); diff --git a/packages/widget/test/provider.test.js b/packages/widget/test/provider.test.js index fca1dfe..ddffa46 100644 --- a/packages/widget/test/provider.test.js +++ b/packages/widget/test/provider.test.js @@ -135,6 +135,11 @@ describe("", () => { expect(provider.runtime.policies.policy.id).toBe("bitgate-commerce"); }); + it("accepts the allowlist policy preset", async () => { + const provider = await mountProvider({ policy: "allowlist" }); + expect(provider.runtime.policies.policy.id).toBe("bitgate-allowlist"); + }); + it("defaults to administrative-only governance", async () => { const provider = await mountProvider({ policy: null }); expect(provider.runtime.policies.policy.id).toBe("bitgate-admin-only"); @@ -154,6 +159,7 @@ describe("", () => { const provider = await mountProvider({ policy: "invented" }); expect(provider.runtime).toBeNull(); expect(provider.getAttribute("data-error")).toMatch(/Unknown policy preset/); + expect(provider.getAttribute("data-error")).toMatch(/allowlist/); }); it("reports a missing relays attribute", async () => { diff --git a/packages/widget/test/theme.test.js b/packages/widget/test/theme.test.js new file mode 100644 index 0000000..0d30a52 --- /dev/null +++ b/packages/widget/test/theme.test.js @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; + +import { BASE_STYLES, THEME_DEFAULTS, themeVar } from "../src/base.js"; + +describe("theme tokens", () => { + it("resolves --bitgate-* first, then deprecated --gov-*, then the default", () => { + for (const name of Object.keys(THEME_DEFAULTS)) { + expect(themeVar(/** @type {keyof typeof THEME_DEFAULTS} */ (name))).toBe( + `var(--bitgate-${name}, var(--gov-${name}, ${THEME_DEFAULTS[name]}))`, + ); + } + }); + + it("keeps --gov-* host defaults so existing overrides still apply", () => { + for (const [name, value] of Object.entries(THEME_DEFAULTS)) { + expect(BASE_STYLES).toContain(`--gov-${name}: ${value}`); + } + }); + + it("does not assign --bitgate-* on :host (that would shadow --gov-* overrides)", () => { + expect(BASE_STYLES).not.toMatch(/--bitgate-[a-z]+:/); + }); + + it("uses the fallback chain at every painted token", () => { + expect(BASE_STYLES).toContain(themeVar("fg")); + expect(BASE_STYLES).toContain(themeVar("muted")); + expect(BASE_STYLES).toContain(themeVar("border")); + expect(BASE_STYLES).toContain(themeVar("surface")); + expect(BASE_STYLES).toContain(themeVar("radius")); + }); +}); diff --git a/packages/widget/test/version.test.js b/packages/widget/test/version.test.js new file mode 100644 index 0000000..f62c267 --- /dev/null +++ b/packages/widget/test/version.test.js @@ -0,0 +1,28 @@ +// @vitest-environment happy-dom + +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +import { VERSION, exposeBitGateVersion } from "../src/index.js"; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const rootPkg = JSON.parse(readFileSync(join(HERE, "..", "..", "..", "package.json"), "utf8")); + +describe("VERSION", () => { + it("matches package.json", () => { + expect(VERSION).toBe(rootPkg.version); + }); + + it("is published on window.bitgate when the widget is imported", () => { + expect(/** @type {any} */ (window).bitgate.version).toBe(VERSION); + }); + + it("writes version onto an existing window.bitgate object", () => { + const target = { bitgate: { extra: true } }; + exposeBitGateVersion(target); + expect(target.bitgate.version).toBe(VERSION); + expect(target.bitgate.extra).toBe(true); + }); +}); diff --git a/scripts/esbuild-bitgate-version.mjs b/scripts/esbuild-bitgate-version.mjs new file mode 100644 index 0000000..4ce0534 --- /dev/null +++ b/scripts/esbuild-bitgate-version.mjs @@ -0,0 +1,30 @@ +/** + * esbuild plugin: replace `@bitgate/core`'s version module with the value from + * package.json so browser bundles cannot drift from the package. + */ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); + +/** @returns {string} */ +export function readLibraryVersion() { + return JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8")).version; +} + +/** + * @param {string} [injected] + * @returns {import("esbuild").Plugin} + */ +export function bitgateVersionPlugin(injected = readLibraryVersion()) { + return { + name: "bitgate-version", + setup(pluginBuild) { + pluginBuild.onLoad({ filter: /[/\\]core[/\\]src[/\\]version\.js$/ }, () => ({ + contents: `export const VERSION = ${JSON.stringify(injected)};\n`, + loader: "js", + })); + }, + }; +}