feat: 支持 Firefox 浏览器#15
Open
Fldicoahkiin wants to merge 6 commits into
Open
Conversation
- 新增 browser-compat.ts,提供 compatStorage.session 包装层 - 运行时探测 chrome.storage.session 可用性,不可用时降级到 storage.local - .gitignore 添加 dist-firefox/ 和 *.xpi
- tab-store.ts 4 处 chrome.storage.session 调用替换为 compatStorage.session - popup-cache.ts 1 处 chrome.storage.session.set 替换为 compatStorage.session.set - index.ts onStartup 中添加 clearLegacySessionKeys 清理降级遗留数据
- 新增 build-scripts/package-firefox.mjs - 复制 dist/ 并转换 manifest.json(service_worker → background.scripts,添加 browser_specific_settings.gecko) - 解决 CRXJS ES module 代码分割问题:shared chunks 包裹 IIFE 隔离作用域 - package.json 添加 build:firefox 脚本
- meta 步骤输出 xpi_name - 打包 zip 后新增 Firefox .xpi 打包步骤 - 上传产物包含 .xpi 和 .sha256
Firefox 的 chrome.scripting.executeScript 可能返回原始 Promise 而非自动 await 后的 resolved 值,导致页面检测结果为空。
Reviewer's Guide添加 Firefox 浏览器支持:围绕 Firefox 构建与打包流水线流程图flowchart TD
A[pnpm build] --> B[dist/]
B --> C[Run package-firefox.mjs]
C --> D[Copy dist/ to dist-firefox/]
D --> E[Inline ES module chunks into background.js]
E --> F[Transform manifest.json<br>service_worker -> background.scripts<br>add browser_specific_settings.gecko]
F --> G[Create stackprism-vX.Y.Z.xpi in release/]
subgraph GitHubActions_release_workflow
H[Compute meta<br>zip_name, crx_name, xpi_name]
H --> I[打包 zip]
H --> J[打包 Firefox .xpi<br>node build-scripts/package-firefox.mjs]
J --> K[Generate xpi SHA256]
I --> L[Upload assets to GitHub Release<br>zip, zip.sha256, xpi, xpi.sha256]
K --> L
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your Experience打开你的 dashboard 以:
Getting HelpOriginal review guide in EnglishReviewer's GuideAdds Firefox browser support with a minimal compatibility layer around chrome.storage.session, a Firefox-specific packaging script/flow, and small behavior fixes for background scripts and CI release automation. Flow diagram for Firefox build and packaging pipelineflowchart TD
A[pnpm build] --> B[dist/]
B --> C[Run package-firefox.mjs]
C --> D[Copy dist/ to dist-firefox/]
D --> E[Inline ES module chunks into background.js]
E --> F[Transform manifest.json<br>service_worker -> background.scripts<br>add browser_specific_settings.gecko]
F --> G[Create stackprism-vX.Y.Z.xpi in release/]
subgraph GitHubActions_release_workflow
H[Compute meta<br>zip_name, crx_name, xpi_name]
H --> I[打包 zip]
H --> J[打包 Firefox .xpi<br>node build-scripts/package-firefox.mjs]
J --> K[Generate xpi SHA256]
I --> L[Upload assets to GitHub Release<br>zip, zip.sha256, xpi, xpi.sha256]
K --> L
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - 我发现了 3 个问题,并留下了一些整体反馈:
build-scripts/package-firefox.mjs中的 ES 模块内联逻辑依赖于对import{...}from"..."和export{...}这类模式非常严格的正则匹配,如果 CRXJS 的输出格式稍有变化就很容易失效;建议要么使用一个轻量的 JS 解析器(例如 acorn),要么放宽 / 统一这些正则,使其能容忍空白字符和不同语法形式。compatStorage.session.remove辅助方法目前只接受string[],但底层的 Chrome API 接受string | string[];将签名扩展为同时支持这两种形式,可以降低后续调用点误用的风险,也能更贴近原生 API。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The ES module inlining logic in `build-scripts/package-firefox.mjs` relies on very specific regexes for `import{...}from"..."` and `export{...}` patterns, which could easily break if CRXJS output formatting changes; consider either using a small JS parser (e.g. acorn) or loosening/centralizing these regexes to tolerate whitespace and different syntaxes.
- The `compatStorage.session.remove` helper only accepts `string[]`, whereas the underlying Chrome API accepts `string | string[]`; aligning the signature to support both would make it harder to misuse in future call sites and keep it closer to the native API.
## Individual Comments
### Comment 1
<location path="src/utils/browser-compat.ts" line_range="18-23" />
<code_context>
+
+export const compatStorage = {
+ session: {
+ get: async (key: string): Promise<Record<string, unknown>> => {
+ if (await checkSessionSupport()) {
+ return chrome.storage.session.get(key)
+ }
+ const result = await chrome.storage.local.get(SESSION_PREFIX + key)
+ const raw = result[SESSION_PREFIX + key]
+ return raw ? { [key]: raw } : {}
+ },
</code_context>
<issue_to_address>
**issue (bug_risk):** 回退到 local storage 的 session get 逻辑会丢失存储在本地存储中的合法假值。
在使用 local storage 的分支中,`raw ? { [key]: raw } : {}` 会把合法的假值(`0`、`false`、`''`)当作缺失值处理,因此永远无法返回这些值。应当检查键是否存在,而不是检查其真值性,例如:
```ts
const storageKey = SESSION_PREFIX + key
const result = await chrome.storage.local.get(storageKey)
if (Object.prototype.hasOwnProperty.call(result, storageKey)) {
return { [key]: result[storageKey] }
}
return {}
```
</issue_to_address>
### Comment 2
<location path="build-scripts/package-firefox.mjs" line_range="5" />
<code_context>
+import { resolve, dirname, basename } from 'node:path'
+import { execFileSync } from 'node:child_process'
+
+const root = resolve(import.meta.dirname, '..')
+const distDir = resolve(root, 'dist')
+const firefoxDir = resolve(root, 'dist-firefox')
</code_context>
<issue_to_address>
**issue (bug_risk):** `import.meta.dirname` 是非标准特性,在当前的 Node 版本中会失败。
请改用 Node 标准的 `import.meta.url` 方案:
```js
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const root = resolve(__dirname, '..')
```
这样可以在各受支持的 Node 版本上可靠工作。
</issue_to_address>
### Comment 3
<location path="build-scripts/package-firefox.mjs" line_range="24-36" />
<code_context>
+// 2. Store each chunk's exports in a per-chunk namespace
+// 3. Replace the entry chunk's imports with variable declarations from those namespaces
+
+const parseAllImportBindings = (code) => {
+ const re = /import\{([^}]*)\}from"([^"]*)"/g
+ const imports = []
+ let match
+ while ((match = re.exec(code)) !== null) {
+ const bindings = match[1].split(',').map(s => {
+ const parts = s.trim().split(/\s+as\s+/)
+ return { exported: parts[0].trim(), local: (parts[1] || parts[0]).trim() }
+ })
+ imports.push({ path: match[2], bindings })
+ }
+ return imports
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** import/export 解析与一种非常特定的压缩后代码形态强耦合,在打包器稍有调整时就可能出错。
这些正则假设了一种非常狭窄的格式(无空格、双引号、无换行、只支持具名 import/export)。只要 CRXJS/打包器输出有变化(例如空格、引号形式不同,或出现默认/命名空间导入),就可能在不报错的情况下破坏生成的 background bundle。
为了提高稳健性,可以:
- 先进行格式归一化 / 放宽格式要求(例如允许任意空白),或者
- 明确只支持当前模式,对不支持的形态抛出清晰的错误。
第二种方式更安全:与其生成无效的 `background.js`,不如尽早以可读错误失败。
```suggestion
const parseAllImportBindings = (code) => {
// Supports minified and non-minified named imports:
// import { foo, bar as baz } from "chunk.js"
// Allows arbitrary whitespace and both quote styles.
const namedImportRe = /import\s*\{\s*([^}]*)\}\s*from\s*(['"])(.*?)\2/g
const imports = []
let match
while ((match = namedImportRe.exec(code)) !== null) {
const [, rawBindings, , importPath] = match
const bindings = rawBindings
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(s => {
const parts = s.split(/\s+as\s+/)
const exported = parts[0]?.trim()
const local = (parts[1] || parts[0] || '').trim()
if (!exported || !local) {
throw new Error(
`[package-firefox] Unsupported import binding "${s}" in "${importPath}". ` +
`Only named imports of the form 'import { foo, bar as baz } from "./chunk.js"' are supported.`,
)
}
return { exported, local }
})
imports.push({ path: importPath, bindings })
}
// Detect other import forms (default, namespace, or side-effect imports) and fail fast.
// This intentionally does *not* match the named import shape above.
const unsupportedImportRe = /import\s+(?!\{)[^'";]+(?:['"][^'"]*['"])?/g
if (unsupportedImportRe.test(code)) {
throw new Error(
'[package-firefox] Unsupported import statement detected in background chunks. ' +
'This script only supports named imports of the form ' +
'"import { foo, bar as baz } from \\"./chunk.js\\"". ' +
'Please adjust the bundler output or extend package-firefox.mjs to handle this shape.',
)
}
// If there are "import {" sequences that we failed to parse, also fail fast.
if (imports.length === 0 && /import\s*\{/.test(code)) {
throw new Error(
'[package-firefox] Failed to parse named imports in background chunks. ' +
'Expected imports of the form "import { foo, bar as baz } from \\"./chunk.js\\"".',
)
}
return imports
}
```
</issue_to_address>帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've found 3 issues, and left some high level feedback:
- The ES module inlining logic in
build-scripts/package-firefox.mjsrelies on very specific regexes forimport{...}from"..."andexport{...}patterns, which could easily break if CRXJS output formatting changes; consider either using a small JS parser (e.g. acorn) or loosening/centralizing these regexes to tolerate whitespace and different syntaxes. - The
compatStorage.session.removehelper only acceptsstring[], whereas the underlying Chrome API acceptsstring | string[]; aligning the signature to support both would make it harder to misuse in future call sites and keep it closer to the native API.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The ES module inlining logic in `build-scripts/package-firefox.mjs` relies on very specific regexes for `import{...}from"..."` and `export{...}` patterns, which could easily break if CRXJS output formatting changes; consider either using a small JS parser (e.g. acorn) or loosening/centralizing these regexes to tolerate whitespace and different syntaxes.
- The `compatStorage.session.remove` helper only accepts `string[]`, whereas the underlying Chrome API accepts `string | string[]`; aligning the signature to support both would make it harder to misuse in future call sites and keep it closer to the native API.
## Individual Comments
### Comment 1
<location path="src/utils/browser-compat.ts" line_range="18-23" />
<code_context>
+
+export const compatStorage = {
+ session: {
+ get: async (key: string): Promise<Record<string, unknown>> => {
+ if (await checkSessionSupport()) {
+ return chrome.storage.session.get(key)
+ }
+ const result = await chrome.storage.local.get(SESSION_PREFIX + key)
+ const raw = result[SESSION_PREFIX + key]
+ return raw ? { [key]: raw } : {}
+ },
</code_context>
<issue_to_address>
**issue (bug_risk):** Fallback session get loses valid falsy values stored in local storage.
In the local-storage path, `raw ? { [key]: raw } : {}` treats valid falsy values (`0`, `false`, `''`) as missing, so they can never be returned. Instead, check for key presence, not truthiness, e.g.:
```ts
const storageKey = SESSION_PREFIX + key
const result = await chrome.storage.local.get(storageKey)
if (Object.prototype.hasOwnProperty.call(result, storageKey)) {
return { [key]: result[storageKey] }
}
return {}
```
</issue_to_address>
### Comment 2
<location path="build-scripts/package-firefox.mjs" line_range="5" />
<code_context>
+import { resolve, dirname, basename } from 'node:path'
+import { execFileSync } from 'node:child_process'
+
+const root = resolve(import.meta.dirname, '..')
+const distDir = resolve(root, 'dist')
+const firefoxDir = resolve(root, 'dist-firefox')
</code_context>
<issue_to_address>
**issue (bug_risk):** `import.meta.dirname` is non-standard and will fail on current Node versions.
Use Node’s standard `import.meta.url` pattern instead:
```js
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const root = resolve(__dirname, '..')
```
This will work reliably across supported Node versions.
</issue_to_address>
### Comment 3
<location path="build-scripts/package-firefox.mjs" line_range="24-36" />
<code_context>
+// 2. Store each chunk's exports in a per-chunk namespace
+// 3. Replace the entry chunk's imports with variable declarations from those namespaces
+
+const parseAllImportBindings = (code) => {
+ const re = /import\{([^}]*)\}from"([^"]*)"/g
+ const imports = []
+ let match
+ while ((match = re.exec(code)) !== null) {
+ const bindings = match[1].split(',').map(s => {
+ const parts = s.trim().split(/\s+as\s+/)
+ return { exported: parts[0].trim(), local: (parts[1] || parts[0]).trim() }
+ })
+ imports.push({ path: match[2], bindings })
+ }
+ return imports
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Import/export parsing is tightly coupled to a very specific minified shape and may break with small bundler changes.
These regexes assume a very narrow format (no spaces, double quotes, no line breaks, named-only imports/exports). Any change in CRXJS/bundler output (e.g., different spacing, quotes, or default/namespace imports) could silently break the background bundle.
To improve robustness, either:
- Normalize/relax formatting (e.g., tolerate arbitrary whitespace), or
- Explicitly support only the current patterns and throw a clear error on unsupported shapes.
The second option is safer: fail fast with a descriptive error instead of emitting an invalid `background.js`.
```suggestion
const parseAllImportBindings = (code) => {
// Supports minified and non-minified named imports:
// import { foo, bar as baz } from "chunk.js"
// Allows arbitrary whitespace and both quote styles.
const namedImportRe = /import\s*\{\s*([^}]*)\}\s*from\s*(['"])(.*?)\2/g
const imports = []
let match
while ((match = namedImportRe.exec(code)) !== null) {
const [, rawBindings, , importPath] = match
const bindings = rawBindings
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(s => {
const parts = s.split(/\s+as\s+/)
const exported = parts[0]?.trim()
const local = (parts[1] || parts[0] || '').trim()
if (!exported || !local) {
throw new Error(
`[package-firefox] Unsupported import binding "${s}" in "${importPath}". ` +
`Only named imports of the form 'import { foo, bar as baz } from "./chunk.js"' are supported.`,
)
}
return { exported, local }
})
imports.push({ path: importPath, bindings })
}
// Detect other import forms (default, namespace, or side-effect imports) and fail fast.
// This intentionally does *not* match the named import shape above.
const unsupportedImportRe = /import\s+(?!\{)[^'";]+(?:['"][^'"]*['"])?/g
if (unsupportedImportRe.test(code)) {
throw new Error(
'[package-firefox] Unsupported import statement detected in background chunks. ' +
'This script only supports named imports of the form ' +
'"import { foo, bar as baz } from \\"./chunk.js\\"". ' +
'Please adjust the bundler output or extend package-firefox.mjs to handle this shape.',
)
}
// If there are "import {" sequences that we failed to parse, also fail fast.
if (imports.length === 0 && /import\s*\{/.test(code)) {
throw new Error(
'[package-firefox] Failed to parse named imports in background chunks. ' +
'Expected imports of the form "import { foo, bar as baz } from \\"./chunk.js\\"".',
)
}
return imports
}
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
- browser-compat: storage.local 降级 get 改用 hasOwnProperty 检查,避免 0/false/'' 等 falsy 值被丢弃 - package-firefox: import.meta.dirname 替换为 fileURLToPath,兼容 Node 20 CI 环境 - package-firefox: import/export 解析增加守卫,遇到不支持的模块语法时抛出明确错误
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
概述
添加 Firefox 浏览器支持,扩展 StackPrism 的平台覆盖范围。
改动说明
兼容层(最小侵入)
src/utils/browser-compat.ts,提供compatStorage.session包装层chrome.storage.session可用性,Firefox 128+ 直接使用原生 API,旧版降级到storage.localtab-store.ts和popup-cache.ts中 5 处chrome.storage.session调用Firefox 打包
build-scripts/package-firefox.mjsservice_worker→background.scriptsbrowser_specific_settings.geckopnpm build:firefox即可生成.xpi文件Firefox 兼容修复
detection.ts:executeScriptMAIN world 结果兼容 Firefox 的 Promise 返回行为CI
.xpi并上传到 GitHub Release技术细节
world: 'MAIN'和完整storage.session)chrome.*API 中仅storage.session需要兼容包装,其余在 Firefox MV3 下完全兼容webextension-polyfill未引入 — Firefox MV3 原生支持chrome.*回调式 API测试
pnpm build)pnpm build:firefox)pnpm test:unit)pnpm lint)background.scripts+browser_specific_settings.gecko)Summary by Sourcery
为扩展添加 Firefox 打包和运行时兼容性,以支持在 Firefox 和 Chrome 上同时运行该扩展。
New Features:
.xpi安装包。chrome.storage.session的浏览器上也能正常工作。Bug Fixes:
executeScript执行结果,以符合 Firefox 后台脚本的行为。Enhancements:
Build:
build:firefoxnpm 脚本以及用于 Firefox 打包的脚本,以重写后台 chunk 和 manifest,从而实现 Firefox 兼容。CI:
.xpi构件。Original summary in English
Summary by Sourcery
Add Firefox packaging and runtime compatibility to support running the extension on Firefox alongside Chrome.
New Features:
Bug Fixes:
Enhancements:
Build:
CI: