Dynamics Ninja Logo

Blog.

Debugging PCF in Typescript

Cover Image for Debugging PCF in Typescript
·
7 min read

I see a lot of people lately started debugging PCFs by using Fiddler Autoresponder which is awesome and it's definitely a way to do it.

If you are still one of those that are not using Fiddler when debugging your controls you should definitely need to read a deep-dive article by Diana Birkelbach which will guide you on how to set up Fiddler for the first time.

Debugging with Fiddler is a huge time saver for every developer out there, but what happens when it's not enough.

Issues

You have that is using modern features like async/await pattern that is almost unreadable when translated to Javascript.

You separated complex control logic across multiple TypeScript files to make it more readable, but in the end, it's bundled in a single file and you lost that readability when debugging.

You have a critical data related issue on the production environment and you need to debug it there, but there is only production ready minified code that can't be debugged.

Solution

All the issues mentioned abouve can cause a lot of headaches when you are developing complex controls, but luckily there is always a tool that can help you with all the problems.

The answer to your problems is TypeScript's feature called Source Map.

Source Map

Source Map is a feature that allows us to debug through our TypeScript files rather than generated JavaScript.

Those files basically contain all the code from your TypeScript files inside one bundled file which is pretty much unreadable for a human, but machines can do some magic with them.

{
   "version":3,
   "sources":[
      "webpack://pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad/webpack/bootstrap",
      "webpack://pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad/./FancyTextControl/TextboxHelper.ts",
      "webpack://pcf_tools_652ac3f36e1e4bca82eb3c1dc44e6fad/./FancyTextControl/index.ts"
   ],
   "names": [],
   "mappings":";;QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;;;;;;;;AClFA;AAAA;AAAA;AAAA,4BAIC;;AAHiB,iCAAd,UAA6B,MAA7B,EAAsD,eAAtD,EAA6E;AACzE,UAAM,CAAC,WAAP,GAAqB,eAAe,GAAG,EAAvC;AACN,GAFgB;;AAGlB;AAAC,CAJD;;AAAuB,sC;;;;;;;;;;;;;;;;;;;ACCvB;;AAEA;AAAA;AAAA;AAAA,+BAwCC;;AAjCO,oCAAP,UAAY,OAAZ,EAA0D,mBAA1D,EAA2F,KAA3F,EAAiI,SAAjI,EAA0J;AACzJ,SAAK,oBAAL,GAA4B,mBAA5B;AAEA,QAAI,OAAO,GAAG,QAAQ,CAAC,aAAT,CAAuB,OAAvB,CAAd;AAEA,QAAI,eAAe,GAAG,OAAO,CAAC,SAAR,CAAkB,SAAlB,CAA4B,wBAA5B,CAAtB;AACA,kCAAc,cAAd,CAA6B,OAA7B,EAAqC,UAArC;AAEA,WAAO,CAAC,KAAR,GAAgB,OAAO,CAAC,UAAR,CAAmB,SAAnB,CAA6B,GAA7B,IAAoC,EAApD;AACA,WAAO,CAAC,gBAAR,CAAyB,OAAzB,EAAkC,KAAK,cAAL,CAAoB,IAApB,CAAyB,IAAzB,CAAlC;AAEA,SAAK,QAAL,GAAgB,OAAhB;AACA,aAAS,CAAC,WAAV,CAAsB,OAAtB;AACA,GAbM;;AAeA,0CAAP,UAAkB,OAAlB,EAA8D;AAC7D,SAAK,QAAL,CAAc,QAAd,GAAyB,OAAO,CAAC,IAAR,CAAa,iBAAtC;AACA,GAFM;;AAIA,0CAAP;AACC,WAAO;AACN,eAAS,EAAE,KAAK;AADV,KAAP;AAGA,GAJM;;AAMA,uCAAP;AACC,SAAK,QAAL,CAAc,mBAAd,CAAkC,QAAlC,EAA4C,KAAK,cAAjD;AACA,GAFM;;AAIA,8CAAP;AACC,SAAK,UAAL,GAAkB,KAAK,QAAL,CAAc,KAAhC;;AACA,SAAK,oBAAL;AACA,GAHM;;AAIR;AAAC,CAxCD;;AAAa,4C",
   "file":"bundle.js",
   "sourcesContent":[
      " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./FancyTextControl/index.ts\");\n",
      "export abstract  class TextboxHelper{\r\n    public static setPlaceholder(texbox:HTMLInputElement, placeholderText: string): void { \r\n        texbox.placeholder = placeholderText + \"\";\r\n\t}\r\n}",
      "import { IInputs, IOutputs } from \"./generated/ManifestTypes\";\r\nimport {TextboxHelper} from \"./TextboxHelper\";\r\n\r\nexport class FancyTextControl implements ComponentFramework.StandardControl {\r\n\r\n\tprivate _textbox: HTMLInputElement;\r\n\tprivate _notifyOutputChanged: () => void;\r\n\r\n\tprivate _textValue: string;\r\n\r\n\tpublic init(context: ComponentFramework.Context, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement) {\r\n\t\tthis._notifyOutputChanged = notifyOutputChanged;\r\n\r\n\t\tlet textbox = document.createElement(\"input\");\r\n\r\n\t\tlet placeholderText = context.resources.getString(\"Text_Input_Placeholder\");\r\n\t\tTextboxHelper.setPlaceholder(textbox,\"Type1...\");\r\n\t\t\r\n\t\ttextbox.value = context.parameters.textValue.raw || \"\";\r\n\t\ttextbox.addEventListener(\"input\", this.textboxOnInput.bind(this));\r\n\r\n\t\tthis._textbox = textbox;\r\n\t\tcontainer.appendChild(textbox);\r\n\t}\r\n\r\n\tpublic updateView(context: ComponentFramework.Context): void {\r\n\t\tthis._textbox.readOnly = context.mode.isControlDisabled;\r\n\t}\r\n\r\n\tpublic getOutputs(): IOutputs {\r\n\t\treturn {\r\n\t\t\ttextValue: this._textValue\r\n\t\t};\r\n\t}\r\n\r\n\tpublic destroy(): void {\r\n\t\tthis._textbox.removeEventListener(\"change\", this.textboxOnInput);\r\n\t}\r\n\r\n\tpublic textboxOnInput(): void { \r\n\t\tthis._textValue = this._textbox.value;\r\n\t\tthis._notifyOutputChanged();\r\n\t}\r\n}"
   ],
   "sourceRoot":""
}

