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", + })); + }, + }; +}