browser-extension-manager 1.6.1 → 1.7.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.
@@ -0,0 +1,131 @@
1
+ # Build System
2
+
3
+ BXM uses **gulp + webpack + sass + custom HTML templating + an electron-builder-style packaging step** to compile extension source into a Chrome-loadable, multi-browser-ready build.
4
+
5
+ ## Pipeline overview
6
+
7
+ `src/` (consumer authored) → `dist/` (intermediate) → `packaged/<browser>/raw/` (Chrome-loadable) → `packaged/<browser>/<name>.zip` (store upload).
8
+
9
+ ```
10
+ src/
11
+ ├── manifest.json # JSON5 source — comments, single quotes OK
12
+ ├── views/<component>/index.html # HTML templates
13
+ ├── _locales/en/messages.json # i18n catalog source
14
+ ├── assets/
15
+ │ ├── js/components/<component>/index.js # entry points (webpack bundles these)
16
+ │ ├── css/main.scss + components/ # SCSS
17
+ │ └── images/icon.png # source icon (1024×1024)
18
+ └── ...
19
+ ↓ gulp build (BXM_BUILD_MODE=true)
20
+ dist/
21
+ ├── manifest.json # still JSON5 — used by serve, not Chrome
22
+ ├── views/<component>/index.html # templated
23
+ ├── assets/
24
+ │ ├── js/components/<component>.bundle.js # webpack output
25
+ │ ├── css/components/<component>.bundle.css # sass output
26
+ │ └── images/ # icons (multiple sizes)
27
+ └── _locales/<lang>/messages.json # auto-translated (see docs/translations.md)
28
+ ↓ packaging step
29
+ packaged/
30
+ ├── chromium/
31
+ │ ├── raw/ # strict-JSON manifest, Chrome-loadable
32
+ │ │ ├── manifest.json # comments stripped, valid JSON
33
+ │ │ └── ... # everything from dist/
34
+ │ └── <ExtensionName>.zip # store upload
35
+ ├── firefox/raw/ + .zip
36
+ └── opera/raw/ + .zip
37
+ ```
38
+
39
+ ## Gulp tasks
40
+
41
+ Auto-loaded from [src/gulp/tasks/](../src/gulp/tasks/) via [src/gulp/main.js](../src/gulp/main.js).
42
+
43
+ | Task | Source | Purpose |
44
+ |---|---|---|
45
+ | `defaults` | [tasks/defaults.js](../src/gulp/tasks/defaults.js) | Copy framework defaults from `dist/defaults/` to consumer project on first run / setup. See [defaults.md](defaults.md). |
46
+ | `distribute` | [tasks/distribute.js](../src/gulp/tasks/distribute.js) | Copy consumer's `src/` files (HTML, manifest, locales, etc.) to `dist/` |
47
+ | `sass` | [tasks/sass.js](../src/gulp/tasks/sass.js) | Compile SCSS → CSS bundles with the load-path system (see [css.md](css.md)) |
48
+ | `webpack` | [tasks/webpack.js](../src/gulp/tasks/webpack.js) | Bundle JS per component entry point with Babel transpilation |
49
+ | `html` | [tasks/html.js](../src/gulp/tasks/html.js) | Run views through the two-step templating system (see [templating.md](templating.md)) |
50
+ | `icons` | [tasks/icons.js](../src/gulp/tasks/icons.js) | Generate icon variants from `src/assets/images/icon.png` |
51
+ | `translate` | [tasks/translate.js](../src/gulp/tasks/translate.js) | Auto-translate `_locales/en/messages.json` to 16 languages via Claude CLI (see [translations.md](translations.md)) |
52
+ | `package` | [tasks/package.js](../src/gulp/tasks/package.js) | Bundle dist/ into packaged/<browser>/raw + zip; runs `build:pre` / `build:post` hooks ([hooks.md](hooks.md)) |
53
+ | `serve` | [tasks/serve.js](../src/gulp/tasks/serve.js) | Dev server: WebSocket-based live reload, watches `src/` |
54
+ | `audit` | [tasks/audit.js](../src/gulp/tasks/audit.js) | Build-pipeline-specific checks (icons exist, manifest is valid, etc.) |
55
+
56
+ ## Webpack
57
+
58
+ [src/gulp/tasks/webpack.js](../src/gulp/tasks/webpack.js) discovers component entry points (`src/assets/js/components/<name>/index.js`) and bundles each to `dist/assets/js/components/<name>.bundle.js`.
59
+
60
+ **Babel transpilation** — `@babel/preset-env` so the bundles work in older browsers. SW + content scripts have stricter constraints (no `eval`, no ES module syntax at top level in some configs).
61
+
62
+ **Template replacement** — a webpack plugin replaces these markers in bundles at build time:
63
+ - `%%% version %%%` → `package.json#version`
64
+ - `%%% brand.name %%%` → config brand name
65
+ - `%%% brand.url %%%` → config brand URL
66
+ - `%%% environment %%%` → `'production'` or `'development'`
67
+ - `%%% liveReloadPort %%%` → WebSocket port (35729 default)
68
+ - `%%% webManagerConfiguration %%%` → JSON config blob
69
+
70
+ **Strip-dev-blocks** — [src/gulp/plugins/webpack/strip-dev-blocks.js](../src/gulp/plugins/webpack/strip-dev-blocks.js) removes `/* dev */ ... /* /dev */` blocks from production bundles.
71
+
72
+ **Custom aliases** — set via `resolve.alias` in webpack.js:
73
+ ```js
74
+ resolve: {
75
+ alias: {
76
+ '__theme__': path.resolve(paths.root, 'assets/themes/<active-theme>'),
77
+ }
78
+ }
79
+ ```
80
+
81
+ ## Sass
82
+
83
+ [src/gulp/tasks/sass.js](../src/gulp/tasks/sass.js) compiles per-component SCSS bundles. Load-path resolution lets consumer SCSS `@use 'browser-extension-manager'`, `@use 'theme'`, and `@use 'components/popup'` resolve through a search chain. Full details in [css.md](css.md).
84
+
85
+ ## HTML templating
86
+
87
+ Views in `src/views/<component>/index.html` go through two passes of `{{ }}` token replacement. See [templating.md](templating.md).
88
+
89
+ ## Packaging
90
+
91
+ [tasks/package.js](../src/gulp/tasks/package.js):
92
+
93
+ 1. **Pre-hook** — runs `hooks/build:pre.js` if present
94
+ 2. **Per-browser manifest normalization** — converts JSON5 → strict JSON for Chrome/Edge/Opera (Firefox tolerates JSON5 but normalized anyway)
95
+ 3. **Per-browser asset copy** to `packaged/<browser>/raw/`
96
+ 4. **Zip** to `packaged/<browser>/<name>.zip`
97
+ 5. **Post-hook** — runs `hooks/build:post.js`
98
+ 6. **Auto-publish** (if `BXM_IS_PUBLISH=true`) — uploads to Chrome Web Store / Firefox Add-ons / Edge Add-ons stores. See [publishing.md](publishing.md).
99
+
100
+ ## Build modes
101
+
102
+ Env vars that drive the pipeline:
103
+
104
+ - `BXM_BUILD_MODE=true` — production build (minified, no sourcemaps, dev-blocks stripped)
105
+ - `BXM_IS_PUBLISH=true` — also publish to extension stores after packaging
106
+ - `BXM_LIVERELOAD_PORT=35729` — WebSocket port for `serve` task (override if 35729 collides)
107
+ - `BXM_TEST_MODE=true` — running in BXM's test framework. Powers `Manager.isTesting()` (see [test-framework.md](test-framework.md)).
108
+ - `BXM_LOG_FILE` — override the stdout/stderr tee path, or set to `false` to disable it (see [Log files](#log-files)).
109
+
110
+ ## Live reload
111
+
112
+ `npm start` (= `gulp` with no args, by default invokes `serve`) watches `src/` and recompiles on change. A WebSocket server on `BXM_LIVERELOAD_PORT` (35729) notifies the extension's contexts. Background SW reloads itself via `chrome.runtime.reload()`; other contexts reload via `window.location.reload()`.
113
+
114
+ ## Log files
115
+
116
+ The gulp pipeline tees all output to `logs/dev.log` (`npm start`) / `logs/build.log` (`npm run build`), and `npx mgr test` tees to `logs/test.log`. Full reference — file table, capture behavior, `BXM_LOG_FILE` controls: [logging.md](logging.md).
117
+
118
+ ## Output for Chrome's "Load unpacked"
119
+
120
+ Point Chrome at `packaged/chromium/raw/` — that's the strict-JSON, fully-assembled Chrome-loadable build. NOT `dist/` (which has JSON5 manifest mid-pipeline). The test framework's boot layer auto-targets `packaged/chromium/raw/` for the same reason — see [test-boot-layer.md](test-boot-layer.md).
121
+
122
+ ## See also
123
+
124
+ - [components.md](components.md) — the seven component contexts
125
+ - [templating.md](templating.md) — `{{ }}` token replacement
126
+ - [css.md](css.md) — SCSS load paths
127
+ - [defaults.md](defaults.md) — `src/defaults/` template system
128
+ - [hooks.md](hooks.md) — `build:pre` / `build:post`
129
+ - [translations.md](translations.md) — auto-translate `_locales/`
130
+ - [publishing.md](publishing.md) — store auto-publishing
131
+ - [test-boot-layer.md](test-boot-layer.md) — verify the packaged extension actually boots in Chromium
@@ -0,0 +1,45 @@
1
+ # CDP Debugging (driving a live browser)
2
+
3
+ How to launch a browser you can CONTROL — see the extension live, screenshot it, click, type, read console logs, inspect network requests — for agents (Claude via MCP/CDP) and humans. For BXM this is THE dev surface: the extension only exists inside a running Chrome.
4
+
5
+ > Mirrored across the four sister frameworks (UJM / BEM / BXM / EM) — same core section, framework-flavored. Edit all four together.
6
+
7
+ ## Launching a controllable Chrome (the canonical command)
8
+
9
+ ```bash
10
+ open -gna "Google Chrome" --args \
11
+ --remote-debugging-port=9223 \
12
+ --user-data-dir="$HOME/Library/Application Support/chrome-profiles/agent" \
13
+ --no-first-run --no-default-browser-check \
14
+ --disable-background-timer-throttling \
15
+ --disable-backgrounding-occluded-windows \
16
+ --disable-renderer-backgrounding
17
+ ```
18
+
19
+ Verify it's up: `curl -s http://127.0.0.1:9223/json/version`
20
+
21
+ The rules that make this work (each one learned the hard way):
22
+
23
+ - **`open -gna` launches WITHOUT stealing focus.** `-g` = don't bring to foreground, `-n` = new instance (required — without it `open` just activates the already-running daily Chrome and the `--args` are ignored). Launching the Chrome binary directly ALWAYS activates the app and steals focus. Do NOT use `-j`/`--hide` — animations need a visible window; instead the three `--disable-*` flags keep timers/rAF/rendering at FULL speed while the window sits behind your work (verified: rAF at the display's native 120fps while backgrounded, focus never moved).
24
+ - **`--user-data-dir` is REQUIRED, not optional.** Chrome 136+ **silently ignores** `--remote-debugging-port` on the default profile — no error, no port, nothing (verified on Chrome 149). This is the #1 "why isn't CDP up" trap.
25
+ - **The profile dir IS the persistent state** — logins AND installed extensions. Cookies + localStorage survive relaunches (verified by round-trip), and so does an unpacked extension you've loaded (see below). Ecosystem convention: ONE shared profile at `~/Library/Application Support/chrome-profiles/agent` across all four frameworks, so setup is one-time.
26
+ - **One Chrome instance per profile dir — but MANY agents per instance.** CDP is multi-client (verified: two concurrent clients driving different tabs of one instance): agents and sessions attach to the SAME port, each drives its own tab, and all share the profile's logins and extensions. One agent per tab is the only rule. A second launch with the same dir just opens a window in the existing instance and **ignores the new debug port** — attach to the running one instead. Reach for a second profile + port (`…/b` on 9224) only for a different IDENTITY (a different account = a different cookie jar) or hard isolation.
27
+ - It runs **side-by-side with the daily Chrome** — a different `--user-data-dir` is a fully separate instance.
28
+ - **Quit by profile match, never by app name**: `pkill -f "chrome-profiles/agent"`. (`osascript 'tell app "Google Chrome" to quit'` hits the daily browser too — same app name.)
29
+
30
+ ## Loading the extension (BXM specifics)
31
+
32
+ - **`--load-extension` does NOT work on stable Chrome** — silently ignored (verified on Chrome 149; support was removed from branded Chrome around v137). Do not build tooling on it.
33
+ - Instead, load it ONCE through the UI: in the agent-profile Chrome, open `chrome://extensions` → enable **Developer mode** → **Load unpacked** → select the build output. **The persistent profile keeps it installed across every relaunch** — reload it from `chrome://extensions` (or the ⟳ button) after rebuilds.
34
+ - The extension's surfaces appear as CDP targets: the background service worker (`type: "service_worker"`, `chrome-extension://<id>/…`), the popup while it's open, and content scripts inside the page targets they're injected into.
35
+
36
+ ## Driving it
37
+
38
+ | Client | Good for | Port handoff |
39
+ |---|---|---|
40
+ | `chrome-devtools` MCP | rich interaction — click, fill, type, screenshots, network requests, console messages, performance traces | `CHROME_CDP_PORT` env var, **expanded ONCE when the Claude session spawns its MCP — set it BEFORE launching `claude`** (mid-session changes do nothing) |
41
+ | Any CDP client — including EM's `npx mgr cdp` run from any EM project | quick JS eval, per-renderer screenshots | per invocation: `EM_CDP_PORT=9223 npx mgr cdp eval "<url-substring>" 'document.title'` |
42
+
43
+ Port conventions: **9222** = Electron apps (EM), **9223+** = Chrome instances.
44
+
45
+ Navigating to a brand's UJM dev site? BrowserSync serves over HTTPS (self-signed cert). Prefer `https://localhost:4000`; fall back to the machine's local network IP (e.g. `https://192.168.x.x:4000`) if localhost doesn't connect. Port 4000 by default, increments to 4001+ when multiple sites run. Read the exact URL from `.temp/_config_browsersync.yml` at the root of the WEBSITE project (the UJM consumer — e.g. `<brand>-website/.temp/_config_browsersync.yml`, NOT this extension repo).
package/docs/cli.md ADDED
@@ -0,0 +1,70 @@
1
+ # CLI
2
+
3
+ `npx bxm <command>` — aliases `xm`, `ext`, `mgr`, `browser-extension-manager`.
4
+
5
+ ## Commands
6
+
7
+ | Command | Aliases | Purpose |
8
+ |---|---|---|
9
+ | `setup` | `-s`, `--setup` | Scaffold a consumer project (copy `src/defaults/`, install peer deps, write projectScripts). Default when no command given. |
10
+ | `clean` | `-c`, `--clean` | Remove `dist/`, `packaged/`, `.cache/`, `.temp/` |
11
+ | `install` | `-i`, `i`, `--install` | Install peer deps (gulp, etc.) |
12
+ | `test` | `-t`, `--test` | Run framework + project test suites. Positional target scopes by source + path (`project:` / `mgr:` / bare path); `--filter` matches test names; `--extended` enables real-external-API tests. See [test-framework.md](test-framework.md). |
13
+ | `version` | `-v`, `--version` | Print BXM, Node, peer-dep versions |
14
+
15
+ ## Entry point
16
+
17
+ [bin/browser-extension-manager](../bin/browser-extension-manager) — yargs-based shim that loads [src/cli.js](../src/cli.js).
18
+
19
+ [src/cli.js](../src/cli.js) is the alias resolver: it maps short flags and positional args to a command module under [src/commands/](../src/commands/), then invokes it with the parsed options.
20
+
21
+ ## Adding a new command
22
+
23
+ 1. Create `src/commands/<name>.js` exporting `async function (options) { /* ... */ }`
24
+ 2. Add to `ALIASES` in [src/cli.js](../src/cli.js):
25
+ ```js
26
+ const ALIASES = {
27
+ clean: ['-c', '--clean'],
28
+ setup: ['-s', '--setup'],
29
+ <name>: ['-x', '--<name>'],
30
+ };
31
+ ```
32
+ 3. Optionally add to `projectScripts` in [package.json](../package.json) so consumers get a wrapper npm script on `npx bxm setup`.
33
+ 4. Document under this page.
34
+
35
+ ## Command options
36
+
37
+ Yargs parses `--foo bar` and `--foo=bar` into `options.foo`. Positional args go into `options._[]`. Each command reads what it needs:
38
+
39
+ ```js
40
+ // src/commands/test.js
41
+ module.exports = async function (options) {
42
+ const layer = options.layer || 'all';
43
+ const target = (options._ && options._[1]) || null; // positional: `npx bxm test <target>`
44
+ const filter = options.filter || null;
45
+ const reporter = options.reporter || 'pretty';
46
+ // ...
47
+ };
48
+ ```
49
+
50
+ ## Env var conventions
51
+
52
+ Commands read BXM-prefixed env vars for behavior switches (one exception: `TEST_EXTENDED_MODE` is deliberately unprefixed — the SAME name across BEM/BXM/UJM/EM):
53
+
54
+ | Env | Used by | Purpose |
55
+ |---|---|---|
56
+ | `BXM_BUILD_MODE=true` | gulp tasks | Production build mode |
57
+ | `BXM_IS_PUBLISH=true` | gulp/package | Also publish to extension stores after packaging |
58
+ | `BXM_LOG_FILE` | gulp + test runners | Override the stdout/stderr tee path, or `false` to disable (see [logging.md](logging.md)) |
59
+ | `BXM_TEST_MODE=true` | test runners | Powers `Manager.isTesting()` (auto-set by `npx bxm test`) |
60
+ | `TEST_EXTENDED_MODE=true` | test runners | Run tests that hit REAL external services (`--extended` is the CLI shorthand; see [test-framework.md](test-framework.md)) |
61
+ | `BXM_TEST_BOOT_PROJECT` | test/boot | Override project root for boot tests |
62
+ | `BXM_TEST_BOOT_DIR` | test/boot | Override extension dir directly |
63
+ | `BXM_TEST_DEBUG=1` | test runners | Pipe Chromium stderr to console |
64
+ | `BXM_LIVERELOAD_PORT` | gulp/serve | WebSocket port (default 35729) |
65
+
66
+ ## See also
67
+
68
+ - [build-system.md](build-system.md) — `gulp` is what most CLI commands ultimately invoke
69
+ - [test-framework.md](test-framework.md) — `npx bxm test` command surface
70
+ - [defaults.md](defaults.md) — `npx bxm setup` invokes the defaults task
@@ -0,0 +1,10 @@
1
+ # Common Mistakes to Avoid
2
+
3
+ 1. **Defining a local `escapeHTML` / `sanitizeURL` helper** — Use the canonical inline form `webManager.utilities().escapeHTML(value)` / `.sanitizeURL(url)` at every call site. Do NOT alias / destructure / `.bind()`. This is the most common XSS-introduction vector in BXM extensions.
4
+ 2. **Using raw `chrome.*` instead of `extension.*`** — Use the extension API wrapper for cross-browser compat (Chrome / Firefox / Edge).
5
+ 3. **Putting service-worker-incompatible code in `background`** — Service workers can't hold WebSockets or long-running state. Use the `offscreen` component for persistent work.
6
+ 4. **Trying to share Manager state across components** — Each component has its own Manager singleton. Use `extension.runtime.sendMessage` / `extension.storage` to communicate.
7
+ 5. **Writing `<html>`/`<head>`/`<body>` in component views** — Views are HTML fragments; BXM wraps them with `page-template.html` at build time.
8
+ 6. **Hand-editing `dist/manifest.json` or `dist/*` files** — Edit `config/manifest.json` (or component sources); BXM regenerates dist on every build.
9
+ 7. **Installing BXM's dependencies as direct consumer deps** — Consumer projects must NOT `npm install firebase`, `web-manager`, or any other BXM/web-manager transitive dep. BXM's webpack config includes `resolve.modules` pointing at the framework's own `node_modules/`. If a dependency isn't resolving, the fix is in BXM's webpack config — not the consumer's `package.json`. Mirrors EM and UJM.
10
+ 8. **Touching Firebase directly in consumer code** — Firebase is owned by web-manager. Consumer code NEVER does `import firebase from 'firebase/app'` or `require('firebase')`. Instead: `import webManager from 'web-manager'` → `webManager.auth()`, `webManager.firestore()`. Same rule in EM and UJM.
@@ -0,0 +1,87 @@
1
+ # Component Architecture
2
+
3
+ Extensions are organized around **components**, each representing a distinct browser-extension context. Each component bundles a view, styles, and script.
4
+
5
+ ## The seven component contexts
6
+
7
+ | Component | Runs in | When |
8
+ |---|---|---|
9
+ | `background` | MV3 service worker | Always — extension's source of truth, handles auth, messaging, lifecycle |
10
+ | `popup` | Browser-action popup tab | When the user clicks the extension's toolbar button |
11
+ | `options` | Standalone tab (or embedded settings page) | When the user opens extension settings |
12
+ | `sidepanel` | Chrome side panel (Chrome 114+) | When the user opens the side panel |
13
+ | `content` | Each web page the user visits | Injected by manifest `content_scripts` (or programmatically) |
14
+ | `pages` | A custom extension page (e.g. dashboard, welcome) | Routed via `chrome.tabs.create({ url: chrome.runtime.getURL('views/pages/index.html') })` |
15
+ | `offscreen` | Offscreen document (Chrome 109+) | Persistent SW-adjacent context for WebSocket, DOM parsing, long-running tasks |
16
+
17
+ ## Each component has three parts
18
+
19
+ For a component named `<component>`:
20
+
21
+ | Part | Source file | Compiled output |
22
+ |---|---|---|
23
+ | **View** (HTML) | `src/views/<component>/index.html` | `dist/views/<component>/index.html` |
24
+ | **Styles** (SCSS) | `src/assets/css/components/<component>/index.scss` | `dist/assets/css/components/<component>.bundle.css` |
25
+ | **Script** (JS) | `src/assets/js/components/<component>/index.js` | `dist/assets/js/components/<component>.bundle.js` |
26
+
27
+ The build pipeline wires these together — `views/<component>/index.html` automatically references its matching `.bundle.css` and `.bundle.js`.
28
+
29
+ ## Manifest wiring per component
30
+
31
+ `src/manifest.json` (JSON5, merged with BXM's default manifest at build — see [build-system.md](build-system.md)) registers each component with the browser:
32
+
33
+ | Component | Manifest field |
34
+ |---|---|
35
+ | `popup` | `action.default_popup: 'views/popup/index.html'` |
36
+ | `options` | `options_ui.page: 'views/options/index.html'` |
37
+ | `sidepanel` | `side_panel.default_path: 'views/sidepanel/index.html'` |
38
+ | `background` | `background.service_worker: 'assets/js/components/background.bundle.js'` |
39
+ | `content` | `content_scripts` (match patterns + the content bundle) |
40
+ | `offscreen` | created programmatically — requires the `offscreen` permission (see [offscreen.md](offscreen.md)) |
41
+ | `pages` | none — opened via `extension.tabs.create({ url: extension.runtime.getURL('views/pages/index.html') })` |
42
+
43
+ ## Manager-per-context
44
+
45
+ Each component context gets its own Manager class with a one-line bootstrap. See [managers.md](managers.md) for the full list and import paths.
46
+
47
+ ## Boot order across contexts
48
+
49
+ There is no global "boot order" — each context boots independently when the browser instantiates it. BUT they coordinate via messaging:
50
+
51
+ 1. **Background SW** boots first when the extension is installed/reloaded — it's the source of truth.
52
+ 2. **Popup / options / sidepanel / pages** boot lazily when the user opens them. On boot they `bxm:syncAuth` to background to align with the canonical auth state.
53
+ 3. **Content scripts** boot per-page-load (or per-tab navigation, depending on `run_at`).
54
+ 4. **Offscreen** is created on demand by background (e.g. when it needs DOM parsing or a long-lived WebSocket).
55
+
56
+ See [auth.md](auth.md) for the detailed sign-in / sign-out / context-load flows.
57
+
58
+ ## Adding a new component type to the framework
59
+
60
+ This is rare — only needed if you're adding a brand new context kind to BXM (e.g. devtools panel). For most consumer needs, "add a new page" means creating a new entry under `src/views/pages/<name>/` (one component, many pages).
61
+
62
+ If you DO need to add a new top-level component type:
63
+
64
+ 1. **Framework styles** — `src/assets/css/components/<component>/index.scss`
65
+ 2. **Default template files** copied into consumer projects:
66
+ - `src/defaults/src/assets/css/components/<component>/index.scss`
67
+ - `src/defaults/src/assets/js/components/<component>/index.js`
68
+ - `src/defaults/src/views/<component>/index.html`
69
+ 3. **Manager class** if the new context needs its own bootstrap surface — `src/<component>.js` (mirror `src/popup.js` shape).
70
+ 4. **Export in package.json**:
71
+ ```json
72
+ {
73
+ "exports": {
74
+ "./<component>": "./dist/<component>.js"
75
+ }
76
+ }
77
+ ```
78
+ 5. **Mix in cross-context helpers** at the bottom of the new Manager file — see [environment-detection.md](environment-detection.md).
79
+
80
+ ## See also
81
+
82
+ - [managers.md](managers.md) — Manager classes, one-line bootstrap per context
83
+ - [build-system.md](build-system.md) — how components compile through webpack/sass/html
84
+ - [defaults.md](defaults.md) — the `src/defaults/` template system
85
+ - [css.md](css.md) — SCSS load paths for component styles
86
+ - [offscreen.md](offscreen.md) — offscreen document lifecycle + messaging patterns
87
+ - [xss-prevention.md](xss-prevention.md) — escaping/sanitizing the strings components render
package/docs/css.md ADDED
@@ -0,0 +1,91 @@
1
+ # CSS Architecture
2
+
3
+ SCSS-based, theme-pluggable, with a load-path system that lets consumer SCSS reference framework + theme styles via short names (`@use 'browser-extension-manager'`, `@use 'theme'`).
4
+
5
+ ## Main entry
6
+
7
+ [src/assets/css/browser-extension-manager.scss](../src/assets/css/browser-extension-manager.scss) is the framework's CSS entry point. Consumers `@use` it to get framework defaults + utilities + theme.
8
+
9
+ ## Core modules
10
+
11
+ | Module | Source |
12
+ |---|---|
13
+ | `core/_initialize.scss` | Base resets (box-sizing, body defaults) |
14
+ | `core/_utilities.scss` | Utility classes (`.shadow-lg`, `.text-truncate`, spacing, color, etc.) |
15
+ | `core/_animations.scss` | Keyframe animations + transition mixins |
16
+
17
+ ## Per-component styles
18
+
19
+ Each component can have framework defaults in `src/assets/css/components/<name>/index.scss`. These define the FRAMEWORK'S default look — e.g. `src/assets/css/components/popup/index.scss` defines popup-specific layout that ALL BXM extensions inherit unless they override.
20
+
21
+ Consumer extensions add their OWN per-component overrides in `src/assets/css/components/<name>/index.scss` (in the consumer project, not the framework).
22
+
23
+ ## Load-path resolution
24
+
25
+ The SCSS load path is set up by [src/gulp/tasks/sass.js](../src/gulp/tasks/sass.js) with this search order:
26
+
27
+ 1. **Framework CSS** — `node_modules/browser-extension-manager/dist/assets/css`
28
+ 2. **Active theme** — `node_modules/browser-extension-manager/dist/assets/themes/<theme-id>`
29
+ 3. **Project dist** — `<consumer>/dist/assets/css`
30
+ 4. **node_modules** — for npm-installed SCSS packages
31
+
32
+ So this just works in a consumer's `src/assets/css/main.scss`:
33
+
34
+ ```scss
35
+ // 1. Resolves to BXM's main entry — sets up Bootstrap, utilities, etc.
36
+ @use 'browser-extension-manager' as * with (
37
+ $primary: #5B47FB,
38
+ );
39
+
40
+ // 2. Resolves to the active theme's _theme.scss
41
+ @use 'theme' as *;
42
+
43
+ // 3. Resolves to BXM's bundled popup defaults
44
+ @use 'components/popup' as *;
45
+
46
+ // 4. Resolves to npm-installed CSS package
47
+ @use 'pkg:bootstrap-icons' as *;
48
+
49
+ // Consumer-authored overrides come last
50
+ .my-custom-rule { color: $primary; }
51
+ ```
52
+
53
+ ## Component bundle output
54
+
55
+ Each component context gets a CSS bundle:
56
+
57
+ - `src/assets/css/components/<component>/index.scss` → `dist/assets/css/components/<component>.bundle.css`
58
+
59
+ The HTML pipeline auto-injects `<link rel="stylesheet" href="../assets/css/components/<component>.bundle.css">` into each view via the page template.
60
+
61
+ ## Theme integration
62
+
63
+ Themes vendor their own SCSS under `src/assets/themes/<theme-id>/`. The load path resolves `@use 'theme'` to the active theme — flip `config.theme.id` to switch themes without code changes. See [themes.md](themes.md).
64
+
65
+ ## Adding a utility class
66
+
67
+ [src/assets/css/core/_utilities.scss](../src/assets/css/core/_utilities.scss):
68
+
69
+ ```scss
70
+ .shadow-lg {
71
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
72
+ }
73
+ ```
74
+
75
+ After adding, run `npm run prepare` in BXM (or `npm start` in the consumer) and the new utility is available framework-wide.
76
+
77
+ ## Why this load-path system
78
+
79
+ Without it, every consumer SCSS file would need:
80
+
81
+ ```scss
82
+ @use '../../../../node_modules/browser-extension-manager/dist/assets/css/browser-extension-manager' as *;
83
+ ```
84
+
85
+ Brittle, ugly, breaks with hoisted/non-hoisted npm installs. The load-path lets `@use 'browser-extension-manager'` work regardless of where BXM is installed. Same pattern UJM uses for Jekyll themes.
86
+
87
+ ## See also
88
+
89
+ - [themes.md](themes.md) — theme system that the load path resolves
90
+ - [components.md](components.md) — three-part component structure (view + styles + script)
91
+ - [build-system.md](build-system.md) — gulp/sass task details
@@ -0,0 +1,54 @@
1
+ # Defaults System
2
+
3
+ `src/defaults/` is the starter template that BXM copies into consumer projects on first run (`npx bxm setup`). It mirrors a "fresh BXM project" — manifest, views, components, config, .nvmrc, .gitignore, .env, etc.
4
+
5
+ ## How it works
6
+
7
+ 1. During BXM's own build (`prepare-package`), files in `src/defaults/` are copied to `dist/defaults/`.
8
+ 2. When a consumer runs `npx bxm setup`, the `gulp defaults` task copies files from `dist/defaults/` into the consumer's project root.
9
+ 3. File behavior (overwrite, skip, template, rename) is controlled by `FILE_MAP` in [src/gulp/tasks/defaults.js](../src/gulp/tasks/defaults.js).
10
+
11
+ ## FILE_MAP rules
12
+
13
+ ```js
14
+ const FILE_MAP = {
15
+ 'src/**/*': { overwrite: false }, // never overwrite user code
16
+ 'hooks/**/*': { overwrite: false }, // never overwrite hooks
17
+ '_.gitignore': { name: () => '.gitignore' }, // rename on copy
18
+ '_.env': { name: () => '.env', overwrite: false },
19
+ '.nvmrc': { template: { node: '22' } }, // run templating against the source
20
+ 'package.json': { skip: (cwd) => /* dynamic skip */ },
21
+ };
22
+ ```
23
+
24
+ ## Rule types
25
+
26
+ | Rule | Behavior |
27
+ |---|---|
28
+ | `overwrite: false` | Never replace if the file exists in the project. Default for `src/**`. |
29
+ | `overwrite: true` | Always overwrite — for files BXM owns (e.g. `.github/workflows/build.yml`). |
30
+ | `skip: function` | Dynamic skip — `(cwd) => boolean`. Skip in monorepo subdirs, etc. |
31
+ | `template: data` | Run the source through templating with `data` as the var bag, then write. |
32
+ | `name: function` | Rename on copy — typically used to add a leading `.` (`_.gitignore` → `.gitignore`). |
33
+
34
+ ## Why the underscore prefix?
35
+
36
+ Files like `_.gitignore`, `_.env` are stored with a `_` prefix in `src/defaults/` so they don't interfere with BXM's own development (the framework repo doesn't want its `.env` overwritten by the template). The `name` rule renames them to the real `.foo` filename on copy.
37
+
38
+ ## Adding a new default
39
+
40
+ 1. Drop the file under `src/defaults/<path-where-it-goes-in-the-consumer>`
41
+ 2. If it needs special handling, add an entry to `FILE_MAP` in [tasks/defaults.js](../src/gulp/tasks/defaults.js)
42
+ 3. Run `npm run prepare` to refresh BXM's `dist/`
43
+ 4. Verify in a fresh consumer project: `mkdir test-consumer && cd test-consumer && npm i ../browser-extension-manager && npx bxm setup`
44
+
45
+ ## Why this exists
46
+
47
+ Consumers shouldn't have to hand-author boilerplate (manifest, sample views, sample SCSS, sample background.js, .nvmrc, .gitignore). The defaults system seeds a working extension in one command. Same idea as `create-react-app` or `vite create`, just integrated with BXM's CLI.
48
+
49
+ When BXM ships a framework improvement (e.g. a better default popup template), bumping BXM in a consumer + running `npx bxm setup` again pulls the improvements WITHOUT overwriting user changes (because most `src/**` entries are `overwrite: false`).
50
+
51
+ ## See also
52
+
53
+ - [cli.md](cli.md) — `npx bxm setup` invokes the defaults task
54
+ - [build-system.md](build-system.md) — gulp tasks pipeline
@@ -0,0 +1,79 @@
1
+ # Environment Detection
2
+
3
+ `getEnvironment()` returns exactly ONE of three mutually-exclusive, exhaustive values:
4
+
5
+ ```javascript
6
+ Manager.getEnvironment() // 'development' | 'testing' | 'production'
7
+
8
+ Manager.isDevelopment() // true ONLY in development
9
+ Manager.isTesting() // true ONLY in testing
10
+ Manager.isProduction() // true ONLY in production
11
+ ```
12
+
13
+ **The Manager is the single source of truth.** `getEnvironment()` is the ONLY function that reads the raw signals (`BXM_TEST_MODE` / `chrome.runtime.getManifest().update_url` / `BXM_BUILD_MODE` / `NODE_ENV` / `config.bxm.environment`). The three `is*()` checks **derive** from it live on every call — they never read raw signals themselves, so they can never disagree with `getEnvironment()`.
14
+
15
+ **One implementation, mixed into all eight Managers.** BXM has eight Manager entry points (build / background / popup / options / content / sidepanel / page / offscreen). The helpers are defined once in [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) and mixed into each via `attachTo(Manager)`, available as both prototype methods (`manager.isTesting()`) and statics (`Manager.isTesting()`).
16
+
17
+ ```javascript
18
+ manager.getEnvironment() // same answer in every extension context
19
+ Manager.isTesting() // static form, for build-time scripts
20
+ ```
21
+
22
+ **Resolution order:** testing wins first, then production, else development. The three checks are mutually exclusive — exactly one is true. `isDevelopment()` is **false** during testing, and `isProduction()` is a real positive check (it is NOT `!isDevelopment()`).
23
+
24
+ ## Available helpers
25
+
26
+ | Helper | Returns |
27
+ |---|---|
28
+ | `getEnvironment()` | `'development' \| 'testing' \| 'production'` — the SSOT resolver; the only reader of raw signals. |
29
+ | `isDevelopment()` | `true` ONLY in development (unpacked extension via chrome://extensions / dev build), and NOT testing. Derives from `getEnvironment()`. |
30
+ | `isTesting()` | `true` ONLY in testing (`BXM_TEST_MODE === 'true'`). **Takes precedence** — a test run is not development. |
31
+ | `isProduction()` | `true` ONLY in production (packed / store-installed extension, `manifest.update_url` present). A **real positive check** — NOT `!isDevelopment()`. |
32
+
33
+ ## Gating side effects — use the INTENTIONAL check
34
+
35
+ Because there are three environments, never gate a side effect on a two-value assumption. State what you mean:
36
+
37
+ ```javascript
38
+ // Production-only (skip real telemetry / production behavior in dev AND testing):
39
+ if (isProduction()) { /* do the real thing */ }
40
+ if (!isProduction()) { /* skip / use the safe local behavior */ }
41
+
42
+ // Local-or-test (anything that should run in BOTH dev and testing):
43
+ if (isDevelopment() || isTesting()) { /* DevTools menu items, verbose logging */ }
44
+ ```
45
+
46
+ **Avoid** `if (!isDevelopment())` or `if (env !== 'development')` to gate production behavior — those wrongly include `testing` as production and leak real side effects during test runs. This is the bug class that motivated the 3-value model. (A genuinely dev-only feature like live-reload is the exception: `env !== 'development'` correctly skips it in both testing and production.)
47
+
48
+ ## URL helpers
49
+
50
+ BXM does **not** own backend URL helpers (`getApiUrl` / `getFunctionsUrl` / `getWebsiteUrl`). Extension code that needs a backend URL reads it from the `web-manager` runtime singleton in the runtime contexts (popup / options / sidepanel / background), which follows the same local-in-dev/testing, production-otherwise convention. The rule "call the getter, never hardcode" still applies; the implementation lives in `web-manager`.
51
+
52
+ ## Where they live
53
+
54
+ Source: [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) for `getEnvironment()` + `is*()` + `getVersion()`. The module exposes the functions plus an `attachTo(Manager)` mixin. Attached at the bottom of all eight Manager files ([build.js](../src/build.js), [background.js](../src/background.js), [popup.js](../src/popup.js), [options.js](../src/options.js), [content.js](../src/content.js), [sidepanel.js](../src/sidepanel.js), [page.js](../src/page.js), [offscreen.js](../src/offscreen.js)), so every extension context resolves the environment identically.
55
+
56
+ ## How detection works
57
+
58
+ `getEnvironment()` resolves in this precedence order:
59
+
60
+ 1. **Testing** — `process.env.BXM_TEST_MODE === 'true'`, `globalThis.BXM_TEST_MODE === true`, or a build baked with `config.bxm.environment === 'testing'` (set by the harness before any consumer JS runs). A test run is a test run regardless of any other signal.
61
+ 2. **Production / Development (runtime)** — `chrome.runtime.getManifest().update_url`: present → production (packed / store-installed), absent → development (unpacked). This is the authoritative runtime signal in an extension context. In build-time Node, `chrome` is undefined, so it falls through.
62
+ 3. **Build-time + config signals** — `BXM_BUILD_MODE === 'true'` → production; `NODE_ENV === 'development'` → development; `config.bxm.environment` (`'development'` / `'production'`) override.
63
+ 4. **Default** — development. BXM's deployed artifacts always carry their signal (a packed / store extension has `manifest.update_url`; build-time Node sets `BXM_BUILD_MODE`), so reaching here means a bare tooling / unpacked context where development is correct. (Contrast BEM/EM, whose deployed *runtime* can legitimately lack a signal, so they default to **production**.)
64
+
65
+ ## Adding a new helper
66
+
67
+ Write the function in [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) (or a new `src/utils/<topic>-helpers.js` module), expose it from `attachTo(Manager)`, then call `attachTo` at the bottom of all eight Manager files. Don't define helpers on individual Manager prototypes — that leads to duplicated semantics. For anything environment-derived, derive from `getEnvironment()` rather than reading `chrome.runtime` / `process.env` directly, so there is one source of truth and no chance of drift.
68
+
69
+ ## Why this matters
70
+
71
+ **One signal, used everywhere.** The test runner sets `BXM_TEST_MODE=true`; every piece of code that calls `isTesting()` (framework or consumer) then sees `true` — no need to invent a per-module env var.
72
+
73
+ **Sub-modules check the same signal.** When framework code (an auto-update probe, an analytics flush) needs to skip side effects in tests, it checks `isTesting()` — the same answer the consumer's own code gets. No drift.
74
+
75
+ **`is*()` can never disagree with `getEnvironment()`.** Because the checks derive from the single resolver instead of reading raw signals (`manifest.update_url` vs `BXM_BUILD_MODE`), there is exactly one definition of "what environment is this," and a wrong-but-confident gate is structurally impossible.
76
+
77
+ ## See also
78
+
79
+ - [test-framework.md](test-framework.md) — `BXM_TEST_MODE` is set automatically by the test runners; extended mode (`--extended` / `TEST_EXTENDED_MODE=true`) gates real external APIs.