Preferred way of using Bootstrap in Webpack

17,632

Solution 1

I am not sure if this is the best way, but following work for me well with vue.js webapp. You can see the working code here.

I have included files required by bootstrap in index.html like this:

<head>
  <meta charset="utf-8">
  <title>Hey</title>
  <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  <link rel="stylesheet" href="/static/bootstrap.css" type="text/css">

  <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.7/js/tether.min.js" integrity="sha384-XTs3FgkjiBgo8qjEjBk0tGmf3wPrWtA6coPfQDfFEY8AnYJwjalXCiosYRBIBZX8" crossorigin="anonymous"></script>
  <script  href="/static/bootstrap.js"></script>
</head>

And this works, you can execute the repo. Why I went this way was I had to customise some config in bootstrap so I had to change the variables file and build the code of bootstrap which outputted me bootstrap.js and bootstrap.cssfiles, which I am using here.


There is an alternative way suggested here by using the npm package and a webpack customisation.

First install bootstrap in your project:

npm install [email protected]

And make sure you can use sass-loader in your components:

npm install sass-loader node-sass --save-dev

now go to your webpack config file and add a sassLoader object with the following:

sassLoader: {
    includePaths: [
        path.resolve(projectRoot, 'node_modules/bootstrap/scss/'),
    ],
},

projectRoot should just point to where you can navigate to node_packages from, in my case this is: path.resolve(__dirname, '../')

Now you can use bootstrap directly in your .vue files and webpack will compile it for you when you add the following:

<style lang="scss">
  @import "bootstrap";
</style>

Solution 2

I highly recommend using bootstrap-loader. You add a config file (.bootstraprc in your root folder) where you can exclude the bootstrap elements you don't want and tell where your variables.scss and bootstrap.overrides.scss are. Define you SCSS variables, do your overrides, add your webpack entry and get on with your life.

Solution 3

I use webpack to build bootstrap directly from .less and .scss files. This allows customizing bootstrap by overriding the source in .less/.scss and still get all the benefits of webpack.

Your code is missing an entry point for any .css/.less/.scss files. You need to include an entry point for the compiled css files. For this entry point, I declare an array with const and then include paths inside the array to the source files I want webpack to compile.

Currently, I'm using bootstrap 3 with a base custom template and also a 2nd custom theme. The base template uses bootstrap .less file styling and it has specific source overrides written in .less files.

The 2nd custom theme uses .sass file styling and has similar overrides to bootstrap's base written in .scss files. So, I need to try to optimize all this styling for production (currently coming in about 400kb but that's a little heavy because we choose to avoid CDN's due to targeting use in China).

Below is a reference webpack.config.js which works to build from .less/.scss/.css files, and also does a few other things like build typescript modules and uses Babel for converting es6/typescript to browser compatible javascript. The output ultimately ends up in my /static/dist folder.

const path = require('path');
const webpack = require('webpack');

// plugins
const ForkTsCheckerNotifierWebpackPlugin = require('fork-ts-checker-notifier-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');
const MomentLocalesPlugin = require('moment-locales-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserJSPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');


// take debug mode from the environment
const debug = (process.env.NODE_ENV !== 'production');
// Development asset host (webpack dev server)
const publicHost = debug ? 'http://localhost:9001' : '';
const rootAssetPath = path.join(__dirname, 'src');


const manifestOptions = {
    publicPath: `${publicHost}/static/dist/`,
  };

const babelLoader = {
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
    presets: [
        '@babel/preset-env'
    ]
  }
};

const app_css = [
    // specific order generally matters
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'css', 'fonts', 'Roboto', 'css', 'fonts.css'),
    path.join(__dirname, 'node_modules', 'font-awesome', 'css', 'font-awesome.css'),
    // This is bootstrap 3.3.7 base styling writtin in .less
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets',  'less', '_main_full', 'bootstrap.less'),
    // bootstrap theme in .scss -> src\bp\folder\theme\src\scss\styles.scss
    path.join(__dirname, 'src', 'bp', 'folder', 'theme', 'src', 'scss', 'styles.scss'),
    // back to .less -> 'src/bootstrap-template1/assets/less/_main_full/core.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'core.less'),
    // 'src/bootstrap-template1/assets/less/_main_full/components.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'components.less'),
    //'src/bootstrap-template1/assets/less/_main_full/colors.less',
    path.join(__dirname, 'src', 'bootstrap-template1', 'assets', 'less', '_main_full', 'colors.less'),
    // <!-- syntax highlighting in .css --> src/bp/folder/static/css/pygments.css
    path.join(__dirname, 'src', 'bp', 'folder', 'static', 'css', 'pygments.css'),
    // <!-- lato/ptsans font we want to serve locally --> src/fonts/googlefonts.css'
    path.join(__dirname, 'src', 'fonts', 'googlefonts.css'),
    // a .css style -> 'src/bp/appbase/styles/md_table_generator.css'
    path.join(__dirname, 'src', 'bp', 'appbase', 'styles', 'md_table_generator.css'),
    // another .css style -> hopscotch 'src/libs/hopscotch/dist/css/hopscotch.min.css'
    path.join(__dirname, 'src', 'libs', 'hopscotch', 'dist', 'css', 'hopscotch.min.css'),
    //LAST final custom snippet styles to ensure they take priority 'src/css/style.css',
    path.join(__dirname, 'src', 'css', 'style.css')
];

