Leveraging the Webpack Define Plugin in SPFx Projects

Here's a post that teaches you a technique that you can start leveraging in your SharePoint Framework (SPFx) projects right away!

By Last Updated: October 7, 2024 9 minutes read

Here’s a post that teaches you a technique that you can start leveraging in your SharePoint Framework (SPFx) projects right away! You know those projects where you have certain values you use in development or testing while others that you use in production? This would include things like different account or IDs used for telemetry services, different endpoints to sample or production data that you’re interacting with in your project.

How do you deal with those today? Some developers use different configuration files for each environment. Others change URLs at the last minute either manually or within an automated build process. Those ways work, but they are not ideal. What you really want are different configurations, similar to what we have in ASP.NET where there are different web.config files for each build type.

Did you know we can do the same thing in SPFx projects? Yup… thanks to leveraging a Webpack plugin, we can set values that are replaced at the time we build our projects and generate the resulting JavaScript bundle that SharePoint will use to load and run our application.

In fact, the SPFx build toolchain already adds some of these define plugins for us.

How Does It Work?

The webpack Define Plugin is quite basic in operation. When you run a webpack bundle it looks for specific constants in the resulting bundle and replaces it with the specified string. For instance, if you have seen in someone’s SPFx code sample have if DEBUG somewhere, they are using this capability.

But have you ever searched for where DEBUG is defined? You can’t find it! That’s because it’s defined as part of the webpack configuration that is dynamically generated on each bundle process by the SPFx build toolchain.

There is a way you can see what constants are defined using the webpack Define plugin in an SPFx bundling process.

Inspecting the Define Plugin Configuration in the SPFx Build Toolchain

It helps to first see what the default SPFx build process is doing behind the curtain. The webpack configuration is generated in memory when you run the task gulp build so this isn’t something you normally see. However, you can edit a SPFx’s project’s gulpfile.js to report the details on this. Add the following to the end of your gulpfile.js.

// existing gulpfile.js code goes here...

// add the following to the end of a default project's gulpfile.js
build.configureWebpack.mergeConfig({
  additionalConfiguration: (generatedConfig) => {
    console.log('<webpack.config type="object">');
    console.log(generatedConfig);
    console.log('</webpack.config>');

    console.log('<webpack.config type="string">');
    console.log(JSON.stringify(generatedConfig));
    console.log('</webpack.config>');

    return generatedConfig;
  }
});

This will hook into the existing webpack configuration and do two things:

  1. write out the webpack configuration created by the SPFx build toolchain as a JSON object
  2. repeat the previous step, but write it out using JSON.stringify()

Why do I do it twice? Because the first time you can see the plugin that’s used, but not the settings:

<webpack.config type="object">
{ module: { .. },
  plugins:
   [ SetPublicPathPlugin { options: [Object] },
     ModuleConcatenationPlugin { options: {} },
     DefinePlugin { definitions: [Object] } ] }
</webpack.config>
<webpack.config type="string">
{
  "module": { .. },
  // omitted config not relevant here...
  "plugins": [
    {
      "options": {
        "scriptName": {
          "name": "\\/[name](/blog/_[a-z0-9-]+)*\\\\.js",
          "isTokenized": true
        }
      }
    },
    {
      "options": {}
    },
    {
      "definitions": {
        "process.env.NODE_ENV": "\"dev\"",
        "DEBUG": true,
        "DEPRECATED_UNIT_TEST": false,
        "NPM_BUILD": false,
        "BUILD_NUMBER": "'dev-0000-00-00.000'",
        "DATACENTER": true
      }
    }
  ]
}
</webpack.config>

If you look at the first block in the output above (where type="object"), notice the plugins section lists the plugins used, but it’s hard to see their settings. The second block above (where type="string"), notice you can see the actual settings of the plugins. The last block of definitions is what is used by the webpack Define plugin. Notice the DEBUG:true? That’s how you can, within your SPFx project, use if (DEBUG) {..} else {..} to do things only when you’ve build your project in debug mode.

Now that you can see what is in the config, let’s edit it for our use.

Let’s first look at a real-world scenario for this…

Scenario: Using Azure Application Insights in SharePoint Framework Projects

One of my favorite uses for this is to set configuration settings between debug & release builds. A great use case for this is Azure Application Insights. App Insights works by setting an instrumentation key in your code to have your project write telemetry data to your specific instance of App Insights. The instrumentation key is a GUID.

I don’t want to have my debug builds and tests logging data to my App Insights instance, rather I only want a real production build doing that. Ideally I’ll switch the instrumentation key between builds. My debug build will have a bogus GUID of 00000000-0000-0000-0000-000000000000 while my production build will have a real one like 63YoMama-2e26-4Wrs-8Pnk-0fArmyBootsd.

But instead of switching this out, let’s change it at build time, depending if it’s a debug or release build!

Check out my "Mastering the SharePoint Framework" on-demand course for developers!

Are you ready to learn the SharePoint Framework? Check out our on-demand video course with over 30 hours of demos, explanation and downloadable code: Mastering the SharePoint Framework!

Start with the FREE Starter bundle that includes three (3) chapters including a walk-through of setting up your developer environment to start creating SharePoint Framework components.

Once you start working, jump to the Fundamentals bundle to learn the basics and start creating SharePoint Framework components. The Ultimate bundle will make you a master at the SharePoint Framework with automated testing, continuous monitoring, implement CI/CD practices, and learn other advanced techniques.

The Ultimate bundle includes the Fundamentals bundle and grants you access to our live monthly office hours meetings as well as access to our student-only mastermind group.

Step 1: Set up Replaceable Strings

To make like easy, I like to define constants that I can use in my project to ensure I don’t get things wrong. So, what I’ll do here is create a new TypeScript declaration file in the root of my project’s src folder named webpack-definePlugin-variables.d.ts with the following contents:

