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'
43import { 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
610const root = resolve ( dirname ( fileURLToPath ( import . meta. url ) ) , '..' )
711const distDir = resolve ( root , 'dist' )
@@ -15,74 +19,11 @@ if (!existsSync(distDir)) {
1519rmSync ( firefoxDir , { recursive : true , force : true } )
1620cpSync ( 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 = / i m p o r t \{ ( [ ^ } ] * ) \} f r o m " ( [ ^ " ] * ) " / 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 + a s \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 && / i m p o r t \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 ( / e x p o r t \{ ( [ ^ } ] * ) \} ; ? \s * $ / )
44- if ( ! match ) {
45- if ( / e x p o r t \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 + a s \s + / )
52- return { local : parts [ 0 ] . trim ( ) , exported : ( parts [ 1 ] || parts [ 0 ] ) . trim ( ) }
53- } )
54- }
55-
56- const stripModuleSyntax = ( code ) =>
57- code . replace ( / i m p o r t \{ [ ^ } ] * \} f r o m " [ ^ " ] * " ; ? / g, '' ) . replace ( / e x p o r t \{ [ ^ } ] * \} ; ? / g, '' )
58-
59- const resolveImports = ( filePath ) => {
60- const code = readFileSync ( filePath , 'utf8' )
61- const dir = dirname ( filePath )
62- const importRe = / i m p o r t \{ [ ^ } ] * \} f r o m " ( \. \/ [ ^ " ] + ) " / 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
8627const loaderPath = resolve ( firefoxDir , 'service-worker-loader.js' )
8728const loaderCode = readFileSync ( loaderPath , 'utf8' )
8829const entryMatch = loaderCode . match ( / i m p o r t \s + ' \. \/ ( a s s e t s \/ [ ^ ' ] + ) ' / )
@@ -92,38 +33,19 @@ if (!entryMatch) {
9233}
9334
9435const 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
13759manifest . 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
15274const version = manifest . version
15375const 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+
15588console . log ( `[package-firefox] created release/${ xpiName } ` )
0 commit comments