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.
Scenario
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.
Sourcemaps
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:
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 SPFx
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?
Extending the Webpack configuration
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;
},
});
Inspecting the existing Webpack configuration
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.
Setting up sourcemaps in production
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.
Step 1: Loading the Typescript-generated sourcemap files
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",
}
}
);
Step 2: Instruct Webpack to generate sourcemaps
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.
Step 3: Configure the minimizer plugin to keep sourcemap references intact
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
}
Putting it all together
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;
}
});
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.
Hosting the js.map 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.
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!
Conclusion
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!
Sources
- Microsoft Docs - Extending Webpack in the SharePoint Framework toolchain
- Waldek Mastykarz blog on debugging production code with previous SPFx versions
- Terser Webpack plugin
spfx typescript sharepoint
Support me by sharing this
More
More blogs
Extending Microsoft 365 with custom retention controls
Thinking about a Purview post by Joanne C Klein, I've developed a small Microsoft 365 extension to view & manage retention controls.
Read moreBuilding 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 moreHow 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 moreThanks
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