extension 3.15.0-next.1 → 3.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
  [npm-version-url]: https://www.npmjs.com/package/extension
3
3
  [npm-downloads-image]: https://img.shields.io/npm/dm/extension.svg?color=0971fe
4
4
  [npm-downloads-url]: https://www.npmjs.com/package/extension
5
+ [stars-image]: https://img.shields.io/github/stars/extension-js/extension.js?style=flat&color=0971fe
6
+ [stars-url]: https://github.com/extension-js/extension.js/stargazers
5
7
  [action-image]: https://github.com/extension-js/extension.js/actions/workflows/ci.yml/badge.svg?branch=main&color=0971fe
6
8
  [action-url]: https://github.com/extension-js/extension.js/actions
7
9
  [discord-image]: https://img.shields.io/discord/1253608412890271755?label=Discord&logo=discord&style=flat&color=0971fe
@@ -9,133 +11,166 @@
9
11
  [snyk-image]: https://snyk.io/test/github/extension-js/extension/badge.svg?color=0971fe
10
12
  [snyk-url]: https://snyk.io/test/github/extension-js/extension
11
13
 
12
- [![Version][npm-version-image]][npm-version-url] [![Downloads][npm-downloads-image]][npm-downloads-url] [![CI][action-image]][action-url] [![Discord][discord-image]][discord-url]
14
+ # Extension.js [![Version][npm-version-image]][npm-version-url] [![Downloads][npm-downloads-image]][npm-downloads-url] [![Stars][stars-image]][stars-url] [![CI][action-image]][action-url] [![Discord][discord-image]][discord-url]
13
15
 
14
- # Extension.js
15
-
16
- > The cross-browser extension framework
16
+ > Build extensions for Chrome, Edge, and Firefox. No build config required.
17
17
 
18
18
  <img alt="Logo" align="right" src="https://avatars.githubusercontent.com/u/172809806" width="15.5%" />
19
19
 