// avoids "TS2304: Cannot find name 'CONSTANT'" TSC error on builds because these
// strings are not defined in the code, rather webpack replaces them on build

declare var AZURE_APPINSIGHTS_INSTRUMENTATIONKEY: string

Step 2: Add Replaceable String in Code

The next step is to use this string in your code where your project needs the value. In an effort to focus on the webpack Define plugin and not Azure Application Insights, check out posts by Chris O’Brien & Velin Georgiev on using App Insights in SPFx projects.

To keep it simple, you need to do something like this:

// omitting other imports for brevity...
import {AppInsights} from "applicationinsights-js";

export default class AppInsightsCustomizerApplicationCustomizer
  extends BaseApplicationCustomizer<IAppInsightsCustomizerApplicationCustomizerProperties> {

  @override
  public onInit(): Promise<void> {
    let appInsightsKey: string = "/* update with YOUR App Insights key: */";
    AppInsights.downloadAndSetup({ instrumentationKey: appInsightsKey });

    // simple usage - all params will be derived..
    AppInsights.trackPageView();

    return Promise.resolve<void>();
  }
}

Update the code above to use the new constant that will get replaced by the webpack Define plugin:

// omitting other imports for brevity...
import {AppInsights} from "applicationinsights-js";

export default class AppInsightsCustomizerApplicationCustomizer
  extends BaseApplicationCustomizer<IAppInsightsCustomizerApplicationCustomizerProperties> {

  @override
  public onInit(): Promise<void> {
    // replace the instrumentation key member with the constant previously defined
    AppInsights.downloadAndSetup({ instrumentationKey: AZURE_APPINSIGHTS_INSTRUMENTATIONKEY });

    // simple usage - all params will be derived..
    AppInsights.trackPageView();

    return Promise.resolve<void>();
  }
}
Important: How do you properly initialize your component?
Learn why, in this case, why the onInit() method is a better option than using the class constructor in my article: SPFx Basics: Initializing components - constructor vs. onInit()

Step 3: Update the Webpack Config

The last step is to update the webpack configuration when a build happens.

For this, open the gulpfile.js in your project and make a few edits. I’ve commented the code below to explain what I’ve done, but to explain it in a bit more detail…

  1. Import (or require()) in the webpack npm package to get automatic IntelliSense in VSCode as it will find the TypeScript type declarations even though it’s a JavaScript file.
  2. Tap into the webpack config process by calling mergeConfig() that passes in the webpack config object that the build toolchain generated. This allows you to make some changes and return it back to the build pipeline that it will use when executing webpack.
  3. Obtain a reference to the Define plugin.
  4. Determine if the user is running a debug or ship/production build by checking the existing Define definition DEBUG.
  5. Add a new definition for our custom string to the list of Define definitions. Set the value based on the build:
// existing gulpfile.js imports & code goes here

const webpack = require('webpack');

// update webpack config
build.configureWebpack.mergeConfig({
  additionalConfiguration: (generatedConfig) => {
    // find the Define plugins
    let plugin, pluginDefine;
    for (var i = 0; i < generatedConfig.plugins.length; i++) {
      plugin = generatedConfig.plugins[i];
      if (plugin instanceof webpack.DefinePlugin) {
        pluginDefine = plugin;
      }
    }

    // determine if in debug / production build
    const isDebugMode = pluginDefine.definitions.DEBUG;

    // set azure appinsights string replacements
    pluginDefine.definitions.AZURE_APPINSIGHTS_INSTRUMENTATIONKEY = (isDebugMode)
      ? JSON.stringify('00000000-0000-0000-0000-000000000000')
      : JSON.stringify('63YoMama-2e26-4Wrs-8Pnk-0fArmyBootsd');

    // return modified config => SPFx build pipeline
    return generatedConfig;
  }
});

That’s it! You can run the config debug dump again from the start of this post, or just run your builds.

Testing

Run your first debug build using gulp bundle. Locate the generated JavaScript bundle ./dist/*.js and search for the string AppInsights.downloadAndSetup({ instrumentationKey:. Notice it is using the placeholder instrumentation key of all zeros.

Now run the same build, but this time do it with the production build flag: gulp bundle -p or gulp bundle –ship. Find the same file and run the same search (might help to turn on word wrapping as the bundle is minified to one line). Notice it’s using the real instrumentation key!

There you have it, a much easier way to change configuration values within your project by modifying the bundling process.

There’s One More Thing

While this is a neat little trick, there’s one thing I am not thrilled with: you still have configuration settings in your project. That isn’t ideal. Why? Because changing these values requires a code change and it also means these settings are in source control.

The ideal set up would be to instead have these values set at the build time, either from your workstation or as part of an automated process like Azure DevOps build / release pipelines.

To do this, first change the gulpfile.js to pull the values from environment variables which you can set either in your CI/CD steps, or from the command line:

pluginDefine.definitions.AZURE_APPINSIGHTS_INSTRUMENTATIONKEY = (isDebugMode)
  ? JSON.stringify('00000000-0000-0000-0000-000000000000')
  : JSON.stringify(process.env.AZURE_APPINSIGHTS_INSTRUMENTATIONKEY);

Now test it by running the following on the command line:

# if windows, use this
setx AZURE_APPINSIGHTS_INSTRUMENTATIONKEY=63YoMama-2e26-4Wrs-8Pnk-0fArmyBootsd
# if linux / macos, use this
export AZURE_APPINSIGHTS_INSTRUMENTATIONKEY=63YoMama-2e26-4Wrs-8Pnk-0fArmyBootsd

gulp bundle --ship

I add a bit more error trapping in my gulpfile.js to check if the environment variable is present. If not, I throw an error.