An ASP.NET Core Razor Pages Bootstrap 4 Application using Webpack, Typescript, and npm

This article shows how an ASP.NET Core Razor Pages application could be setup to use webpack, Typescript and npm to build, and bundle the client js, CSS for development and production. The application uses Bootstrap 4.

Code: https://github.com/damienbod/AspNetCorePagesWebpack

The example is setup so that the vendor ( 3rd Party packages ) javascript files are used as part of the application in development, but CDN links are used for the production deployment. The vendor CSS files, (bootstrap 4) are loaded in the same way, locally for development, and CDNs for production. SASS is used to build the application CSS and this is built into the application bundle.

Getting the client packages using npm

A package.json file is added to the root of the project. This file is used to install the required npm packages and to define the scripts used for the client builds. All the packages for the project and the client build packages are added to this file.

{
 "scripts": {
  "build": "webpack --env=development",
  "build-watch": "webpack --env=development --watch",
  "release": "webpack --env=production",
  "publish": "npm run release && dotnet publish -c Release"
 },
 "dependencies": {
  "bootstrap": "4.1.1",
  "jquery": "3.3.1",
  "jquery-validation": "1.17.0",
  "jquery-validation-unobtrusive": "3.2.10",
  "core-js": "2.5.7",
  "zone.js": "0.8.26",
  "es6-promise": "^4.2.4",
  "ie-shim": "0.1.0",
  "isomorphic-fetch": "^2.2.1",
  "rxjs": "6.2.1"
 },
 "devDependencies": {
  "@types/node": "^10.3.4",
  "awesome-typescript-loader": "^5.2.0",
  "clean-webpack-plugin": "~0.1.19",
  "codelyzer": "^4.3.0",
  "concurrently": "^3.6.0",
  "copy-webpack-plugin": "^4.5.1",
  "css-loader": "~0.28.11",
  "file-loader": "^1.1.11",
  "html-webpack-plugin": "~3.2.0",
  "jquery": "^3.3.1",
  "json-loader": "^0.5.7",
  "mini-css-extract-plugin": "~0.4.0",
  "node-sass": "^4.9.0",
  "raw-loader": "^0.5.1",
  "rimraf": "^2.6.2",
  "sass-loader": "^7.0.3",
  "source-map-loader": "^0.2.3",
  "style-loader": "^0.21.0",
  "ts-loader": "~4.4.1",
  "tslint": "^5.10.0",
  "tslint-loader": "^3.6.0",
  "typescript": "~2.9.2",
  "uglifyjs-webpack-plugin": "^1.2.6",
  "url-loader": "^1.0.1",
  "webpack": "~4.12.0",
  "webpack-bundle-analyzer": "^2.13.1",
  "webpack-cli": "~3.0.6"
 }
}

Install nodeJS if not already installed and update npm (npm install -g npm) after installing nodeJS. Then install the packages.

> 
> npm install

A webpack config file is added to the root of the project. The development build or the production build can be started using this file.

/// <binding ProjectOpened='Run - Development' />

module.exports = function(env) {
  return require(`./Client/webpack.${env}.js`)
}

The webpack development build creates 3 files, a polyfills file, the vendor bundle file using the vendor.development.ts file and the app bundle using the main.ts as an entry point. The built files are created in the wwwroot/dist. The assets are copied 1 to 1 to the wwwroot. The CSS for bootstrap 4 is loaded directly to the wwwroot, and not bundled with the application CSS. This is then added in the header of the _Layout.cshtml. The sass files are built into the application directly into the app bundle.

const path = require('path');
const rxPaths = require('rxjs/_esm5/path-mapping');

const webpack = require('webpack');

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

const helpers = require('./webpack.helpers');

const ROOT = path.resolve(__dirname, '..');

