Thumbnail Debugging production code in SPFx

Debugging production code in SPFx

Have you ever been in a situation where you needed to debug a production SharePoint Framework solution? I know I did! In this blog post, I’ll show you a way to debug your production code against your TypeScript files.

Note: A while ago in 2018, Waldek Mastykarz wrote a blog post on debugging production SPFx code. The SPFx build pipeline has changed a bit since then, still using Webpack, but using a different plugin to minify code. My colleague Bart-Jan Dekker found out how to get it working for newer versions of SPFx (1.18 and 1.19). So a huge thank you to both Bart-Jan and Waldek 🙏

Say you’ve deployed a new SPFx application and users have been encountering issues with it. The first thing you do is open the DevTools browser extension to see what’s up. But well, the devtools console may render errors that aren’t immediately useful to you… or maybe it doesn’t render errors at all! Sometimes you just want to step through the code to see what happens. But production code is minified, so stepping through it with breakpoints is complicated.

How about debugging the minified JavaScript code against your TypeScript source files? Just like you can do while developing. That would be awesome, right? And it’s possible… with a little bit of setup.

Debugging minified code is a challenge as old as JavaScript. To overcome this challenge, sourcemaps have been invented. Sourcemaps are files (With the file extension .js.map) that map the minified code back to the original source code. Every minified JavaScript-file can contain a reference to such a sourcemap file. The following screenshot is an example of how such a reference looks:

Sourcemap reference

The referred file contains the original code as well as mapping information to be able to map the minified code to the original code. So you don’t even need the original code, as it is already included in the sourcemap file. (Yes, those files can be huge!) The browser DevTools can load these sourcemap files, so that the original code is displayed in the debugger and breakpoints can be set. The VS Code debugger can load them as well, which is what it does when you use VS Code to step through your code while debugging a project in development.

Sourcemaps in action

In SharePoint Framework projects, sourcemap files are by default only generated while running in debug mode. To accomplish this, SPFx uses the TypeScript compiler as well the Webpack bundling tool.

SPFx first calls the TypeScript compiler to compile TypeScript files to JavaScript. The output of this compilation step also includes .js.map files, one file for every TypeScript file.

Webpack is then used for bundling the compiled JavaScript files. A source-map-loader plugin is used to also load the sourcemap files for further processing. The SourceMapDevToolPlugin then uses those loaded sourcemaps to generate a final sourcemap file for each JavaScript bundle file. It also adds the necessary sourcemap references to the generated JavaScript bundle files.

When an SPFx webpart is added to a SharePoint page, the JavaScript bundle file is loaded, and the browser DevTools can load the sourcemap files to display the original TypeScript code for you to step through with breakpoints.

💪 So debugging while developing is easy, but how about debugging production code?

As soon as you build your SPFx solution for production (using gulp bundle --ship), there are no sourcemap files and no references to them. This is caused by the Webpack configuration that’s used by the SPFx build process. This configuration is different when building in production mode: Typescript sourcemap files are generated by the typescript compiler, but they’re not processed by Webpack, Webpack doesn’t generate a sourcemap file for the bundle and it also doesn’t include a reference to a sourcemap in the minified JavaScript file bundle.

To get sourcemaps working for production builds, we need to extend the Webpack configuration, which can be done by adding some code to the gulpfile.js file:

build.configureWebpack.mergeConfig({
  additionalConfiguration: (config) => {
    const ship = process.argv.indexOf("--ship") > -1;

    // only change the webpack config for production builds
    if (ship) {
        // adapt the webpack configuration here     
    }
    
    // return the adapted configuration to the build pipeline
    return config;
  },
});

If you like to know more or less exactly how Microsoft does it, you can write the webpack config to a file and inspect it. You can do this by adding the following code to the gulpfile.js:

build.configureWebpack.mergeConfig({
  additionalConfiguration: (config) => {
    const fs = require('fs')
    fs.writeFileSync('webpackconfig.json', JSON.stringify(config));    
    
    return config;
  },
});

This will export the webpack configuration to a json file. You can of course export both the debug and the production version to check for differences.

So… back to business, how can we adapt the webpack config correctly?

