Skip to content

Commit 1e8b5f4

Browse files
committed
refactor: Firefox 打包脚本改用 esbuild 和 archiver
- 用 esbuild bundle + format:'iife' 替代手写 ESM 解析器,不再依赖 minifier 输出格式 - 用 archiver 替代系统 zip 命令,兼容 Windows 构建环境 - gecko.id 修正为 stackprism@setube.github.io
1 parent 4e08d94 commit 1e8b5f4

3 files changed

Lines changed: 618 additions & 121 deletions

File tree

build-scripts/package-firefox.mjs

Lines changed: 34 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { cpSync, rmSync, readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
2-
import { resolve, dirname, basename } from 'node:path'
3-
import { execFileSync } from 'node:child_process'
1+
import { cpSync, rmSync, readFileSync, writeFileSync, mkdirSync, existsSync, createWriteStream } from 'node:fs'
2+
import { resolve, dirname } from 'node:path'
43
import { fileURLToPath } from 'node:url'
4+
import { createRequire } from 'node:module'
5+
import archiver from 'archiver'
6+
7+
const require = createRequire(import.meta.url)
8+
const esbuild = require('esbuild')
59

610
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
711
const distDir = resolve(root, 'dist')
@@ -15,74 +19,11 @@ if (!existsSync(distDir)) {
1519
rmSync(firefoxDir, { recursive: true, force: true })
1620
cpSync(distDir, firefoxDir, { recursive: true })
1721

18-
// --- Inline ES module chunks into a single background script ---
22+
// --- Bundle background script as IIFE ---
1923
// CRXJS outputs background as ES modules with code-split shared chunks.
20-
// Firefox background scripts don't support ES modules, so we:
21-
// 1. Wrap each shared chunk in an IIFE to isolate scope (prevents variable name collisions)
22-
// 2. Store each chunk's exports in a per-chunk namespace
23-
// 3. Replace the entry chunk's imports with variable declarations from those namespaces
24-
25-
const parseAllImportBindings = (code) => {
26-
const re = /import\{([^}]*)\}from"([^"]*)"/g
27-
const imports = []
28-
let match
29-
while ((match = re.exec(code)) !== null) {
30-
const bindings = match[1].split(',').map(s => {
31-
const parts = s.trim().split(/\s+as\s+/)
32-
return { exported: parts[0].trim(), local: (parts[1] || parts[0]).trim() }
33-
})
34-
imports.push({ path: match[2], bindings })
35-
}
36-
if (!imports.length && /import\s/.test(code)) {
37-
throw new Error('[package-firefox] unsupported import syntax in entry chunk — cannot inline')
38-
}
39-
return imports
40-
}
41-
42-
const parseExportBindings = (code) => {
43-
const match = code.match(/export\{([^}]*)\};?\s*$/)
44-
if (!match) {
45-
if (/export\s/.test(code)) {
46-
throw new Error('[package-firefox] unsupported export syntax in shared chunk — cannot inline')
47-
}
48-
return []
49-
}
50-
return match[1].split(',').map(s => {
51-
const parts = s.trim().split(/\s+as\s+/)
52-
return { local: parts[0].trim(), exported: (parts[1] || parts[0]).trim() }
53-
})
54-
}
55-
56-
const stripModuleSyntax = (code) =>
57-
code.replace(/import\{[^}]*\}from"[^"]*";?/g, '').replace(/export\{[^}]*\};?/g, '')
58-
59-
const resolveImports = (filePath) => {
60-
const code = readFileSync(filePath, 'utf8')
61-
const dir = dirname(filePath)
62-
const importRe = /import\{[^}]*\}from"(\.\/[^"]+)"/g
63-
const deps = []
64-
let match
65-
while ((match = importRe.exec(code)) !== null) {
66-
deps.push(resolve(dir, match[1]))
67-
}
68-
return { code, deps }
69-
}
24+
// Firefox background scripts don't support ES modules, so we rebundle
25+
// the entry point into a single IIFE via esbuild.
7026

