mattermost/webapp/channels/webpack.config.js
Harrison Healey 4ffc81cc68
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Waiting to run
Web App CI / check-types (push) Waiting to run
Web App CI / test (push) Waiting to run
Web App CI / build (push) Waiting to run
Skip Webpack image optimizations during dev (#34677)
2025-12-09 20:28:36 +00:00

496 lines
17 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console, no-process-env */
const path = require('path');
const url = require('url');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExternalTemplateRemotesPlugin = require('external-remotes-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const webpack = require('webpack');
const {ModuleFederationPlugin} = require('webpack').container;
const WebpackPwaManifest = require('webpack-pwa-manifest');
const packageJson = require('./package.json');
const NPM_TARGET = process.env.npm_lifecycle_event;
const targetIsBuild = NPM_TARGET?.startsWith('build');
const targetIsRun = NPM_TARGET?.startsWith('run');
const targetIsStats = NPM_TARGET === 'stats';
const targetIsDevServer = NPM_TARGET?.startsWith('dev-server');
const targetIsEsLint = !targetIsBuild && !targetIsRun && !targetIsDevServer;
const DEV = targetIsRun || targetIsStats || targetIsDevServer;
const STANDARD_EXCLUDE = [
/node_modules/,
];
let publicPath = '/static/';
// Allow overriding the publicPath in dev from the exported SiteURL.
if (DEV) {
const siteURL = process.env.MM_SERVICESETTINGS_SITEURL || '';
if (siteURL) {
publicPath = path.join(new url.URL(siteURL).pathname, 'static') + '/';
}
}
// Track the build time so that we can bust any caches that may have incorrectly cached remote_entry.js from before we
// started setting Cache-Control: no-cache for that file on the server. This can be removed in 2024 after those cached
// entries are guaranteed to have expired.
const buildTimestamp = Date.now();
var config = {
entry: ['./src/root.tsx'],
output: {
publicPath,
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
assetModuleFilename: 'files/[contenthash][ext]',
clean: true,
},
module: {
rules: [
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: STANDARD_EXCLUDE,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
// Babel configuration is in .babelrc because jest requires it to be there.
},
},
},
{
test: /\.json$/,
include: [
path.resolve(__dirname, 'src/i18n'),
],
exclude: [/en\.json$/],
type: 'asset/resource',
generator: {
filename: 'i18n/[name].[contenthash].json',
},
},
{
test: /\.(css|scss)$/,
exclude: /\/highlight\.js\//,
use: [
DEV ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
},
],
},
{
test: /\.scss$/,
use: [
{
loader: 'sass-loader',
options: {
sassOptions: {
loadPaths: ['src/sass'],
},
},
},
],
},
{
test: /\.(png|eot|tiff|svg|woff2|woff|ttf|gif|mp3|jpg)$/,
type: 'asset/resource',
use: [
// Skip image optimizations during development to speed up build time
!DEV && {
loader: 'image-webpack-loader',
options: {},
},
],
},
{
test: /\.apng$/,
type: 'asset/resource',
},
{
test: /\/highlight\.js\/.*\.css$/,
type: 'asset/resource',
},
],
},
resolve: {
modules: [
'node_modules',
'./src',
],
alias: {
'mattermost-redux/test': 'packages/mattermost-redux/test',
'mattermost-redux': 'packages/mattermost-redux/src',
'@mui/styled-engine': '@mui/styled-engine-sc',
// This alias restricts single version of styled components across all packages
'styled-components': path.resolve(__dirname, '..', 'node_modules', 'styled-components'),
},
extensions: ['.ts', '.tsx', '.js', '.jsx'],
fallback: {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
buffer: require.resolve('buffer/'),
},
},
performance: {
hints: 'warning',
},
target: 'web',
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
}),
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[name].[contenthash].css',
}),
new HtmlWebpackPlugin({
filename: 'root.html',
inject: 'head',
template: 'src/root.html',
scriptLoading: 'blocking',
meta: {
csp: {
'http-equiv': 'Content-Security-Policy',
content: generateCSP(),
},
},
}),
new CopyWebpackPlugin({
patterns: [
{from: 'src/images/emoji', to: 'emoji'},
{from: 'src/images/img_trans.gif', to: 'images'},
{from: 'src/images/logo-email.png', to: 'images'},
{from: 'src/images/favicon', to: 'images/favicon'},
{from: 'src/images/appIcons.png', to: 'images'},
{from: 'src/images/logo-email.png', to: 'images'},
{from: 'src/images/browser-icons', to: 'images/browser-icons'},
{from: 'src/images/cloud', to: 'images'},
{from: 'src/images/welcome_illustration_new.png', to: 'images'},
{from: 'src/images/logo_email_blue.png', to: 'images'},
{from: 'src/images/logo_email_dark.png', to: 'images'},
{from: 'src/images/logo_email_gray.png', to: 'images'},
{from: 'src/images/forgot_password_illustration.png', to: 'images'},
{from: 'src/images/invite_illustration.png', to: 'images'},
{from: 'src/images/channel_icon.png', to: 'images'},
{from: 'src/images/c_avatar.png', to: 'images'},
{from: 'src/images/c_download.png', to: 'images'},
{from: 'src/images/c_socket.png', to: 'images'},
{from: 'src/images/admin-onboarding-background.jpg', to: 'images'},
{from: 'src/images/cloud-laptop.png', to: 'images'},
{from: 'src/images/cloud-laptop-error.png', to: 'images'},
{from: 'src/images/cloud-laptop-warning.png', to: 'images'},
{from: 'src/images/cloud-upgrade-person-hand-to-face.png', to: 'images'},
{from: 'src/images/payment_processing.png', to: 'images'},
{from: 'src/images/purchase_alert.png', to: 'images'},
{from: '../node_modules/pdfjs-dist/cmaps', to: 'cmaps'},
{from: 'src/components/initial_loading_screen/initial_loading_screen.css', to: 'css'},
],
}),
// Generate manifest.json, honouring any configured publicPath. This also handles injecting
// <link rel="apple-touch-icon" ... /> and <meta name="apple-*" ... /> tags into root.html.
new WebpackPwaManifest({
name: 'Mattermost',
short_name: 'Mattermost',
start_url: '..',
description: 'Mattermost is an open source, self-hosted Slack-alternative',
background_color: '#ffffff',
inject: true,
ios: true,
fingerprints: false,
orientation: 'any',
filename: 'manifest.json',
icons: [{
src: path.resolve('src/images/favicon/android-chrome-192x192.png'),
type: 'image/png',
sizes: '192x192',
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-120x120.png'),
type: 'image/png',
sizes: '120x120',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-144x144.png'),
type: 'image/png',
sizes: '144x144',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-152x152.png'),
type: 'image/png',
sizes: '152x152',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-57x57.png'),
type: 'image/png',
sizes: '57x57',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-60x60.png'),
type: 'image/png',
sizes: '60x60',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-72x72.png'),
type: 'image/png',
sizes: '72x72',
ios: true,
}, {
src: path.resolve('src/images/favicon/apple-touch-icon-76x76.png'),
type: 'image/png',
sizes: '76x76',
ios: true,
}, {
src: path.resolve('src/images/favicon/favicon-16x16.png'),
type: 'image/png',
sizes: '16x16',
}, {
src: path.resolve('src/images/favicon/favicon-32x32.png'),
type: 'image/png',
sizes: '32x32',
}, {
src: path.resolve('src/images/favicon/favicon-96x96.png'),
type: 'image/png',
sizes: '96x96',
}],
}),
new MonacoWebpackPlugin({
languages: [],
// don't include features we disable. these generally correspond to the options
// passed to editor initialization in note-content-editor.tsx
// @see https://github.com/microsoft/monaco-editor/blob/main/webpack-plugin/README.md#options
features: [
'!bracketMatching',
'!codeAction',
'!codelens',
'!colorPicker',
'!comment',
'!diffEditor',
'!diffEditorBreadcrumbs',
'!folding',
'!gotoError',
'!gotoLine',
'!gotoSymbol',
'!gotoZoom',
'!inspectTokens',
'!multicursor',
'!parameterHints',
'!quickCommand',
'!quickHelp',
'!quickOutline',
'!referenceSearch',
'!rename',
'!snippet',
'!stickyScroll',
'!suggest',
'!toggleHighContrast',
'!unicodeHighlighter',
],
}),
],
};
function generateCSP() {
let csp = 'script-src \'self\' js.stripe.com/v3';
if (DEV) {
// Development source maps require eval
csp += ' \'unsafe-eval\'';
}
return csp;
}
async function initializeModuleFederation() {
function makeSharedModules(packageNames, singleton) {
const sharedObject = {};
for (const packageName of packageNames) {
const version = packageJson.dependencies[packageName];
sharedObject[packageName] = {
// Ensure only one copy of this package is ever loaded
singleton,
// Setting this to true causes the app to error out if the required version is not satisfied
strictVersion: singleton,
// Set these to match the specific version that the web app includes
requiredVersion: singleton ? version : undefined,
version,
};
}
return sharedObject;
}
async function getRemoteContainers() {
const products = [];
const remotes = {};
for (const product of products) {
remotes[product.name] = `${product.name}@[window.basename]/static/products/${product.name}/remote_entry.js?bt=${buildTimestamp}`;
}
return {remotes};
}
const {remotes} = await getRemoteContainers();
const moduleFederationPluginOptions = {
name: 'mattermost_webapp',
remotes,
shared: [
// Shared modules will be made available to other containers (ie products and plugins using module federation).
// To allow for better sharing, containers shouldn't require exact versions of packages like the web app does.
// Other containers will use these shared modules if their required versions match. If they don't match, the
// version packaged with the container will be used.
makeSharedModules([
'@mattermost/client',
'@mattermost/types',
'luxon',
], false),
// Other containers will be forced to use the exact versions of shared modules that the web app provides.
makeSharedModules([
'history',
'react',
'react-beautiful-dnd',
'react-bootstrap',
'react-dom',
'react-intl',
'react-redux',
'react-router-dom',
'styled-components',
], true),
],
};
// Desktop specific code for remote module loading
moduleFederationPluginOptions.exposes = {
'./app': 'components/app',
'./store': 'stores/redux_store',
'./styles': './src/sass/styles.scss',
'./registry': 'module_registry',
};
moduleFederationPluginOptions.filename = `remote_entry.js?bt=${buildTimestamp}`;
config.plugins.push(new ModuleFederationPlugin(moduleFederationPluginOptions));
// Add this plugin to perform the substitution of window.basename when loading remote containers
config.plugins.push(new ExternalTemplateRemotesPlugin());
config.plugins.push(new webpack.DefinePlugin({
REMOTE_CONTAINERS: JSON.stringify(remotes),
}));
}
if (DEV) {
// Development mode configuration
config.mode = 'development';
config.devtool = 'eval-cheap-module-source-map';
} else {
// Production mode configuration
config.mode = 'production';
config.devtool = 'source-map';
}
const env = {};
if (DEV) {
env.PUBLIC_PATH = JSON.stringify(publicPath);
} else {
env.NODE_ENV = JSON.stringify('production');
}
config.plugins.push(new webpack.DefinePlugin({
'process.env': env,
}));
if (targetIsDevServer) {
const proxyToServer = {
logLevel: 'silent',
target: process.env.MM_SERVICESETTINGS_SITEURL ?? 'http://localhost:8065',
xfwd: true,
};
config = {
...config,
devtool: 'eval-cheap-module-source-map',
devServer: {
liveReload: true,
proxy: [
{
context: '/api',
...proxyToServer,
ws: true,
},
{
context: '/plugins',
...proxyToServer,
},
{
context: '/static/plugins',
...proxyToServer,
},
],
port: 9005,
devMiddleware: {
writeToDisk: false,
},
historyApiFallback: {
index: '/static/root.html',
},
},
performance: false,
optimization: {
...config.optimization,
splitChunks: false,
},
};
}
// Export PRODUCTION_PERF_DEBUG=1 when running webpack to enable support for the react profiler
// even while generating production code. (Performance testing development code is typically
// not helpful.)
// See https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html and
// https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977
if (process.env.PRODUCTION_PERF_DEBUG) {
console.log('Enabling production performance debug settings'); //eslint-disable-line no-console
config.resolve.alias['react-dom'] = 'react-dom/profiling';
config.resolve.alias['schedule/tracing'] = 'schedule/tracing-profiling';
config.optimization = {
// Skip minification to make the profiled data more useful.
minimize: false,
};
}
if (targetIsEsLint) {
// ESLint can't handle setting an async config, so just skip the async part
module.exports = config;
} else {
module.exports = async () => {
// Do this asynchronously so we can determine whether which remote modules are available
await initializeModuleFederation();
return config;
};
}