Skip to content

feat: 支持 Firefox 浏览器#15

Open
Fldicoahkiin wants to merge 6 commits into
setube:mainfrom
Fldicoahkiin:feat/firefox-support
Open

feat: 支持 Firefox 浏览器#15
Fldicoahkiin wants to merge 6 commits into
setube:mainfrom
Fldicoahkiin:feat/firefox-support

Conversation

@Fldicoahkiin
Copy link
Copy Markdown

@Fldicoahkiin Fldicoahkiin commented May 26, 2026

概述

添加 Firefox 浏览器支持,扩展 StackPrism 的平台覆盖范围。

改动说明

兼容层(最小侵入)

  • 新增 src/utils/browser-compat.ts,提供 compatStorage.session 包装层
  • 运行时探测 chrome.storage.session 可用性,Firefox 128+ 直接使用原生 API,旧版降级到 storage.local
  • 迁移 tab-store.tspopup-cache.ts 中 5 处 chrome.storage.session 调用
  • 对现有 Chrome 功能零影响

Firefox 打包

  • 新增 build-scripts/package-firefox.mjs
  • 构建后自动转换 manifest.json:
    • service_workerbackground.scripts
    • 添加 browser_specific_settings.gecko
  • 解决 CRXJS ES module 代码分割问题:shared chunks 包裹 IIFE 隔离作用域
  • 运行 pnpm build:firefox 即可生成 .xpi 文件

Firefox 兼容修复

  • detection.tsexecuteScript MAIN world 结果兼容 Firefox 的 Promise 返回行为

CI

  • release 工作流自动打包 Firefox .xpi 并上传到 GitHub Release