An important thing to mention here is that your whole TypeScript code is copied to the source map file inside sourcesContent node.

When using Source Maps you can easily open your TypeScript files in dev tools of your choice and set breakpoints in them to start the much easier debugging session through much more readable TypeScript code.

Generate Source Map file

Now when we know what we need it's time to make it work with the PCF project.

The first thing we need to do is instruct the compiler to generate source maps for us every time we make a change in our code.

This is a pretty straightforward process and it requires only one simple property in the tsconfig.json file located in the root folder of our project.

We need to add sourceMap property to the compilerOptions object inside the JSON file and set the value to true.

Your JSON should look something like this:

{
    "extends": "./node_modules/pcf-scripts/tsconfig_base.json",
    "compilerOptions": {
        "typeRoots": ["node_modules/@types"],
        "sourceMap": true
    }
}

This was the easy part, but most of the people are stuck here because after the build source map file is not generated.

We need to do one more thing to make it work.

Modify the webpack config

Since PCF is using webpack as a bundler tool, we need to instruct it too that we want to use source maps because it's not initially set up there.

Webpack configuration is quite hard to find in your project and that's why most people fail in this step.

Configuration is hidden deep in the node_modules folder.

Path to the configuration file is:

node_modules/pcf-scripts/webpackConfig.js

Once you found the file open it and locate the oobConfig object.

You need to define new property inside this object called devtool and set the value to source-map like it's shown in line number 5.

const oobConfig = {
        // `production` mode will minify, while `development` will optimize for debugging.
        mode: buildMode,
        watch: watchFlag,
        devtool: 'source-map',
        watchOptions: {
            aggregateTimeout: 500
        },
        // Tells webpack where to start walking the graph of dependencies
        entry: entryPoint,
        output: {
            // This library value control what global variable the output control is placed in.
            library: constants.TEMP_NAMESPACE,
            pathinfo: true,
            filename: constants.BUNDLE_NAME,
            path: controlOutputDir
        },
        resolve: {
            // Tell webpack which extensions to try when it is looking for a file.
            extensions: ['.ts', '.tsx', '.js', '.jsx'],
            plugins: [new awesome_typescript_loader_1.TsConfigPathsPlugin()]
        },
        module: {
            rules: [
                {
                    // Tells webpack how to load files with TS or TSX extensions.
                    test: /\.(ts|tsx)$/,
                    use: [
                        babelLoader,
                        {
                            loader: require.resolve('ts-loader')
                        }
                    ],
                    exclude: /node_modules/
                },
                {
                    // Tell webpack how to handle JS or JSX files
                    test: /\.(js|jsx)$/,
                    use: [babelLoader]
                }
            ]
        }
    };

After that you are ready to generate the source map file.

Run a simple npm run build command and you should see that the bundle.js.map file is generated in command line output.

You can find those files in out/controls/<CONTROL_NAME> folder.

One more thing to check after the build is bundle.js file and search for one line at the end of the file.

//# sourceMappingURL=bundle.js.map

This line indicates a reference to the source map file that will be used in this JavaScript file.

Now we have everything to start debugging with the TypeScript files, but there is again a catch before we can actually do it.

If you try to pack the control and deploy it as a solution to the environment you will fast realize that you can't find TypeScript files in the dev tools inside the browser.

The answer is because the source map was not packed inside the solution and deployed to the environment. There is an option called inline source map that will basically pack the whole source map file inside the bundle.js file.

That's not a good practice because the size of the source map is most of the time larger than the original code size.

This is where Fiddler comes to the rescue and if you didn't use Fiddler before please take a look at the post mentioned in the first section before you proceed with this article.

Fiddler Configuration

We need to inject the bundle.js.map file inside the browser to make everything work.

You need to create a simple new rule in Auto Responder rule.

With this, you are finally ready to jump to the browser and test the source maps.

Make sure that Fiddler's Auto Responder is running and clear the browser cache before opening the form that has PCF control on it.

Once you open Dev Tools (Ctrl + Shift + I on Chrome/Edgium) you should navigate to the Sources tab.

You should see pcf_tools_ node there. When you expand it you should find all your TypeScript files there.

Open one of those files set a breakpoint and trigger the action that will hit that breakpoint.

Well done, you are finally debugging your TypeScript files.

Conclusion

This article showed you one more trick with Fiddler's Auto Responder feature that will help you to speed up your development process, but also save your time when debugging when trying to find what causes the bug in the production environment in a more continent way.

Important things to remember are that source maps are not there with the default project configuration, but can be easily added when you get used to it.

I mentioned an inline source map that can be used without Fiddler, but it's not a good idea to pack code one more time to the bundle.js file, definitely not in a production environment since the file size of the control is doubled.

I hope that from now on you will most likely stick with Fiddler debugging and use debugging harness less frequently.

Keep making great PCF controls!