71-
const topologicalSort = (entryPath) => {
72-
const visited = new Set()
73-
const order = []
74-
const visit = (filePath) => {
75-
if (visited.has(filePath)) return
76-
visited.add(filePath)
77-
const { deps } = resolveImports(filePath)
78-
for (const dep of deps) visit(dep)
79-
order.push(filePath)
80-
}
81-
visit(entryPath)
82-
return order
83-
}
84-
85-
// Read the loader to find the entry chunk
8627
const loaderPath = resolve(firefoxDir, 'service-worker-loader.js')
8728
const loaderCode = readFileSync(loaderPath, 'utf8')
8829
const entryMatch = loaderCode.match(/import\s+'\.\/(assets\/[^']+)'/)
@@ -92,38 +33,19 @@ if (!entryMatch) {
9233
}
9334

9435
const entryPath = resolve(firefoxDir, entryMatch[1])
95-
const chunkOrder = topologicalSort(entryPath)
96-
const sharedChunks = chunkOrder.slice(0, -1)
97-
const entryChunkPath = chunkOrder[chunkOrder.length - 1]
98-
99-
const parts = [`var __chunks = {};`]
100-
101-
for (const filePath of sharedChunks) {
102-
const code = readFileSync(filePath, 'utf8')
103-
const chunkId = basename(filePath, '.js')
104-
const exports = parseExportBindings(code)
105-
const stripped = stripModuleSyntax(code)
106-
const exportAssignments = exports
107-
.map(e => `__chunks["${chunkId}"].${e.exported} = ${e.local};`)
108-
.join(' ')
109-
parts.push(`(function() { ${stripped} __chunks["${chunkId}"] = {}; ${exportAssignments} })();`)
110-
}
36+
const backgroundPath = resolve(firefoxDir, 'background.js')
11137

112-
const entryCode = readFileSync(entryChunkPath, 'utf8')
113-
const entryImports = parseAllImportBindings(entryCode)
114-
let entryBody = stripModuleSyntax(entryCode)
115-
if (entryImports.length) {
116-
const varDecls = entryImports.flatMap(imp => {
117-
const chunkId = basename(resolve(dirname(entryChunkPath), imp.path), '.js')
118-
return imp.bindings.map(b => `var ${b.local} = __chunks["${chunkId}"].${b.exported};`)
119-
}).join(' ')
120-
entryBody = varDecls + '\n' + entryBody
121-
}
122-
parts.push(entryBody)
38+
await esbuild.build({
39+
entryPoints: [entryPath],
40+
bundle: true,
41+
format: 'iife',
42+
outfile: backgroundPath,
43+
target: 'es2022',
44+
platform: 'browser',
45+
logLevel: 'warning'
46+
})
12347

124-
const backgroundPath = resolve(firefoxDir, 'background.js')
125-
writeFileSync(backgroundPath, parts.join('\n'))
126-
console.log(`[package-firefox] inlined ${chunkOrder.length} chunks into background.js`)
48+
console.log('[package-firefox] bundled background.js as IIFE')
12749

12850
// --- Transform manifest.json ---
12951

@@ -136,7 +58,7 @@ if (manifest.background?.service_worker) {
13658

13759
manifest.browser_specific_settings = {
13860
gecko: {
139-
id: 'stackprism@stackprism.dev',
61+
id: 'stackprism@setube.github.io',
14062
strict_min_version: '128.0'
14163
}
14264
}
@@ -151,5 +73,16 @@ if (!existsSync(releaseDir)) mkdirSync(releaseDir)
15173

15274
const version = manifest.version
15375
const xpiName = `stackprism-v${version}.xpi`
154-
execFileSync('zip', ['-r', resolve(releaseDir, xpiName), '.'], { cwd: firefoxDir, stdio: 'inherit' })
76+
const xpiPath = resolve(releaseDir, xpiName)
77+
78+
await new Promise((resolve, reject) => {
79+
const output = createWriteStream(xpiPath)
80+
const archive = archiver('zip', { zlib: { level: 9 } })
81+
output.on('close', resolve)
82+
archive.on('error', reject)
83+
archive.pipe(output)
84+
archive.glob('**', { cwd: firefoxDir, dot: true })
85+
archive.finalize()
86+
})
87+
15588
console.log(`[package-firefox] created release/${xpiName}`)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"@types/node": "^20.12.7",
2626
"@vitejs/plugin-vue": "^5.0.4",
2727
"@vue/eslint-config-typescript": "^14.7.0",
28+
"archiver": "^7.0.1",
29+
"esbuild": "^0.21.5",
2830
"eslint": "^10.3.0",
2931
"eslint-plugin-vue": "^10.9.1",
3032
"lucide-vue-next": "^1.0.0",

0 commit comments

Comments
 (0)