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');
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}`);
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);
71 ? { mode: 'production', devtool: 'source-map' }
72 : { mode: 'development', devtool: 'inline-source-map' }),
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',
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') } : {}),
102 strictExportPresence: true,
105 test: /\.js$|\.tsx?$/,
106 exclude: createRegex(
107 excludeNodeModulesExcept(BABEL_INCLUDE_NODE_MODULES),
108 excludeFiles([...BABEL_EXCLUDE_FILES, 'pre.ts', 'unsupported.ts'])
110 use: require.resolve('babel-loader'),
112 ...getCssLoaders({ browserslist: undefined, logical: false }),
113 ...getAssetsLoaders({ inlineIcons: true }),
117 ...getOptimizations({ isProduction: production }),
124 extensions: ['.js', '.tsx', '.ts'],
133 modules: [path.resolve(__dirname), 'node_modules'],
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' }
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'),
147 cacheDirectory: path.resolve('./node_modules/.cache/webpack'),
149 defaultWebpack: ['webpack/lib/'],
150 config: [__filename],
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 } }) => {
161 if (id === ARGON2_CHUNK_NAME) return 'chunk.crypto-argon2.js';
162 return 'chunk.[contenthash:8].js';
165 return 'chunk.[name].js';
167 path: path.resolve(__dirname, 'dist'),
169 assetModuleFilename: 'assets/[hash][ext][query]',
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,
186 ? getManifestVersion()
187 : webpack.DefinePlugin.runtimeValue(getManifestVersion, true),
190 extensions: ['js', 'ts'],
191 overrideConfigFile: path.resolve(__dirname, '.eslintrc.js'),
193 new MiniCssExtractPlugin({
194 filename: 'styles/[name].css',
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 = {
214 "connect-src 'self' https: ws:; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; ",
218 /* sanitize manifest when building for production */
219 if (CLEAN_MANIFEST) {
220 switch (BUILD_TARGET) {
222 manifest.content_scripts[1].matches = [
223 'https://account.proton.me/*',
224 'https://pass.proton.me/*',
228 manifest.externally_connectable.matches = [
229 'https://account.proton.me/*',
230 'https://pass.proton.me/*',
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/*',
243 return Buffer.from(JSON.stringify(manifest, null, 2), 'utf-8');
248 ...(WEBPACK_CIRCULAR_DEPS
250 new CircularDependencyPlugin({
251 exclude: /node_modules/,
252 include: /(packages\/pass|applications\/pass-extension)/,
254 allowAsyncCycles: false,
261 new ProtonIconsTreeShakePlugin({
262 entries: ['dropdown', 'notification', 'onboarding', 'popup', 'settings'],
263 excludeMimeIcons: true,
269 asyncWebAssembly: true,