const vendor_js = [
    //'core-js',
    'whatwg-fetch',
];

const app_js = [
    // a typescript file! :)
    path.join(__dirname, 'src', 'typescript', 'libs', 'md-table', 'src', 'extension.ts'),
    // base bootstrap 3.3.7 javascript
    path.join(__dirname, 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.js'),
    path.join(__dirname, 'src', 'main', 'app.js'),
    // src/bootstrap-template1/assets/js/plugins/forms/styling/uniform.min.js'
    path.join(__dirname, 'node_modules', '@imanov', 'jquery.uniform', 'src', 'js', 'jquery.uniform.js'),
    // src/bootstrap-template1/assets/js/plugins/ui/moment/moment.min.js'
];

function recursiveIssuer(m) {
  if (m.issuer) {
    return recursiveIssuer(m.issuer);
  } else if (m.name) {
    return m.name;
  } else {
    return false;
  }
}

module.exports = {
    context: process.cwd(), // to automatically find tsconfig.json
    // context: __dirname,
    entry: {
        app_css,
        vendor_js,
        app_js,
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        publicPath: `${publicHost}/static/dist/`,
        chunkFilename: '[id].[hash:7].js',
        filename: '[name].[hash:7].js'
    },
    resolve: {
        extensions: [".webpack.js", ".web.js",".tsx", ".ts", ".js", ".css"],
        alias: {
            jquery$: path.resolve(__dirname, 'node_modules', 'jquery', 'dist', 'jquery.js'),
        }
    },
    target: "web",
    devtool: 'source-map',
    devServer: {
        // this devserver proxies all requests to my python development server except 
        // webpack compiled files in the `/static/dist` folder 
        clientLogLevel: 'warning',
        contentBase: path.join(__dirname, './src'),
        publicPath: 'dist',
        open: true,
        historyApiFallback: true,
        stats: 'errors-only',
        headers: {'Access-Control-Allow-Origin': '*'},
        watchContentBase: true,
        port: 9001,
        proxy: {
            '!(/dist/**/**.*)': {
                target: 'http://127.0.0.1:8000',
            },
        },
    },
    mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCssAssetsPlugin({})],
        splitChunks: {
          cacheGroups: {
            appStyles: {
              name: 'app',
              // https://webpack.js.org/plugins/mini-css-extract-plugin/#extracting-css-based-on-entry
              test: (m, c, entry = 'app') =>
                m.constructor.name === 'CssModule' && recursiveIssuer(m) === entry,
              chunks: 'all',
              enforce: true,
            },
          },
        },
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
            jQuery: 'jquery',
            'window.jQuery': 'jquery',
            'window.$': 'jquery'
        }),
        // Strip all locales from moment.js except "zh-cn"
        // ("en" is built into Moment and can’t be removed)
        new MomentLocalesPlugin({
            localesToKeep: ['zh-cn'],
        }),
        new ForkTsCheckerWebpackPlugin({
            tslint: true, useTypescriptIncrementalApi: true
        }),
        new ForkTsCheckerNotifierWebpackPlugin({ title: 'TypeScript', excludeWarnings: false }),
        new MiniCssExtractPlugin({
            filename: '[name].[hash:7].css',
            chunkFilename: '[id].[hash:7].css',
            moduleFilename: ({ name }) => `${name.replace('/js/', '/css/')}.[hash:7].css`
        }),
         new OptimizeCssAssetsPlugin({
            assetNameRegExp: /\.optimize\.css$/g,
            cssProcessor: require('cssnano'),
            cssProcessorPluginOptions: {
            preset: ['default', { discardComments: { removeAll: true } }],
            },
            canPrint: true
        }),
        new ManifestPlugin({
            ...manifestOptions
        }),
    ].concat(debug ? [] : [
        // production webpack plugins go here
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify('production'),
            }
        }),
        new ForkTsCheckerWebpackPlugin({
            async: false,
            useTypescriptIncrementalApi: true,
            memoryLimit: 2048
        }),
    ]),
    module: {
        rules: [
                {
                // jinja/nunjucks templates
                test: /\.jinja2$/,
                loader: 'jinja-loader',
                query: {
                    root:'../templates'
                }
            },
            {
                test: /\.ts(x?)$/,
                exclude: /node_modules/,
                use: [
                    babelLoader,
                    {
                        loader: 'ts-loader',
                        options:
                            {   // disable type checker - we will use it in
                                // fork-ts-checker-webpack-plugin
                                transpileOnly: true
                            }
                    }
                ]
            },
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    babelLoader
                ]
            },
            {
                test: /\.(html|jinja2)$/,
                loader: 'raw-loader'
            },
            {
                test: /\.(sc|sa|c)ss$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                          hmr: debug,
                            // only use if hmr is not working correctly
                          // reloadAll: true,
                        },
                    },
                    {
                        loader: "css-loader",
                    },
                    {
                        loader: "sass-loader"
                    },
                ]
            },
            {
                test: /\.less$/,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                          hmr: debug,
                            // only use if hmr is not working correctly
                          // reloadAll: true,
                        },
                    },
                    {
                        loader: 'css-loader', // translates CSS into CommonJS
                    },
                    {
                        loader: 'less-loader', // compiles Less to CSS
                    },
                ],
            },
            {
                test: /\.(ttf|eot|svg|gif|ico)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[path][name].[hash:7].[ext]',
                            context: rootAssetPath
                        },
                    },
                ],
            },
            {
                test: /\.(jpe?g|png)$/i,
                loader: 'responsive-loader',
                options: {
                    name: '[path][name].[hash:7].[ext]',
                    adapter: require('responsive-loader/sharp'),
                    context: rootAssetPath
                }
            },
            {
                test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                 loader:'url-loader',
                 options:{
                    limit: 10000,
                    mimetype: 'application/font-woff',
                    // name: ('fonts/[path][name].[hash:7].[ext]'),
                    name: ('fonts/[name].[hash:7].[ext]'),
                 }
            },
            {
                test: require.resolve("jquery"),
                use:[
                    { loader: "expose-loader", options:"$" },
                    { loader: "expose-loader", options:"jQuery" },
                    { loader: "expose-loader", options:"jquery" }
                ]
            }
        ]
    },
};
Share:
17,632
Nickvda
Author by

