@@ -2,23 +2,42 @@ import archiver from 'archiver'
2
2
import fs from 'fs-extra'
3
3
import path from 'path'
4
4
import webpack from 'webpack'
5
+ import os from 'os'
5
6
import ProgressBarPlugin from 'progress-bar-webpack-plugin'
6
7
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
7
8
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
8
- import TerserPlugin from 'terser-webpack-plugin '
9
+ import { EsbuildPlugin } from 'esbuild-loader '
9
10
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
10
11
11
12
const outdir = 'build'
12
13
13
14
const __dirname = path . resolve ( )
14
15
const isProduction = process . argv [ 2 ] !== '--development' // --production and --analyze are both production
15
16
const 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
16
35
17
36
async function deleteOldDir ( ) {
18
37
await fs . rm ( outdir , { recursive : true , force : true } )
19
38
}
20
39
21
- async function runWebpack ( isWithoutKatex , isWithoutTiktoken , minimal , callback ) {
40
+ async function runWebpack ( isWithoutKatex , isWithoutTiktoken , minimal , sourceBuildDir , callback ) {
22
41
const shared = [
23
42
'preact' ,
24
43
'webextension-polyfill' ,
@@ -33,6 +52,8 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
33
52
]
34
53
if ( isWithoutKatex ) shared . push ( './src/components' )
35
54
55
+ const sassImpl = await import ( 'sass-embedded' )
56
+
36
57
const compiler = webpack ( {
37
58
entry : {
38
59
'content-script' : {
@@ -54,18 +75,29 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
54
75
} ,
55
76
output : {
56
77
filename : '[name].js' ,
57
- path : path . resolve ( __dirname , outdir ) ,
78
+ path : path . resolve ( __dirname , sourceBuildDir || outdir ) ,
58
79
} ,
59
80
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
+ } ,
61
90
optimization : {
62
91
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 ,
67
100
} ) ,
68
- new CssMinimizerPlugin ( ) ,
69
101
] ,
70
102
concatenateModules : ! isAnalyzing ,
71
103
} ,
@@ -103,6 +135,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
103
135
] ,
104
136
resolve : {
105
137
extensions : [ '.jsx' , '.mjs' , '.js' ] ,
138
+ symlinks : false ,
106
139
alias : {
107
140
parse5 : path . resolve ( __dirname , 'node_modules/parse5' ) ,
108
141
...( minimal
@@ -124,9 +157,23 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
124
157
fullySpecified : false ,
125
158
} ,
126
159
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
+ : [ ] ) ,
127
172
{
128
173
loader : 'babel-loader' ,
129
174
options : {
175
+ cacheDirectory : true ,
176
+ cacheCompression : false ,
130
177
presets : [
131
178
'@babel/preset-env' ,
132
179
{
@@ -149,7 +196,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
149
196
{
150
197
test : / \. s [ a c ] s s $ / ,
151
198
use : [
152
- MiniCssExtractPlugin . loader ,
199
+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
153
200
{
154
201
loader : 'css-loader' ,
155
202
options : {
@@ -158,13 +205,14 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
158
205
} ,
159
206
{
160
207
loader : 'sass-loader' ,
208
+ options : { implementation : sassImpl } ,
161
209
} ,
162
210
] ,
163
211
} ,
164
212
{
165
213
test : / \. l e s s $ / ,
166
214
use : [
167
- MiniCssExtractPlugin . loader ,
215
+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
168
216
{
169
217
loader : 'css-loader' ,
170
218
options : {
@@ -179,7 +227,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
179
227
{
180
228
test : / \. c s s $ / ,
181
229
use : [
182
- MiniCssExtractPlugin . loader ,
230
+ isProduction ? MiniCssExtractPlugin . loader : 'style-loader' ,
183
231
{
184
232
loader : 'css-loader' ,
185
233
} ,
@@ -258,7 +306,12 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback)
258
306
} ,
259
307
} )
260
308
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
+ }
262
315
}
263
316
264
317
async function zipFolder ( dir ) {
@@ -275,28 +328,30 @@ async function copyFiles(entryPoints, targetDir) {
275
328
if ( ! fs . existsSync ( targetDir ) ) await fs . mkdir ( targetDir )
276
329
await Promise . all (
277
330
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
+ }
279
334
} ) ,
280
335
)
281
336
}
282
337
283
- async function finishOutput ( outputDirSuffix ) {
338
+ async function finishOutput ( outputDirSuffix , sourceBuildDir = outdir ) {
284
339
const commonFiles = [
285
340
{ src : 'src/logo.png' , dst : 'logo.png' } ,
286
341
{ src : 'src/rules.json' , dst : 'rules.json' } ,
287
342
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
290
345
291
- { src : 'build /content-script.js' , dst : 'content-script.js' } ,
346
+ { src : ` ${ sourceBuildDir } /content-script.js` , dst : 'content-script.js' } ,
292
347
293
- { src : 'build /background.js' , dst : 'background.js' } ,
348
+ { src : ` ${ sourceBuildDir } /background.js` , dst : 'background.js' } ,
294
349
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' } ,
297
352
{ src : 'src/popup/index.html' , dst : 'popup.html' } ,
298
353
299
- { src : 'build /IndependentPanel.js' , dst : 'IndependentPanel.js' } ,
354
+ { src : ` ${ sourceBuildDir } /IndependentPanel.js` , dst : 'IndependentPanel.js' } ,
300
355
{ src : 'src/pages/IndependentPanel/index.html' , dst : 'IndependentPanel.html' } ,
301
356
]
302
357
@@ -306,6 +361,18 @@ async function finishOutput(outputDirSuffix) {
306
361
[ ...commonFiles , { src : 'src/manifest.json' , dst : 'manifest.json' } ] ,
307
362
chromiumOutputDir ,
308
363
)
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
+ }
309
376
if ( isProduction ) await zipFolder ( chromiumOutputDir )
310
377
311
378
// firefox
@@ -314,43 +381,92 @@ async function finishOutput(outputDirSuffix) {
314
381
[ ...commonFiles , { src : 'src/manifest.v2.json' , dst : 'manifest.json' } ] ,
315
382
firefoxOutputDir ,
316
383
)
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
+ }
325
394
}
326
- // console.log(stats.toString())
327
-
328
- await finishOutputFunc ( )
329
395
}
396
+ if ( isProduction ) await zipFolder ( firefoxOutputDir )
330
397
}
331
398
332
399
async function build ( ) {
333
400
await deleteOldDir ( )
334
401
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
348
458
}
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
+ } ) ,
354
470
)
355
471
}
356
472
0 commit comments