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 +3 -0
- package/dist/background.js +2 -1
- package/dist/build.js +2 -7
- package/dist/commands/install.js +1 -1
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +22 -6
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +32 -0
- package/dist/defaults/test/_init.js +10 -0
- package/dist/test/runner.js +62 -0
- package/dist/test/suites/build/manager.test.js +24 -2
- package/dist/test/suites/build/mode-helpers.test.js +44 -5
- package/dist/utils/mode-helpers.js +71 -46
- package/package.json +1 -1
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
|
package/dist/background.js
CHANGED
|
@@ -513,7 +513,8 @@ class Manager {
|
|
|
513
513
|
|
|
514
514
|
// Setup livereload
|
|
515
515
|
setupLiveReload() {
|
|
516
|
-
//
|
|
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
|
|
95
|
-
|
|
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 () {
|
package/dist/commands/install.js
CHANGED
|
@@ -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.
|
package/dist/defaults/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
});
|
package/dist/test/runner.js
CHANGED
|
@@ -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 "
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
} catch (_) { /* fall through */ }
|
|
42
|
+
return chrome.runtime.getManifest().update_url ? 'production' : 'development';
|
|
43
|
+
} catch (_) { /* fall through to Node/config signals */ }
|
|
33
44
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (process.env.BXM_BUILD_MODE === 'true') return
|
|
37
|
-
if (
|
|
38
|
-
return
|
|
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
|
|
68
|
+
return getEnvironment.call(this) === 'production';
|
|
43
69
|
}
|
|
44
70
|
|
|
45
71
|
function isTesting() {
|
|
46
|
-
|
|
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.
|
|
77
|
-
Manager.prototype.
|
|
78
|
-
Manager.prototype.
|
|
79
|
-
Manager.prototype.
|
|
80
|
-
Manager.
|
|
81
|
-
Manager.
|
|
82
|
-
Manager.
|
|
83
|
-
Manager.
|
|
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,
|