Merge branch 'INDA-330-pii-update' into 'main'
[ProtonMail-WebClient.git] / applications / pass-extension / webpack.config.js
blobb2049e69bff3cd515f603f23580face76efb0cb3
1 const path = require('path');
2 const webpack = require('webpack');
3 const ESLintPlugin = require('eslint-webpack-plugin');
4 const CopyPlugin = require('copy-webpack-plugin');
5 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
6 const CircularDependencyPlugin = require('circular-dependency-plugin');
8 const getCssLoaders = require('@proton/pack/webpack/css.loader');
9 const getAssetsLoaders = require('@proton/pack/webpack/assets.loader');
10 const getOptimizations = require('@proton/pack/webpack/optimization');
11 const ProtonIconsTreeShakePlugin = require('@proton/pass/utils/webpack/icons/plugin');
12 const { excludeNodeModulesExcept, excludeFiles, createRegex } = require('@proton/pack/webpack/helpers/regex');
13 const { BABEL_EXCLUDE_FILES, BABEL_INCLUDE_NODE_MODULES } = require('@proton/pack/webpack/constants');
14 const fs = require('fs');
16 const {
17     BUILD_TARGET,
18     CLEAN_MANIFEST,
19     ENV,
20     HOT_MANIFEST_UPDATE,
21     MANIFEST_KEY,
22     REDUX_DEVTOOLS_PORT,
23     RELEASE,
24     RESUME_FALLBACK,
25     RUNTIME_RELOAD_PORT,
26     RUNTIME_RELOAD,
27     WEBPACK_DEV_PORT,
28     WEBPACK_CIRCULAR_DEPS,
29 } = require('./tools/env');
31 const SUPPORTED_TARGETS = ['chrome', 'firefox', 'safari'];
33 if (!SUPPORTED_TARGETS.includes(BUILD_TARGET)) {
34     throw new Error(`Build target "${BUILD_TARGET}" is not supported`);
37 const CONFIG = fs.readFileSync('./src/app/config.ts', 'utf-8').replaceAll(/(export const |;)/gm, '');
38 const MANIFEST_KEYS = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'manifest-keys.json'), 'utf8'));
39 const PUBLIC_KEY = BUILD_TARGET === 'chrome' ? MANIFEST_KEYS?.[MANIFEST_KEY] : null;
40 const ARGON2_CHUNK_NAME = 'node_modules_pmcrypto_node_modules_openpgp_dist_lightweight_argon2id_min_mjs';
42 console.log(`ENV = ${ENV}`);
43 console.log(`RELEASE = ${RELEASE}`);
44 console.log(`BUILD_TARGET = ${BUILD_TARGET}`);
45 console.log(`MANIFEST_KEY = ${MANIFEST_KEY || 'none'}`);
46 console.log(`PUBLIC_KEY = ${PUBLIC_KEY || 'none'}`);
47 console.log(`CLEAN_MANIFEST = ${CLEAN_MANIFEST}`);
49 if (ENV !== 'production') {
50     console.log(`HOT_MANIFEST_UPDATE = ${HOT_MANIFEST_UPDATE}`);
51     console.log(`REDUX_DEVTOOLS_PORT = ${REDUX_DEVTOOLS_PORT}`);
52     console.log(`RESUME_FALLBACK = ${RESUME_FALLBACK}`);
53     console.log(`RUNTIME_RELOAD = ${RUNTIME_RELOAD}`);
54     console.log(`RUNTIME_RELOAD_PORT = ${RUNTIME_RELOAD_PORT}`);
55     console.log(`WEBPACK_DEV_PORT = ${WEBPACK_DEV_PORT}`);
58 console.log(CONFIG);
60 const production = ENV === 'production';
61 const importScripts = BUILD_TARGET === 'chrome' || BUILD_TARGET === 'safari';
62 const manifest = `manifest-${BUILD_TARGET}.json`;
63 const manifestPath = path.resolve(__dirname, manifest);
65 const nonAccessibleWebResource = (entry) => [entry, './src/lib/utils/web-accessible-resource.ts'];
66 const disableBrowserTrap = (entry) => [entry, './src/lib/utils/disable-browser-trap.ts'];
67 const getManifestVersion = () => JSON.stringify(JSON.parse(fs.readFileSync(manifestPath, 'utf8')).version);
69 module.exports = {
70     ...(production
71         ? { mode: 'production', devtool: 'source-map' }
72         : { mode: 'development', devtool: 'inline-source-map' }),
73     entry: {
74         background: {
75             import: './src/app/worker/index.ts',
76             filename: 'background.js',
77             /* MV3 implementations of non-persistent background scripts change
78              * depending on the platform. On chromium based browsers, we'll be
79              * in a ServiceWorker environment whereas on FF it'll actually be a
80              * non-persistent event page. One of the quirks of chunk loading in
81              * a service-worker is that you have to register any imported script
82              * during the service worker's oninstall phase (`/worker/index.ts`)
83              * https://bugs.chromium.org/p/chromium/issues/detail?id=1198822#c10  */
84             chunkLoading: importScripts ? 'import-scripts' : 'jsonp',
85         },
86         client: './src/app/content/client.ts',
87         dropdown: nonAccessibleWebResource('./src/app/content/injections/apps/dropdown/index.tsx'),
88         elements: './src/app/content/elements.ts',
89         notification: nonAccessibleWebResource('./src/app/content/injections/apps/notification/index.tsx'),
90         onboarding: './src/app/pages/onboarding/index.tsx',
91         orchestrator: disableBrowserTrap('./src/app/content/orchestrator.ts'),
92         popup: './src/app/popup/index.tsx',
93         settings: './src/app/pages/settings/index.tsx',
94         /* Passkey handling not available in Safari */
95         ...(BUILD_TARGET !== 'safari' ? { webauthn: './src/app/content/webauthn.ts' } : {}),
96         /* FF account communication fallback */
97         ...(BUILD_TARGET === 'firefox' ? { account: disableBrowserTrap('./src/app/content/firefox/index.ts') } : {}),
98         /* Safari fork fallback */
99         ...(BUILD_TARGET === 'safari' ? { fork: disableBrowserTrap('./src/app/content/safari/index.ts') } : {}),
100     },
101     module: {
102         strictExportPresence: true,
103         rules: [
104             {
105                 test: /\.js$|\.tsx?$/,
106                 exclude: createRegex(
107                     excludeNodeModulesExcept(BABEL_INCLUDE_NODE_MODULES),
108                     excludeFiles([...BABEL_EXCLUDE_FILES, 'pre.ts', 'unsupported.ts'])
109                 ),
110                 use: require.resolve('babel-loader'),
111             },
112             ...getCssLoaders({ browserslist: undefined, logical: false }),
113             ...getAssetsLoaders({ inlineIcons: true }),
114         ],
115     },
116     optimization: {
117         ...getOptimizations({ isProduction: production }),
118         runtimeChunk: false,
119         splitChunks: false,
120         usedExports: true,
121         chunkIds: 'named',
122     },
123     resolve: {
124         extensions: ['.js', '.tsx', '.ts'],
125         fallback: {
126             crypto: false,
127             buffer: false,
128             stream: false,
129             iconv: false,
130             path: false,
131             punycode: false,
132         },
133         modules: [path.resolve(__dirname), 'node_modules'],
134         alias: {
135             'proton-pass-extension': path.resolve(__dirname, 'src/'),
136             ...(BUILD_TARGET === 'safari'
137                 ? /* exclude `webextension-polyfill` from safari build to avoid
138                    * service-worker registration errors when worker fails. */
139                   { 'webextension-polyfill': '@proton/pass/lib/globals/webextension-polyfill.stub.ts' }
140                 : {}),
141             /* friends don't let friends publish code with `eval` : but that didn't stop `ttag`  */
142             ttag$: path.resolve(__dirname, '../../node_modules/ttag/dist/ttag.min.js'),
143         },
144     },
145     cache: {
146         type: 'filesystem',
147         cacheDirectory: path.resolve('./node_modules/.cache/webpack'),
148         buildDependencies: {
149             defaultWebpack: ['webpack/lib/'],
150             config: [__filename],
151         },
152     },
153     output: {
154         filename: '[name].js',
156         /** Some chunks need to be predictable in order for
157          * importScripts to work properly in the context of
158          * chromium builds (eg crypto lazy loaded modules) */
159         chunkFilename: ({ chunk: { name, id } }) => {
160             if (name === null) {
161                 if (id === ARGON2_CHUNK_NAME) return 'chunk.crypto-argon2.js';
162                 return 'chunk.[contenthash:8].js';
163             }
165             return 'chunk.[name].js';
166         },
167         path: path.resolve(__dirname, 'dist'),
168         clean: true,
169         assetModuleFilename: 'assets/[hash][ext][query]',
170         publicPath: '/',
171     },
172     plugins: [
173         new webpack.EnvironmentPlugin({ NODE_ENV: ENV }),
174         new webpack.DefinePlugin({
175             BUILD_TARGET: JSON.stringify(BUILD_TARGET),
176             DESKTOP_BUILD: false,
177             ENV: JSON.stringify(ENV),
178             EXTENSION_BUILD: true,
179             OFFLINE_SUPPORTED: false,
180             REDUX_DEVTOOLS_PORT,
181             RESUME_FALLBACK,
182             RUNTIME_RELOAD_PORT,
183             RUNTIME_RELOAD,
184             VERSION:
185                 ENV === 'production'
186                     ? getManifestVersion()
187                     : webpack.DefinePlugin.runtimeValue(getManifestVersion, true),
188         }),
189         new ESLintPlugin({
190             extensions: ['js', 'ts'],
191             overrideConfigFile: path.resolve(__dirname, '.eslintrc.js'),
192         }),
193         new MiniCssExtractPlugin({
194             filename: 'styles/[name].css',
195         }),
196         new CopyPlugin({
197             patterns: [
198                 { from: 'public' },
199                 {
200                     from: manifest,
201                     to: 'manifest.json',
202                     transform(content) {
203                         const data = content.toString('utf-8');
204                         const manifest = JSON.parse(data);
206                         if (PUBLIC_KEY) manifest.key = PUBLIC_KEY;
208                         /* add the appropriate CSP policies for the development build to connect
209                          * to unsecure ws:// protocols without firefox trying to upgrade it to
210                          * wss:// - currently the redux cli tools do not support https */
211                         if (ENV !== 'production' && BUILD_TARGET === 'firefox') {
212                             manifest.content_security_policy = {
213                                 extension_pages:
214                                     "connect-src 'self' https: ws:; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; ",
215                             };
216                         }
218                         /* sanitize manifest when building for production */
219                         if (CLEAN_MANIFEST) {
220                             switch (BUILD_TARGET) {
221                                 case 'firefox':
222                                     manifest.content_scripts[1].matches = [
223                                         'https://account.proton.me/*',
224                                         'https://pass.proton.me/*',
225                                     ];
226                                     break;
227                                 case 'chrome':
228                                     manifest.externally_connectable.matches = [
229                                         'https://account.proton.me/*',
230                                         'https://pass.proton.me/*',
231                                     ];
232                                     break;
233                                 case 'safari':
234                                     manifest.content_scripts[1].matches = ['https://account.proton.me/*'];
235                                     manifest.externally_connectable.matches = [
236                                         'https://account.proton.me/*',
237                                         'https://pass.proton.me/*',
238                                     ];
239                                     break;
240                             }
241                         }
243                         return Buffer.from(JSON.stringify(manifest, null, 2), 'utf-8');
244                     },
245                 },
246             ],
247         }),
248         ...(WEBPACK_CIRCULAR_DEPS
249             ? [
250                   new CircularDependencyPlugin({
251                       exclude: /node_modules/,
252                       include: /(packages\/pass|applications\/pass-extension)/,
253                       failOnError: false,
254                       allowAsyncCycles: false,
255                       cwd: process.cwd(),
256                   }),
257               ]
258             : []),
259         ...(production
260             ? [
261                   new ProtonIconsTreeShakePlugin({
262                       entries: ['dropdown', 'notification', 'onboarding', 'popup', 'settings'],
263                       excludeMimeIcons: true,
264                   }),
265               ]
266             : []),
267     ],
268     experiments: {
269         asyncWebAssembly: true,
270     },