browser-extension-manager 1.6.0 → 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,94 @@
1
+ # Cross-Browser API Wrapper (`lib/extension.js`)
2
+
3
+ A singleton that normalizes the `chrome.*` / `browser.*` / `window.*` extension API surface so consumers write their extension once and it works on Chrome, Firefox, Edge, and other Chromium-based browsers.
4
+
5
+ ## Import
6
+
7
+ ```js
8
+ const ext = require('browser-extension-manager/lib/extension');
9
+
10
+ // or as a Manager property:
11
+ const Manager = new (require('browser-extension-manager/popup'));
12
+ await Manager.initialize();
13
+ const { extension } = Manager; // same singleton
14
+ ```
15
+
16
+ ## How it works
17
+
18
+ For each known extension API, the wrapper tries in order:
19
+ 1. `chrome.<api>` (Chrome, Edge, Opera, Brave)
20
+ 2. `window.<api>` (some content-script contexts where chrome is exposed under window)
21
+ 3. `browser.<api>` (Firefox)
22
+ 4. `browser.extension.<api>` (legacy fallback)
23
+
24
+ The first one that resolves becomes the singleton's `<api>` property. Each lookup is wrapped in try/catch so unknown globals don't throw at module-load time — that's what makes the wrapper safe to import from Node contexts too (where none of these globals exist).
25
+
26
+ ## Supported APIs
27
+
28
+ ```
29
+ action, alarms, bookmarks, browsingData, browserAction,
30
+ certificateProvider, commands, contentSettings, contextMenus,
31
+ cookies, debugger, declarativeContent, declarativeNetRequest,
32
+ devtools, dns, documentScan, downloads, enterprise, events,
33
+ extension, extensionTypes, fileBrowserHandler, fileSystemProvider,
34
+ fontSettings, gcm, history, i18n, identity, idle, input, instanceID,
35
+ management, notifications, offscreen, omnibox, pageAction,
36
+ permissions, platformKeys, power, printerProvider, privacy, proxy,
37
+ runtime, scripting, search, sessions, sidePanel, storage, tabGroups,
38
+ tabs, topSites, tts, ttsEngine, userScripts, vpnProvider, wallpaper,
39
+ webNavigation, webRequest, windows
40
+ ```
41
+
42
+ If a browser doesn't expose a given API, the property is `null` (not undefined), so consumers can branch on `if (extension.sidePanel) { ... }`.
43
+
44
+ ## Usage
45
+
46
+ ```js
47
+ const Manager = new (require('browser-extension-manager/popup'));
48
+ await Manager.initialize();
49
+ const { extension } = Manager;
50
+
51
+ // Works on Chrome, Firefox, Edge — identical call sites
52
+ extension.tabs.query({ active: true, currentWindow: true }, (tabs) => {
53
+ console.log('Active tab:', tabs[0]);
54
+ });
55
+
56
+ extension.storage.get('key', (result) => { /* ... */ });
57
+ extension.runtime.sendMessage({ type: 'hello' });
58
+ extension.notifications.create({ /* ... */ });
59
+ ```
60
+
61
+ ## Storage normalization
62
+
63
+ The wrapper auto-resolves `storage` to `storage.sync` when available (preferred — synced across the user's Chrome sign-in), falling back to `storage.local` when sync isn't available (e.g. some Firefox MV2 contexts).
64
+
65
+ ```js
66
+ extension.storage.set({ foo: 'bar' });
67
+ extension.storage.get('foo', (result) => console.log(result.foo));
68
+ ```
69
+
70
+ If you specifically need local-only storage (per-machine, larger quota), use `chrome.storage.local.*` directly — bypass the wrapper.
71
+
72
+ ## Node-safe by design
73
+
74
+ The wrapper imports cleanly from Node (build-time scripts, tests, gulp tasks) — every property is `null` in that context because none of the browser globals exist. This is what makes BXM's build-layer tests possible:
75
+
76
+ ```js
77
+ // build-layer test (Node)
78
+ const ext = require('browser-extension-manager/lib/extension');
79
+ ctx.expect(ext.runtime).toBeNull(); // no chrome global in Node
80
+ ```
81
+
82
+ ## Why not `webextension-polyfill`?
83
+
84
+ `webextension-polyfill` shims Firefox's promise-returning APIs onto Chrome's callback-based APIs. Useful, but:
85
+ - Adds an npm dep that has to be loaded as a content-script before user code
86
+ - Doesn't address the case where APIs simply don't exist on a browser (e.g. `sidePanel` on Firefox)
87
+ - Slow boot in SW context
88
+
89
+ BXM's wrapper is simpler: detect what's there, expose null when it isn't, let user code branch. Combine with the promisification Chrome added in MV3 (callbacks return promises when omitted) for a polyfill-free async-friendly API.
90
+
91
+ ## See also
92
+
93
+ - [managers.md](managers.md) — every Manager exposes `extension` after `initialize()`
94
+ - [components.md](components.md) — which API surface is available in which context
package/docs/hooks.md ADDED
@@ -0,0 +1,101 @@
1
+ # Build Hooks
2
+
3
+ Two lifecycle hooks let consumers run custom logic during the build pipeline.
4
+
5
+ ## Hook files
6
+
7
+ | Hook | Source | When |
8
+ |---|---|---|
9
+ | `build:pre` | `hooks/build:pre.js` (in consumer project) | Before packaging — after `dist/` is built but before `packaged/` is assembled |
10
+ | `build:post` | `hooks/build:post.js` | After packaging — `packaged/<browser>/raw/` and `.zip` exist |
11
+
12
+ Hooks are optional — `gulp/tasks/package.js` checks for the file's presence and runs it if present.
13
+
14
+ ## Hook shape
15
+
16
+ ```js
17
+ // hooks/build:pre.js
18
+ module.exports = async function (index) {
19
+ // index contains build info — package, manifest, config, paths
20
+ console.log('Pre-build hook running for', index.brand.name);
21
+
22
+ // Mutate files, generate assets, validate, anything you want.
23
+ // Return a Promise (or use async fn) to make the build wait.
24
+ };
25
+ ```
26
+
27
+ ## The `index` argument
28
+
29
+ The hook receives a build-info object with:
30
+
31
+ - `index.package` — parsed `package.json`
32
+ - `index.manifest` — parsed `src/manifest.json` (JSON5)
33
+ - `index.config` — parsed `config/browser-extension-manager.json`
34
+ - `index.brand` — shorthand for `index.config.brand`
35
+ - `index.paths` — `{ root, src, dist, packaged }` absolute paths
36
+ - `index.env` — `'production'` or `'development'`
37
+ - `index.browsers` — array of browser targets being built (`['chromium', 'firefox', 'opera']`)
38
+
39
+ ## Common uses
40
+
41
+ ### Sync a CHANGELOG version into the manifest
42
+
43
+ ```js
44
+ // hooks/build:pre.js
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+
48
+ module.exports = async function (index) {
49
+ const changelog = fs.readFileSync(path.join(index.paths.root, 'CHANGELOG.md'), 'utf8');
50
+ const latestVersion = changelog.match(/## \[([\d.]+)\]/)?.[1];
51
+ if (latestVersion && latestVersion !== index.manifest.version) {
52
+ console.warn(`Manifest version (${index.manifest.version}) doesn't match CHANGELOG latest (${latestVersion})`);
53
+ }
54
+ };
55
+ ```
56
+
57
+ ### Inject a build timestamp
58
+
59
+ ```js
60
+ // hooks/build:pre.js
61
+ const fs = require('fs');
62
+ const path = require('path');
63
+
64
+ module.exports = async function (index) {
65
+ const buildInfo = {
66
+ builtAt: new Date().toISOString(),
67
+ gitSha: require('child_process').execSync('git rev-parse HEAD').toString().trim(),
68
+ };
69
+ fs.writeFileSync(path.join(index.paths.dist, 'build-info.json'), JSON.stringify(buildInfo, null, 2));
70
+ };
71
+ ```
72
+
73
+ ### Trigger a post-publish webhook
74
+
75
+ ```js
76
+ // hooks/build:post.js
77
+ module.exports = async function (index) {
78
+ if (process.env.BXM_IS_PUBLISH !== 'true') return; // only after real publish
79
+ await fetch('https://api.myservice.com/extension-released', {
80
+ method: 'POST',
81
+ body: JSON.stringify({ version: index.manifest.version }),
82
+ });
83
+ };
84
+ ```
85
+
86
+ ## Async by default
87
+
88
+ Hooks are awaited — the build waits for them to resolve before continuing. Throw or reject to fail the build (and abort packaging / publishing).
89
+
90
+ ## Where hooks are invoked
91
+
92
+ [src/gulp/tasks/package.js](../src/gulp/tasks/package.js) loads and runs them. Search for `runHook` in that file to see the exact wiring.
93
+
94
+ ## Why not just edit gulp tasks?
95
+
96
+ You COULD fork BXM's gulp tasks for any custom build behavior. Hooks exist so consumers don't need to. Hooks are stable contract (the `index` object shape doesn't change), survive BXM upgrades, and live in the consumer's repo (where build-specific concerns belong).
97
+
98
+ ## See also
99
+
100
+ - [build-system.md](build-system.md) — gulp pipeline details
101
+ - [publishing.md](publishing.md) — store auto-publishing happens AFTER `build:post`
package/docs/icons.md ADDED
@@ -0,0 +1,47 @@
1
+ # Icons
2
+
3
+ BXM generates every extension icon size from ONE consumer-supplied source image — drop a single file, the build derives the rest.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ <consumer>/config/icon.png ← your ONE source icon (png or svg)
9
+ ```
10
+
11
+ That's it. The `icons` gulp task picks up `config/icon.{png,svg}` and generates:
12
+
13
+ ```
14
+ dist/assets/images/icons/icon.png (original, converted to png)
15
+ dist/assets/images/icons/icon-1024x.png
16
+ dist/assets/images/icons/icon-512x.png
17
+ dist/assets/images/icons/icon-256x.png
18
+ dist/assets/images/icons/icon-128x.png
19
+ dist/assets/images/icons/icon-64x.png
20
+ dist/assets/images/icons/icon-48x.png
21
+ dist/assets/images/icons/icon-32x.png
22
+ dist/assets/images/icons/icon-24x.png
23
+ dist/assets/images/icons/icon-16x.png
24
+ ```
25
+
26
+ ## Where the sizes are used
27
+
28
+ - **Manifest** — BXM's manifest template wires the `icons` map to the generated paths (`assets/images/icons/icon-<size>x.png`), covering Chrome's required 16/32/48/128 plus the larger store sizes.
29
+ - **Store packaging** — the `package` task copies `icon-128x.png` into the store-assets directory for listing uploads.
30
+
31
+ ## Sizes — ship ONE large source
32
+
33
+ Supply the source at the largest size you have (1024×1024 recommended) — generation resizes DOWN with quality 100 and strips metadata. Small sources are enlarged (`withoutEnlargement: false`), but upscaled icons look soft; start big.
34
+
35
+ ## Watch behavior
36
+
37
+ In dev (`npm start`) the task watches `config/icon.*` and regenerates on change. In build mode the watcher is skipped — generation runs once in the pipeline.
38
+
39
+ ## Source files
40
+
41
+ - [src/gulp/tasks/icons.js](../src/gulp/tasks/icons.js) — generation task (gulp-responsive-modern)
42
+ - [src/config/manifest.json](../src/config/manifest.json) — framework manifest wiring the `icons` map
43
+
44
+ ## See also
45
+
46
+ - [build-system.md](build-system.md) — where the icons task runs in the pipeline
47
+ - [extension.md](extension.md) — cross-browser API + manifest reference
@@ -0,0 +1,33 @@
1
+ # Logging
2
+
3
+ BXM tees every line of CLI/pipeline output to log files in the consumer project root, so you can `tail -f` or `grep` a run instead of scrolling terminal scrollback. Runtime extension logs live in the browser's own consoles (service-worker console, popup/options DevTools) — this doc covers the file logs BXM itself writes.
4
+
5
+ ## Log files
6
+
7
+ All in `<projectRoot>/logs/`:
8
+
9
+ | File | Source | Lifetime |
10
+ |---|---|---|
11
+ | `dev.log` | Gulp pipeline output on `npm start` | Truncated each run |
12
+ | `build.log` | Gulp pipeline output on `npm run build` (`BXM_BUILD_MODE=true`) | Truncated each run |
13
+ | `test.log` | `npx mgr test` runner output (suite names, pass/fail states, timings) | Truncated each run |
14
+
15
+ `dev.log` and `build.log` are the same gulp tee — which one it writes is chosen by `BXM_BUILD_MODE`, so they never both fill up in one run.
16
+
17
+ ## What gets captured
18
+
19
+ Everything that flows through stdout/stderr: `Manager.logger(...)` output, raw `console.log` calls, gulp task names, webpack/sass output, the works. ANSI color codes are stripped from the file (grep-friendly); the terminal continues to receive colored output unchanged.
20
+
21
+ ## Controls
22
+
23
+ | Var | Effect |
24
+ |---|---|
25
+ | `BXM_LOG_FILE=false` | Disable the tee entirely |
26
+ | `BXM_LOG_FILE=<path>` | Override the log file path |
27
+
28
+ Implementation: [src/utils/attach-log-file.js](../src/utils/attach-log-file.js), attached at the top of [src/gulp/main.js](../src/gulp/main.js) — same pattern as EM's `dev.log`/`build.log` and UJM's.
29
+
30
+ ## See also
31
+
32
+ - [build-system.md](build-system.md) — the gulp pipeline that feeds `dev.log`/`build.log`
33
+ - [test-framework.md](test-framework.md) — the test runner that feeds `test.log`
@@ -0,0 +1,100 @@
1
+ # Manager Classes
2
+
3
+ Each component context has its own Manager class — a one-line import + `initialize()` bootstrap that wires up `extension` (the cross-browser API wrapper), `logger`, `webManager` (Firebase auth), `messenger`, and cross-context helpers.
4
+
5
+ ## Import paths
6
+
7
+ | Context | Import | Source |
8
+ |---|---|---|
9
+ | Build-time (Node) | `require('browser-extension-manager/build')` | [src/build.js](../src/build.js) |
10
+ | Background SW | `require('browser-extension-manager/background')` | [src/background.js](../src/background.js) |
11
+ | Popup | `require('browser-extension-manager/popup')` | [src/popup.js](../src/popup.js) |
12
+ | Options | `require('browser-extension-manager/options')` | [src/options.js](../src/options.js) |
13
+ | Sidepanel | `require('browser-extension-manager/sidepanel')` | [src/sidepanel.js](../src/sidepanel.js) |
14
+ | Pages (custom) | `require('browser-extension-manager/page')` | [src/page.js](../src/page.js) |
15
+ | Content script | `require('browser-extension-manager/content')` | [src/content.js](../src/content.js) |
16
+ | Offscreen | `require('browser-extension-manager/offscreen')` | [src/offscreen.js](../src/offscreen.js) |
17
+
18
+ ## One-line bootstrap
19
+
20
+ Every consumer-side context entry is the same shape:
21
+
22
+ ```js
23
+ // src/assets/js/components/popup/index.js
24
+ import Manager from 'browser-extension-manager/popup';
25
+
26
+ const manager = new Manager();
27
+ await manager.initialize();
28
+
29
+ // Manager now exposes:
30
+ // manager.extension — cross-browser chrome.*/browser.* API wrapper (see docs/extension.md)
31
+ // manager.logger — LoggerLite('popup') with timestamped output
32
+ // manager.webManager — Web Manager (Firebase, auth, analytics, bindings)
33
+ // manager.messenger — wired automatically; chrome.runtime.onMessage listener installed
34
+ // manager.isDevelopment() / isProduction() / isTesting() / getVersion() (cross-context helpers)
35
+ ```
36
+
37
+ The contexts that include `web-manager` (popup / options / sidepanel / page) also run the auth sync handshake automatically — see [auth.md](auth.md).
38
+
39
+ ## Build-time Manager
40
+
41
+ `browser-extension-manager/build` is the build-time Manager — used in gulp tasks, CLI commands, and tests. Different surface from the runtime Managers:
42
+
43
+ ```js
44
+ const Manager = require('browser-extension-manager/build');
45
+
46
+ Manager.getConfig(); // → parsed config/browser-extension-manager.json
47
+ Manager.getManifest(); // → parsed src/manifest.json (JSON5)
48
+ Manager.getPackage('project'); // → cwd's package.json
49
+ Manager.getPackage('main'); // → BXM's own package.json
50
+ Manager.getRootPath('project'); // → process.cwd()
51
+ Manager.getRootPath('main'); // → path to BXM's dist
52
+ Manager.getEnvironment(); // → 'production' if BXM_BUILD_MODE=true else 'development'
53
+ Manager.getLiveReloadPort(); // → 35729 by default
54
+ Manager.isBuildMode(); // → boolean
55
+ Manager.actLikeProduction(); // → buildMode || UJ_AUDIT_FORCE
56
+ Manager.require(name); // → borrow any of BXM's bundled deps (json5, fs-jetpack, etc.)
57
+ Manager.logger(name); // → new logger('name') instance
58
+ Manager.reportBuildError(e); // → notifly + log
59
+ ```
60
+
61
+ Cross-context helpers (`isTesting/isDevelopment/isProduction/getVersion`) are available on `Manager` as static methods AND on instances. See [environment-detection.md](environment-detection.md).
62
+
63
+ ## Boot flow inside `initialize()`
64
+
65
+ Each Manager's `initialize()`:
66
+
67
+ 1. **Read configuration** — from `window.BXM_BUILD_JSON?.config` (injected by webpack at build time)
68
+ 2. **Wire `extension`** — singleton from [src/lib/extension.js](../src/lib/extension.js), normalized chrome.*/browser.* API
69
+ 3. **Construct `logger`** — `new LoggerLite('<context>')` from [src/lib/logger-lite.js](../src/lib/logger-lite.js)
70
+ 4. **Initialize `webManager`** (popup/options/sidepanel/page only) — `webManager.initialize(config)`
71
+ 5. **Set up auth listener** — `webManager.auth().listen((state) => { /* ... */ })`
72
+ 6. **Sync with background** — call `syncWithBackground(this)` from [src/lib/auth-helpers.js](../src/lib/auth-helpers.js); see [auth.md](auth.md)
73
+ 7. **Install broadcast / sign-out / event listeners** — handles signin-from-other-context, signout propagation
74
+ 8. **Return the manager instance**
75
+
76
+ Background.js is more involved — it owns the auth source of truth, listens for `bxm:syncAuth` from other contexts, handles the website token flow. See [auth.md](auth.md).
77
+
78
+ ## Auth-related shortcuts (popup / options / sidepanel / page)
79
+
80
+ These Manager classes expose `manager.openAuthPage(options)` — opens the website's `/token` page (with `authSourceTabId` so tab restoration works after sign-in). See [auth.md](auth.md) for the full flow.
81
+
82
+ ## Mixin: cross-context helpers
83
+
84
+ Every Manager file imports `attachTo` from [src/utils/mode-helpers.js](../src/utils/mode-helpers.js) and calls it at the bottom of the file:
85
+
86
+ ```js
87
+ import { attachTo as attachModeHelpers } from './utils/mode-helpers.js';
88
+ // ... class Manager { ... }
89
+ attachModeHelpers(Manager);
90
+ export default Manager;
91
+ ```
92
+
93
+ That gives every Manager `isDevelopment` / `isProduction` / `isTesting` / `getVersion` on both the prototype and the constructor. See [environment-detection.md](environment-detection.md).
94
+
95
+ ## See also
96
+
97
+ - [components.md](components.md) — the seven component contexts
98
+ - [extension.md](extension.md) — the cross-browser chrome.* API wrapper
99
+ - [auth.md](auth.md) — how background.js coordinates auth across contexts
100
+ - [environment-detection.md](environment-detection.md) — `isTesting / isDevelopment / getVersion`
@@ -0,0 +1,57 @@
1
+ # Offscreen Component
2
+
3
+ Offscreen documents persist in the background (unlike service workers, they don't sleep). Use for WebSocket connections, long-running tasks, audio, clipboard — anything an MV3 service worker can't hold.
4
+
5
+ **Key facts:**
6
+
7
+ - Only one offscreen document can exist per extension.
8
+ - Invisible — no UI, no styling needed.
9
+ - Lightweight Manager (no WebManager, no auth, no theme) — see [managers.md](managers.md).
10
+ - Must be created programmatically from the background service worker.
11
+ - Requires the `offscreen` permission in the manifest (opt-in — see [components.md](components.md)).
12
+
13
+ ## Creating from Background
14
+
15
+ ```javascript
16
+ const OFFSCREEN_PATH = 'views/offscreen/index.html';
17
+
18
+ async function hasOffscreenDocument() {
19
+ const contexts = await chrome.runtime.getContexts({
20
+ contextTypes: ['OFFSCREEN_DOCUMENT'],
21
+ documentUrls: [chrome.runtime.getURL(OFFSCREEN_PATH)],
22
+ });
23
+ return contexts.length > 0;
24
+ }
25
+
26
+ async function setupOffscreenDocument() {
27
+ if (await hasOffscreenDocument()) return;
28
+ await chrome.offscreen.createDocument({
29
+ url: OFFSCREEN_PATH,
30
+ reasons: ['WEB_RTC'],
31
+ justification: 'Maintain persistent connection',
32
+ });
33
+ }
34
+ ```
35
+
36
+ ## Offscreen ↔ Background Communication
37
+
38
+ ```javascript
39
+ // From offscreen → background
40
+ chrome.runtime.sendMessage({ action: 'myAction', data: {...} }, (response) => {
41
+ // handle response
42
+ });
43
+
44
+ // From background, listen for offscreen messages
45
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
46
+ if (message.action === 'myAction') {
47
+ doSomething(message.data).then(sendResponse);
48
+ return true; // keep channel open for async response
49
+ }
50
+ });
51
+ ```
52
+
53
+ ## See also
54
+
55
+ - [components.md](components.md) — the seven component contexts + manifest wiring
56
+ - [managers.md](managers.md) — lightweight vs full Manager per context
57
+ - [extension.md](extension.md) — cross-browser API wrapper
@@ -0,0 +1,175 @@
1
+ # Publishing
2
+
3
+ `BXM_IS_PUBLISH=true npm run build` packages AND uploads to extension stores in one step.
4
+
5
+ ## Supported stores
6
+
7
+ - **Chrome Web Store** (Chrome / Edge / Brave / Opera users)
8
+ - **Firefox Add-ons** (AMO)
9
+ - **Microsoft Edge Add-ons** (separate from Chrome Web Store — Edge Insiders + corporate environments)
10
+
11
+ Only stores with configured credentials get published to. Unconfigured stores are silently skipped.
12
+
13
+ ## Setup — `.env`
14
+
15
+ Add store credentials to your project's `.env` (gitignored):
16
+
17
+ ```bash
18
+ # Chrome Web Store
19
+ CHROME_EXTENSION_ID="..."
20
+ CHROME_CLIENT_ID="..."
21
+ CHROME_CLIENT_SECRET="..."
22
+ CHROME_REFRESH_TOKEN="..."
23
+
24
+ # Firefox Add-ons
25
+ FIREFOX_EXTENSION_ID="..."
26
+ FIREFOX_API_KEY="..."
27
+ FIREFOX_API_SECRET="..."
28
+
29
+ # Microsoft Edge Add-ons
30
+ EDGE_PRODUCT_ID="..."
31
+ EDGE_CLIENT_ID="..."
32
+ EDGE_API_KEY="..."
33
+ ```
34
+
35
+ ## Getting credentials
36
+
37
+ ### Chrome Web Store
38
+
39
+ 1. Create an OAuth client via [Google Cloud Console](https://console.cloud.google.com/) (Web application type)
40
+ 2. Enable the [Chrome Web Store API](https://developer.chrome.com/docs/webstore/using_webstore_api/)
41
+ 3. Generate a refresh token via the OAuth flow (one-time)
42
+ 4. `CHROME_EXTENSION_ID` is in your Chrome Web Store Developer Dashboard URL: `chrome.google.com/webstore/devconsole/<id>`
43
+
44
+ ### Firefox Add-ons
45
+
46
+ 1. Sign in to [Add-on Developer Hub](https://addons.mozilla.org/developers/)
47
+ 2. Generate JWT credentials at "Manage API Keys"
48
+ 3. `FIREFOX_EXTENSION_ID` matches the `id` field in your manifest's `browser_specific_settings.gecko.id`
49
+
50
+ ### Microsoft Edge Add-ons
51
+
52
+ 1. Sign up for the [Microsoft Edge Add-ons Partner Center](https://partner.microsoft.com/dashboard/microsoftedge)
53
+ 2. Generate API credentials in the "API publishing" section
54
+ 3. `EDGE_PRODUCT_ID` is the GUID assigned to your extension by Microsoft
55
+
56
+ ## Publish flow
57
+
58
+ ```bash
59
+ BXM_IS_PUBLISH=true npm run build
60
+ ```
61
+
62
+ What happens:
63
+
64
+ 1. `npm run build` runs the full gulp pipeline (build → package → packaged/<browser>/raw + zip)
65
+ 2. `gulp/tasks/package.js` detects `BXM_IS_PUBLISH=true`
66
+ 3. For each browser, reads the store credentials from `.env`
67
+ 4. If credentials are present, uploads the `.zip` via the store's API
68
+ 5. Logs success / failure per store; exits non-zero if any upload fails
69
+
70
+ ## Manual upload
71
+
72
+ Run `npm run build` without `BXM_IS_PUBLISH=true` — you get unsigned `.zip` files per browser under `packaged/`:
73
+
74
+ ```
75
+ packaged/
76
+ ├── chromium/<ExtensionName>.zip
77
+ ├── firefox/<ExtensionName>.zip
78
+ └── opera/<ExtensionName>.zip
79
+ ```
80
+
81
+ Upload these manually via each store's web dashboard.
82
+
83
+ ## CI / GitHub Actions
84
+
85
+ For automated releases, store credentials as encrypted GitHub Actions secrets. A typical workflow:
86
+
87
+ ```yaml
88
+ - name: Build + publish
89
+ env:
90
+ BXM_IS_PUBLISH: true
91
+ CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
92
+ CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
93
+ CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
94
+ CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
95
+ # ...same for FIREFOX_*, EDGE_*
96
+ run: npm run build
97
+ ```
98
+
99
+ Same pattern EM uses for its desktop apps. Each store does its own review afterward (Chrome / Firefox: hours to a few days; Edge: typically same day).
100
+
101
+ ## What about Safari?
102
+
103
+ Safari (Apple) uses a different extension model and requires Xcode-based packaging via `safari-web-extension-converter`. Not currently in scope for BXM's auto-publish. Manual conversion + App Store Connect upload is the route.
104
+
105
+ ## Store listing description (`config/description.md`)
106
+
107
+ `config/description.md` is the Chrome Web Store listing description. When writing or rewriting it, first read `config/browser-extension-manager.json` (brand), `config/messages.json` (extension name + short description), `src/manifest.json` (permissions/features), and the component JS under `src/assets/js/components/` to understand what the extension actually does — be specific about real features, not generic copy.
108
+
109
+ **Format:**
110
+
111
+ ```
112
+ [emoji] [Key Feature Headline 1]
113
+ [emoji] [Key Feature Headline 2]
114
+ [emoji] [Key Feature Headline 3]
115
+ [emoji] [Key Feature Headline 4]
116
+ [emoji] [Key Feature Headline 5]
117
+
118
+ [Extension Name] is [compelling one-sentence pitch]. [Pain point 1]. [Pain point 2]. [Pain point 3].
119
+ If you [target audience description] — [Extension Name] was made for you.
120
+
121
+ [emoji] How it works
122
+ [Extension Name] [brief mechanism description].
123
+
124
+ [emoji] [Feature 1]: [One-line description]
125
+ [emoji] [Feature 2]: [One-line description]
126
+ [emoji] [Feature 3]: [One-line description]
127
+ [emoji] [Feature 4]: [One-line description]
128
+
129
+ [Brief usage instructions — 2-3 sentences].
130
+ No complicated setup. No learning curve. Just [core value proposition].
131
+
132
+ [emoji] Why [Extension Name] is a game changer
133
+
134
+ [Benefit 1 title]: [Description].
135
+ [Benefit 2 title]: [Description].
136
+ [Benefit 3 title]: [Description].
137
+ [Benefit 4 title]: [Description].
138
+ [Benefit 5 title]: [Description].
139
+
140
+ [emoji] The [Extension Name] Difference
141
+ Most people either:
142
+
143
+ [Alternative 1 that's worse]
144
+ [Alternative 2 that's worse]
145
+
146
+ [Extension Name] gives you a [better option description].
147
+ [Catchy metaphor with emoji]
148
+ [Closing sentence about who benefits].
149
+
150
+ [emoji] Install [Extension Name] now and [call to action].
151
+ [Short imperative sentence].
152
+
153
+ :money_with_wings: Bonus:
154
+ While you're browsing, the extension also finds and applies shopping deals from top partners like Amazon, Capital One, and NordVPN. Get discounts and bonuses without lifting a finger. When you buy through links on our extension, we may earn an affiliate commission.
155
+
156
+ :locked_with_key: Your privacy is respected — we do not sell or misuse your data. By using our extension, you agree to our terms of service and privacy policy.
157
+ When you buy through links on our extension, we may earn an affiliate commission.
158
+ ```
159
+
160
+ **Rules:**
161
+
162
+ - **Keep the Bonus section and Privacy section EXACTLY as shown** — do not modify these
163
+ - Use the extension's actual name from `config/messages.json` — do NOT use `{{ brand.name }}` template variables
164
+ - Be specific about features — reference what the code actually does
165
+ - Tone: enthusiastic, conversational, persuasive; emojis for section headers and feature bullets
166
+ - Feature headlines short and punchy (under 50 characters)
167
+ - 300-500 words (excluding the Bonus and Privacy sections)
168
+
169
+ **After rewriting**, clear stale cached translations — delete `.cache/translations/description/` and the `description` key from `.cache/translate.json` — then have the user run `npm run build` to regenerate translations (see [translations.md](translations.md)).
170
+
171
+ ## See also
172
+
173
+ - [build-system.md](build-system.md) — packaging pipeline that produces the per-browser zips
174
+ - [hooks.md](hooks.md) — `build:pre` and `build:post` hooks run before/after publish
175
+ - [cli.md](cli.md) — env var conventions
@@ -0,0 +1,66 @@
1
+ # Templating
2
+
3
+ HTML views go through a two-step `{{ }}` token replacement during the `gulp/html` task. Same convention as EM and UJM.
4
+
5
+ ## How it works
6
+
7
+ 1. Your view file (`src/views/<component>/index.html`) is templated first — `{{ brand.name }}` etc. resolve against the page-vars object.
8
+ 2. The result is injected into [src/config/page-template.html](../src/config/page-template.html) (the framework's outer HTML shell).
9
+ 3. The outer template is processed again with the same vars.
10
+ 4. Output written to `dist/views/<component>/index.html`.
11
+
12
+ ## Available variables
13
+
14
+ | Token | Source | Example |
15
+ |---|---|---|
16
+ | `{{ brand.name }}` | `config/browser-extension-manager.json` → `brand.name` | `Tabblar` |
17
+ | `{{ brand.url }}` | `config/browser-extension-manager.json` → `brand.url` | `https://tabblar.com` |
18
+ | `{{ brand.id }}` | `config/browser-extension-manager.json` → `brand.id` | `tabblar` |
19
+ | `{{ page.name }}` | Component name (e.g. `popup`, `pages/dashboard`) | `popup` |
20
+ | `{{ page.path }}` | Full view path | `views/popup/index.html` |
21
+ | `{{ page.title }}` | Page title — defaults to brand name | `Tabblar` |
22
+ | `{{ theme.appearance }}` | Theme appearance: `dark` or `light` | `dark` |
23
+ | `{{ cacheBust }}` | Cache-busting timestamp (build time) | `1715568000` |
24
+ | `{{ version }}` | `package.json#version` | `1.1.10` |
25
+ | `{{ environment }}` | `'production'` or `'development'` | `production` |
26
+
27
+ ## Example usage
28
+
29
+ ```html
30
+ <!-- src/views/popup/index.html -->
31
+ <!doctype html>
32
+ <html>
33
+ <head><title>{{ page.title }}</title></head>
34
+ <body>
35
+ <h1>Welcome to {{ brand.name }}</h1>
36
+ <a href="{{ brand.url }}/pricing?ref=ext">Upgrade to Premium</a>
37
+ <p>Version {{ version }}</p>
38
+ </body>
39
+ </html>
40
+ ```
41
+
42
+ ## Page template
43
+
44
+ [src/config/page-template.html](../src/config/page-template.html) is the outer shell that wraps every view. It contains:
45
+ - `<!doctype html>` + `<html>` boilerplate
46
+ - `<meta>` tags (viewport, charset)
47
+ - A `{{ content }}` slot where the per-component HTML is injected
48
+ - `<link rel="stylesheet">` for the component's `.bundle.css`
49
+ - `<script>` for the component's `.bundle.js`
50
+ - Cache-busting query params via `{{ cacheBust }}`
51
+
52
+ Consumers don't author this — BXM owns it. If you need to customize per-view, override directly in `src/views/<component>/index.html` (BXM detects when a view provides its own full `<html>` and skips wrapping).
53
+
54
+ ## Customizing page vars
55
+
56
+ The page-vars object is built by [src/gulp/tasks/html.js](../src/gulp/tasks/html.js). To add a new variable for consumer templates, edit the `buildPageVars()` helper there. Vars added are available in BOTH the view file AND the outer page-template.html.
57
+
58
+ ## Why two passes?
59
+
60
+ Pass 1 lets a view interpolate something into the outer template. For example, a view can do `<!-- bxm:page-title --> Custom Title <!-- /bxm:page-title -->` (a future feature) and have the value flow into `{{ page.title }}` for the outer template's `<title>`. The pattern keeps simple cases simple (`{{ brand.name }}` Just Works) while leaving room for view → shell metadata flow.
61
+
62
+ ## See also
63
+
64
+ - [build-system.md](build-system.md) — gulp pipeline including the html task
65
+ - [css.md](css.md) — how the page template references compiled CSS bundles
66
+ - [themes.md](themes.md) — `{{ theme.appearance }}` derivation