20
- - [Create a new extension](#create-a-new-extension) — How to create a new extension.
21
- - [Watch demo](#watch-demo) — See how creating a new extension works.
22
- - [Start from an example](https://github.com/extension-js/examples) — Start from a working baseline.
23
- - [I have an extension](#i-have-an-extension) — Use only specific parts of Extension.js.
24
-
25
- Create cross-browser extensions without manual build configuration.<br />Use Extension.js to develop, build, and preview across browsers with a unified workflow.
26
-
27
- ## Create a new extension
28
-
29
- Use the `create` command to generate a new extension. Also works with pnpm, yarn, and bun.
30
-
31
20
  ```bash
32
21
  npx extension@latest create my-extension
33
22
  cd my-extension
34
23
  npm run dev
35
24
  ```
36
25
 
37
- ### Watch demo
26
+ Works with `npm`, `pnpm`, `yarn`, and `bun`.
27
+
28
+ [Documentation](https://extension.js.org) · [Templates](https://templates.extension.dev) · [Examples](https://github.com/extension-js/examples) · [Discord](https://discord.gg/v9h2RgeTSN)
29
+
30
+ ## Why Extension.js
31
+
32
+ Browser extensions ship with the worst dev experience in modern web. Manifest V3 fragmentation, browser-specific quirks, no hot reload for content scripts, and a separate build pipeline for every target. Extension.js fixes that.
33
+
34
+ - **Hot Module Replacement** for background, content, popup, and options scripts, including React, Vue, Svelte, and Preact components
35
+ - **Manifest V3 by default**, with automatic adapters for Chrome, Edge, and Firefox targets
36
+ - **One CLI** for Chrome, Edge, Firefox, and any Chromium or Gecko binary
37
+ - **Zero config**, no webpack, no rollup, no plugins to maintain
38
+ - **First-class** TypeScript, React, Vue, Svelte, and Preact support
39
+ - **Production builds** with `extension build --zip`, ready for the Chrome Web Store and Firefox Add-ons
40
+ - **Drop-in** for existing extensions with one `devDependency`
38
41
 
39
- [Watch demo](https://github.com/cezaraugusto/extension/assets/4672033/7263d368-99c4-434f-a60a-72c489672586)
42
+ ## Watch it work
40
43
 
41
- ## Web standards and framework support
44
+ [60-second demo](https://github.com/cezaraugusto/extension/assets/4672033/7263d368-99c4-434f-a60a-72c489672586)
42
45
 
43
- <!-- For a preview of extensions running these technologies, see the [templates](https://templates.extension.dev) website. -->
46
+ Or skip the install and try a [live template](https://templates.extension.dev) in your browser.
47
+
48
+ ## How is this different
49
+
50
+ If you have used [Plasmo](https://www.plasmo.com), [WXT](https://wxt.dev), or [CRXJS](https://crxjs.dev), here is what Extension.js does that the others do not:
51
+
52
+ | Capability | Extension.js |
53
+ | :--------- | :----------- |
54
+ | Run any GitHub sample directly | `extension dev https://github.com/.../sample` |
55
+ | Managed browser binaries | `extension install firefox` downloads an isolated build |
56
+ | Cross-browser HMR for content scripts | Built in, no plugin glue |
57
+ | Production zip for the stores | `extension build --zip` |
58
+ | Framework agnostic | [Vanilla](https://templates.extension.dev/javascript), [TS](https://templates.extension.dev/typescript), [React](https://templates.extension.dev/react), [Vue](https://templates.extension.dev/vue), [Svelte](https://templates.extension.dev/svelte), [Preact](https://templates.extension.dev/preact), no lock-in |
59
+ | Custom Chromium and Gecko binaries | `--chromium-binary`, `--gecko-binary` |
60
+
61
+ ## Frameworks
44
62
 
45
63
  <div align="center">
46
64
 
47
- | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/a9e2541a-96f0-4caa-9fc9-5fc5c3e901c8" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/b42c5330-9e2a-4045-99c3-1f7d264dfaf4" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/f19edff3-9005-4f50-b05c-fba615896a7f" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/ff64721d-d145-4213-930d-e70193f8d57e" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/15f1314a-aa65-4ce2-a3f3-cf53c4f730cf" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/de1082fd-7cf6-4202-8c12-a5c3cd3e5b42" width="70"> | <img src="https://github.com/cezaraugusto/extension.js/assets/4672033/8807efd9-93e5-4db5-a1d2-9ac524f7ecc2" width="70"> |
48
- | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------------------: |
49
- | ESNext<br>[Try out](https://templates.extension.dev/javascript) | TypeScript<br>[Try out](https://templates.extension.dev/typescript) | WASM<br>(soon) | React<br>[Try out](https://templates.extension.dev/react) | Vue<br>[Try out](https://templates.extension.dev/vue) | Svelte<br>[Try out](https://templates.extension.dev/svelte) | Preact<br>[Try out](https://templates.extension.dev/preact) |
65
+ | <img alt="ESNext" src="https://github.com/cezaraugusto/extension.js/assets/4672033/a9e2541a-96f0-4caa-9fc9-5fc5c3e901c8" width="70"> | <img alt="TypeScript" src="https://github.com/cezaraugusto/extension.js/assets/4672033/b42c5330-9e2a-4045-99c3-1f7d264dfaf4" width="70"> | <img alt="WASM" src="https://github.com/cezaraugusto/extension.js/assets/4672033/f19edff3-9005-4f50-b05c-fba615896a7f" width="70"> | <img alt="React" src="https://github.com/cezaraugusto/extension.js/assets/4672033/ff64721d-d145-4213-930d-e70193f8d57e" width="70"> | <img alt="Vue" src="https://github.com/cezaraugusto/extension.js/assets/4672033/15f1314a-aa65-4ce2-a3f3-cf53c4f730cf" width="70"> | <img alt="Svelte" src="https://github.com/cezaraugusto/extension.js/assets/4672033/de1082fd-7cf6-4202-8c12-a5c3cd3e5b42" width="70"> | <img alt="Preact" src="https://github.com/cezaraugusto/extension.js/assets/4672033/8807efd9-93e5-4db5-a1d2-9ac524f7ecc2" width="70"> |
66
+ | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
67
+ | ESNext<br>[Try out](https://templates.extension.dev/javascript) | TypeScript<br>[Try out](https://templates.extension.dev/typescript) | WASM<br>[Try out](https://github.com/extension-js/examples/tree/main/examples/sidebar-transformers-js) | React<br>[Try out](https://templates.extension.dev/react) | Vue<br>[Try out](https://templates.extension.dev/vue) | Svelte<br>[Try out](https://templates.extension.dev/svelte) | Preact<br>[Try out](https://templates.extension.dev/preact) |
50
68
 
51
69
  </div>
52
70
 
53
- <details>
54
- <summary>Get started from a sample</summary>
71
+ ## Browsers
55
72
 
56
- ## Get started
73
+ Use these flags with `extension dev`, `extension start`, or `extension preview`:
57
74
 
58
- Start developing an extension using a sample from Chrome Extension Samples
75
+ - Select a browser: `--browser <chrome | edge | firefox>`
76
+ - Custom Chromium binary: `--chromium-binary <path-to-binary>`
77
+ - Custom Gecko (Firefox) binary: `--gecko-binary <path-to-binary>`
59
78
 
60
- See the example below where we request the sample [page-redder](https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder) from [Google Chrome Extension Samples](https://github.com/GoogleChrome/chrome-extensions-samples).
79
+ ```bash
80
+ # Chrome (system default)
81
+ npx extension@latest dev --browser=chrome
61
82
 
62
- ### Watch demo
83
+ # Edge
84
+ npx extension@latest dev --browser=edge
63
85
 
64
- [Watch demo](https://github.com/cezaraugusto/extension/assets/4672033/ee221a94-6ec7-4e04-8553-8812288927f1)
86
+ # Custom Chrome/Chromium path
87
+ npx extension@latest dev --chromium-binary "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
65
88
 
66
- ### Try Yourself
89
+ # Custom Firefox path
90
+ npx extension@latest dev --gecko-binary "/Applications/Firefox.app/Contents/MacOS/firefox"
91
+ ```
92
+
93
+ <div align="center">
94
+
95
+ | <img alt="Chrome" src="https://media.extension.land/logos/browsers/chrome.svg" width="70"> | <img alt="Edge" src="https://media.extension.land/logos/browsers/edge.svg" width="70"> | <img alt="Firefox" src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> | <img alt="Safari" src="https://media.extension.land/logos/browsers/safari.svg" width="70"> | <img alt="Chromium" src="https://media.extension.land/logos/browsers/chromium.svg" width="70"> | <img alt="Gecko" src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> |
96
+ | :-: | :-: | :-: | :-: | :-: | :-: |
97
+ | Google Chrome<br>✅ Supported | Microsoft Edge<br>✅ Supported | Mozilla Firefox<br>✅ Supported | Apple Safari<br> 🚙 Next | Chromium-based<br>✅ Supported | Gecko-based<br>✅ Supported |
98
+
99
+ </div>
100
+
101
+ ## Ship to the store
102
+
103
+ Build a production-ready bundle and zip it for submission to the Chrome Web Store, Edge Add-ons, or Firefox AMO:
67
104
 
68
105
  ```bash
69
- npx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder --browser=edge
106
+ # Production build
107
+ npx extension@latest build
108
+
109
+ # Production build packaged as a ZIP
110
+ npx extension@latest build --zip
111
+
112
+ # Per-browser builds
113
+ npx extension@latest build --browser=firefox --zip
70
114
  ```
71
115
 
72
- </details>
116
+ Useful flags:
117
+
118
+ - `--zip` packages the build into a ZIP ready for store upload
119
+ - `--zip-source` includes source files for store source-code review
120
+ - `--zip-filename <name>` controls the output filename
121
+ - `--polyfill` enables the cross-browser webextension polyfill
122
+
123
+ ## Manage browser binaries
124
+
125
+ Skip the system-install dance. Extension.js can download and manage isolated browser binaries for clean dev sessions:
73
126
 
74
- ## I have an extension
127
+ ```bash
128
+ # Install a managed Firefox build
129
+ npx extension@latest install firefox
75
130
 
76
- If you have an existing extension which is using a package manager, you can install the Extension.js package and manually create the scripts used to run your extension.
131
+ # Install Chrome and Edge in one go
132
+ npx extension@latest install --browser=all
77
133
 
78
- ### See how it works
134
+ # Print where managed browsers live
135
+ npx extension@latest install --where
136
+ ```
79
137
 
80
- [See how it works](https://github.com/cezaraugusto/extension/assets/4672033/48694a23-b7f1-4098-9c5d-eff49983739c)
138
+ ## Add to an existing extension
81
139
 
82
- **Step 1 - Install extension as a `devDependency`**
140
+ Install Extension.js as a dev dependency and wire up your scripts.
83
141
 
84
142
  ```bash
85
143
  npm install extension@latest --save-dev
86
144
  ```
87
145
 
88
- **Step 2 - Link your npm scripts with the executable Extension.js commands**
89
-
90
146
  ```json
91
147
  {
92
148
  "scripts": {
93
149
  "build": "extension build",
94
150
  "dev": "extension dev",
95
151
  "preview": "extension preview"
96
- },
97
- "devDependencies": {
98
- // ...other dependencies
99
- "extension": "latest"
100
152
  }
101
153
  }
102
154
  ```
103
155
 
104
- Done. You are all set!
105
-
106
- - To develop the extension, run `npm run dev`.
107
- - To build the extension in production mode, run `npm run build`.
108
- - To visualize the extension in production mode, run `npm run build` and `npm run preview`.
109
-
110
- ## Using a specific browser for development
111
-
112
- | <img src="https://media.extension.land/logos/browsers/chrome.svg" width="70"> | <img src="https://media.extension.land/logos/browsers/edge.svg" width="70"> | <img src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> | <img src="https://media.extension.land/logos/browsers/safari.svg" width="70"> | <img src="https://media.extension.land/logos/browsers/chromium.svg" width="70"> | <img src="https://media.extension.land/logos/browsers/firefox.svg" width="70"> |
113
- | :---------------------------------------------------------------------------: | :-------------------------------------------------------------------------: | :----------------------------------------------------------------------------: | :---------------------------------------------------------------------------: | :-----------------------------------------------------------------------------: | :----------------------------------------------------------------------------: |
114
- | Chrome browser<br>✅ | Edge browser<br>✅ | Firefox browser<br>✅ | Safari browser<br>(soon) | Chromium-based<br>✅ | Gecko-based<br>✅ |
115
-
116
- ### Browser flags and custom binaries
117
-
118
- Use these flags with `extension dev`, `extension start`, or `extension preview`:
156
+ Run `npm run dev` to develop, `npm run build` for production, and `npm run preview` to inspect the production output. [See it in action.](https://github.com/cezaraugusto/extension/assets/4672033/48694a23-b7f1-4098-9c5d-eff49983739c)
119
157
 
120
- - Select a browser: `--browser <chrome | edge | firefox>`
121
- - Custom Chromium binary: `--chromium-binary <path-to-binary>`
122
- - Custom Gecko (Firefox) binary: `--gecko-binary <path-to-binary>`
158
+ ## Start from a Chrome sample
123
159
 
124
- Examples:
160
+ Pull any sample from [Chrome Extension Samples](https://github.com/GoogleChrome/chrome-extensions-samples) and run it directly:
125
161
 
126
162
  ```bash
127
- # Chrome (system default)
128
- npx extension@latest dev --browser=chrome
163
+ npx extension@latest dev https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/sample.page-redder --browser=edge
164
+ ```
129
165
 
130
- # Edge
131
- npx extension@latest dev --browser=edge
166
+ [Watch demo](https://github.com/cezaraugusto/extension/assets/4672033/ee221a94-6ec7-4e04-8553-8812288927f1)
132
167
 
133
- # Custom Chrome/Chromium path
134
- npx extension@latest dev --chromium-binary "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
168
+ ## Community
135
169
 
136
- # Custom Firefox path
137
- npx extension@latest dev --gecko-binary "/Applications/Firefox.app/Contents/MacOS/firefox"
138
- ```
170
+ - Star the repo if Extension.js helped you ship faster
171
+ - Join the [Discord](https://discord.gg/v9h2RgeTSN) for help and feedback
172
+ - Open issues and feature requests on [GitHub](https://github.com/extension-js/extension.js/issues)
173
+ - Browse production-ready [examples](https://github.com/extension-js/examples)
139
174
 
140
175
  ## Sponsors
141
176
 
package/dist/322.cjs CHANGED
@@ -817,15 +817,23 @@ exports.modules = {
817
817
  });
818
818
  for (const target of matchingTargets){
819
819
  const targetId = target?.targetId;
820
- if (targetId) try {
821
- const sessionId = await this.attachToTarget(targetId);
820
+ if (!targetId) continue;
821
+ let sessionId;
822
+ try {
823
+ sessionId = await this.attachToTarget(targetId);
824
+ } catch {
825
+ continue;
826
+ }
827
+ try {
822
828
  await this.sendCommand('Runtime.enable', {}, sessionId);
823
- await this.sendCommand('Runtime.evaluate', {
824
- expression: '(function(){ try { if (!chrome || !chrome.runtime || !chrome.runtime.reload) return false; chrome.runtime.reload(); return true; } catch (error) { return false; } })()',
825
- returnByValue: true
826
- }, sessionId);
829
+ } catch {
827
830
  return true;
828
- } catch {}
831
+ }
832
+ this.sendCommand('Runtime.evaluate', {
833
+ expression: '(function(){ try { if (!chrome || !chrome.runtime || !chrome.runtime.reload) return false; chrome.runtime.reload(); return true; } catch (error) { return false; } })()',
834
+ returnByValue: true
835
+ }, sessionId).catch(()=>{});
836
+ return true;
829
837
  }
830
838
  }
831
839
  return false;
@@ -964,6 +972,7 @@ exports.modules = {
964
972
  let firstEvalId = null;
965
973
  let evalIdCount = 0;
966
974
  let urlDerivedId = null;
975
+ const versionFallbackIds = [];
967
976
  for (const t of targets || []){
968
977
  const url = String(t?.url || '');
969
978
  const type = String(t?.type || '');
@@ -994,9 +1003,12 @@ exports.modules = {
994
1003
  const versionMatches = expectedVersion ? gotVersion === expectedVersion : false;
995
1004
  const manifestVersionMatches = expectedManifestVersion ? gotManifestVersion === expectedManifestVersion : false;
996
1005
  if (nameMatches && (!profileCandidateId || id === profileCandidateId)) return id;
997
- if (expectedVersion && versionMatches && (expectedManifestVersion ? manifestVersionMatches : true) && (!profileCandidateId || id === profileCandidateId)) return id;
1006
+ if (expectedVersion && versionMatches && (expectedManifestVersion ? manifestVersionMatches : true) && (!profileCandidateId || id === profileCandidateId)) {
1007
+ if (!versionFallbackIds.includes(id)) versionFallbackIds.push(id);
1008
+ }
998
1009
  } catch {}
999
1010
  }
1011
+ if (1 === versionFallbackIds.length && (expectedName ? !expectedNameIsMsg : true)) return versionFallbackIds[0];
1000
1012
  if (1 === evalIdCount && firstEvalId) {
1001
1013
  if (!hasExpectedManifestIdentity) return firstEvalId;
1002
1014
  deferredFirstEvalId = deferredFirstEvalId || firstEvalId;
@@ -1322,13 +1334,18 @@ exports.modules = {
1322
1334
  if (!sessionId) return;
1323
1335
  const session = this.watchedPageSessions.get(sessionId);
1324
1336
  if (!session) return;
1337
+ if (this.suppressInitialContextBurst.has(sessionId)) return;
1325
1338
  const context = params?.context;
1326
1339
  if (!context) return;
1327
1340
  this.evaluateContentScriptOnNewContext(sessionId, session, context).catch(()=>{});
1328
1341
  return;
1329
1342
  }
1330
1343
  if ('Target.detachedFromTarget' === method) {
1331
- if (sessionId) this.watchedPageSessions.delete(sessionId);
1344
+ if (sessionId) {
1345
+ this.watchedPageSessions.delete(sessionId);
1346
+ this.releaseSessionContexts(sessionId);
1347
+ this.suppressInitialContextBurst.delete(sessionId);
1348
+ }
1332
1349
  }
1333
1350
  });
1334
1351
  }
@@ -1354,10 +1371,15 @@ exports.modules = {
1354
1371
  targetId,
1355
1372
  url
1356
1373
  });
1374
+ this.suppressInitialContextBurst.add(sessionId);
1357
1375
  try {
1358
1376
  await this.cdp.sendCommand('Runtime.enable', {}, sessionId);
1377
+ setTimeout(()=>{
1378
+ this.suppressInitialContextBurst.delete(sessionId);
1379
+ }, 250);
1359
1380
  } catch {
1360
1381
  this.watchedPageSessions.delete(sessionId);
1382
+ this.suppressInitialContextBurst.delete(sessionId);
1361
1383
  }
1362
1384
  }
1363
1385
  async evaluateContentScriptOnNewContext(sessionId, session, context) {
@@ -1780,9 +1802,15 @@ exports.modules = {
1780
1802
  if (existing && typeof existing.cleanup === 'function') existing.cleanup();
1781
1803
  } catch (error) {}
1782
1804
  if (${allowCoarseCleanup ? 'true' : 'false'}) {
1805
+ // Bundle-scoped sweep: only remove roots owned by this bundleId. The inner
1806
+ // wrapper tags every host with data-extjs-reinject-key on mount, so this
1807
+ // catches roots from a manifest-cached run before the outer cleanup
1808
+ // registry was populated. A broad querySelector('[data-extension-root]')
1809
+ // would destroy roots owned by sibling content_scripts entries that
1810
+ // didn't change — leaving the page with only this bundle's roots.
1783
1811
  try {
1784
- const staleRoots = Array.from(document.querySelectorAll(rootSelector));
1785
- for (const root of staleRoots) {
1812
+ const staleKeyed = Array.from(document.querySelectorAll(keyedSelector));
1813
+ for (const root of staleKeyed) {
1786
1814
  if (root && typeof root.remove === 'function') root.remove();
1787
1815
  }
1788
1816
  } catch (error) {}
@@ -1933,22 +1961,39 @@ exports.modules = {
1933
1961
  }
1934
1962
  async collectExecutionContexts(sessionId) {
1935
1963
  if (!this.cdp) return [];
1964
+ const cached = this.sessionContexts.get(sessionId);
1965
+ if (cached) return Array.from(cached.values());
1936
1966
  const contextsById = new Map();
1967
+ this.sessionContexts.set(sessionId, contextsById);
1937
1968
  const unsubscribe = this.cdp.onProtocolEvent((message)=>{
1938
1969
  if (String(message.sessionId || '') !== sessionId) return;
1939
- if ('Runtime.executionContextCreated' !== String(message.method || '')) return;
1940
- const context = message.params?.context;
1941
- const contextId = context?.id;
1942
- if ('number' == typeof contextId) contextsById.set(contextId, context);
1970
+ const method = String(message.method || '');
1971
+ if ('Runtime.executionContextCreated' === method) {
1972
+ const context = message.params?.context;
1973
+ const contextId = context?.id;
1974
+ if ('number' == typeof contextId) contextsById.set(contextId, context);
1975
+ return;
1976
+ }
1977
+ if ('Runtime.executionContextDestroyed' === method) {
1978
+ const params = message.params || {};
1979
+ if ('number' == typeof params.executionContextId) contextsById.delete(params.executionContextId);
1980
+ return;
1981
+ }
1982
+ if ('Runtime.executionContextsCleared' === method) contextsById.clear();
1943
1983
  });
1944
- try {
1945
- await this.cdp.sendCommand('Runtime.enable', {}, sessionId);
1946
- await new Promise((resolve)=>setTimeout(resolve, 100));
1947
- } finally{
1948
- unsubscribe();
1949
- }
1984
+ this.sessionContextsUnsubscribe.set(sessionId, unsubscribe);
1985
+ await this.cdp.sendCommand('Runtime.enable', {}, sessionId);
1986
+ await new Promise((resolve)=>setTimeout(resolve, 100));
1950
1987
  return Array.from(contextsById.values());
1951
1988
  }
1989
+ releaseSessionContexts(sessionId) {
1990
+ const unsubscribe = this.sessionContextsUnsubscribe.get(sessionId);
1991
+ if (unsubscribe) try {
1992
+ unsubscribe();
1993
+ } catch {}
1994
+ this.sessionContextsUnsubscribe.delete(sessionId);
1995
+ this.sessionContexts.delete(sessionId);
1996
+ }
1952
1997
  constructor(args){
1953
1998
  cdp_extension_controller_define_property(this, "outPath", void 0);
1954
1999
  cdp_extension_controller_define_property(this, "browser", void 0);
@@ -1963,6 +2008,9 @@ exports.modules = {
1963
2008
  cdp_extension_controller_define_property(this, "activeContentScriptRules", []);
1964
2009
  cdp_extension_controller_define_property(this, "contentScriptTargetListenerInstalled", false);
1965
2010
  cdp_extension_controller_define_property(this, "watchedPageSessions", new Map());
2011
+ cdp_extension_controller_define_property(this, "sessionContexts", new Map());
2012
+ cdp_extension_controller_define_property(this, "sessionContextsUnsubscribe", new Map());
2013
+ cdp_extension_controller_define_property(this, "suppressInitialContextBurst", new Set());
1966
2014
  this.outPath = args.outPath;
1967
2015
  this.browser = args.browser;
1968
2016
  this.cdpPort = args.cdpPort;
package/dist/browsers.cjs CHANGED
@@ -14,7 +14,10 @@ var __webpack_modules__ = {
14
14
  var external_pintor_ = __webpack_require__("pintor");
15
15
  var external_pintor_default = /*#__PURE__*/ __webpack_require__.n(external_pintor_);
16
16
  var messages = __webpack_require__("./browsers/browsers-lib/messages.ts");
17
- function markBannerPrinted() {}
17
+ const BANNER_PRINTED_EVENT = 'extensionjs:banner-printed';
18
+ function markBannerPrinted() {
19
+ process.emit(BANNER_PRINTED_EVENT);
20
+ }
18
21
  const printedKeys = new Set();
19
22
  function readUpdateSuffixOnce() {
20
23
  const suffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX;
@@ -251,9 +254,23 @@ var __webpack_modules__ = {
251
254
  if (!fs__rspack_import_0.existsSync(dir)) return null;
252
255
  try {
253
256
  const re = new RegExp(`^content-${index}\\.[a-f0-9]+\\.${ext}$`, 'i');
254
- for (const name of fs__rspack_import_0.readdirSync(dir))if (re.test(name)) return path__rspack_import_1.join(dir, name);
255
- } catch {}
256
- return null;
257
+ let latestPath = null;
258
+ let latestMtimeMs = -1 / 0;
259
+ for (const name of fs__rspack_import_0.readdirSync(dir)){
260
+ if (!re.test(name)) continue;
261
+ const candidate = path__rspack_import_1.join(dir, name);
262
+ try {
263
+ const stat = fs__rspack_import_0.statSync(candidate);
264
+ if (stat.mtimeMs > latestMtimeMs) {
265
+ latestMtimeMs = stat.mtimeMs;
266
+ latestPath = candidate;
267
+ }
268
+ } catch {}
269
+ }
270
+ return latestPath;
271
+ } catch {
272
+ return null;
273
+ }
257
274
  }
258
275
  function readContentScriptRules(compilation, extensionRoot) {
259
276
  const manifest = readManifestFromCompilation(compilation) || readManifestFromDisk(extensionRoot) || {};
@@ -1787,8 +1804,10 @@ var __webpack_modules__ = {
1787
1804
  return p;
1788
1805
  }
1789
1806
  };
1790
- if (hasExplicitProfile) userProfilePath = external_path_.resolve(rawProfile.trim());
1791
- else if (useSystemProfile) userProfilePath = '';
1807
+ if (hasExplicitProfile) {
1808
+ const trimmedProfile = rawProfile.trim();
1809
+ userProfilePath = external_path_.isAbsolute(trimmedProfile) ? trimmedProfile : external_path_.resolve(contextDir, trimmedProfile);
1810
+ } else if (useSystemProfile) userProfilePath = '';
1792
1811
  else {
1793
1812
  const outPath = compilation?.options?.output?.path || external_path_.resolve(process.cwd(), 'dist/chrome');
1794
1813
  const distRoot = external_path_.dirname(outPath);
@@ -4095,7 +4114,7 @@ var __webpack_modules__ = {
4095
4114
  const rdpPort = this.resolveRdpPort(compilation);
4096
4115
  const client = this.client || await this.connectClient(rdpPort);
4097
4116
  const normalized = (changedAssets || []).map((n)=>String(n || '')).map((n)=>n.replace(/\\/g, '/'));
4098
- const isManifestChanged = normalized.includes('manifest.json');
4117
+ const isManifestChanged = normalized.some((n)=>'manifest.json' === n || n.endsWith('/manifest.json'));
4099
4118
  const isLocalesChanged = normalized.some((n)=>/(^|\/)__?locales\/.+\.json$/i.test(n));
4100
4119
  const isContentScriptChanged = normalized.some((n)=>/(^|\/)content_scripts\/content-\d+(?:\.[a-f0-9]+)?\.(js|css)$/i.test(n));
4101
4120
  let isServiceWorkerChanged = false;
@@ -4107,7 +4126,7 @@ var __webpack_modules__ = {
4107
4126
  const sw = parsed?.background?.service_worker;
4108
4127
  if ('string' == typeof sw && sw) {
4109
4128
  const swUnix = sw.replace(/\\/g, '/');
4110
- isServiceWorkerChanged = normalized.includes(swUnix);
4129
+ isServiceWorkerChanged = normalized.some((n)=>n === swUnix || n.endsWith('/' + swUnix));
4111
4130
  }
4112
4131
  }
4113
4132
  } catch {}
@@ -4560,8 +4579,10 @@ var __webpack_modules__ = {
4560
4579
  return p;
4561
4580
  }
4562
4581
  };
4563
- if ('string' == typeof profile && profile.trim().length > 0) profilePath = external_path_.resolve(profile.trim());
4564
- else if (useSystemProfile) profilePath = '';
4582
+ if ('string' == typeof profile && profile.trim().length > 0) {
4583
+ const trimmedProfile = profile.trim();
4584
+ profilePath = external_path_.isAbsolute(trimmedProfile) ? trimmedProfile : external_path_.resolve(contextDir, trimmedProfile);
4585
+ } else if (useSystemProfile) profilePath = '';
4565
4586
  else {
4566
4587
  const base = external_path_.resolve(distRoot, 'extension-js', 'profiles', `${browser}-profile`);
4567
4588
  const persist = Boolean(configOptions.persistProfile);
@@ -4648,9 +4669,6 @@ var __webpack_modules__ = {
4648
4669
  String(debugPort)
4649
4670
  ] : [],
4650
4671
  '--foreground',
4651
- '--disable-background-timer-throttling',
4652
- '--disable-backgrounding-occluded-windows',
4653
- '--disable-renderer-backgrounding',
4654
4672
  ...additionalArgs
4655
4673
  ];
4656
4674
  return {
@@ -4671,9 +4689,6 @@ var __webpack_modules__ = {
4671
4689
  String(debugPort)
4672
4690
  ] : [],
4673
4691
  '--foreground',
4674
- '--disable-background-timer-throttling',
4675
- '--disable-backgrounding-occluded-windows',
4676
- '--disable-renderer-backgrounding',
4677
4692
  ...additionalArgs
4678
4693
  ];
4679
4694
  return {
@@ -4982,9 +4997,6 @@ var __webpack_modules__ = {
4982
4997
  '-wait-for-browser'
4983
4998
  ] : [],
4984
4999
  '--foreground',
4985
- '--disable-background-timer-throttling',
4986
- '--disable-backgrounding-occluded-windows',
4987
- '--disable-renderer-backgrounding',
4988
5000
  ...firefoxArgs
4989
5001
  ];
4990
5002
  this.child = await this.spawnFirefoxChild(binaryPath, args, wslFallbackBinary);
package/dist/cli.cjs CHANGED
@@ -15,7 +15,10 @@ var __webpack_modules__ = {
15
15
  var external_pintor_ = __webpack_require__("pintor");
16
16
  var external_pintor_default = /*#__PURE__*/ __webpack_require__.n(external_pintor_);
17
17
  var messages = __webpack_require__("./browsers/browsers-lib/messages.ts");
18
- function markBannerPrinted() {}
18
+ const BANNER_PRINTED_EVENT = 'extensionjs:banner-printed';
19
+ function markBannerPrinted() {
20
+ process.emit(BANNER_PRINTED_EVENT);
21
+ }
19
22
  const printedKeys = new Set();
20
23
  function readUpdateSuffixOnce() {
21
24
  const suffix = process.env.EXTENSION_CLI_UPDATE_SUFFIX;
@@ -252,9 +255,23 @@ var __webpack_modules__ = {
252
255
  if (!fs__rspack_import_0.existsSync(dir)) return null;
253
256
  try {
254
257
  const re = new RegExp(`^content-${index}\\.[a-f0-9]+\\.${ext}$`, 'i');
255
- for (const name of fs__rspack_import_0.readdirSync(dir))if (re.test(name)) return path__rspack_import_1.join(dir, name);
256
- } catch {}
257
- return null;
258
+ let latestPath = null;
259
+ let latestMtimeMs = -1 / 0;
260
+ for (const name of fs__rspack_import_0.readdirSync(dir)){
261
+ if (!re.test(name)) continue;
262
+ const candidate = path__rspack_import_1.join(dir, name);
263
+ try {
264
+ const stat = fs__rspack_import_0.statSync(candidate);
265
+ if (stat.mtimeMs > latestMtimeMs) {
266
+ latestMtimeMs = stat.mtimeMs;
267
+ latestPath = candidate;
268
+ }
269
+ } catch {}
270
+ }
271
+ return latestPath;
272
+ } catch {
273
+ return null;
274
+ }
258
275
  }
259
276
  function readContentScriptRules(compilation, extensionRoot) {
260
277
  const manifest = readManifestFromCompilation(compilation) || readManifestFromDisk(extensionRoot) || {};
@@ -1786,8 +1803,10 @@ var __webpack_modules__ = {
1786
1803
  return p;
1787
1804
  }
1788
1805
  };
1789
- if (hasExplicitProfile) userProfilePath = external_path_.resolve(rawProfile.trim());
1790
- else if (useSystemProfile) userProfilePath = '';
1806
+ if (hasExplicitProfile) {
1807
+ const trimmedProfile = rawProfile.trim();
1808
+ userProfilePath = external_path_.isAbsolute(trimmedProfile) ? trimmedProfile : external_path_.resolve(contextDir, trimmedProfile);
1809
+ } else if (useSystemProfile) userProfilePath = '';
1791
1810
  else {
1792
1811
  const outPath = compilation?.options?.output?.path || external_path_.resolve(process.cwd(), 'dist/chrome');
1793
1812
  const distRoot = external_path_.dirname(outPath);
@@ -4094,7 +4113,7 @@ var __webpack_modules__ = {
4094
4113
  const rdpPort = this.resolveRdpPort(compilation);
4095
4114
  const client = this.client || await this.connectClient(rdpPort);
4096
4115
  const normalized = (changedAssets || []).map((n)=>String(n || '')).map((n)=>n.replace(/\\/g, '/'));
4097
- const isManifestChanged = normalized.includes('manifest.json');
4116
+ const isManifestChanged = normalized.some((n)=>'manifest.json' === n || n.endsWith('/manifest.json'));
4098
4117
  const isLocalesChanged = normalized.some((n)=>/(^|\/)__?locales\/.+\.json$/i.test(n));
4099
4118
  const isContentScriptChanged = normalized.some((n)=>/(^|\/)content_scripts\/content-\d+(?:\.[a-f0-9]+)?\.(js|css)$/i.test(n));
4100
4119
  let isServiceWorkerChanged = false;
@@ -4106,7 +4125,7 @@ var __webpack_modules__ = {
4106
4125
  const sw = parsed?.background?.service_worker;
4107
4126
  if ('string' == typeof sw && sw) {
4108
4127
  const swUnix = sw.replace(/\\/g, '/');
4109
- isServiceWorkerChanged = normalized.includes(swUnix);
4128
+ isServiceWorkerChanged = normalized.some((n)=>n === swUnix || n.endsWith('/' + swUnix));
4110
4129
  }
4111
4130
  }
4112
4131
  } catch {}
@@ -4559,8 +4578,10 @@ var __webpack_modules__ = {
4559
4578
  return p;
4560
4579
  }
4561
4580
  };
4562
- if ('string' == typeof profile && profile.trim().length > 0) profilePath = external_path_.resolve(profile.trim());
4563
- else if (useSystemProfile) profilePath = '';
4581
+ if ('string' == typeof profile && profile.trim().length > 0) {
4582
+ const trimmedProfile = profile.trim();
4583
+ profilePath = external_path_.isAbsolute(trimmedProfile) ? trimmedProfile : external_path_.resolve(contextDir, trimmedProfile);
4584
+ } else if (useSystemProfile) profilePath = '';
4564
4585
  else {
4565
4586
  const base = external_path_.resolve(distRoot, 'extension-js', 'profiles', `${browser}-profile`);
4566
4587
  const persist = Boolean(configOptions.persistProfile);
@@ -4647,9 +4668,6 @@ var __webpack_modules__ = {
4647
4668
  String(debugPort)
4648
4669
  ] : [],
4649
4670
  '--foreground',
4650
- '--disable-background-timer-throttling',
4651
- '--disable-backgrounding-occluded-windows',
4652
- '--disable-renderer-backgrounding',
4653
4671
  ...additionalArgs
4654
4672
  ];
4655
4673
  return {
@@ -4670,9 +4688,6 @@ var __webpack_modules__ = {
4670
4688
  String(debugPort)
4671
4689
  ] : [],
4672
4690
  '--foreground',
4673
- '--disable-background-timer-throttling',
4674
- '--disable-backgrounding-occluded-windows',
4675
- '--disable-renderer-backgrounding',
4676
4691
  ...additionalArgs
4677
4692
  ];
4678
4693
  return {
@@ -4981,9 +4996,6 @@ var __webpack_modules__ = {
4981
4996
  '-wait-for-browser'
4982
4997
  ] : [],
4983
4998
  '--foreground',
4984
- '--disable-background-timer-throttling',
4985
- '--disable-backgrounding-occluded-windows',
4986
- '--disable-renderer-backgrounding',
4987
4999
  ...firefoxArgs
4988
5000
  ];
4989
5001
  this.child = await this.spawnFirefoxChild(binaryPath, args, wslFallbackBinary);
@@ -6142,6 +6154,7 @@ Cross-Browser Compatibility
6142
6154
  }
6143
6155
  function resolveWorkspaceDevelopRoot(startDir) {
6144
6156
  let currentDir = external_path_default().resolve(startDir);
6157
+ if (currentDir.split(external_path_default().sep).includes('node_modules')) return;
6145
6158
  for(let depth = 0; depth < 8; depth += 1){
6146
6159
  const candidate = external_path_default().join(currentDir, 'programs', 'develop');
6147
6160
  const resolved = resolveDevelopRootFromDir(candidate);
@@ -1,4 +1,5 @@
1
1
  import type { BrowserType } from '../browsers-types';
2
+ export declare const BANNER_PRINTED_EVENT = "extensionjs:banner-printed";
2
3
  export declare function isBannerPrinted(): boolean;
3
4
  type Info = {
4
5
  extensionId?: string;
@@ -24,6 +24,14 @@ export declare function isCanonicalContentScriptAsset(assetName: string): boolea
24
24
  /**
25
25
  * Resolves the on-disk file for a canonical content bundle. In development the
26
26
  * emitted name may include a fullhash (e.g. content_scripts/content-0.a1b2c3d4.js).
27
+ *
28
+ * Dev mode runs with `output.clean: false`, so successive rebuilds leave old
29
+ * hashed bundles on disk for the same `content-<index>` entry. Returning the
30
+ * first `readdirSync` match would pick a stale bundle whenever the filesystem
31
+ * order doesn't align with write order — and Firefox / Chromium would then
32
+ * `executeScript` an outdated content script, making reload look unreliable.
33
+ * Pick the most recently written match instead so the live reload always
34
+ * reflects the latest rebuild.
27
35
  */
28
36
  export declare function resolveEmittedContentScriptFile(extensionOutPath: string, index: number, ext: 'js' | 'css'): string | null;
29
37
  export declare function getChangedContentScriptEntryNames(modifiedFilePaths: string[], dependencyPathsByEntry: Map<string, Set<string>>): string[];
@@ -20,6 +20,9 @@ export declare class CDPExtensionController {
20
20
  private activeContentScriptRules;
21
21
  private contentScriptTargetListenerInstalled;
22
22
  private watchedPageSessions;
23
+ private sessionContexts;
24
+ private sessionContextsUnsubscribe;
25
+ private suppressInitialContextBurst;
23
26
  constructor(args: {
24
27
  outPath: string;
25
28
  browser: 'chrome' | 'edge' | 'chromium-based';
@@ -86,5 +89,6 @@ export declare class CDPExtensionController {
86
89
  private resolveExecutionContextId;
87
90
  private getTopFrameId;
88
91
  private collectExecutionContexts;
92
+ private releaseSessionContexts;
89
93
  }
90
94
  export {};
package/package.json CHANGED
@@ -38,7 +38,7 @@
38
38
  "extension": "./bin/extension.cjs"
39
39
  },
40
40
  "name": "extension",
41
- "version": "3.15.0-next.1",
41
+ "version": "3.15.1",
42
42
  "description": "Create cross-browser extensions with no build configuration.",
43
43
  "homepage": "https://extension.js.org/",
44
44
  "bugs": {
@@ -100,9 +100,9 @@
100
100
  "cross-spawn": "^7.0.6",
101
101
  "edge-location": "2.2.0",
102
102
  "firefox-location2": "3.0.0",
103
- "extension-create": "3.15.0-next.1",
104
- "extension-develop": "3.15.0-next.1",
105
- "extension-install": "3.15.0-next.1",
103
+ "extension-create": "3.15.1",
104
+ "extension-develop": "3.15.1",
105
+ "extension-install": "3.15.1",
106
106
  "commander": "^14.0.3",
107
107
  "pintor": "0.3.0",
108
108
  "semver": "^7.7.3",