console.log('@@@@@@@@@ USING DEVELOPMENT @@@@@@@@@@@@@@@');

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  performance: {
    hints: false
  },
  entry: {
    polyfills: './Client/polyfills.ts',
      vendor: './Client/vendor.development.ts',
      app: './Client/main.ts'
  },

  output: {
    path: ROOT + '/wwwroot/',
    filename: 'dist/[name].bundle.js',
    chunkFilename: 'dist/[id].chunk.js',
    publicPath: '/'
  },

  resolve: {
    extensions: ['.ts', '.js', '.json'],
    alias: rxPaths()
  },

  devServer: {
    historyApiFallback: true,
    contentBase: path.join(ROOT, '/wwwroot/'),
    watchOptions: {
      aggregateTimeout: 300,
      poll: 1000
    }
  },

  module: {
    rules: [
      {
        test: /\.ts$/,
        use: [
          'awesome-typescript-loader',
          'source-map-loader'
        ]
      },
      {
        test: /\.(png|jpg|gif|woff|woff2|ttf|svg|eot)$/,
        use: 'file-loader?name=assets/[name]-[hash:6].[ext]'
      },
      {
        test: /favicon.ico$/,
        use: 'file-loader?name=/[name].[ext]'
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.scss$/,
          include: path.join(ROOT, 'Client/styles'),
        use: ['style-loader', 'css-loader', 'sass-loader']
      },
      {
        test: /\.scss$/,
          exclude: path.join(ROOT, 'Client/styles'),
        use: ['raw-loader', 'sass-loader']
      },
      {
        test: /\.html$/,
        use: 'raw-loader'
      }
    ],
    exprContextCritical: false
  },
  plugins: [
    function() {
      this.plugin('watch-run', function(watching, callback) {
        console.log(
          '\x1b[33m%s\x1b[0m',
          `Begin compile at ${new Date().toTimeString()}`
        );
        callback();
      });
    },

    new webpack.optimize.ModuleConcatenationPlugin(),

    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      'window.jQuery': 'jquery'
    }),

    // new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'polyfills'] }),

    new CleanWebpackPlugin(['./wwwroot/dist', './wwwroot/assets'], {
      root: ROOT
    }),

    new HtmlWebpackPlugin({
        filename: '../Pages/Shared/_Layout.cshtml',
        inject: 'body',
        template: 'Client/_Layout.cshtml'
    }),

    new CopyWebpackPlugin([
        { from: './Client/assets/*.*', to: 'assets/', flatten: true }
    ]),

    new CopyWebpackPlugin([
        { from: './node_modules/bootstrap/dist/css/*.*', to: 'css/', flatten: true }
    ])
  ]
};

The ASP.NET Core _Layout.cshtml src file is added to the Client folder. When webpack builds, the required bundles are added to the file, and copied to the Shared/Pages required by the Razor Pages.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <environment exclude="Development">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
    </environment>

    <environment include="Development">
        <link href="~/css/bootstrap.min.css" rel="stylesheet" />
    </environment>

    <title>@ViewData["Title"] - ASP.NET Core Pages Webpack</title>
</head>
<body>
    <div class="container">
        <nav class="bg-dark mb-4 navbar navbar-dark navbar-expand-md">
            <a asp-page="/Index" class="navbar-brand">
                <em>ASP.NET Core Pages Webpack</em>
            </a>
            <button aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation" class="navbar-toggler" data-target="#topNavbarCollapse" data-toggle="collapse" type="button">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="topNavbarCollapse">
                <ul class="mr-auto navbar-nav">
                    <li class="nav-item">
                        <a asp-page="/Index" class="nav-link">Home</a>
                    </li>
                    <li class="nav-item">
                        <a asp-page="/About" class="nav-link">About</a>
                    </li>
                    <li class="nav-item">
                        <a asp-page="/Contact" class="nav-link">Contact</a>
                    </li>
                </ul>
                <ul class="navbar-nav">
                    <li class="nav-item">
                        <a class="nav-link" href="https://twitter.com/damien_bod">
                            <img height="30" src="assets/damienbod.jpg" />
                        </a>
                    </li>
                </ul>
            </div>
        </nav>

    </div>
    
    <partial name="_CookieConsentPartial" />

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; 2018 - ASP.NET Core Pages Webpack Bootstrap 4</p>
        </footer>
    </div>

    <environment exclude="Development">
        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
    </environment>

    @RenderSection("Scripts", required: false)
</body>
</html>

Client Production build

The webpack production is is very similar except that most, if not all of the vendor client libraries are removed and loaded using CDNs. You can choose where the production scripts should be read from, depending on the project. The vendor.production.ts in this project is empty.

The main.ts is the entry point for the application scripts. All typescript code can be added here.

import './styles/app.scss';

// Write your ts code here
console.log("My site scripts if needed");

In Visual Studio, the npm Task Runner can be installed and used to do the client builds.

Or from the cmd

>
> npm run build
>

When the application is started, the client bundles are used in the Pages application.

Links:

https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-2.1&tabs=visual-studio

https://getbootstrap.com/

https://webpack.js.org/

https://www.typescriptlang.org/

https://nodejs.org/en/

https://www.npmjs.com/

Advertisements

3 comments

  1. […] An ASP.NET Core Razor Pages Bootstrap 4 Application using Webpack, Typescript, and npm (Damien Bowden) […]

  2. Hi there, apologies if this question has an obvious answer but I’m currently working my way through the mire of my first webpack setup and found your article helpful.

    Is there a reason you have two sections for the SASS/SCSS loaders? My understanding is that you would only need one so it’s not clear to me what the point of the second one is.

  3. Will the ts files be compiled automatically on save or build?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: