@@ -2,23 +2,42 @@ import archiver from 'archiver'
22import fs from 'fs-extra'
33import path from 'path'
44import webpack from 'webpack'
5+ import os from 'os'
56import ProgressBarPlugin from 'progress-bar-webpack-plugin'
67import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
78import MiniCssExtractPlugin from 'mini-css-extract-plugin'
8- import TerserPlugin from 'terser-webpack-plugin '
9+ import { EsbuildPlugin } from 'esbuild-loader '
910import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
1011
1112const outdir = 'build'
1213
1314const __dirname = path . resolve ( )
1415const isProduction = process . argv [ 2 ] !== '--development' // --production and --analyze are both production
1516const isAnalyzing = process . argv [ 2 ] === '--analyze'
17+ const parallelBuild = process . env . BUILD_PARALLEL === '0' ? false : true
18+ const isWatchOnce = ! ! process . env . BUILD_WATCH_ONCE
19+ // Cache compression control: default none; allow override via env
20+ const cacheCompressionEnv = process . env . BUILD_CACHE_COMPRESSION
21+ let cacheCompressionOption
22+ if ( cacheCompressionEnv ) {
23+ const v = String ( cacheCompressionEnv ) . toLowerCase ( )
24+ if ( v === '0' || v === 'false' || v === 'none' ) cacheCompressionOption = false
25+ else if ( v === 'brotli' ) cacheCompressionOption = 'brotli'
26+ else cacheCompressionOption = 'gzip' // treat any truthy/unknown as gzip
27+ }
28+ const cpuCount = os . cpus ( ) ?. length || 1
29+ const envWorkers = process . env . BUILD_THREAD_WORKERS
30+ ? parseInt ( process . env . BUILD_THREAD_WORKERS , 10 )
31+ : undefined
32+ const threadWorkers = Number . isInteger ( envWorkers ) && envWorkers > 0 ? envWorkers : cpuCount
33+ // Enable threads by default; allow disabling via BUILD_THREAD=0
34+ const enableThread = process . env . BUILD_THREAD === '0' ? false : true
1635
1736async function deleteOldDir ( ) {
1837 await fs . rm ( outdir , { recursive : true , force : true } )
1938}
2039
21- async function runWebpack ( isWithoutKatex , isWithoutTiktoken , minimal , callback ) {
40+ async function runWebpack ( isWithoutKatex , isWithoutTiktoken , minimal , sourceBuildDir , callback ) {
2241 const shared = [
2342 'preact' ,
2443 'webextension-polyfill' ,
@@ -33,6 +52,8 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
3352 ]
3453 if ( isWithoutKatex ) shared . push ( './src/components' )
3554
55+ const sassImpl = await import ( 'sass-embedded' )
56+
3657 const compiler = webpack ( {
3758 entry : {
3859 'content-script' : {
@@ -54,18 +75,29 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
5475 } ,
5576 output : {
5677 filename : '[name].js' ,
57- path : path . resolve ( __dirname , outdir ) ,
78+ path : path . resolve ( __dirname , sourceBuildDir || outdir ) ,
5879 } ,
5980 mode : isProduction ? 'production' : 'development' ,
60- devtool : isProduction ? false : 'inline-source-map' ,
81+ devtool : isProduction ? false : 'cheap-module-source-map' ,
82+ cache : {
83+ type : 'filesystem' ,
84+ // default none; override via BUILD_CACHE_COMPRESSION=gzip|brotli
85+ compression : cacheCompressionOption ?? false ,
86+ buildDependencies : {
87+ config : [ path . resolve ( 'build.mjs' ) ] ,
88+ } ,
89+ } ,
6190 optimization : {
6291 minimizer : [
63- new TerserPlugin ( {
64- terserOptions : {
65- output : { ascii_only : true } ,
66- } ,
92+ // Use esbuild for JS minification (faster than Terser)
93+ new EsbuildPlugin ( {
94+ target : 'es2017' ,
95+ legalComments : 'none' ,
96+ } ) ,
97+ // Use esbuild-based CSS minify via css-minimizer plugin
98+ new CssMinimizerPlugin ( {
99+ minify : CssMinimizerPlugin . esbuildMinify ,
67100 } ) ,
68- new CssMinimizerPlugin ( ) ,
69101 ] ,
70102 concatenateModules : ! isAnalyzing ,
71103 } ,
@@ -103,6 +135,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
103135 ] ,
104136 resolve : {
105137 extensions : [ '.jsx' , '.mjs' , '.js' ] ,
138+ symlinks : false ,
106139 alias : {
107140 parse5 : path . resolve ( __dirname , 'node_modules/parse5' ) ,
108141 ...( minimal
@@ -124,9 +157,23 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
124157 fullySpecified : false ,
125158 } ,
126159 use : [
160+ ...( enableThread
161+ ? [
162+ {
163+ loader : 'thread-loader' ,
164+ options : {
165+ workers : threadWorkers ,
166+ // Ensure one-off dev build exits quickly
167+ poolTimeout : isProduction ? 2000 : isWatchOnce ? 0 : Infinity ,
168+ } ,
169+ } ,
170+ ]
171+ : [ ] ) ,
127172 {
128173 loader : 'babel-loader' ,
129174 options : {
175+ cacheDirectory : true ,
176+ cacheCompression : false ,
130177 presets : [
131178 '@babel/preset-env' ,
132179 {
@@ -149,7 +196,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
149196 {
150197 test : / \. s [ a c ] s s $ / ,
151198 use : [
152- MiniCssExtractPlugin . loader ,
199+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
153200 {
154201 loader : 'css-loader' ,
155202 options : {
@@ -158,13 +205,14 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
158205 } ,
159206 {
160207 loader : 'sass-loader' ,
208+ options : { implementation : sassImpl } ,
161209 } ,
162210 ] ,
163211 } ,
164212 {
165213 test : / \. l e s s $ / ,
166214 use : [
167- MiniCssExtractPlugin . loader ,
215+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
168216 {
169217 loader : 'css-loader' ,
170218 options : {
@@ -179,7 +227,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
179227 {
180228 test : / \. c s s $ / ,
181229 use : [
182- MiniCssExtractPlugin . loader ,
230+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
183231 {
184232 loader : 'css-loader' ,
185233 } ,
@@ -258,7 +306,12 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
258306 } ,
259307 } )
260308 if ( isProduction ) compiler . run ( callback )
261- else compiler . watch ( { } , callback )
309+ else {
310+ const watching = compiler . watch ( { } , ( err , stats ) => {
311+ callback ( err , stats )
312+ if ( process . env . BUILD_WATCH_ONCE ) watching . close ( ( ) => { } )
313+ } )
314+ }
262315}
263316
264317async function zipFolder ( dir ) {
@@ -275,28 +328,30 @@ async function copyFiles(entryPoints, targetDir) {
275328 if ( ! fs . existsSync ( targetDir ) ) await fs . mkdir ( targetDir )
276329 await Promise . all (
277330 entryPoints . map ( async ( entryPoint ) => {
278- await fs . copy ( entryPoint . src , `${ targetDir } /${ entryPoint . dst } ` )
331+ if ( await fs . pathExists ( entryPoint . src ) ) {
332+ await fs . copy ( entryPoint . src , `${ targetDir } /${ entryPoint . dst } ` )
333+ }
279334 } ) ,
280335 )
281336}
282337
283- async function finishOutput ( outputDirSuffix ) {
338+ async function finishOutput ( outputDirSuffix , sourceBuildDir = outdir ) {
284339 const commonFiles = [
285340 { src : 'src/logo.png' , dst : 'logo.png' } ,
286341 { src : 'src/rules.json' , dst : 'rules.json' } ,
287342
288- { src : 'build /shared.js' , dst : 'shared.js' } ,
289- { src : 'build /content-script.css' , dst : 'content-script.css' } , // shared
343+ { src : ` ${ sourceBuildDir } /shared.js` , dst : 'shared.js' } ,
344+ { src : ` ${ sourceBuildDir } /content-script.css` , dst : 'content-script.css' } , // shared
290345
291- { src : 'build /content-script.js' , dst : 'content-script.js' } ,
346+ { src : ` ${ sourceBuildDir } /content-script.js` , dst : 'content-script.js' } ,
292347
293- { src : 'build /background.js' , dst : 'background.js' } ,
348+ { src : ` ${ sourceBuildDir } /background.js` , dst : 'background.js' } ,
294349
295- { src : 'build /popup.js' , dst : 'popup.js' } ,
296- { src : 'build /popup.css' , dst : 'popup.css' } ,
350+ { src : ` ${ sourceBuildDir } /popup.js` , dst : 'popup.js' } ,
351+ { src : ` ${ sourceBuildDir } /popup.css` , dst : 'popup.css' } ,
297352 { src : 'src/popup/index.html' , dst : 'popup.html' } ,
298353
299- { src : 'build /IndependentPanel.js' , dst : 'IndependentPanel.js' } ,
354+ { src : ` ${ sourceBuildDir } /IndependentPanel.js` , dst : 'IndependentPanel.js' } ,
300355 { src : 'src/pages/IndependentPanel/index.html' , dst : 'IndependentPanel.html' } ,
301356 ]
302357
@@ -306,6 +361,18 @@ async function finishOutput(outputDirSuffix) {
306361 [ ...commonFiles , { src : 'src/manifest.json' , dst : 'manifest.json' } ] ,
307362 chromiumOutputDir ,
308363 )
364+ // In development, ensure placeholder CSS files exist to avoid 404 noise
365+ if ( ! isProduction ) {
366+ const chromiumCssPlaceholders = [
367+ path . join ( chromiumOutputDir , 'popup.css' ) ,
368+ path . join ( chromiumOutputDir , 'content-script.css' ) ,
369+ ]
370+ for ( const p of chromiumCssPlaceholders ) {
371+ if ( ! ( await fs . pathExists ( p ) ) ) {
372+ await fs . outputFile ( p , '/* dev placeholder */\n' )
373+ }
374+ }
375+ }
309376 if ( isProduction ) await zipFolder ( chromiumOutputDir )
310377
311378 // firefox
@@ -314,43 +381,92 @@ async function finishOutput(outputDirSuffix) {
314381 [ ...commonFiles , { src : 'src/manifest.v2.json' , dst : 'manifest.json' } ] ,
315382 firefoxOutputDir ,
316383 )
317- if ( isProduction ) await zipFolder ( firefoxOutputDir )
318- }
319-
320- function generateWebpackCallback ( finishOutputFunc ) {
321- return async function webpackCallback ( err , stats ) {
322- if ( err || stats . hasErrors ( ) ) {
323- console . error ( err || stats . toString ( ) )
324- return
384+ // In development, ensure placeholder CSS files exist to avoid 404 noise
385+ if ( ! isProduction ) {
386+ const firefoxCssPlaceholders = [
387+ path . join ( firefoxOutputDir , 'popup.css' ) ,
388+ path . join ( firefoxOutputDir , 'content-script.css' ) ,
389+ ]
390+ for ( const p of firefoxCssPlaceholders ) {
391+ if ( ! ( await fs . pathExists ( p ) ) ) {
392+ await fs . outputFile ( p , '/* dev placeholder */\n' )
393+ }
325394 }
326- // console.log(stats.toString())
327-
328- await finishOutputFunc ( )
329395 }
396+ if ( isProduction ) await zipFolder ( firefoxOutputDir )
330397}
331398
332399async function build ( ) {
333400 await deleteOldDir ( )
334401 if ( isProduction && ! isAnalyzing ) {
335- // await runWebpack(
336- // true,
337- // false,
338- // generateWebpackCallback(() => finishOutput('-without-katex')),
339- // )
340- // await new Promise((r) => setTimeout(r, 5000))
341- await runWebpack (
342- true ,
343- true ,
344- true ,
345- generateWebpackCallback ( ( ) => finishOutput ( '-without-katex-and-tiktoken' ) ) ,
346- )
347- await new Promise ( ( r ) => setTimeout ( r , 10000 ) )
402+ const tmpFull = `${ outdir } /.tmp-full`
403+ const tmpMin = `${ outdir } /.tmp-min`
404+ if ( parallelBuild ) {
405+ await Promise . all ( [
406+ new Promise ( ( resolve , reject ) =>
407+ runWebpack ( true , true , true , tmpMin , async ( err , stats ) => {
408+ if ( err || stats . hasErrors ( ) ) {
409+ console . error ( err || stats . toString ( ) )
410+ reject ( err || new Error ( 'webpack error' ) )
411+ return
412+ }
413+ await finishOutput ( '-without-katex-and-tiktoken' , tmpMin )
414+ resolve ( )
415+ } ) ,
416+ ) ,
417+ new Promise ( ( resolve , reject ) =>
418+ runWebpack ( false , false , false , tmpFull , async ( err , stats ) => {
419+ if ( err || stats . hasErrors ( ) ) {
420+ console . error ( err || stats . toString ( ) )
421+ reject ( err || new Error ( 'webpack error' ) )
422+ return
423+ }
424+ await finishOutput ( '' , tmpFull )
425+ resolve ( )
426+ } ) ,
427+ ) ,
428+ ] )
429+ await fs . rm ( tmpFull , { recursive : true , force : true } )
430+ await fs . rm ( tmpMin , { recursive : true , force : true } )
431+ } else {
432+ await new Promise ( ( resolve , reject ) =>
433+ runWebpack ( true , true , true , tmpMin , async ( err , stats ) => {
434+ if ( err || stats . hasErrors ( ) ) {
435+ console . error ( err || stats . toString ( ) )
436+ reject ( err || new Error ( 'webpack error' ) )
437+ return
438+ }
439+ await finishOutput ( '-without-katex-and-tiktoken' , tmpMin )
440+ resolve ( )
441+ } ) ,
442+ )
443+ await new Promise ( ( resolve , reject ) =>
444+ runWebpack ( false , false , false , tmpFull , async ( err , stats ) => {
445+ if ( err || stats . hasErrors ( ) ) {
446+ console . error ( err || stats . toString ( ) )
447+ reject ( err || new Error ( 'webpack error' ) )
448+ return
449+ }
450+ await finishOutput ( '' , tmpFull )
451+ resolve ( )
452+ } ) ,
453+ )
454+ await fs . rm ( tmpFull , { recursive : true , force : true } )
455+ await fs . rm ( tmpMin , { recursive : true , force : true } )
456+ }
457+ return
348458 }
349- await runWebpack (
350- false ,
351- false ,
352- false ,
353- generateWebpackCallback ( ( ) => finishOutput ( '' ) ) ,
459+
460+ await new Promise ( ( resolve , reject ) =>
461+ runWebpack ( false , false , false , outdir , async ( err , stats ) => {
462+ if ( err || stats . hasErrors ( ) ) {
463+ console . error ( err || stats . toString ( ) )
464+ reject ( err || new Error ( 'webpack error' ) )
465+ return
466+ }
467+ await finishOutput ( '' )
468+ resolve ( )
469+ } ) ,
354470 )
355471}
356472
0 commit comments