forked from DimensionDev/Maskbook
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathwebpack.config.ts
402 lines (384 loc) · 16.4 KB
/
webpack.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
import path from 'path'
import fs, { promises } from 'fs'
import { Configuration, HotModuleReplacementPlugin, EnvironmentPlugin, ProvidePlugin, RuleSetRule } from 'webpack'
// Merge declaration of Configuration defined in webpack
import type { Configuration as DevServerConfiguration } from 'webpack-dev-server'
//#region Development plugins
import WebExtensionHotLoadPlugin from '@dimensiondev/webpack-web-ext-plugin'
import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin'
import ReactRefreshTypeScriptTransformer from 'react-refresh-typescript'
import WatchMissingModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin'
import NotifierPlugin from 'webpack-notifier'
import ForkTSCheckerPlugin from 'fork-ts-checker-webpack-plugin'
import ForkTSCheckerNotifier from 'fork-ts-checker-notifier-webpack-plugin'
//#endregion
//#region Production plugins
import { SSRPlugin } from './scripts/SSRPlugin'
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
//#endregion
//#region Other plugins
import CopyPlugin from 'copy-webpack-plugin'
import HTMLPlugin from 'html-webpack-plugin'
import WebExtensionTarget from 'webpack-target-webextension'
import ManifestPlugin from 'webpack-extension-manifest-plugin'
import Webpack5AssetModuleTransformer from './scripts/transformers/webpack-5-asset-module-backport'
//#endregion
import git from '@nice-labs/git-rev'
import * as modifiers from './scripts/manifest-modifiers'
const src = (file: string) => path.join(__dirname, file)
const publicDir = src('./public')
export default function (cli_env: Record<string, boolean> = {}, argv: any) {
const target = getBuildPresets(cli_env)
const env: 'production' | 'development' = argv.mode ?? 'production'
const dist = env === 'production' ? src('./build') : src('./dist')
const enableHMR = env === 'development' && !Boolean(process.env.NO_HMR)
/**
* On iOS, eval is async.
*/
const sourceMapKind: Configuration['devtool'] = target.Safari ? false : 'eval-source-map'
const config: Configuration = {
name: 'main',
mode: env,
devtool: env === 'development' ? sourceMapKind : false,
entry: {}, // ? Defined later
resolve: {
extensions: ['.js', '.ts', '.tsx'],
//#region requirements of https://github.com/crimx/webpack-target-webextension
mainFields: ['browser', 'module', 'main'],
aliasFields: ['browser'],
//#endregion
alias: { 'async-call-rpc$': 'async-call-rpc/full' },
// If anyone need profiling React please checkout: https://github.com/facebook/create-react-app/blob/865ea05bc93fd2ac56b7e561181c7dc2cead3e78/packages/react-scripts/config/webpack.config.js#L304
},
module: {
// So it will not be conflict with the ProviderPlugin below.
noParse: /webextension-polyfill/,
// Treat as missing export as error
strictExportPresence: true,
rules: [
// Opt in source map
{ test: /(async-call|webextension).+\.js$/, enforce: 'pre', use: ['source-map-loader'] },
{ parser: { requireEnsure: false, amd: false, system: false } },
getTypeScriptLoader(),
],
//#region Dismiss warning in gun
wrappedContextCritical: false,
exprContextCritical: false,
unknownContextCritical: false,
//#endregion
},
// ModuleNotFoundPlugin & ModuleScopePlugin not included please leave a comment if someone need it.
plugins: [
new EnvironmentPlugin({ NODE_ENV: env, ...getGitInfo(), ...getCompilationInfo() }),
new WatchMissingModulesPlugin(path.resolve('node_modules')),
// copy assets
new CopyPlugin({
patterns: [{ from: publicDir, to: dist, globOptions: { ignore: ['index.html'] } }],
}),
getManifestPlugin(),
...getBuildNotificationPlugins(),
...getWebExtensionReloadPlugin(),
...getSSRPlugin(),
...getHotModuleReloadPlugin(),
].filter(Boolean),
optimization: {
minimize: false,
splitChunks: {
// Chrome bug https://bugs.chromium.org/p/chromium/issues/detail?id=1108199
automaticNameDelimiter: '-',
maxInitialRequests: Infinity,
chunks: 'all',
cacheGroups: {
// per-npm-package splitting
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
return `npm.${packageName.replace('@', '')}`
},
},
},
},
},
output: {
futureEmitAssets: true,
path: dist,
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
globalObject: 'globalThis',
},
target: WebExtensionTarget(nodeConfig), // See https://github.com/crimx/webpack-target-webextension,
// @ts-ignore sometimes ts don't merge declaration for unknown reason and report
// Object literal may only specify known properties, and 'devServer' does not exist in type 'Configuration'.ts(2322)
devServer: {
// Have to write disk cause plugin cannot be loaded over network
writeToDisk: true,
compress: false,
hot: enableHMR,
hotOnly: enableHMR,
// WDS does not support chrome-extension:// browser-extension://
disableHostCheck: true,
// Workaround of https://github.com/webpack/webpack-cli/issues/1955
injectClient: (config) => enableHMR && config.name !== 'injected-script',
injectHot: (config) => enableHMR && config.name !== 'injected-script',
headers: {
// We're doing CORS request for HMR
'Access-Control-Allow-Origin': '*',
},
// If the content script runs in https, webpack will connect https://localhost:HMR_PORT
https: true,
} as DevServerConfiguration,
}
//#region Define entries
if (!(target.Firefox || target.Safari)) {
// Define "browser" globally in Chrome
config.plugins!.push(new ProvidePlugin({ browser: 'webextension-polyfill' }))
}
config.entry = {
'options-page': withReactDevTools(src('./packages/maskbook/src/extension/options-page/index.tsx')),
'content-script': withReactDevTools(src('./packages/maskbook/src/content-script.ts')),
'background-service': src('./packages/maskbook/src/background-service.ts'),
popup: withReactDevTools(src('./packages/maskbook/src/extension/popup-page/index.tsx')),
debug: src('./packages/maskbook/src/extension/debug-page'),
}
for (const entry in config.entry) {
config.entry[entry] = iOSWebExtensionShimHack(...toArray(config.entry[entry]))
}
config.plugins!.push(
// @ts-ignore
getHTMLPlugin({ chunks: ['options-page'], filename: 'index.html' }),
getHTMLPlugin({ chunks: ['background-service'], filename: 'background.html' }),
getHTMLPlugin({ chunks: ['popup'], filename: 'popup.html' }),
getHTMLPlugin({ chunks: ['content-script'], filename: 'generated__content__script.html' }),
getHTMLPlugin({ chunks: ['debug'], filename: 'debug.html' }),
) // generate pages for each entry
//#endregion
if (argv.profile) config.plugins!.push(new BundleAnalyzerPlugin())
return [
config,
{
name: 'injected-script',
entry: { 'injected-script': src('./packages/maskbook/src/extension/injected-script/index.ts') },
devtool: false,
output: config.output,
module: { rules: [getTypeScriptLoader(false)] },
resolve: config.resolve,
// We're not using this server, only need it write to disk.
devServer: {
writeToDisk: true,
hot: false,
injectClient: false,
injectHot: false,
port: 35938,
overlay: false,
},
optimization: { splitChunks: false, minimize: false },
plugins: [
new EnvironmentPlugin({ NODE_ENV: env }),
env === 'production' && new CleanWebpackPlugin({}),
].filter(Boolean),
} as Configuration,
]
/** If you are using Firefox and want to use React devtools, use Firefox nightly or start without the flag --firefox, then open about:config and switch network.websocket.allowInsecureFromHTTPS to true */
function withReactDevTools(...src: string[]) {
if (target.Firefox && target.Firefox !== 'nightly') return src
if (env === 'development') return ['react-devtools', ...src]
return src
}
function iOSWebExtensionShimHack(...path: string[]) {
if (!(target.Safari || target.StandaloneGeckoView)) return path
return [...path, src('./packages/maskbook/src/polyfill/permissions.js')]
}
function getTypeScriptLoader(hmr = enableHMR): RuleSetRule {
return {
test: /\.(ts|tsx)$/,
include: src('./packages/maskbook/src'),
loader: require.resolve('ts-loader'),
options: {
transpileOnly: true,
compilerOptions: {
noEmit: false,
importsNotUsedAsValues: 'remove',
jsx: env === 'production' ? 'react-jsx' : 'react-jsxdev',
},
getCustomTransformers: () => ({
before: [Webpack5AssetModuleTransformer(), hmr && ReactRefreshTypeScriptTransformer()].filter(
Boolean,
),
}),
},
}
}
function getBuildNotificationPlugins() {
if (env === 'production') return []
const opt = { title: 'Maskbook', excludeWarnings: true, skipFirstNotification: true, skipSuccessful: true }
return [new NotifierPlugin(opt), new ForkTSCheckerPlugin(), new ForkTSCheckerNotifier(opt)]
}
function getWebExtensionReloadPlugin() {
const dist = env === 'production' ? src('./build') : src('./dist')
if (env === 'production') return []
let args: ConstructorParameters<typeof WebExtensionHotLoadPlugin>[0] | undefined = undefined
if (target.FirefoxDesktop && enableHMR) return [] // stuck on 99% [0] after emitting cause HMR not working
if (target.FirefoxDesktop)
args = {
sourceDir: dist,
target: 'firefox-desktop',
firefoxProfile: src('.firefox'),
keepProfileChanges: true,
// --firefox=nightly
firefox: typeof target.FirefoxDesktop === 'string' ? target.FirefoxDesktop : undefined,
}
else if (target.Chromium)
args = {
sourceDir: dist,
target: 'chromium',
chromiumProfile: src('.chrome'),
keepProfileChanges: true,
}
else if (target.FirefoxForAndroid)
args = {
sourceDir: dist,
target: 'firefox-android',
}
if (args) return [new WebExtensionHotLoadPlugin(args)]
return []
}
function getManifestPlugin() {
const manifest = require('./packages/maskbook/src/manifest.json')
if (target.Chromium) modifiers.chromium(manifest)
else if (target.FirefoxDesktop) modifiers.firefox(manifest)
else if (target.FirefoxForAndroid) modifiers.firefox(manifest)
else if (target.StandaloneGeckoView) modifiers.geckoview(manifest)
else if (target.Safari) modifiers.safari(manifest)
else if (target.E2E) modifiers.E2E(manifest)
if (env === 'development') modifiers.development(manifest)
else modifiers.production(manifest)
return new ManifestPlugin({ config: { base: manifest } })
}
function getHotModuleReloadPlugin() {
if (!enableHMR) return []
// overlay is not working in our environment
return [new HotModuleReplacementPlugin(), new ReactRefreshWebpackPlugin({ overlay: false })]
}
/** Get environment targets */
function getCompilationInfo() {
let buildTarget: 'chromium' | 'firefox' | 'safari' | 'E2E' = 'chromium'
let firefoxVariant: 'fennec' | 'geckoview' | undefined = undefined
let architecture: 'web' | 'app' = 'web'
let resolution: 'desktop' | 'mobile' = 'desktop'
let buildType: 'stable' | 'beta' | 'insider' = 'stable'
if (target.Chromium) buildTarget = 'chromium'
if (target.Firefox) buildTarget = 'firefox'
// Firefox browser on mobile which can use extension
if (target.FirefoxForAndroid) firefoxVariant = 'fennec'
// Android
if (target.StandaloneGeckoView) {
firefoxVariant = 'geckoview'
architecture = 'app'
}
if (target.Safari) {
buildTarget = 'safari'
architecture = 'app'
}
if (architecture === 'app' || firefoxVariant === 'fennec' || firefoxVariant === 'geckoview')
resolution = 'mobile'
if (target.E2E) buildTarget = 'E2E'
if (target.Beta) buildType = 'beta'
if (target.Insider) buildType = 'insider'
// build the envs
const allEnv = {
STORYBOOK: false,
target: buildTarget,
build: buildType,
architecture,
resolution,
}
if (firefoxVariant) allEnv[firefoxVariant] = firefoxVariant
return allEnv
}
function getSSRPlugin() {
if (env === 'development') return []
return [
// TODO: Help wanted
// new SSRPlugin('popup.html', src('./packages/maskbook/src/extension/popup-page/index.tsx'), 'Mask Network'),
]
}
}
/** All targets available: --firefox --firefox-android --firefox-gecko --chromium --wk-webview --e2e */
function getBuildPresets(argv: any) {
return {
Firefox: (argv.firefox || argv['firefox-android'] || argv['firefox-gecko']) as 'nightly' | boolean,
FirefoxDesktop: argv.firefox as string | boolean,
FirefoxForAndroid: !!argv['firefox-android'],
StandaloneGeckoView: !!argv['firefox-gecko'],
Chromium: !!argv.chromium,
Safari: !!argv['wk-webview'],
E2E: !!argv.e2e,
Beta: !!argv.beta,
Insider: !!argv.insider,
ReproducibleBuild: !!argv['reproducible-build'],
}
}
export type Target = ReturnType<typeof getBuildPresets>
/** Get git info */
function getGitInfo() {
if (git.isRepository())
return {
BUILD_DATE: new Date().toISOString(),
VERSION: git.describe('--dirty'),
TAG_NAME: git.tag(),
COMMIT_HASH: git.commitHash(true),
COMMIT_DATE: git.commitDate().toISOString(),
REMOTE_URL: git.remoteURL(),
BRANCH_NAME: git.branchName(),
DIRTY: git.isDirty(),
TAG_DIRTY: git.isTagDirty(),
}
return {
BUILD_DATE: new Date(0).toISOString(),
VERSION: require('./package.json').version + '-reproducible',
TAG_NAME: 'N/A',
COMMIT_HASH: 'N/A',
COMMIT_DATE: 'N/A',
REMOTE_URL: 'N/A',
BRANCH_NAME: 'N/A',
DIRTY: false,
TAG_DIRTY: false,
}
}
function toArray(x: string | string[]) {
return typeof x === 'string' ? [x] : x
}
function getHTMLPlugin(options: HTMLPlugin.Options = {}) {
const templateContent = fs.readFileSync(src('./scripts/template.html'), 'utf8')
return new HTMLPlugin({
templateContent,
inject: 'body',
...options,
})
}
const nodeConfig: Configuration['node'] = {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
Buffer: true,
process: 'mock',
global: true,
setImmediate: true,
}
// Cleanup old HMR files
promises.readdir(path.join(__dirname, './dist')).then(
async (files) => {
for (const file of files) {
if (!file.includes('hot')) continue
await promises.unlink(path.join(__dirname, './dist/', file)).catch(() => {})
}
},
() => {},
)