Learning from how Microsoft does it in debug builds, we need to take a couple of steps to add sourcemaps to production builds successfully.

The first thing we need to do is instruct Webpack to include previously generated sourcemaps by using the source-map-loader plugin. If we would skip this step, Webpack would be able to generate sourcemaps, but it would have no notion of the original TypeScript sourcemaps. The result would be a sourcemap that refers to the uncompressed JavaScript, instead of the original TypeScript code. Which still is hard to read. Fortunately, configuring the source-map-loader is easy.

By default, the sourcemap loader will try to load every sourcemap in the directory. External node modules might have references to sourcemaps that are not included in the package and therefore generate warnings. Since we only want sourcemaps for our own code, we can also exclude the node_modules folder, so Webpack will not try to load sourcemaps from external code.

The following configuration section does exactly that and can be added to the webpack configuration in the gulpfile.js file:

config.module.rules.push(
  {
    "test": /\.js$/,
    "enforce": "pre",
    "exclude": [
      /node_modules/
    ],
    "use": {
      "loader": "source-map-loader",
    }
  }
);

To instruct Webpack to start generating sourcemaps, we need to add the SourceMapDevToolPlugin to the plugins list. Webpack will then process the loaded sourcemaps and generate a sourcemap file for every minified bundle file.

// We shouldn't use the devtool property at the same time as the SourceMapDevToolPlugin, so we reset it to undefined.
config.devtool = undefined;
config.plugins.push(new webpack.SourceMapDevToolPlugin({
    append: '\n//# sourceMappingURL=http://localhost:8282/[url]',
    filename: '[file].map'
}));

In our scenario we’re influencing the sourcemap reference that’s added by prefixing it with a specific localhost base URL. We’ll need this later on as we want to point the browser to the location where these js.map files are hosted. In this case we’re going to host them locally.

Lastly, we need to override the minimizer plugin that’s used by Webpack to minify the JavaScript: terser-webpack-plugin. The default configuration of Terser in the SPFx build pipeline cleans all comments from the code. However, sourcemap references are essentially comments as well, structured as follows as we saw: //# sourceMappingURL=<url>. We need to instruct terser so that it understands that sourcemap references shouldn’t be removed while minimizing.

// remove the existing minimizer object (terser)
config.optimization.minimizer.pop();

// create a new terser configuration object with enabled sourcemaps and a setting to not remove sourcemap reference comments.
const TerserPlugin = require('terser-webpack-plugin');
const terserPlugin = new TerserPlugin({
    sourceMap: true,
    output: {
        comments: /# sourceMappingURL=http:\/\/localhost:8282/,
        wrap_func_args: false
    },
    compress: { passes: 3, warnings: false },
    mangle: true
});

// add the new terser configuration object to the webpack minimizer configuration
config.optimization.minimizer.push(terserPlugin);

The above is the code you need for SPFx 1.19. SPFx 1.18 uses a different version of Terser, and therefore needs a slightly different setup, configuring the options via a different route:

const terserPlugin = new TerserPlugin({
    sourceMap: true
});
terserPlugin.options.terserOptions = {
    output: {
        comments: /# sourceMappingURL=http:\/\/localhost:8282/,
        wrap_func_args: false
    },
    compress: {
        passes: 3,
        warnings: false
    },
    mangle: true
}

By using the following code segment, we can do all three things. The code segment can be added to gulpfile.js:

