-
-
Notifications
You must be signed in to change notification settings - Fork 38
feat: 支持 Firefox 浏览器 #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Fldicoahkiin
wants to merge
6
commits into
setube:main
Choose a base branch
from
Fldicoahkiin:feat/firefox-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
e9e82c4
feat: 添加 Firefox 浏览器兼容层
Fldicoahkiin e8fc1f0
refactor: tab-store 和 popup-cache 使用 compatStorage 包装 storage.session
Fldicoahkiin d68378c
feat: 添加 Firefox 打包脚本
Fldicoahkiin bc4c218
ci: release 工作流添加 Firefox .xpi 打包
Fldicoahkiin d4fb5fb
fix: executeScript MAIN world 结果兼容 Firefox Promise 返回
Fldicoahkiin 4e08d94
fix: 修复 falsy 值丢失与 Node 20 兼容性问题
Fldicoahkiin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| import { cpSync, rmSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs' | ||
| import { resolve, dirname, basename } from 'node:path' | ||
| import { execFileSync } from 'node:child_process' | ||
| import { fileURLToPath } from 'node:url' | ||
|
|
||
| const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') | ||
| const distDir = resolve(root, 'dist') | ||
| const firefoxDir = resolve(root, 'dist-firefox') | ||
|
|
||
| if (!existsSync(distDir)) { | ||
| console.error('[package-firefox] dist/ not found, run `pnpm build` first') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| rmSync(firefoxDir, { recursive: true, force: true }) | ||
| cpSync(distDir, firefoxDir, { recursive: true }) | ||
|
|
||
| // --- Inline ES module chunks into a single background script --- | ||
| // CRXJS outputs background as ES modules with code-split shared chunks. | ||
| // Firefox background scripts don't support ES modules, so we: | ||
| // 1. Wrap each shared chunk in an IIFE to isolate scope (prevents variable name collisions) | ||
| // 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 }) | ||
| } | ||
| if (!imports.length && /import\s/.test(code)) { | ||
| throw new Error('[package-firefox] unsupported import syntax in entry chunk — cannot inline') | ||
| } | ||
| return imports | ||
| } | ||
|
|
||
| const parseExportBindings = (code) => { | ||
| const match = code.match(/export\{([^}]*)\};?\s*$/) | ||
| if (!match) { | ||
| if (/export\s/.test(code)) { | ||
| throw new Error('[package-firefox] unsupported export syntax in shared chunk — cannot inline') | ||
| } | ||
| return [] | ||
| } | ||
| return match[1].split(',').map(s => { | ||
| const parts = s.trim().split(/\s+as\s+/) | ||
| return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() } | ||
| }) | ||
| } | ||
|
|
||
| const stripModuleSyntax = (code) => | ||
| code.replace(/import\{[^}]*\}from"[^"]*";?/g, '').replace(/export\{[^}]*\};?/g, '') | ||
|
|
||
| const resolveImports = (filePath) => { | ||
| const code = readFileSync(filePath, 'utf8') | ||
| const dir = dirname(filePath) | ||
| const importRe = /import\{[^}]*\}from"(\.\/[^"]+)"/g | ||
| const deps = [] | ||
| let match | ||
| while ((match = importRe.exec(code)) !== null) { | ||
| deps.push(resolve(dir, match[1])) | ||
| } | ||
| return { code, deps } | ||
| } | ||
|
|
||
| const topologicalSort = (entryPath) => { | ||
| const visited = new Set() | ||
| const order = [] | ||
| const visit = (filePath) => { | ||
| if (visited.has(filePath)) return | ||
| visited.add(filePath) | ||
| const { deps } = resolveImports(filePath) | ||
| for (const dep of deps) visit(dep) | ||
| order.push(filePath) | ||
| } | ||
| visit(entryPath) | ||
| return order | ||
| } | ||
|
|
||
| // Read the loader to find the entry chunk | ||
| const loaderPath = resolve(firefoxDir, 'service-worker-loader.js') | ||
| const loaderCode = readFileSync(loaderPath, 'utf8') | ||
| const entryMatch = loaderCode.match(/import\s+'\.\/(assets\/[^']+)'/) | ||
| if (!entryMatch) { | ||
| console.error('[package-firefox] could not resolve service-worker-loader entry') | ||
| process.exit(1) | ||
| } | ||
|
|
||
| const entryPath = resolve(firefoxDir, entryMatch[1]) | ||
| const chunkOrder = topologicalSort(entryPath) | ||
| const sharedChunks = chunkOrder.slice(0, -1) | ||
| const entryChunkPath = chunkOrder[chunkOrder.length - 1] | ||
|
|
||
| const parts = [`var __chunks = {};`] | ||
|
|
||
| for (const filePath of sharedChunks) { | ||
| const code = readFileSync(filePath, 'utf8') | ||
| const chunkId = basename(filePath, '.js') | ||
| const exports = parseExportBindings(code) | ||
| const stripped = stripModuleSyntax(code) | ||
| const exportAssignments = exports | ||
| .map(e => `__chunks["${chunkId}"].${e.exported} = ${e.local};`) | ||
| .join(' ') | ||
| parts.push(`(function() { ${stripped} __chunks["${chunkId}"] = {}; ${exportAssignments} })();`) | ||
| } | ||
|
|
||
| const entryCode = readFileSync(entryChunkPath, 'utf8') | ||
| const entryImports = parseAllImportBindings(entryCode) | ||
| let entryBody = stripModuleSyntax(entryCode) | ||
| if (entryImports.length) { | ||
| const varDecls = entryImports.flatMap(imp => { | ||
| const chunkId = basename(resolve(dirname(entryChunkPath), imp.path), '.js') | ||
| return imp.bindings.map(b => `var ${b.local} = __chunks["${chunkId}"].${b.exported};`) | ||
| }).join(' ') | ||
| entryBody = varDecls + '\n' + entryBody | ||
| } | ||
| parts.push(entryBody) | ||
|
|
||
| const backgroundPath = resolve(firefoxDir, 'background.js') | ||
| writeFileSync(backgroundPath, parts.join('\n')) | ||
| console.log(`[package-firefox] inlined ${chunkOrder.length} chunks into background.js`) | ||
|
|
||
| // --- Transform manifest.json --- | ||
|
|
||
| const manifestPath = resolve(firefoxDir, 'manifest.json') | ||
| const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) | ||
|
|
||
| if (manifest.background?.service_worker) { | ||
| manifest.background = { scripts: ['background.js'] } | ||
| } | ||
|
|
||
| manifest.browser_specific_settings = { | ||
| gecko: { | ||
| id: 'stackprism@stackprism.dev', | ||
| strict_min_version: '128.0' | ||
| } | ||
| } | ||
|
|
||
| writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)) | ||
| console.log('[package-firefox] manifest.json transformed') | ||
|
|
||
| // --- Package .xpi --- | ||
|
|
||
| const releaseDir = resolve(root, 'release') | ||
| if (!existsSync(releaseDir)) mkdirSync(releaseDir) | ||
|
|
||
| const version = manifest.version | ||
| const xpiName = `stackprism-v${version}.xpi` | ||
| execFileSync('zip', ['-r', resolve(releaseDir, xpiName), '.'], { cwd: firefoxDir, stdio: 'inherit' }) | ||
| console.log(`[package-firefox] created release/${xpiName}`) | ||
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
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| const SESSION_PREFIX = '__sp_session__:' | ||
|
|
||
| let sessionSupported: boolean | null = null | ||
|
|
||
| const checkSessionSupport = async (): Promise<boolean> => { | ||
| if (sessionSupported !== null) return sessionSupported | ||
| try { | ||
| await chrome.storage.session.get('__probe__') | ||
| sessionSupported = true | ||
| } catch { | ||
| sessionSupported = false | ||
| } | ||
| return sessionSupported | ||
| } | ||
|
|
||
| export const compatStorage = { | ||
| session: { | ||
| get: async (key: string): Promise<Record<string, unknown>> => { | ||
| if (await checkSessionSupport()) { | ||
| return chrome.storage.session.get(key) | ||
| } | ||
| const storageKey = SESSION_PREFIX + key | ||
| const result = await chrome.storage.local.get(storageKey) | ||
| return Object.prototype.hasOwnProperty.call(result, storageKey) | ||
| ? { [key]: result[storageKey] } | ||
| : {} | ||
| }, | ||
| set: async (items: Record<string, unknown>): Promise<void> => { | ||
| if (await checkSessionSupport()) { | ||
| return chrome.storage.session.set(items) | ||
| } | ||
| const prefixed: Record<string, unknown> = {} | ||
| for (const [key, value] of Object.entries(items)) { | ||
| prefixed[SESSION_PREFIX + key] = value | ||
| } | ||
| return chrome.storage.local.set(prefixed) | ||
| }, | ||
| remove: async (keys: string[]): Promise<void> => { | ||
| if (await checkSessionSupport()) { | ||
| return chrome.storage.session.remove(keys) | ||
| } | ||
| return chrome.storage.local.remove(keys.map(k => SESSION_PREFIX + k)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const clearLegacySessionKeys = async (): Promise<void> => { | ||
| if (await checkSessionSupport()) return | ||
| const all = await chrome.storage.local.get(null) | ||
| const sessionKeys = Object.keys(all).filter(k => k.startsWith(SESSION_PREFIX)) | ||
| if (sessionKeys.length) { | ||
| await chrome.storage.local.remove(sessionKeys) | ||
| } | ||
| } |
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.
Uh oh!
There was an error while loading. Please reload this page.