browser-extension-manager 1.4.0 → 1.5.0

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/README.md CHANGED
@@ -61,8 +61,11 @@ BXM ships a built-in four-layer test framework. Write tests under `test/<layer>/
61
61
  npx bxm test # all layers
62
62
  npx bxm test --layer build # build layer only (plain Node, fast)
63
63
  npx bxm test --layer boot # real-Chromium end-to-end test
64
+ npx bxm test --integration # also run integration suites against REAL external services (Firebase, etc.)
64
65
  ```
65
66
 
67
+ Tests run against the **real** harness — a real MV3 service worker, a real Chromium tab, the real packaged extension. **Never mock** (`chrome`, the Manager, contexts are all real); only pure, I/O-free functions are called directly. Real external APIs are gated behind `--integration` (skipped in-source otherwise, never mocked).
68
+
66
69
  Test files use Jest-compatible matchers:
67
70
 
68
71
  ```js
@@ -513,7 +513,8 @@ class Manager {
513
513
 
514
514
  // Setup livereload
515
515
  setupLiveReload() {
516
- // Quit if not in dev mode
516
+ // Dev-only feature skip in testing and production (environment is one of
517
+ // 'development' | 'testing' | 'production').
517
518
  if (this.environment !== 'development') return;
518
519
 
519
520
  // Get port from config or use default
package/dist/build.js CHANGED
@@ -91,13 +91,8 @@ Manager.actLikeProduction = function () {
91
91
  }
92
92
  Manager.prototype.actLikeProduction = Manager.actLikeProduction;
93
93
 
94
- // getEnvironment: returns the environment based on the build mode
95
- Manager.getEnvironment = function () {
96
- return Manager.isBuildMode()
97
- ? 'production'
98
- : 'development';
99
- }
100
- Manager.prototype.getEnvironment = Manager.getEnvironment;
94
+ // getEnvironment() is the SSOT and lives in src/utils/mode-helpers.js (alongside the is*()
95
+ // family). It's mixed onto the Manager via the attachTo() call below, same as in EM/UJM.
101
96
 
102
97
  // getManifest: requires and parses config.yml
103
98
  Manager.getManifest = function () {
@@ -17,7 +17,7 @@ module.exports = async function (options) {
17
17
 
18
18
  try {
19
19
  // Install production
20
- if (['prod', 'p', 'production'].includes(type)) {
20
+ if (['live', 'prod', 'p', 'production'].includes(type)) {
21
21
  // Log
22
22
  logger.log('Installing production...');
23
23
 
@@ -0,0 +1,15 @@
1
+ # CHANGELOG
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+
7
+ ## Changelog Categories
8
+
9
+ - `BREAKING` for breaking changes.
10
+ - `Added` for new features.
11
+ - `Changed` for changes in existing functionality.
12
+ - `Deprecated` for soon-to-be removed features.
13
+ - `Removed` for now removed features.
14
+ - `Fixed` for any bug fixes.
15
+ - `Security` in case of vulnerabilities.
@@ -1,15 +1,25 @@
1
1
  # ========== Default Values ==========
2
2
  # Browser Extension Manager (BXM) — consumer project
3
3
 
4
- > **Auto-managed file.** Everything between `# ========== Default Values ==========` and `# ========== Custom Values ==========` is owned by `browser-extension-manager` and rewritten on every `npx mgr setup`. Put your own project-specific notes BELOW the `Custom Values` marker — that section is preserved verbatim across setups.
5
-
6
4
  ## Framework
7
5
 
8
6
  This project consumes **Browser Extension Manager** (BXM) — a comprehensive framework for building modern cross-browser extensions (Chrome, Firefox, Edge, Opera, Brave). BXM provides one-line bootstrap per extension context, a component-based architecture (view + styles + script per context), a multi-browser build/release pipeline that produces store-uploadable zips, cross-context auth synchronization, and a built-in four-layer test framework.
9
7
 