build.configureWebpack.mergeConfig({
  additionalConfiguration: (config) => {
    const ship = process.argv.indexOf("--ship") > -1;

    // only change the webpack config for production builds
    if (ship) {
      // configure webpack to bundle existing typescript source maps. (Which is any sourcemap not in node_modules)
      config.module.rules.push(
        {
          "test": /\.js$/,
          "enforce": "pre",
          "exclude": [
            /node_modules/
          ],
          "use": {
            "loader": "source-map-loader",
          }
        }
      );

      // configure webpack to generate sourcemaps and refer to them with a localhost url
      const webpack = require('webpack');
      config.devtool = undefined;
      config.plugins.push(new webpack.SourceMapDevToolPlugin({
        append: '\n//# sourceMappingURL=http://localhost:8282/[url]',
        filename: '[file].map'
      }));

      // remove the existing minimizer object (terser)
      config.optimization.minimizer.pop();

      // create a new terser configuration object with enabled sourcemaps and a setting to not remove sourcemap reference comments.
      const TerserPlugin = require('terser-webpack-plugin');
      const terserPlugin = new TerserPlugin({
        sourceMap: true,
        output: {
          comments: /# sourceMappingURL=http:\/\/localhost:8282/,
          wrap_func_args: false
        },
        compress: { passes: 3, warnings: false },
        mangle: true
      });

      // add the new terser configuration object to the webpack minimizer configuration
      config.optimization.minimizer.push(terserPlugin);
    }

    // return the adapted configuration to the build pipeline
    return config;
  }
});
Note: Meddling with the webpack configuration can be complicated, and differs with used webpack versions. The above works for SharePoint Framework 1.19 and can work for 1.18 with the slightly different Terser configuration mentioned in Step 3.

If we now run gulp bundle --ship and gulp package-solution --ship, we should get an sppkg package file with the sourcemap references included in the minified JavaScript bundle files.

One important thing to realize is: the .js.map files are NOT included in the sppkg package file. Which is the reason we’ve added a localhost URL to the Webpack config. We’re actually saying to the browser: If there are sourcemap files available, they’ll be available from this location: http://localhost:8282/<filename>.js.map. We ourselves are still responsible for hosting the sourcemap files in that specified location.

So what we often do is build & deploy SPFx using CI/CD (In our case: Azure DevOps Pipelines). While building, we add an extra step to copy the .js.map files (which are located in the /dist/ folder) as a build artifact. The way we do that is by adding the following step to the build pipeline:

- task: CopyFiles@2
  displayName: 'Copy JSMap Files to: $(Build.ArtifactStagingDirectory)'
  inputs:
    sourcefolder: './dist'
    contents: '*.js.map'
    TargetFolder: '$(Build.ArtifactStagingDirectory)/jsmaps'

Whenever we want to debug production code, we can download the appropriate sourcemaps from Azure DevOps and host them using the following commandline script:

npx http-server -p 8282

This will run a lightweight node http server and host the files in the current directory. In this case: the directory where we downloaded our sourcemap files. It will host the files on http://localhost:8282. We can now refresh our browser on a SharePoint page where our SPFx webpart is located, and the browser will pick up the sourcemap files from our disk and allow us to debug the production code against our TypeScript files.

Debugging production code

Another idea would be to host the .js.map files on a central website or repository, so that debugging would be even easier. If you have great ideas, I’m always open to hear them!

💖 But as it is this is already fantastic!

Debugging production code can be a challenge, but with the right setup, it can be made easier. By extending the Webpack configuration, we can make sure that sourcemap references are included in the minified JavaScript files. By hosting the sourcemap files locally, we can debug the production code against our original TypeScript code. This way, we can step through the code and see what’s going on. Which is awesome!

👏 Once again, thanks to my colleague Bart-Jan Dekker for figuring out most of this himself!

🚀 Happy coding!


spfx typescript sharepoint
Support me by sharing this

More

More blogs

Building a SharePoint New Site Form Look-Alike
Building a SharePoint New Site Form Look-Alike

A post on building a SPFx Form Customizer with a Dynamic Form with field overrides to create an experience that looks like the default SharePoint new site form.

Read more
How to copy views in SharePoint
How to copy views in SharePoint

There's currently no way to copy list and library views in the SharePoint UI. I've created a SPFx sample demonstrating how it can be done using code.

Read more
Moving existing Content Types to the Content Type Hub
Moving existing Content Types to the Content Type Hub

How to start leveraging the power of the Content Type Hub on your existing information architecture.

Read more

Thanks

Thanks for reading

Thanks for reading my blog, I hope you got what you came for. Blogs of others have been super important during my work. This site is me returning the favor. If you read anything you do not understand because I failed to clarify it enough, please drop me a post using my socials or the contact form.


Warm regards,
Martin

Microsoft MVP | Microsoft 365 Architect

Microsoft MVP horizontal