Nickvda

I usually develop and or maintain document management systems. Oh, and I like front-end. A lot.

Updated on July 08, 2022

Comments

  • Nickvda
    Nickvda almost 2 years

    Greetings one and all,

    I have been playing around with Bootstrap for Webpack, but I'm at the point of tearing my hair out. I have literally gone through loads of blog articles and they either use the 7 months outdated 'bootstrap-webpack' plugin (which, surprisingly does not work out of the box) or.. They include the Bootstrap files through import 'node_modules/*/bootstrap/css/bootstrap.css'.

    Surely, there must be a cleaner and more efficient way of going about this?

    This is my current webpack.config.js file:

    var webpack = require('webpack');
    var ExtractTextPlugin = require('extract-text-webpack-plugin');
    var autoprefixer = require('autoprefixer');
    var path = require('path');
    
    module.exports = {
        entry: {
            app: path.resolve(__dirname, 'src/js/main.js')
        },
        module: {
            loaders: [{
                test: /\.js[x]?$/,
                loaders: ['babel-loader?presets[]=es2015&presets[]=react'],
                exclude: /(node_modules|bower_components)/
            }, {
                test: /\.css$/,
                loaders: ['style', 'css']
            }, {
                test: /\.scss$/,
                loaders: ['style', 'css', 'postcss', 'sass']
            }, {
                test: /\.sass$/,
                loader: 'style!css!sass?sourceMap'
            },{
                test: /\.less$/,
                loaders: ['style', 'css', 'less']
            }, {
                test: /\.woff$/,
                loader: "url-loader?limit=10000&mimetype=application/font-woff&name=[path][name].[ext]"
            }, {
                test: /\.woff2$/,
                loader: "url-loader?limit=10000&mimetype=application/font-woff2&name=[path][name].[ext]"
            }, {
                test: /\.(eot|ttf|svg|gif|png)$/,
                loader: "file-loader"
            }]
        },
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '/js/bundle.js',
            sourceMapFilename: '/js/bundle.map',
            publicPath: '/'
        },
        plugins: [
            new ExtractTextPlugin('style.css')
        ],
        postcss: [
            autoprefixer({
                browsers: ['last 2 versions']
            })
        ],
        resolve: {
            extensions: ['', '.js', '.sass'],
            modulesDirectories: ['src', 'node_modules']
        },
        devServer: {
            inline: true,
            contentBase: './dist'
        }
    };
    

    I could go and require('bootstrap') (with some way of getting jQuery in it to work), but.. I'm curious to what you all think and do.

    Thanks in advance :)