技术细节

  • 目标最低版本:Firefox 128+(2024 年 7 月 ESR,支持 world: 'MAIN' 和完整 storage.session
  • 25 个 chrome.* API 中仅 storage.session 需要兼容包装,其余在 Firefox MV3 下完全兼容
  • webextension-polyfill 未引入 — Firefox MV3 原生支持 chrome.* 回调式 API
  • 仅修改 ~8 行现有代码,新增 ~200 行

测试

  • Chrome 构建通过(pnpm build
  • Firefox 构建通过(pnpm build:firefox
  • 单元测试全部通过(pnpm test:unit
  • ESLint 通过(pnpm lint
  • Chrome manifest 无 Firefox 字段泄漏
  • Firefox manifest 结构正确(background.scripts + browser_specific_settings.gecko
  • background.js 无 ES module 语法残留

Summary by Sourcery

为扩展添加 Firefox 打包和运行时兼容性,以支持在 Firefox 和 Chrome 上同时运行该扩展。

New Features:

  • 引入 Firefox 构建流水线,从现有的 dist 输出生成 .xpi 安装包。
  • 提供浏览器存储兼容层,使会话数据在不支持 chrome.storage.session 的浏览器上也能正常工作。

Bug Fixes:

  • 处理返回原始 Promise 的 executeScript 执行结果,以符合 Firefox 后台脚本的行为。

Enhancements:

  • 重构后台标签页和弹出页的存储逻辑,使用新的兼容存储封装器,并在启动时清理遗留的会话键。

Build:

  • 新增 build:firefox npm 脚本以及用于 Firefox 打包的脚本,以重写后台 chunk 和 manifest,从而实现 Firefox 兼容。

CI:

  • 扩展发布工作流,以在现有发布资源之外,构建、生成校验和并上传 Firefox .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:

  • Introduce a Firefox build pipeline that generates a .xpi package from the existing dist output.
  • Provide a browser storage compatibility layer so session data works on browsers without chrome.storage.session support.

Bug Fixes:

  • Handle executeScript results that return a raw Promise to align with Firefox background script behavior.

Enhancements:

  • Refactor background tab and popup storage to use the new compatibility storage wrapper and clean up legacy session keys on startup.

Build:

  • Add a build:firefox npm script and a packaging script that rewrites background chunks and the manifest for Firefox compatibility.

CI:

  • Extend the release workflow to build, checksum, and upload the Firefox .xpi artifact alongside existing release assets.

- 新增 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 值,导致页面检测结果为空。
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 26, 2026

Reviewer's Guide

添加 Firefox 浏览器支持:围绕 chrome.storage.session 提供最小兼容层、增加 Firefox 专用的打包脚本/流程,并对后台脚本行为和 CI 发布自动化做小幅修复。

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
Loading

File-Level Changes

Change Details Files
引入兼容性存储层,使 chrome.storage.session 的使用能在 Chrome 和 Firefox 上工作(包括不支持 storage.session 的旧版 Firefox)。
  • 添加 compatStorage.session 封装,在运行时特性检测 chrome.storage.session,若不可用则回退到 chrome.storage.local,并使用专用键前缀
  • 实现 clearLegacySessionKeys,在真实的 storage.session 可用时清理带前缀的 session 键
  • compatStorage 接入 tab-store 的读/写/删除路径以及 popup-cache 的写入路径,其它 chrome.* API 保持不变
  • 在后台启动时调用 clearLegacySessionKeys,以从带前缀的 local-storage 数据迁移出去
src/utils/browser-compat.ts
src/background/tab-store.ts
src/background/popup-cache.ts
src/background/index.ts
添加 Firefox 构建/打包流水线,将 MV3 Chrome bundle 转换为 Firefox 兼容的后台脚本和 manifest,并产出 .xpi 构件。
  • 创建 build-scripts/package-firefox.mjs,用于将 dist 复制到 dist-firefox,并对 Firefox 的后台代码和 manifest.json 进行后处理
  • 通过对 CRXJS ES module 的后台 chunk 做拓扑排序,将其内联到单一的 background.js 中,对共享 chunk 用 IIFE 包裹,并通过 __chunks 命名空间合成 import/export 连接
  • manifest.background.service_worker 转换为 background.scripts,并添加 browser_specific_settings.gecko,将 strict_min_version 设为 128.0
  • 将处理后的 Firefox dist 压缩为带版本号的 .xpi 文件,输出到 release/ 目录
  • package.json 中添加 build:firefox 脚本,在执行常规构建后运行 Firefox 打包脚本
build-scripts/package-firefox.mjs
package.json
修补后台检测行为,使其与 Firefox 的 executeScript Promise 语义保持一致。
  • detection.ts 中捕获 executeScript 的结果,并检测其是否为类 Promise;在需要时对其进行 await,以处理 Firefox 对 MAIN world 脚本返回原始 Promise 的情况
src/background/detection.ts
扩展发布工作流,以构建、校验并将 Firefox .xpi 附加到 GitHub Releases,与现有构件一同发布。
  • 在工作流的 meta 步骤输出中暴露 xpi_name,便于复用
  • 添加打包步骤,运行 Firefox 打包脚本并为 .xpi 生成 SHA256 校验文件
  • 在创建 GitHub Release 时,将 .xpi 及其 .sha256 文件一并包含在资产列表中
.github/workflows/release-extension.yml

Tips and commands

Interacting with Sourcery

  • 触发新评审: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 通过回复某条评审评论,请求 Sourcery 从该评论创建 issue。你也可以回复该评论并写上 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 概要: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在你想要的位置生成 PR 概要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成概要。
  • 生成评审者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成评审者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve 来解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这非常有用。
  • 撤销所有 Sourcery 评审: 在 pull request 中评论 @sourcery-ai dismiss 来撤销所有现有的 Sourcery 评审。如果你想从一个全新的评审开始,这尤其有用——别忘了再评论 @sourcery-ai review 来触发新的评审!

Customizing Your Experience

打开你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 自动生成的 pull request 概要、评审者指南等。
  • 更改评审语言。
  • 添加、删除或编辑自定义评审指令。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds 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 pipeline

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
Loading

File-Level Changes

Change Details Files
Introduce a compatibility storage layer so chrome.storage.session usage works on both Chrome and Firefox (including older Firefox without storage.session).
  • Add compatStorage.session wrapper that feature-detects chrome.storage.session at runtime and falls back to chrome.storage.local with a dedicated key prefix
  • Implement clearLegacySessionKeys to clean up prefixed session keys when real storage.session is available
  • Wire compatStorage into tab-store read/write/remove paths and popup-cache write path, leaving other chrome.* APIs untouched
  • Invoke clearLegacySessionKeys on background startup to migrate away from prefixed local-storage data
src/utils/browser-compat.ts
src/background/tab-store.ts
src/background/popup-cache.ts
src/background/index.ts
Add a Firefox build/packaging pipeline that converts the MV3 Chrome bundle into a Firefox-compatible background script and manifest, and emits an .xpi artifact.
  • Create build-scripts/package-firefox.mjs to copy dist to dist-firefox and post-process background code and manifest.json for Firefox
  • Inline CRXJS ES module background chunks into a single background.js by topo-sorting chunks, wrapping shared chunks in IIFEs, and synthesizing import/export wiring via a __chunks namespace
  • Transform manifest.background.service_worker to background.scripts and add browser_specific_settings.gecko with a strict_min_version of 128.0
  • Zip the processed Firefox dist into a versioned .xpi under release/
  • Add a package.json build:firefox script that runs the normal build then the Firefox packaging script
build-scripts/package-firefox.mjs
package.json
Patch background detection behavior to match Firefox executeScript promise semantics.
  • Capture executeScript result in detection.ts and detect if it is a Promise-like, awaiting it when necessary to handle Firefox returning a raw Promise for MAIN world scripts
src/background/detection.ts
Extend the release workflow to build, checksum, and attach the Firefox .xpi to GitHub Releases alongside existing artifacts.
  • Expose xpi_name in the workflow meta step outputs for reuse
  • Add a packaging step that runs the Firefox packaging script and emits a SHA256 checksum file for the .xpi
  • Include the .xpi and its .sha256 file in the assets list when creating the GitHub Release
.github/workflows/release-extension.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery 对开源项目是免费的——如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进后续的评审。
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.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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/utils/browser-compat.ts Outdated
Comment thread build-scripts/package-firefox.mjs Outdated
Comment thread build-scripts/package-firefox.mjs
- browser-compat: storage.local 降级 get 改用 hasOwnProperty 检查,避免 0/false/'' 等 falsy 值被丢弃
- package-firefox: import.meta.dirname 替换为 fileURLToPath,兼容 Node 20 CI 环境
- package-firefox: import/export 解析增加守卫,遇到不支持的模块语法时抛出明确错误
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant