main

mattermost/focalboard

Last updated at: 29/12/2023 09:51

webpack.config.js

TLDR

This file is the webpack configuration file for the Mattermost Plugin. It sets up the entry points, resolves modules, defines module rules, and adds plugins based on the target and mode.

Methods

None

Classes

None

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
const exec = require('child_process').exec;

const path = require('path');

const webpack = require('webpack');
const {ModuleFederationPlugin} = require('webpack').container;

const tsTransformer = require('@formatjs/ts-transformer');

const PLUGIN_ID = require('../plugin.json').id;

const NPM_TARGET = process.env.npm_lifecycle_event; //eslint-disable-line no-process-env
const TARGET_IS_PRODUCT = NPM_TARGET?.endsWith(':product');

let mode = 'production';
let devtool;
const plugins = [];
if (NPM_TARGET === 'debug' || NPM_TARGET === 'debug:watch' || NPM_TARGET === 'start:product') {
    mode = 'development';
    devtool = 'source-map';
    plugins.push(
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('development'),
        }),
    );
}

if (NPM_TARGET === 'build:watch' || NPM_TARGET === 'debug:watch' || NPM_TARGET === 'live-watch') {
    plugins.push({
        apply: (compiler) => {
            compiler.hooks.watchRun.tap('WatchStartPlugin', () => {
                // eslint-disable-next-line no-console
                console.log('Change detected. Rebuilding webapp.');
            });
            compiler.hooks.afterEmit.tap('AfterEmitPlugin', () => {
                let command = 'cd .. && make deploy-from-watch';
                if (NPM_TARGET === 'live-watch') {
                    command = 'cd .. && make deploy-to-mattermost-directory';
                }
                exec(command, (err, stdout, stderr) => {
                    if (stdout) {
                        process.stdout.write(stdout);
                    }
                    if (stderr) {
                        process.stderr.write(stderr);
                    }
                });
            });
        },
    });
}

const config = {
    entry: TARGET_IS_PRODUCT ? './src/remote_entry.ts' : './src/plugin_entry.ts',
    resolve: {
        modules: [
            'src',
            'node_modules',
            path.resolve(__dirname),
        ],
        alias: {
            moment: path.resolve(__dirname, '../../webapp/node_modules/moment/'),
        },
        extensions: ['*', '.js', '.jsx', '.ts', '.tsx'],
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: {
                    loader: 'ts-loader',
                    options: {
                        getCustomTransformers: {
                            before: [
                                tsTransformer.transform({
                                    overrideIdFn: '[sha512:contenthash:base64:6]',
                                    ast: true,
                                }),
                            ],
                        },
                    },
                },
                exclude: [/node_modules/],

            },
            {
                test: /\.html$/,
                type: 'asset/resource',
            },
            {
                test: /\.s[ac]ss$/i,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                    path.resolve(__dirname, 'loaders/globalScssClassLoader'),
                ],
            },
            {
                test: /\.css$/i,
                use: [
                    'style-loader',
                    'css-loader',
                ],
            },
            {
                test: /\.(tsx?|js|jsx|mjs|html)$/,
                use: [
                ],
                exclude: [/node_modules/],
            },
            {
                test: /\.(png|eot|tiff|svg|ttf|jpg|gif)$/,
                type: 'asset/resource',
                generator: {
                    filename: '[name][ext]',
                    publicPath: TARGET_IS_PRODUCT ? undefined : '/static/',
                }
            },
            {
                test: /\.(woff2|woff)$/,
                type: 'asset/resource',
                generator: {
                    filename: '[name][ext]',
                    publicPath: TARGET_IS_PRODUCT ? undefined : '/plugins/focalboard/static/',
                }
            },
        ],
    },
    devtool,
    mode,
    plugins,
};

if (TARGET_IS_PRODUCT) {
    // Set up module federation
    function makeSingletonSharedModules(packageNames) {
        const sharedObject = {};

        for (const packageName of packageNames) {
            sharedObject[packageName] = {

                // Ensure only one copy of this package is ever loaded
                singleton: true,

                // Set this to false to prevent Webpack from packaging any "fallback" version of this package so that
                // only the version provided by the web app will be used
                import: false,

                // Set these to false so that any version provided by the web app will be accepted
                requiredVersion: false,
                version: false
            };
        }

        return sharedObject;
    }

    config.plugins.push(new ModuleFederationPlugin({
        name: 'boards',
        filename: 'remote_entry.js',
        exposes: {
            '.': './src/index',

            // This probably won't need to be exposed in the long run, but its a POC for exposing multiple modules
            './manifest': './src/manifest',
        },
        shared: [
            '@mattermost/client',
            'prop-types',

            makeSingletonSharedModules([
                'react',
                'react-dom',
                'react-intl',
                'react-redux',
                'react-router-dom',
            ]),
        ],
    }));

    config.plugins.push(new webpack.DefinePlugin({
        'process.env.TARGET_IS_PRODUCT': TARGET_IS_PRODUCT, // TODO We might want a better name for this
    }));

    config.output = {
        path: path.join(__dirname, '/dist'),
        chunkFilename: '[name].[contenthash].js',
    };
} else {
    config.resolve.alias['react-intl'] = path.resolve(__dirname, '../../webapp/node_modules/react-intl/');

    config.externals = {
        react: 'React',
        'react-dom': 'ReactDOM',
        redux: 'Redux',
        'react-redux': 'ReactRedux',
        'mm-react-router-dom': 'ReactRouterDom',
        'prop-types': 'PropTypes',
        'react-bootstrap': 'ReactBootstrap',
    };

    config.output = {
        devtoolNamespace: PLUGIN_ID,
        path: path.join(__dirname, '/dist'),
        publicPath: '/',
        filename: 'main.js',
    };
}

const env = {};
env.RUDDER_KEY = JSON.stringify(process.env.RUDDER_KEY || ''); //eslint-disable-line no-process-env
env.RUDDER_DATAPLANE_URL = JSON.stringify(process.env.RUDDER_DATAPLANE_URL || ''); //eslint-disable-line no-process-env

config.plugins.push(new webpack.DefinePlugin({
    'process.env': env,
}));

if (NPM_TARGET === 'start:product') {
    const url = new URL(process.env.MM_BOARDS_DEV_SERVER_URL ?? 'http://localhost:9006');

    config.devServer = {
        server: {
            type: url.protocol.substring(0, url.protocol.length - 1),
            options: {
                minVersion: process.env.MM_SERVICESETTINGS_TLSMINVER ?? 'TLSv1.2',
                key: process.env.MM_SERVICESETTINGS_TLSKEYFILE,
                cert: process.env.MM_SERVICESETTINGS_TLSCERTFILE,
            },
        },
        host: url.hostname,
        port: url.port,
        devMiddleware: {
            writeToDisk: false,
        },
        static: {
            directory: path.join(__dirname, '../../webapp/static'),
            publicPath: '/static',
        },
    };
}

module.exports = config;