10
- **Framework's own docs** (read these for deep-dives; both paths point to the same files, the absolute path works regardless of working directory):
11
- - Top-level overview: `/Users/ian/Developer/Repositories/ITW-Creative-Works/browser-extension-manager/CLAUDE.md` (or `node_modules/browser-extension-manager/CLAUDE.md`)
12
- - Subsystem references: `/Users/ian/Developer/Repositories/ITW-Creative-Works/browser-extension-manager/docs/` (or `node_modules/browser-extension-manager/docs/`)
8
+ ## 🚨 READ THE FRAMEWORK DOCS FIRST
9
+
10
+ **Before doing ANY work on this codebase, Claude MUST read the framework documentation — that is where the architecture, conventions, APIs, and gotchas live. Skipping these will result in solutions that conflict with framework patterns.**
11
+
12
+ **Required reading:**
13
+ - **`node_modules/browser-extension-manager/CLAUDE.md`** — top-level overview + index
14
+ - **`node_modules/browser-extension-manager/docs/`** — subsystem deep references (read the relevant ones for the task at hand)
15
+
16
+ ## 🚨 READ WEB-MANAGER TOO
17
+
18
+ **BXM ships `web-manager` as a runtime singleton across every extension context** (background service worker, popup, options, sidepanel, content scripts) — it powers auth, Firebase, reactive `data-wm-bind` directives, analytics, error tracking, and utilities (`escapeHTML`, etc.). Any task that touches auth flows, Firestore reads/writes, subscription resolution, push notifications, or DOM bindings means you are working with web-manager as much as with BXM.
19
+
20
+ **Required reading:**
21
+ - **`node_modules/web-manager/CLAUDE.md`** — top-level overview + index
22
+ - **`node_modules/web-manager/docs/`** — module deep references (Auth, Bindings, Firestore, Notifications, etc.)
13
23
 
14
24
  ## Quick start
15
25
 
@@ -18,8 +28,12 @@ npm start # dev with live reload (gulp → webpack → serve)
18
28
  npm run build # production build → dist/ + packaged/<browser>/raw/ + .zip per browser
19
29
  BXM_IS_PUBLISH=true npm run build # build + auto-upload to Chrome / Firefox / Edge stores
20
30
  npx mgr test # run framework + project test suites
