Are you working in an existing code base with tons of ES5 JavaScript, and you’re jealous of your friends working on greenfield projects using ES6/ES7/ES8+ or ES2015/ES2016/ES2017+ JavaScript?  Here’s how we moved our code base forward to start using newer features of JavaScript without losing our minds.

I’ve made a sample project using the ASP.NET MVC default project (and removing some of the noise). You can see all the code if you want to follow along or fork this on GitHub.

Install NPM packages

First, install node and npm, then install webpack and Babel from the command line in the web project root.

npm install --save-dev babel-core babel-loader babel-preset-env

npm install --save-dev webpack webpack-cli

When you are done, you should have a  devDependencies section in your package.json file, also in the web root, that looks something like this. You will almost certainly have different versions, and you will probably have more than just this in your  devDependencies section, and that’s fine.

  "devDependencies": {
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.4",
    "babel-preset-env": "^1.6.1",
    "webpack": "^4.4.1",
    "webpack-cli": "^2.0.13"

Set up Visual Studio

The plan is to leave old JavaScript files with the *.js extension and create new ES2015+ JavaScript files with the *.es6 extension.  You can use *.es2015 or any other extension not already in use if you prefer.  We just need some file marker so webpack knows which files need Babel transpilation and which don’t.

In Visual Studio, you’ll need to set up this new extension to use the JavaScript editor.  You can do that in Tools > Options > Text Editor > File Extension. If you skip this step, Visual Studio will treat *.es6 like text files instead of JavaScript files.


Configure webpack and Babel

Let’s set up the webpack.config.js file in the web root to work with the newer JavaScript syntax.

Below, the resolve section tells webpack we are using these two extensions for JavaScript files, and webpack should be looking for both *.js and *.es6 files when making bundles.

The rules section tells webpack that it needs to use Babel to load *.es6 extension files. There are a lot more Babel options you can add here. For example, want to use async/await in your *.es6 files? Add a Babel plugin for syntax-async-functions.

The isRelease flag is used to toggle between development and production settings for the build. This is using webpack 4’s new mode setting, which should set up most things correctly. The devtool option uses either a quick and dirty source map or a more production-ready one.

const path = require("path");

module.exports = env => {
    const isRelease = env === "RELEASE";
    return {
        mode: isRelease ? "production" : "development",
        devtool: isRelease ? "source-map" : "cheap-module-source-map",
        entry: {
            index: "index.es6",
            about: "about.js"
        output: {
            filename: "[name].js",
            path: path.resolve(__dirname, "dist"),
            publicPath: "/dist"
        resolve: {
            extensions: [".js", ".es6"],
            modules: ["Scripts", "node_modules"]
        module: {
            rules: [
                    test: /\.es6$/,
                    exclude: /(node_modules)/,
                    use: {
                        loader: "babel-loader",
                        options: {
                            presets: ["env"]

Single-Page vs. Multi-Page applications

In the entry section above, I’ve got two values for the two pages in this sample. index will be built using the index.es6 file as an entry point, and about will be built using about.js as the entry point. If your whole app is a SPA, you will have just one entry. If you have a multi-page application, you’ll need an entry for each page. Webpack crawls the entry to bundle up all the dependencies.

We are outputting our files to a /dist folder. For simplicity, the file name will be the entry name plus “.js”.


We needs some scripts to test out the different JavaScript syntax styles.

Here is index.es6, using the new JavaScript import and templated string options.

import $ from "jquery";

$("#setInCode").text(`This value was set in code using jQuery at ${new Date().toLocaleString()}.`);

And here is about.js, using older AMD-style modules and ES5 style JavaScript.

define(["jquery"], function($) {
    $("#setInCode").text("This value was set in code using jQuery at " + new Date().toLocaleString() + ".");

These scripts both find an element with jQuery and set some text to it and add the date/time. We want something simple to test if our pages are working with both old and new-style JavaScript.

Script references

Since I’m using a boilerplate ASP.NET MVC project template, the _Layout.cshtml file already has:

@RenderSection("scripts", required: false)

Each page, such as Index.cshtml and About.cshtml will then reference the built-by-webpack scripts in the /dist folder with. Index.cshtml will reference /dist/index.js, and About.cshtml will reference /dist/about.js like this.

@section scripts
    <script src="/dist/index.js"></script>

Build it

So we’ve got our npm packages installed, the webpack.config.js file is set up, our pages call our webpack-built scripts in the/dist folder . Let’s run it and test it out.

Add a new section for scripts to your package.json file:

  "scripts": {
    "build": "webpack --colors --watch",
    "build:release": "webpack --colors --env RELEASE"

This allows us to run npm build and npm build:release as shortcuts for running webpack commands. You can name the commands anything you like.

The options show colors in the console, and for the build script, it sets up a watch on the files to re-run the build if anything changes. You’ll want to use this for development. For the build:release script, we pass in —env RELEASE so the webpack.config.js script knows we want the build optimized for production.

You can also add another section to package.json to link up these build commands to the Visual Studio Task Runner Explorer.

  "-vs-binding": {
    "ProjectOpened": [

This will run your new build command when the project opens. Since it keeps a watch running, that should be all you need to development.

Task Runner

For production, you’ll want to stop the build task and manually run the build:release task in the Task Runner Explorer (or run it from command line, or as a before/after build script for your ASP.NET project, or on your build server…).

Run it

Run the web app after the build command completes and you should be able to browse to the Index and About pages and see the jQuery execute in both pages the same way, even though the syntax for importing jQuery was very different for each page.

Debug it

Note we are referencing index.js in Index.cshtml and not the source code file name index.es6. That’s because we’re not referencing the source code. We’re referencing the webpack built code in the /dist folder.

Don’t worry. You can still set breakpoints and watches and debug with your favorite browser dev tools when referencing the built code through source maps. When opening a file, look for the one served by webpack://. It will be the original source code seen through the source map. The other file will be the bundled/minified one.

open this

don't open this

Now what?

If you are on a project that isn’t using the newer JavaScript syntax and features, talk with your team. Chances are good they would like to be using the new stuff, too.

You don’t need to wait until you get put on a new project. You can start coding with the latest JavaScript syntax today, but still keep backward compatibility for your project’s ES5 script.