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 +6 -0
- package/dist/background.js +2 -1
- package/dist/build.js +2 -7
- package/dist/commands/install.js +1 -1
- package/dist/commands/test.js +34 -5
- package/dist/defaults/CHANGELOG.md +15 -0
- package/dist/defaults/CLAUDE.md +39 -6
- package/dist/defaults/docs/README.md +17 -0
- package/dist/defaults/test/README.md +38 -0
- package/dist/defaults/test/_init.js +10 -0
- package/dist/gulp/main.js +13 -0
- package/dist/test/runner.js +111 -3
- package/dist/test/suites/build/attach-log-file.test.js +90 -0
- package/dist/test/suites/build/manager.test.js +24 -2
- package/dist/test/suites/build/mode-helpers.test.js +44 -5
- package/dist/test/utils/extended-mode-warning.js +13 -0
- package/dist/utils/attach-log-file.js +105 -0
- package/dist/utils/mode-helpers.js +71 -46
- package/package.json +7 -7
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
|
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
package/dist/commands/test.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
15
|
-
process.env.
|
|
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}"` : ''}${
|
|
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.
|
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,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
|
|
package/dist/test/runner.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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 "
|
|
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
|
-
//
|
|
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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-extension-manager",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
80
|
-
"@babel/preset-env": "^7.29.
|
|
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
|
-
"web-manager": "^4.
|
|
100
|
-
"webpack": "^5.
|
|
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.
|
|
103
|
+
"ws": "^8.21.0",
|
|
104
104
|
"yargs": "^18.0.0"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|