31
+ npx mgr install dev # use LOCAL browser-extension-manager source (to test framework edits)
32
+ npx mgr install live # restore the published browser-extension-manager from npm
21
33
  ```
22
34
 
35
+ > Editing the BXM framework source while working here? Run `npx mgr install dev` so this project picks up your uncommitted framework changes (it otherwise uses its installed `node_modules/browser-extension-manager`). Run `npx mgr install live` to switch back.
36
+
23
37
  Load the unpacked extension in Chrome: point chrome://extensions → "Load unpacked" at `packaged/chromium/raw/`.
24
38
 
25
39
  ## Where things live
@@ -57,10 +71,12 @@ After `initialize()`, every Manager exposes:
57
71
  - `manager.logger` — timestamped per-context logger
58
72
  - `manager.webManager` — Web Manager singleton (Firebase, auth, analytics, reactive `data-wm-bind` directives)
59
73
  - `manager.messenger` — `chrome.runtime.onMessage` listener wired automatically
60
- - `manager.isDevelopment()` / `isProduction()` / `isTesting()` / `getVersion()` — cross-context helpers
74
+ - `manager.isDevelopment()` / `isProduction()` / `isTesting()` / `getVersion()` — cross-context helpers. `getEnvironment()` returns `'development' | 'testing' | 'production'` (mutually exclusive; testing wins). Gate side effects on the intentional check (`isProduction()` for prod-only; `isDevelopment() || isTesting()` for local-or-test) — never `!isDevelopment()`.
61
75
 
62
76
  Auth UI is declarative — add `.auth-signin-btn` / `.auth-signout-btn` / `.auth-account-btn` to buttons; BXM wires them. Show/hide based on auth state via `data-wm-bind="@show auth.user"`.
63
77
 
78
+ <!-- Everything above this marker is owned by the framework and rewritten on every `npx mgr setup`. Add your project-specific notes below — they are preserved across setups. -->
79
+
64
80
  # ========== Custom Values ==========
65
81
 
66
82
  ## Project-specific notes
@@ -0,0 +1,17 @@
1
+ # Project docs
2
+
3
+ Per-subsystem deep references live here. Keep `CLAUDE.md` short — it should read as a **table of contents** that points at files in this directory.
4
+
5
+ ## Pattern
6
+
7
+ When you find yourself adding more than a paragraph to `CLAUDE.md`, create a new `docs/<topic>.md` instead and link to it from `CLAUDE.md`. Goal: the project's `CLAUDE.md` stays under ~250 lines.
8
+
9
+ Examples of good `docs/*.md` topics:
10
+ - Subsystem deep-dives (one per area of the codebase)
11
+ - Architectural decisions / "why we built it this way"
12
+ - Defaults tables, behavior matrices, edge cases
13
+ - Setup walkthroughs that don't belong in `README.md`
14
+
15
+ ## See also
16
+
17
+ The framework's own docs follow this same pattern — browse `node_modules/browser-extension-manager/docs/` for the canonical examples.
@@ -0,0 +1,32 @@
1
+ # Project tests
2
+
3
+ Drop your project test suites here. The framework auto-runs them alongside its own when you run `npx mgr test`.
4
+
5
+ ## Layers
6
+
7
+ Match the framework's four layers — Browser Extension Manager's test runner discovers files by the directory they sit in:
8
+
9
+ | Directory | Runtime | Use for |
10
+ |---|---|---|
11
+ | `test/build/` | Plain Node | Build-time logic, manifest validation, pure utilities |
12
+ | `test/background/` | MV3 service worker context | Background messaging, auth source-of-truth, alarms |
13
+ | `test/view/` | Popup / options / sidepanel page | DOM, view-side controllers, `data-wm-bind` directives |
14
+ | `test/boot/` | Consumer's actual built extension | End-to-end smoke tests (does the extension load, does the background register, do views render) |
15
+
16
+ ## Quick example
17
+
18
+ ```js
19
+ // test/build/my-feature.test.js
20
+ const assert = require('browser-extension-manager/test/assert');
21
+
22
+ module.exports = {
23
+ 'my feature does the thing': async () => {
24
+ const result = await doTheThing();
25
+ assert.equal(result, 'expected');
26
+ },
27
+ };
28
+ ```
29
+
30
+ ## See also
31
+
32
+ `node_modules/browser-extension-manager/docs/test-framework.md` — full reference for the test framework (layers, assert API, fixtures, runner internals).
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test lifecycle hook for this project. Runs once before any suite (not a test itself).
3
+ * See browser-extension-manager/docs/test-framework.md → "test/_init.js".
4
+ */
5
+
6
+ module.exports = ({ projectRoot }) => ({
7
+ // Seed any fixture a suite needs before it runs.
8
+ async setup() {
9
+ },
10
+ });
@@ -49,6 +49,10 @@ async function run(options = {}) {
49
49
  console.log('');
50
50
  console.log(chalk.bold(' Browser Extension Manager Tests'));
51
51
 
52
+ // Run the optional test/_init.js setup() hooks (framework + consumer) ONCE,
53
+ // before any suite. There is no cleanup hook — tests clean up after themselves.
54
+ await runInitSetups();
55
+
52
56
  const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
53
57
 
54
58
  if (sources.framework.length > 0) {
@@ -404,4 +408,62 @@ function relativizePath(file, source) {
404
408
  return path.relative(path.join(process.cwd(), 'test'), file);
405
409
  }
406
410
 
411
+ // ---------------------------------------------------------------------------
412
+ // test/_init.js — pre-test lifecycle hook (setup only)
413
+ //
414
+ // Mirrors the backend framework's hook so all four frameworks share one shape.
415
+ // A project may add `<cwd>/test/_init.js` exporting a FUNCTION —
416
+ // `module.exports = (ctx) => ({ setup })` — called with `{ projectRoot }` and
417
+ // returning an object with an async `setup({ projectRoot })` that runs ONCE
418
+ // before any suite (e.g. to scaffold a fixture file the boot layer needs).
419
+ // There is no `cleanup` hook: tests clean up after themselves. Unlike the
420
+ // backend framework, there is no `accounts` field here — these frameworks have
421
+ // no auth/user system.
422
+ // ---------------------------------------------------------------------------
423
+
424
+ function loadInit(testDir, label) {
425
+ const initPath = path.join(testDir, '_init.js');
426
+
427
+ if (!jetpack.exists(initPath)) {
428
+ return {};
429
+ }
430
+
431
+ try {
432
+ const fn = require(initPath);
433
+
434
+ if (typeof fn !== 'function') {
435
+ console.log(chalk.red(` ✗ ${label} test/_init.js must export a function: module.exports = (ctx) => ({ ... })`));
436
+ return {};
437
+ }
438
+
439
+ const mod = fn({ projectRoot: process.cwd() });
440
+ return mod && typeof mod === 'object' ? mod : {};
441
+ } catch (e) {
442
+ console.log(chalk.red(` ✗ Failed to load ${label} test/_init.js: ${e.message}`));
443
+ return {};
444
+ }
445
+ }
446
+
447
+ async function runInitSetups() {
448
+ const frameworkTestsDir = path.resolve(__dirname, '../../test');
449
+ const projectTestsDir = path.join(process.cwd(), 'test');
450
+
451
+ const hooks = [
452
+ loadInit(frameworkTestsDir, 'framework'),
453
+ loadInit(projectTestsDir, 'project'),
454
+ ];
455
+
456
+ const setups = hooks.filter((h) => typeof h.setup === 'function').map((h) => h.setup);
457
+
458
+ for (const setup of setups) {
459
+ process.stdout.write(chalk.gray(' Running test/_init.js setup... '));
460
+ try {
461
+ await setup({ projectRoot: process.cwd() });
462
+ console.log(chalk.green('✓'));
463
+ } catch (e) {
464
+ console.log(chalk.red(`✗ (${e.message})`));
465
+ }
466
+ }
467
+ }
468
+
407
469
  module.exports = { run, SkipError };
@@ -111,29 +111,51 @@ module.exports = {
111
111
  },
112
112
  },
113
113
  {
114
- name: 'getEnvironment returns "development" when BXM_BUILD_MODE !== "true"',
114
+ name: 'getEnvironment returns "testing" when BXM_TEST_MODE === "true" (takes precedence)',
115
+ run: (ctx) => {
116
+ const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
117
+ const origTest = process.env.BXM_TEST_MODE;
118
+ const origBuild = process.env.BXM_BUILD_MODE;
119
+ process.env.BXM_TEST_MODE = 'true';
120
+ process.env.BXM_BUILD_MODE = 'true'; // even with build mode set, testing wins
121
+ try {
122
+ ctx.expect(Manager.getEnvironment()).toBe('testing');
123
+ } finally {
124
+ if (origTest === undefined) delete process.env.BXM_TEST_MODE; else process.env.BXM_TEST_MODE = origTest;
125
+ if (origBuild === undefined) delete process.env.BXM_BUILD_MODE; else process.env.BXM_BUILD_MODE = origBuild;
126
+ }
127
+ },
128
+ },
129
+ {
130
+ name: 'getEnvironment returns "development" when BXM_BUILD_MODE !== "true" (and not testing)',
115
131
  run: (ctx) => {
116
132
  const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
117
133
  const original = process.env.BXM_BUILD_MODE;
134
+ const origTest = process.env.BXM_TEST_MODE;
118
135
  delete process.env.BXM_BUILD_MODE;
136
+ delete process.env.BXM_TEST_MODE;
119
137
  try {
120
138
  ctx.expect(Manager.getEnvironment()).toBe('development');
121
139
  } finally {
122
140
  if (original !== undefined) process.env.BXM_BUILD_MODE = original;
141
+ if (origTest !== undefined) process.env.BXM_TEST_MODE = origTest;
123
142
  }
124
143
  },
125
144
  },
126
145
  {
127
- name: 'getEnvironment returns "production" when BXM_BUILD_MODE === "true"',
146
+ name: 'getEnvironment returns "production" when BXM_BUILD_MODE === "true" (and not testing)',
128
147
  run: (ctx) => {
129
148
  const Manager = require(path.join(__dirname, '..', '..', '..', 'build.js'));
130
149
  const original = process.env.BXM_BUILD_MODE;
150
+ const origTest = process.env.BXM_TEST_MODE;
151
+ delete process.env.BXM_TEST_MODE;
131
152
  process.env.BXM_BUILD_MODE = 'true';
132
153
  try {
133
154
  ctx.expect(Manager.getEnvironment()).toBe('production');
134
155
  } finally {
135
156
  if (original === undefined) delete process.env.BXM_BUILD_MODE;
136
157
  else process.env.BXM_BUILD_MODE = original;
158
+ if (origTest !== undefined) process.env.BXM_TEST_MODE = origTest;
137
159
  }
138
160
  },
139
161
  },
@@ -29,9 +29,10 @@ module.exports = {
29
29
  description: 'utils/mode-helpers — cross-context isDevelopment/isTesting/getVersion',
30
30
  tests: [
31
31
  {
32
- name: 'exports { attachTo, isDevelopment, isProduction, isTesting, getVersion }',
32
+ name: 'exports { attachTo, getEnvironment, isDevelopment, isProduction, isTesting, getVersion }',
33
33
  run: (ctx) => {
34
34
  ctx.expect(typeof helpers.attachTo).toBe('function');
35
+ ctx.expect(typeof helpers.getEnvironment).toBe('function');
35
36
  ctx.expect(typeof helpers.isDevelopment).toBe('function');
36
37
  ctx.expect(typeof helpers.isProduction).toBe('function');
37
38
  ctx.expect(typeof helpers.isTesting).toBe('function');
@@ -50,18 +51,29 @@ module.exports = {
50
51
  },
51
52
  },
52
53
  {
53
- name: 'isDevelopment() is true under NODE_ENV=development',
54
+ name: 'isDevelopment() is true under NODE_ENV=development (and not testing)',
54
55
  run: (ctx) => {
55
- withEnv({ NODE_ENV: 'development', BXM_BUILD_MODE: null }, () => {
56
+ withEnv({ NODE_ENV: 'development', BXM_BUILD_MODE: null, BXM_TEST_MODE: null }, () => {
56
57
  ctx.expect(helpers.isDevelopment()).toBe(true);
57
58
  });
58
59
  },
59
60
  },
60
61
  {
61
- name: 'isDevelopment() is false when BXM_BUILD_MODE=true',
62
+ name: 'isDevelopment() is false / isProduction() true when BXM_BUILD_MODE=true (and not testing)',
62
63
  run: (ctx) => {
63
- withEnv({ NODE_ENV: null, BXM_BUILD_MODE: 'true' }, () => {
64
+ withEnv({ NODE_ENV: null, BXM_BUILD_MODE: 'true', BXM_TEST_MODE: null }, () => {
64
65
  ctx.expect(helpers.isDevelopment()).toBe(false);
66
+ ctx.expect(helpers.isProduction()).toBe(true);
67
+ });
68
+ },
69
+ },
70
+ {
71
+ name: 'testing takes precedence — is* are mutually exclusive (exactly one true)',
72
+ run: (ctx) => {
73
+ withEnv({ BXM_TEST_MODE: 'true', BXM_BUILD_MODE: 'true' }, () => {
74
+ ctx.expect(helpers.isTesting()).toBe(true);
75
+ ctx.expect(helpers.isDevelopment()).toBe(false);
76
+ ctx.expect(helpers.isProduction()).toBe(false);
65
77
  });
66
78
  },
67
79
  },
@@ -70,6 +82,8 @@ module.exports = {
70
82
  run: (ctx) => {
71
83
  function FakeManager() {}
72
84
  helpers.attachTo(FakeManager);
85
+ ctx.expect(typeof FakeManager.getEnvironment).toBe('function');
86
+ ctx.expect(typeof FakeManager.prototype.getEnvironment).toBe('function');
73
87
  ctx.expect(typeof FakeManager.isDevelopment).toBe('function');
74
88
  ctx.expect(typeof FakeManager.prototype.isDevelopment).toBe('function');
75
89
  ctx.expect(typeof FakeManager.isTesting).toBe('function');
@@ -77,6 +91,31 @@ module.exports = {
77
91
  ctx.expect(typeof FakeManager.getVersion).toBe('function');
78
92
  },
79
93
  },
94
+ {
95
+ // The core invariant of the SSOT refactor: is*() DERIVE from getEnvironment(), so they
96
+ // can NEVER disagree with it, and exactly one is always true. (In build-time Node `chrome`
97
+ // is undefined, so getEnvironment() resolves via the env-var fallback.)
98
+ name: 'invariant: is*() exactly matches getEnvironment() + mutually exclusive (every scenario)',
99
+ run: (ctx) => {
100
+ const scenarios = [
101
+ { env: { BXM_TEST_MODE: 'true', BXM_BUILD_MODE: 'true', NODE_ENV: null }, expect: 'testing' },
102
+ { env: { BXM_TEST_MODE: null, BXM_BUILD_MODE: 'true', NODE_ENV: null }, expect: 'production' },
103
+ { env: { BXM_TEST_MODE: null, BXM_BUILD_MODE: null, NODE_ENV: 'development' }, expect: 'development' },
104
+ { env: { BXM_TEST_MODE: null, BXM_BUILD_MODE: null, NODE_ENV: null }, expect: 'development' }, // BXM defaults dev (unpacked)
105
+ ];
106
+ for (const s of scenarios) {
107
+ withEnv(s.env, () => {
108
+ const e = helpers.getEnvironment();
109
+ ctx.expect(e).toBe(s.expect);
110
+ ctx.expect(helpers.isDevelopment()).toBe(e === 'development');
111
+ ctx.expect(helpers.isTesting()).toBe(e === 'testing');
112
+ ctx.expect(helpers.isProduction()).toBe(e === 'production');
113
+ const trueCount = [helpers.isDevelopment(), helpers.isTesting(), helpers.isProduction()].filter(Boolean).length;
114
+ ctx.expect(trueCount).toBe(1);
115
+ });
116
+ }
117
+ },
118
+ },
80
119
  {
81
120
  name: 'getVersion() reads cwd package.json#version in Node context',
82
121
  run: (ctx) => {
@@ -1,54 +1,75 @@
1
- // Runtime mode helpers (BEM/EM-pattern), shared across BXM's eight context Managers
1
+ // Runtime mode helpers (BEM/EM/UJM-pattern), shared across BXM's eight context Managers
2
2
  // (build / background / popup / options / content / sidepanel / page / offscreen).
3
3
  //
4
- // Three orthogonal concepts:
5
- // isDevelopment() true when running unpacked from disk (an unpacked extension
6
- // loaded via chrome://extensions or a dev build of the framework).
7
- // Browser detection uses `chrome.runtime.getManifest().update_url`
8
- // packed extensions from the Web Store have one, unpacked ones
9
- // do not. Falls back to NODE_ENV in Node contexts.
10
- // isProduction() — inverse. Running from a packed .crx / store-installed extension.
11
- // isTesting() — true when BXM's test framework is running this process. Set by
12
- // BXM's test runners (BXM_TEST_MODE=true) and consumer test setups
13
- // that want the same signal.
4
+ // `getEnvironment()` is the SINGLE SOURCE OF TRUTH: it is the ONLY function that reads the
5
+ // raw signals (BXM_TEST_MODE / manifest.update_url / BXM_BUILD_MODE / NODE_ENV /
6
+ // config.bxm.environment) and resolves them to exactly ONE of three mutually-exclusive
7
+ // values. The three is*() checks DERIVE from it — they never read raw signals themselves,
8
+ // so they can never disagree with getEnvironment().
14
9
  //
15
- // Use these whenever behavior should differ by *what kind of process* you're in —
16
- // shorter timeouts in tests, DevTools menu items only in dev, prompts suppressed in
17
- // tests. Don't use them for "should we hit dev or prod backends" — that's a config
18
- // concern; use `getEnvironment()` for that (in build.js).
10
+ // isDevelopment() `getEnvironment() === 'development'`: running unpacked from disk (an
11
+ // unpacked extension via chrome://extensions or a dev build), and NOT
12
+ // testing.
13
+ // isTesting() — `getEnvironment() === 'testing'`: BXM's test framework is running this
14
+ // process (BXM_TEST_MODE=true). TAKES PRECEDENCE — a test run is not dev.
15
+ // isProduction() — `getEnvironment() === 'production'`: running from a packed .crx /
16
+ // store-installed extension, and NOT testing. A real positive check —
17
+ // NOT `!isDevelopment()`.
19
18
  //
20
- // Context caveat: in build-time Node (gulp / CLI), `chrome` is undefined. We detect
21
- // via `typeof chrome` so the same code works in every context. In test mode the
22
- // browser-side check is short-circuited via BXM_TEST_MODE so `isDevelopment()`
23
- // returns a stable value regardless of which test layer is running.
19
+ // To gate "anything non-production" use `!isProduction()` or `isDevelopment() ||
20
+ // isTesting()` intentionally never assume two values.
21
+ //
22
+ // Context caveat: in build-time Node (gulp / CLI), `chrome` is undefined. getEnvironment()
23
+ // detects via `typeof chrome` so the same code works in every context. Browser detection
24
+ // uses `chrome.runtime.getManifest().update_url` — packed store extensions have one,
25
+ // unpacked ones do not.
24
26
 
25
- function isDevelopment() {
26
- // Browser-side: rely on `update_url` being absent for unpacked extensions.
27
- // (Store-installed extensions have `update_url` set to clients2.google.com / similar.)
27
+ // getEnvironment() — the SINGLE SOURCE OF TRUTH. Reads every raw signal and resolves to
28
+ // exactly ONE of 'development' | 'testing' | 'production' (mutually exclusive; testing wins).
29
+ // Precedence: testing production development.
30
+ function getEnvironment() {
31
+ // 1. Testing wins — set by BXM's test runners / harness, or a testing-baked build.
32
+ // Works in Node (process.env), extension contexts (globalThis set before consumer JS),
33
+ // and config-baked builds (config.bxm.environment === 'testing').
34
+ if (typeof process !== 'undefined' && process.env && process.env.BXM_TEST_MODE === 'true') return 'testing';
35
+ if (typeof globalThis !== 'undefined' && globalThis.BXM_TEST_MODE === true) return 'testing';
36
+ if (this && this.config && this.config.bxm && this.config.bxm.environment === 'testing') return 'testing';
37
+
38
+ // 2. Browser-side: packed/store extensions have `update_url`; unpacked ones do not.
39
+ // This is the authoritative runtime signal in an extension context.
28
40
  if (typeof chrome !== 'undefined' && chrome.runtime && typeof chrome.runtime.getManifest === 'function') {
29
41
  try {
30
- const manifest = chrome.runtime.getManifest();
31
- return !manifest.update_url;
32
- } catch (_) { /* fall through */ }
42
+ return chrome.runtime.getManifest().update_url ? 'production' : 'development';
43
+ } catch (_) { /* fall through to Node/config signals */ }
33
44
  }
34
- // Node / build-time fallback.
35
- if (process.env.NODE_ENV === 'development') return true;
36
- if (process.env.BXM_BUILD_MODE === 'true') return false;
37
- if (this && this.config && this.config.bxm && this.config.bxm.environment === 'development') return true;
38
- return false;
45
+
46
+ // 3. Node / build-time + config signals.
47
+ if (process.env.BXM_BUILD_MODE === 'true') return 'production';
48
+ if (process.env.NODE_ENV === 'development') return 'development';
49
+ if (this && this.config && this.config.bxm && this.config.bxm.environment === 'development') return 'development';
50
+ if (this && this.config && this.config.bxm && this.config.bxm.environment === 'production') return 'production';
51
+
52
+ // 4. Default: development. BXM's deployed artifacts ALWAYS carry their signal — a packed /
53
+ // store extension has `manifest.update_url`, and build-time Node sets BXM_BUILD_MODE. So
54
+ // reaching here means a bare tooling / unpacked context, where development is the sensible
55
+ // answer. (Contrast BEM/EM, whose deployed RUNTIME can legitimately lack a signal, so they
56
+ // default to production.)
57
+ return 'development';
58
+ }
59
+
60
+ // The three checks DERIVE from getEnvironment() — they never read raw signals, so they can
61
+ // never disagree with it. isDevelopment() is NOT true in testing; isProduction() is a real
62
+ // positive check (never `!isDevelopment()`).
63
+ function isDevelopment() {
64
+ return getEnvironment.call(this) === 'development';
39
65
  }
40
66
 
41
67
  function isProduction() {
42
- return !this.isDevelopment();
68
+ return getEnvironment.call(this) === 'production';
43
69
  }
44
70
 
45
71
  function isTesting() {
46
- // Canonical signal — set by BXM's test runners and consumer test setups alike.
47
- // Works in Node (process.env) AND in extension contexts (the harness extension
48
- // sets globalThis.BXM_TEST_MODE before any consumer code runs).
49
- if (typeof process !== 'undefined' && process.env && process.env.BXM_TEST_MODE === 'true') return true;
50
- if (typeof globalThis !== 'undefined' && globalThis.BXM_TEST_MODE === true) return true;
51
- return false;
72
+ return getEnvironment.call(this) === 'testing';
52
73
  }
53
74
 
54
75
  // `getVersion()` — returns the extension's version string.
@@ -71,20 +92,24 @@ function getVersion() {
71
92
  }
72
93
 
73
94
  // Mix the helpers into a Manager constructor's prototype + the constructor itself
74
- // (so `Manager.isTesting()` works statically too, matching BEM/EM pattern).
95
+ // (so `Manager.isTesting()` works statically too, matching BEM/EM/UJM pattern).
96
+ // getEnvironment() is the SSOT and is attached here too — build.js no longer defines it.
75
97
  function attachTo(Manager) {
76
- Manager.prototype.isDevelopment = isDevelopment;
77
- Manager.prototype.isProduction = isProduction;
78
- Manager.prototype.isTesting = isTesting;
79
- Manager.prototype.getVersion = getVersion;
80
- Manager.isDevelopment = isDevelopment;
81
- Manager.isProduction = isProduction;
82
- Manager.isTesting = isTesting;
83
- Manager.getVersion = getVersion;
98
+ Manager.prototype.getEnvironment = getEnvironment;
99
+ Manager.prototype.isDevelopment = isDevelopment;
100
+ Manager.prototype.isProduction = isProduction;
101
+ Manager.prototype.isTesting = isTesting;
102
+ Manager.prototype.getVersion = getVersion;
103
+ Manager.getEnvironment = getEnvironment;
104
+ Manager.isDevelopment = isDevelopment;
105
+ Manager.isProduction = isProduction;
106
+ Manager.isTesting = isTesting;
107
+ Manager.getVersion = getVersion;
84
108
  }
85
109
 
86
110
  module.exports = {
87
111
  attachTo,
112
+ getEnvironment,
88
113
  isDevelopment,
89
114
  isProduction,
90
115
  isTesting,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-extension-manager",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Browser Extension Manager dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {