browser-extension-manager 1.4.0 → 1.6.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,14 @@ 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 project: # ONLY your project's tests (mgr: → only framework tests)
65
+ npx bxm test --extended # also run extended suites against REAL external services (Firebase, etc.)
64
66
  ```
65
67
 
68
+ 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 **extended mode** — `--extended` or the shared, unprefixed `TEST_EXTENDED_MODE=true` env var (skipped in-source otherwise, never mocked).
69
+
70
+ All CLI output also lands in `logs/` (ANSI-stripped, truncated each run) — `test.log` from `npx bxm test`, `dev.log` from `npm start`, `build.log` from `npm run build`. Details: [docs/logging.md](docs/logging.md).
71
+
66
72
  Test files use Jest-compatible matchers:
67
73
 
68
74
  ```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
 
@@ -4,15 +4,34 @@ const fs = require('fs');
4
4
  const Manager = new (require('../build.js'));
5
5
  const logger = Manager.logger('test');
6
6
  const { run } = require('../test/runner.js');
7
+ const attachLogFile = require('../utils/attach-log-file.js');
8
+ const { EXTENDED_MODE_WARNING } = require('../test/utils/extended-mode-warning.js');
7
9
 
8
10
  module.exports = async function (options) {
11
+ // Tee all test output to <projectRoot>/logs/test.log (ANSI-stripped) — mirrors
12
+ // EM's test.log and BEM's test.log pattern.
13
+ attachLogFile(path.join(process.cwd(), 'logs', 'test.log'));
14
+
9
15
  const layer = options.layer || 'all';
16
+ // Positional target: `npx mgr test <target>` where target supports source
17
+ // prefixes — `project:`, `project:<path>`, `mgr:`, `bxm:`, or a bare `<path>`.
18
+ const target = (options._ && options._[1]) || null;
19
+ // `--filter` flag: substring match on test NAMES/descriptions (orthogonal to target).
10
20
  const filter = options.filter || null;
11
21
  const reporter = options.reporter || 'pretty';
12
- const integration = options.integration === true || options.integration === 'true';
22
+ // Extended mode opt into tests that hit REAL external services (Firebase via web-manager,
23
+ // push, any network call) instead of skipping them. Off by default so `npx mgr test` stays
24
+ // fast and offline-safe. The canonical signal is the unprefixed `TEST_EXTENDED_MODE` env var
25
+ // — the SAME name across BEM/BXM/UJM/EM (cross-framework parity); `--extended` is the CLI
26
+ // shorthand. Once set on process.env it propagates to every spawned test environment (the
27
+ // in-process Node runner, and Puppeteer's Chromium which inherits process.env).
28
+ const extended = options.extended === true
29
+ || options.extended === 'true'
30
+ || process.env.TEST_EXTENDED_MODE === 'true'
31
+ || process.env.TEST_EXTENDED_MODE === '1';
13
32
 
14
- if (integration) {
15
- process.env.BXM_TEST_INTEGRATION = '1';
33
+ if (extended) {
34
+ process.env.TEST_EXTENDED_MODE = 'true';
16
35
  }
17
36
 
18
37
  // Canonical signal — every Manager picks this up via isTesting().
@@ -32,10 +51,15 @@ module.exports = async function (options) {
32
51
  }
33
52
 
34
53
  if (reporter !== 'json') {
35
- logger.log(`Running tests (layer=${layer}${filter ? ` filter="${filter}"` : ''}${integration ? ' +integration' : ''})`);
54
+ logger.log(`Running tests (layer=${layer}${target ? ` target="${target}"` : ''}${filter ? ` filter="${filter}"` : ''}${extended ? ' +extended' : ''})`);
55
+ logger.log(`Test mode: ${extended ? 'extended (real external APIs)' : 'normal (external APIs skipped)'}`);
56
+ if (extended) {
57
+ logger.warn(EXTENDED_MODE_WARNING[0]);
58
+ EXTENDED_MODE_WARNING.slice(1).forEach((line) => logger.warn(line));
59
+ }
36
60
  }
37
61
 
38
- const result = await run({ layer, filter, reporter });
62
+ const result = await run({ layer, target, filter, reporter });
39
63
 
40
64
  if (reporter === 'json') {
41
65
  // Final machine-readable summary.
@@ -50,6 +74,11 @@ module.exports = async function (options) {
50
74
 
51
75
  if (result.failed > 0) {
52
76
  process.exitCode = 1;
77
+ await attachLogFile.detach();
53
78
  throw new Error(`${result.failed} test(s) failed`);
54
79
  }
80
+
81
+ // Flush test.log fully and restore stdout/stderr. Stream writes are async, so
82
+ // detach() resolves once the buffered tail (the Results block) is on disk.
83
+ await attachLogFile.detach();
55
84
  };
@@ -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,19 @@ 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 test build/config # bare path: run tests matching a path in BOTH sources
32
+ npx mgr test project: # run ONLY your project tests (project:<path> to narrow)
33
+ npx mgr test mgr: # run ONLY framework tests (bxm: / framework: are equivalent aliases)
34
+ npx mgr test bxm:build/config # run only framework tests matching a path
35
+ # Positional target selects which test FILES run; --filter=<substring> matches test NAMES within them
36
+ npx mgr test --extended # also run tests that hit REAL external services (off by default; TEST_EXTENDED_MODE=true is the env equivalent — shared name across BEM/BXM/UJM/EM)
37
+ # (output is teed to logs/ — dev.log on `npm start`, build.log on `npm run build`, test.log on `npx mgr test`; cat instead of scrolling scrollback)
38
+ npx mgr install dev # use LOCAL browser-extension-manager source (to test framework edits)
39
+ npx mgr install live # restore the published browser-extension-manager from npm
21
40
  ```
22
41
 
42
+ > 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.
43
+
23
44
  Load the unpacked extension in Chrome: point chrome://extensions → "Load unpacked" at `packaged/chromium/raw/`.
24
45
 
25
46
  ## Where things live
@@ -57,10 +78,22 @@ After `initialize()`, every Manager exposes:
57
78
  - `manager.logger` — timestamped per-context logger
58
79
  - `manager.webManager` — Web Manager singleton (Firebase, auth, analytics, reactive `data-wm-bind` directives)
59
80
  - `manager.messenger` — `chrome.runtime.onMessage` listener wired automatically
60
- - `manager.isDevelopment()` / `isProduction()` / `isTesting()` / `getVersion()` — cross-context helpers
81
+ - `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
82
 
62
83
  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
84
 
85
+ ## Dependency resolution
86
+
87
+ - **Do NOT install framework dependencies directly** (`firebase`, `web-manager`, etc.). BXM's webpack config resolves them through the framework's own `node_modules/`. If something doesn't resolve, the issue is in BXM's webpack config — not your `package.json`.
88
+ - **web-manager owns Firebase.** Never `import firebase from 'firebase/app'`. Use `import webManager from 'web-manager'` → `webManager.auth()`, `webManager.firestore()`.
89
+ - **`Manager.require(name)`** resolves from BXM's module context at runtime for unbundled code (gulp tasks, test fixtures).
90
+
91
+ ## Testing
92
+
93
+ Every feature ships with tests at every layer it has a surface in: **logic** (`test/build/`, `test/background/`), **UI** (`test/view/` — real events on the real DOM), and **end-to-end** (`test/boot/`). Skip a layer only when the feature genuinely has no surface there — "the logic test covers it" does not excuse the UI test. See `test/README.md` and `node_modules/browser-extension-manager/docs/test-framework.md`.
94
+
95
+ <!-- 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. -->
96
+
64
97
  # ========== Custom Values ==========
65
98
 
66
99
  ## 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,38 @@
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
+ ## Coverage
17
+
18
+ Every feature ships with tests at every layer it has a surface in — logic (`build`/`background`), UI (`view`), end-to-end (`boot`). Skip a layer only when the feature genuinely has no surface there; "the logic test covers it" does not excuse the UI test.
19
+
20
+ Tests that hit REAL external services (Firebase, push, network) are skipped by default — gate them on `process.env.TEST_EXTENDED_MODE` (`if (process.env.TEST_EXTENDED_MODE !== 'true') ctx.skip('extended mode off');`) and run them with `npx mgr test --extended` (or `TEST_EXTENDED_MODE=true`). `TEST_EXTENDED_MODE` is the shared, unprefixed name across BEM/BXM/UJM/EM. Never mock the external service — skip it in-source.
21
+
22
+ ## Quick example
23
+
24
+ ```js
25
+ // test/build/my-feature.test.js
26
+ const assert = require('browser-extension-manager/test/assert');
27
+
28
+ module.exports = {
29
+ 'my feature does the thing': async () => {
30
+ const result = await doTheThing();
31
+ assert.equal(result, 'expected');
32
+ },
33
+ };
34
+ ```
35
+
36
+ ## See also
37
+
38
+ `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
+ });
package/dist/gulp/main.js CHANGED
@@ -14,6 +14,19 @@ const projectRoot = Manager.getRootPath('project');
14
14
  // Load .env file from project root
15
15
  require('dotenv').config({ path: path.join(projectRoot, '.env') });
16
16
 
17
+ // Tee all stdout/stderr to <projectRoot>/logs/<dev|build>.log for easy `tail -f` / grep / Claude
18
+ // inspection — captures gulp task output, webpack/serve output, console.log calls, the works.
19
+ // build.log for production builds (BXM_BUILD_MODE=true), dev.log for `npm start`.
20
+ // Disable via BXM_LOG_FILE=false. Override path via BXM_LOG_FILE=<path>.
21
+ const attachLogFile = require('../utils/attach-log-file.js');
22
+ const logFileEnv = process.env.BXM_LOG_FILE;
23
+ if (logFileEnv !== 'false' && logFileEnv !== '0') {
24
+ const defaultName = Manager.isBuildMode() ? 'build.log' : 'dev.log';
25
+ const logPath = (logFileEnv && logFileEnv !== 'true') ? logFileEnv : path.join(projectRoot, 'logs', defaultName);
26
+ attachLogFile(logPath);
27
+ logger.log(`Logs tee'd to ${logPath}`);
28
+ }
29
+
17
30
  // Log
18
31
  logger.log('Starting...', argv);
19
32
 
@@ -39,16 +39,21 @@ class SkipError extends Error {
39
39
 
40
40
  async function run(options = {}) {
41
41
  options.layer = options.layer || 'all';
42
+ options.target = options.target || null;
42
43
  options.filter = options.filter || null;
43
44
  options.reporter = options.reporter || 'pretty';
44
45
 
45
46
  const startTime = Date.now();
46
47
 
47
- const sources = discoverTestFiles();
48
+ const sources = discoverTestFiles(options.target);
48
49
 
49
50
  console.log('');
50
51
  console.log(chalk.bold(' Browser Extension Manager Tests'));
51
52
 
53
+ // Run the optional test/_init.js setup() hooks (framework + consumer) ONCE,
54
+ // before any suite. There is no cleanup hook — tests clean up after themselves.
55
+ await runInitSetups();
56
+
52
57
  const results = { passed: 0, failed: 0, skipped: 0, tests: [] };
53
58
 
54
59
  if (sources.framework.length > 0) {
@@ -356,7 +361,49 @@ function reportResults(results, durationMs) {
356
361
  console.log(chalk.gray(`\n Total: ${total} tests in ${durationMs}ms\n`));
357
362
  }
358
363
 
359
- function discoverTestFiles() {
364
+ // Parse a positional test target into a source filter + path part.
365
+ // Source prefixes (standardized across all OMEGA frameworks):
366
+ // 'mgr:' / 'bxm:' / 'framework:' → framework tests ('mgr:' is the universal alias)
367
+ // 'project:' → project tests
368
+ // no prefix → both sources, matched by path
369
+ function parseTarget(target) {
370
+ if (!target) {
371
+ return { source: null, pathPart: null };
372
+ }
373
+
374
+ const m = String(target).match(/^(project|mgr|bxm|framework):(.*)$/);
375
+ if (m) {
376
+ const source = m[1] === 'project' ? 'project' : 'framework';
377
+ return { source, pathPart: m[2] || null };
378
+ }
379
+
380
+ return { source: null, pathPart: target };
381
+ }
382
+
383
+ // Narrow a source's file list by the parsed target. A source-prefixed target
384
+ // excludes the other source entirely; the path part (if any) matches by
385
+ // relative path prefix.
386
+ function filterBySource(source, files, sourceFilter, pathPart) {
387
+ if (sourceFilter && sourceFilter !== source) {
388
+ return [];
389
+ }
390
+ if (!pathPart) {
391
+ return files;
392
+ }
393
+
394
+ return files.filter((file) => {
395
+ const rel = relativizePath(file, source);
396
+ const relNoExt = rel.replace(/\.js$/, '').replace(/\.test$/, '');
397
+ const partNoExt = pathPart.replace(/\.js$/, '').replace(/\.test$/, '');
398
+ return rel.startsWith(pathPart)
399
+ || relNoExt === partNoExt
400
+ || relNoExt.startsWith(partNoExt + '/')
401
+ || rel.includes(pathPart);
402
+ });
403
+ }
404
+
405
+ function discoverTestFiles(target) {
406
+ const { source: sourceFilter, pathPart } = parseTarget(target);
360
407
  const framework = [];
361
408
  const project = [];
362
409
 
@@ -394,7 +441,10 @@ function discoverTestFiles() {
394
441
  });
395
442
  }
396
443
 
397
- return { framework, project };
444
+ return {
445
+ framework: filterBySource('framework', framework, sourceFilter, pathPart),
446
+ project: filterBySource('project', project, sourceFilter, pathPart),
447
+ };
398
448
  }
399
449
 
400
450
  function relativizePath(file, source) {
@@ -404,4 +454,62 @@ function relativizePath(file, source) {
404
454
  return path.relative(path.join(process.cwd(), 'test'), file);
405
455
  }
406
456
 
457
+ // ---------------------------------------------------------------------------
458
+ // test/_init.js — pre-test lifecycle hook (setup only)
459
+ //
460
+ // Mirrors the backend framework's hook so all four frameworks share one shape.
461
+ // A project may add `<cwd>/test/_init.js` exporting a FUNCTION —
462
+ // `module.exports = (ctx) => ({ setup })` — called with `{ projectRoot }` and
463
+ // returning an object with an async `setup({ projectRoot })` that runs ONCE
464
+ // before any suite (e.g. to scaffold a fixture file the boot layer needs).
465
+ // There is no `cleanup` hook: tests clean up after themselves. Unlike the
466
+ // backend framework, there is no `accounts` field here — these frameworks have
467
+ // no auth/user system.
468
+ // ---------------------------------------------------------------------------
469
+
470
+ function loadInit(testDir, label) {
471
+ const initPath = path.join(testDir, '_init.js');
472
+
473
+ if (!jetpack.exists(initPath)) {
474
+ return {};
475
+ }
476
+
477
+ try {
478
+ const fn = require(initPath);
479
+
480
+ if (typeof fn !== 'function') {
481
+ console.log(chalk.red(` ✗ ${label} test/_init.js must export a function: module.exports = (ctx) => ({ ... })`));
482
+ return {};
483
+ }
484
+
485
+ const mod = fn({ projectRoot: process.cwd() });
486
+ return mod && typeof mod === 'object' ? mod : {};
487
+ } catch (e) {
488
+ console.log(chalk.red(` ✗ Failed to load ${label} test/_init.js: ${e.message}`));
489
+ return {};
490
+ }
491
+ }
492
+
493
+ async function runInitSetups() {
494
+ const frameworkTestsDir = path.resolve(__dirname, '../../test');
495
+ const projectTestsDir = path.join(process.cwd(), 'test');
496
+
497
+ const hooks = [
498
+ loadInit(frameworkTestsDir, 'framework'),
499
+ loadInit(projectTestsDir, 'project'),
500
+ ];
501
+
502
+ const setups = hooks.filter((h) => typeof h.setup === 'function').map((h) => h.setup);
503
+
504
+ for (const setup of setups) {
505
+ process.stdout.write(chalk.gray(' Running test/_init.js setup... '));
506
+ try {
507
+ await setup({ projectRoot: process.cwd() });
508
+ console.log(chalk.green('✓'));
509
+ } catch (e) {
510
+ console.log(chalk.red(`✗ (${e.message})`));
511
+ }
512
+ }
513
+ }
514
+
407
515
  module.exports = { run, SkipError };
@@ -0,0 +1,90 @@
1
+ // Build-layer tests for src/utils/attach-log-file.js — tee process.stdout/stderr to a file
2
+ // with ANSI stripping. Each test attaches, writes, detaches, then inspects the file.
3
+ //
4
+ // CRITICAL: these tests run INSIDE a live `npx mgr test` process whose own output is being
5
+ // teed to logs/test.log by the singleton. So they must NOT touch the singleton — exercising
6
+ // attach()/detach() on it would detach the live tee mid-run and truncate logs/test.log. Each
7
+ // test uses its OWN `createTee()` instance, which stacks under the live singleton tee and
8
+ // restores it cleanly on detach.
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+
14
+ module.exports = {
15
+ type: 'suite',
16
+ layer: 'build',
17
+ description: 'attach-log-file — tee stdout/stderr to a file',
18
+ tests: [
19
+ {
20
+ name: 'exports the expected surface',
21
+ run: (ctx) => {
22
+ const mod = require(path.join(__dirname, '..', '..', '..', 'utils', 'attach-log-file.js'));
23
+ ctx.expect(typeof mod).toBe('function');
24
+ ctx.expect(typeof mod.detach).toBe('function');
25
+ ctx.expect(typeof mod.stripAnsi).toBe('function');
26
+ ctx.expect(typeof mod.createTee).toBe('function');
27
+ },
28
+ },
29
+ {
30
+ name: 'stripAnsi removes color escape codes',
31
+ run: (ctx) => {
32
+ const { stripAnsi } = require(path.join(__dirname, '..', '..', '..', 'utils', 'attach-log-file.js'));
33
+ const colored = '\x1B[31mred\x1B[0m and \x1B[32mgreen\x1B[0m';
34
+ ctx.expect(stripAnsi(colored)).toBe('red and green');
35
+ },
36
+ },
37
+ {
38
+ name: 'attach + stdout.write + detach: file contains the writes',
39
+ run: async (ctx) => {
40
+ const attach = require(path.join(__dirname, '..', '..', '..', 'utils', 'attach-log-file.js'));
41
+ // Isolated instance — stacks under the live test.log tee, never clobbers it.
42
+ const tee = attach.createTee();
43
+ const tmpPath = path.join(os.tmpdir(), `bxm-log-${Date.now()}.log`);
44
+ try {
45
+ const stream = tee.attach(tmpPath);
46
+ process.stdout.write('hello world\n');
47
+ process.stdout.write('\x1B[31mcolored\x1B[0m line\n');
48
+ // Wait for stream to flush before detaching + reading.
49
+ await new Promise((resolve) => stream.write('', resolve));
50
+ tee.detach();
51
+ // detach() ends the stream; wait for the close event.
52
+ await new Promise((resolve) => stream.on('close', resolve));
53
+
54
+ const contents = fs.readFileSync(tmpPath, 'utf8');
55
+ ctx.expect(contents).toContain('hello world');
56
+ ctx.expect(contents).toContain('colored line');
57
+ ctx.expect(contents).not.toContain('\x1B[');
58
+ } finally {
59
+ tee.detach();
60
+ try { fs.unlinkSync(tmpPath); } catch (e) {}
61
+ }
62
+ },
63
+ },
64
+ {
65
+ name: 'idempotent: attaching twice with same path returns same stream',
66
+ run: (ctx) => {
67
+ const attach = require(path.join(__dirname, '..', '..', '..', 'utils', 'attach-log-file.js'));
68
+ const tee = attach.createTee();
69
+ const tmpPath = path.join(os.tmpdir(), `bxm-log-idem-${Date.now()}.log`);
70
+ try {
71
+ const s1 = tee.attach(tmpPath);
72
+ const s2 = tee.attach(tmpPath);
73
+ ctx.expect(s1).toBe(s2);
74
+ } finally {
75
+ tee.detach();
76
+ try { fs.unlinkSync(tmpPath); } catch (e) {}
77
+ }
78
+ },
79
+ },
80
+ {
81
+ name: 'attach with falsy path returns null and does nothing',
82
+ run: (ctx) => {
83
+ const attach = require(path.join(__dirname, '..', '..', '..', 'utils', 'attach-log-file.js'));
84
+ const tee = attach.createTee();
85
+ ctx.expect(tee.attach(null)).toBe(null);
86
+ ctx.expect(tee.attach('')).toBe(null);
87
+ },
88
+ },
89
+ ],
90
+ };
@@ -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) => {
@@ -0,0 +1,13 @@
1
+ // TEST_EXTENDED_MODE warning — SSOT for consistent messaging.
2
+ //
3
+ // Mirrors BEM/EM/UJM: `TEST_EXTENDED_MODE` is the shared, unprefixed env var that opts a
4
+ // test run into hitting REAL external services instead of skipping/stubbing them. Off by
5
+ // default so `npx mgr test` stays fast and offline-safe. Used by the test command (printed to
6
+ // console + teed to logs/test.log).
7
+ const EXTENDED_MODE_WARNING = [
8
+ '⚠️⚠️⚠️ WARNING: TEST_EXTENDED_MODE IS TRUE ⚠️⚠️⚠️',
9
+ 'Tests that hit real external services (Firebase via web-manager, push, any network call) are ENABLED!',
10
+ 'This makes real network calls from the background service worker, popup, and content scripts against live backends.',
11
+ ];
12
+
13
+ module.exports = { EXTENDED_MODE_WARNING };
@@ -0,0 +1,105 @@
1
+ // attachLogFile(filePath) — duplicate process.stdout + process.stderr writes to a log file.
2
+ //
3
+ // Inspired by BEM's per-command log pattern, mirrored from EM. Lets devs (and Claude) `tail -f` a
4
+ // log file to see every line of output a process produces — test runner output, child process
5
+ // stdout/stderr, console.log calls, the works.
6
+ //
7
+ // ANSI color codes are stripped from the file output so it's grep-friendly. The console
8
+ // continues to receive the original colored output unchanged.
9
+ //
10
+ // The default export is a process-wide SINGLETON (the common case: a CLI command tees its
11
+ // whole run to one file). `attachLogFile.createTee()` returns an INDEPENDENT tee with its own
12
+ // state. Tees STACK: a later attach() captures the CURRENT `process.stdout.write` (which may
13
+ // already be an outer tee) as its "original", so writes fan out through every layer and
14
+ // detach() restores the exact prior writer in LIFO order. That stacking is what lets the
15
+ // attach-log-file unit tests exercise attach/detach on a throwaway instance WITHOUT killing
16
+ // the live singleton tee that's capturing the actual test run — the bug that previously
17
+ // truncated `logs/test.log` to ~9 lines (the test detached the live tee mid-run).
18
+ //
19
+ // Idempotent: calling attach() twice with the same path on one tee returns the existing handle.
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
25
+
26
+ function stripAnsi(s) {
27
+ return String(s).replace(ANSI_PATTERN, '');
28
+ }
29
+
30
+ // Factory — each call returns an independent tee with its own closure state.
31
+ function createTee() {
32
+ let activeStream = null;
33
+ let activePath = null;
34
+ let originalStdoutWrite = null;
35
+ let originalStderrWrite = null;
36
+
37
+ function attach(filePath) {
38
+ if (!filePath) return null;
39
+ const abs = path.resolve(filePath);
40
+
41
+ if (activeStream && activePath === abs) return activeStream;
42
+ if (activeStream) detach();
43
+
44
+ // Truncate fresh on each invocation — same as BEM's `flags: 'w'`. Devs running multiple
45
+ // sessions back to back don't want stale lines from the previous run mixed in.
46
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
47
+ const stream = fs.createWriteStream(abs, { flags: 'w' });
48
+
49
+ // Header so the file is self-documenting.
50
+ stream.write(`# bxm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
51
+
52
+ // Capture whatever the CURRENT writer is — could be the raw stream OR an outer tee.
53
+ // Restoring this exact reference on detach() is what makes stacked tees safe.
54
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
55
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
56
+
57
+ process.stdout.write = function (chunk, ...rest) {
58
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
59
+ return originalStdoutWrite(chunk, ...rest);
60
+ };
61
+ process.stderr.write = function (chunk, ...rest) {
62
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
63
+ return originalStderrWrite(chunk, ...rest);
64
+ };
65
+
66
+ activeStream = stream;
67
+ activePath = abs;
68
+
69
+ return stream;
70
+ }
71
+
72
+ // Restores stdout/stderr and ends the stream. Returns a Promise that resolves once all
73
+ // buffered writes have been flushed to disk — await it before process.exit(), otherwise the
74
+ // tail of the log is silently dropped.
75
+ function detach() {
76
+ if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
77
+ if (originalStderrWrite) process.stderr.write = originalStderrWrite;
78
+ const stream = activeStream;
79
+ activeStream = null;
80
+ activePath = null;
81
+ originalStdoutWrite = null;
82
+ originalStderrWrite = null;
83
+
84
+ return new Promise((resolve) => {
85
+ if (!stream) {
86
+ return resolve();
87
+ }
88
+ stream.end(resolve);
89
+ });
90
+ }
91
+
92
+ return { attach, detach };
93
+ }
94
+
95
+ // Process-wide singleton — the production entry point.
96
+ const singleton = createTee();
97
+
98
+ function attachLogFile(filePath) {
99
+ return singleton.attach(filePath);
100
+ }
101
+
102
+ module.exports = attachLogFile;
103
+ module.exports.detach = singleton.detach;
104
+ module.exports.stripAnsi = stripAnsi;
105
+ module.exports.createTee = createTee;
@@ -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.6.0",
4
4
  "description": "Browser Extension Manager dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
@@ -76,8 +76,8 @@
76
76
  "homepage": "https://template.itwcreativeworks.com",
77
77
  "dependencies": {
78
78
  "@anthropic-ai/claude-agent-sdk": "^0.2.138",
79
- "@babel/core": "^7.29.0",
80
- "@babel/preset-env": "^7.29.5",
79
+ "@babel/core": "^7.29.7",
80
+ "@babel/preset-env": "^7.29.7",
81
81
  "@popperjs/core": "^2.11.8",
82
82
  "babel-loader": "^10.1.1",
83
83
  "chalk": "^5.6.2",
@@ -95,12 +95,12 @@
95
95
  "minimatch": "^10.2.5",
96
96
  "node-powertools": "^3.0.0",
97
97
  "npm-api": "^1.0.1",
98
- "sass": "^1.99.0",
99
- "web-manager": "^4.1.42",
100
- "webpack": "^5.106.2",
98
+ "sass": "^1.100.0",
99
+ "web-manager": "^4.2.0",
100
+ "webpack": "^5.107.2",
101
101
  "wonderful-fetch": "^2.0.5",
102
102
  "wonderful-version": "^1.3.2",
103
- "ws": "^8.20.0",
103
+ "ws": "^8.21.0",
104
104
  "yargs": "^18.0.0"
105
105
  },
106
106
  "peerDependencies": {