Setting up a TyeScript project for Dataverse
In one of my recent projects, I had the need to set up a new TypeScript project. To be honest it was the first time I did this from scratch, which resulted in a few situations where I had to search for tutorials/explanations and combine all of those. That is the reason for me to write this blog post and describe how to do that.
There are a few great posts about this topic out there already (for example from Scott Durow or Oliver Flint). I found that all of them either describe more than I think I need in my projects, less than I need or are split into different posts which makes it harder to follow.
After you read this post you will know how to create a TypeScript project from scratch.
The following configuration is working as of writing this blog post (December 2020). Everything in the Front-end space is evolving very fast. There might be stuff that is not working as described when you read the post. If you find something please let me know.
At the end of this post, you can find a “Summary” section where I list all the commands we executed and the configuration files we created. To have a quicker start and skip all the explanations you could directly go there.
You can find the project with all the configuration on GitHub.
Why TypeScript?
Let’s take a minute and talk about this question first.
As described in one of my previous blog posts I do see three big advantages of using TypeScript over vanilla JavaScript
Strongly typed
Since TypeScript is strongly typed the job of writing it gets a lot easier. If you use a proper IDE (Visual Studio Code for example) it will show you type errors while developing.
Backward compatibility
In the end, TypeScript has to be transpiled/compiled to JavaScript. This could, for example, be done via the “tsc” command, which will transpile the TS file to the configured target ECMAScript version (ECMAScript is a standard to define JavaScript. You can read more about that here as well as about the browser support here). With the ability to target different/older versions of the standard the TS will get automatically transpiled into a JS file that is compatible with older browser versions. This means one can write readable code without thinking about compatibility. When transpiled it gets something that is compatible. Of course, it’s not always that easy. In the screenshot below you can see a part of our transpiled demo file.
Tests & Debugging
With TypeScript it is much easier to create tests that could be run automatically in a pipeline. Another big plus is that it is possible to debug the code with the help of source map files.
Objectives
The objectives of this post is to show how to set up a Typescript project for a Dataverse implementation from scratch.
This includes the following techniques (in addition to the basic TypeScript)
Type declaration
Since TypeScript is a strongly typed language (as mentioned above) we need to import the type declaration of Xrm. With that TypeScript “knows” which functions there are and which types to use.
There is the possibility to create type declarations based on the entities there are in Dataverse. One tool to achieve this is delegateas/XrmDefinitelyTyped. For a starting point, I think that is not mandatory (even though it could help), therefore we will not cover it in this blog post.
Bundling
One common technique when it comes to front end development is bundling. This is for example automatically activated/configured if you create a new Angular or React project.
The idea is to combine several ts files into one js file. With that development becomes easier, since one can separate files, and still there only will be a small number of different files to deploy and serve.
We will use a tool/package called Webpack to achieve this.
Linting
A linter is basically a spellchecker for one’s JavaScript/TypeScript code. It scans the code and enforces semantic code rules. For example to use let instead of var.
To achieve this we will use ESLint.
Code formatting
There are several tools that enforce correct code formatting. For example, so that one is not using both tabs and spaces in the same file.
Prettier is the tool we will use for that.
Delimitations
I would like to be clear on the delimitation. I will not talk about tests and deployment of the Webresources. This would definitely be too much for one blog post. I might create separated posts about those areas.
Prerequisites
In this chapter we will briefly learn about the prerequisites needed for the rest of this blog post.
Software
You need to have NodeJS and NPM installed on your machine. I do assume that is already the case, if not you can download it here.
I will use Visual Studio Code for everything related to TypeScript. One could of course use any other IDE.
Folder structure
I like clean folder structures where one knows directly what to expect, therefore I am going with the following:
Within the bigger project (which probably includes Plugins, custom workflow actions, Batchjobs and whatever) I have a folder called “front-end”. There we have one folder called “ts” that will include all our stuff around TypeScript (.ts files, tests, configuration, …) and a “Webresources” folder. The Webresources folder contains the actual webresources that get deployed to Dataverse. This also means that there is a js folder which will be the output folder for our transpile job.
front-end ├── ts │ ├── src │ │ ├── code │ │ │ ├── utils │ │ │ │ ├── *.ts │ │ │ ├── *.ts │ ├── test ├── Webresources │ ├── html │ │ ├── *.html │ ├── css │ │ ├── *.css │ ├── images │ │ ├── *.jpg │ │ ├── *.png │ │ ├── *.svg │ ├── js │ │ ├── *.js
Files
For the test purpose we need two files.
helper.ts
This file should be created in the “utils” folder in ts/src/code and contain the following code
export async function loadRecords( entityName: string, query: string, maxPageSize?: number, errorCallback?: (error: any) => void, ): Promise<any[] | null> { return new Promise<any>((resolve, reject: any) => { Xrm.WebApi.online.retrieveMultipleRecords(entityName, query, maxPageSize).then( function success(result) { if ( result !== null && result !== undefined && result.entities !== null && result.entities.length >= 1 ) { resolve(result.entities); } else { resolve(null); } }, function (error) { if (errorCallback !== null && errorCallback !== undefined) { errorCallback(error); reject(); } }, ); }); }
demo.ts
This file should be created in the “ts/src/code” folder and contain the following code
import * as helper from "./utils/helper"; export async function onLoad(executionContext: Xrm.Events.EventContext): Promise<void> { const formContext = executionContext.getFormContext(); const openChildren = await getActiveChildAccounts(formContext.data.entity.getId()); if (openChildren !== null) { formContext.ui.setFormNotification( "There are " + openChildren.length + " active Accounts related to this contact.", "INFO", "AmountChildAccounts", ); } } async function getActiveChildAccounts(parentId: string): Promise<any[] | null> { return helper.loadRecords( "account", "?$select=primarycontactid&$filter=statecode eq 0 and _primarycontactid_value eq " + parentId, ); }
Setup
Let’s get started with the real fun stuff. We will create and configure our Typescript project.
Basic setup
The first step will be to setup a basic TypeScript project. Later we will add all the mentioned tools.
Install packages
For the basic setup, we will open our front-end folder with Visual Studio Code and execute the following commands.
Open correct folder
In the Terminal (within VS Code) we go to the “ts” folder with the help of the following command.
cd ts
All the commands that follow will be executed in the Terminal within Visual Studio Code and within the “ts” folder.
npm init
This will guide you through some questions and create a “package.json” file.
npm init
Install TypeScript
Next step is to install TypeScript. This could be done globally
npm install -g typescript
or locally in the current folder (recommended).
npm install typescript --save-dev
Install Node types
After that we have to install node types
npm install @types/node --save-dev
Install XRM types
In addition to that, we need general Xrm Typings. Those can be installed with the following command.
npm install @types/xrm --save-dev
For the ease of this post, I will not go into detail about custom typings based on one’s solution (like one could create with tools like delegateas/XrmDefinitelyTyped for example).
Init tsc
The last step, for now, is to init the tsc command. This will create a tsconfig.json. In that file, one could configure how the transpiler behave. For example, one could define the target ECMAScript version here.
tsc --init
The created tsconfig.json should look something like the following (just with a lot more comments)
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ "target": "es5", "module": "commonjs", /* Strict Type-Checking Options */ "strict": true, "esModuleInterop": true, /* Advanced Options */ "skipLibCheck": true, "forceConsistentCasingInFileNames": true } }
Configurations
If you now run “tsc” it will generate two js files within the same folder as the ts files are. This behaviour we have to change.
To do so we will add/change a few things to the tsconfig.json (read more about what is possible). The first thing is that we would like to have the created JS-files in the js folder under webresources, to do that we will add (or comment out) the “outDir” config and set it to “../Webresources/js”. In addition to that, we would like to include all the files we have in the “src/code” folder, so that we just can run “tsc” without specifying which file to transpile. That can be done by adding an additional row at the end of the file.
"include": ["./src/code/**/*"]
Another thing I use to change is the target to “es6” (the default is “es5”). If we look at the browsers that are supported for Dataverse and which browsers support which version of ECMA Script “es6” should be fine since IE is not recommended to use for Dataverse. We also align the module configuration from “commonjs” to “es6”.
In addition to that we will add the following rows.
"allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "resolveJsonModule": true,
Let me briefly explain every configuration.
allowJs
Makes it possible to import js files in addition to just ts and tsx. Read more.
allowSyntheticDefaultImports
Allows an easier import syntax even when the module doesn’t export anything. Read more.
moduleResolution
“node” is today’s standard. Read more.
resolveJsonModule
Allows import of modules with a .json extension. Read more.
The whole file should now look like this.
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ "target": "es6", "module": "es6", "outDir": "../Webresources/js", /* Strict Type-Checking Options */ "strict": true, "esModuleInterop": true, /* Advanced Options */ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "resolveJsonModule": true, }, "include": ["./src/code/**/*"] }
If we now run “tsc” the js files will be created in the Webresource/js folder. The basic setup is created and works.
Add Webpack
Next step is to add webpack to our project.
Install packages
We have to install the following packages
webpack
The basic webpack package
webpack-cli
Package to be able to use webpack in the command line.
webpack-merge
Plugin to be able to merge different configs.
clean-webpack-plugin
Plugin to clean the output dir before every build.
ts-loader
OOB webpack can only load js files. With this loader, it is possible to load TypeScript files as well.
This can be done with the following command
npm install --save-dev ts-loader webpack webpack-cli webpack-merge clean-webpack-plugin
Configuration – webpack.config.*.js
For webpack to work we need a webpack.config.js. Since the config between development and production will differ we will create 3 config files in the ts folder.
webpack.config.shared.js
Contains all configuration that is shared between dev and prod. The file should have the following content.
const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { devtool: 'source-map', entry: { demo: './src/code/demo.ts' }, output: { filename: '[name].js', sourceMapFilename: 'maps/[name].js.map', path: path.resolve(__dirname, '../Webresources/js'), library: ['bebe', '[name]'], libraryTarget: 'var' }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, plugins: [ new CleanWebpackPlugin() ], resolve: { extensions: ['.ts', '.js' ], } };
devtool
Defines that we have separated source map files.
entry
A list of entry points where webpack starts to build the relation tree. In this example, we would like to start at the demo.ts.
output
Configures how the output file should be created. In our case the filename will be the name of the entry followed by “.js”. All the source map files will be stored in a dedicated folder “maps” and have the following schema as the file name: <entry name>.js.map. The path is our “webresources/js” folder. The library is “bebe” followed by the entry name, this means that all the functions will be called by “bebe.<entry name>.<function name>” (in our case “bebe.demo.onLoad”). So the first library should be your prefix. The last part is that the library should be within a var.
module
Here we add the ts-loader so that webpack knows how to handle TypeScript files.
plugins
We have added one plugin, “CleanWebpackPlugin”, which will clean the output folder before every build.
resolve
The last configuration defines which files to process. In our case only ts and js.
webpack.config.dev.js
Contains configuration that is specific to development. The file should have the following content.
const { merge } = require('webpack-merge') const sharedConfig = require('./webpack.config.shared') module.exports = merge(sharedConfig,{ mode: 'development', optimization: { minimize: false }, });
This file merges the sharedConfiguration wit everything we added here.
mode
Defines what the target mode is. Either Development or Production. This will change some stuff under the hood.
optimization
This configuration defines that we do not want to minify our js when we build as development.
webpack.config.prod.js
Contains configuration that is specific to production. The file should have the following content.
const { merge } = require('webpack-merge') const sharedConfig = require('./webpack.config.shared') module.exports = merge(sharedConfig, { mode: 'production' });
This file merges the sharedConfiguration wit everything we added here.
mode
Defines what the target mode is. Either Development or Production. This will change some stuff under the hood.
Configuration – package.json
Another part we have to configure is the package.json.
We will add the following rows under “scripts” within the package.json.
"build-dev": "webpack --config webpack.config.dev.js", "build": "webpack --config webpack.config.prod.js"
With that we do have two new commands to execute. One to build our solution in development and one for production.
If we now execute “npm run build-dev” we will have one demo.js and one demo.js.map in our webresouce/js folder. The demo.js file includes our function from the helper.ts file as well.
Add ESLint & Prettier
Next up are both ESLint and Prettier.
Install packages
We have to install the following packages
eslint
Core eslint package.
@typescript-eslint/parser
This package makes it possible for ESLint to handle TypeScript files.
@typescript-eslint/eslint-plugin
Plugin that contains some standard rules for TypeScript
prettier
Core package of prettier
eslint-config-prettier
To disable ESLint rules that might be in conflict with prettier rules.
eslint-plugin-prettier
Plugin that runs prettier from ESLint.
eslint-loader
Needed to include eslint in webpack build.
This can be done with the following command
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier eslint-loader
In addition to that we have to install both prettier and eslint globally. This is needed to be able to execute them via the command line later.
npm install -g eslint prettier
Configuration
ESLint
First of all we have to create the basic configuration of ESLint. To do that we could run the init command of ESLint.
npx eslint --init
Since we in either way have to change the generated file we can skip this command and copy the following content to a new file, “.eslintrc.js”, in the ts folder. Read more about the ESLint config here.
By using a JavaScript file instead of a JSON file one could add comments for other developers.
module.exports = { "env": { "browser": true, "commonjs": true, "es6": true }, "extends": [ "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", "plugin:prettier/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json", "sourceType": "module" }, "plugins": [ "@typescript-eslint", "prettier" ], "rules": { "@typescript-eslint/no-explicit-any": "off" } }
env
List of environments to define available global variables.
extends
Contains a list of stuff (Plugins for example) the ESLint configuration extends.
parser
Defines which parser to use. In our case the typescript parser we installed.
parserOptions
Configures options for the parser. In our case the project and the sourceType to make it possible to import modules.
plugins
A list of plugins that should be used.
rules
Contains the project-specific rules that are not already defined in the standard plugins we extend. In our case we disable the “no-explicite-any” rule.
With this configuration prettier will be executed whenever ESLint is executed.
Prettier
The configuration of prettier is much easier. Here we have to create another file in the ts folder called “.prettierrc.js”. The content should be the following
module.exports = { "semi": true, "trailingComma": "all", "singleQuote": false, "printWidth": 120, "tabWidth": 4, "endOfLine":"auto" }
Webpack
To run the ESLint within the webpack pipeline we have to add the following entry as the first one to the list of rules within the webpack.config.shared.js.
{ test: /\.(ts|tsx)$/, loader: 'eslint-loader', include: path.resolve(process.cwd(), 'src'), enforce: 'pre', options: { fix: true, } }
Run both
To run ESLint we have two different alternatives
- From the command line
- Directly in VS Code via an extension
Command line
To be able to run those from the command line we add the following two lines to our tsconfig.json file within the scripts object.
"format": "prettier ./src/code/**/*.ts --write", "lint": "eslint ./src/code/**/*.ts --fix"
format
When executed it will fix all the ts files format via prettier.
lint
When executed it will lint all the ts files via eslint.
VS Code
Normally one would like to run both prettier and eslint while developing and not only when transpiling the TypeScript files. This can be achieved by installing two plugins to VS code.
Prettier should work directly and show errors whenever a file is not following the configured formatting standard.
To get ESLint working we have to execute one additional step. Since our configuration of ESLint is not in our project root folder, but in the ts folder we have to tell ESLint where to search for it. To do so we create a “settings.json” file within a “.vscode” folder which should be located in the project root folder. The content of that file should be.
{ "eslint.workingDirectories": [ "ts" ] }
This manual one-time configuration has to be done by every developer since the .vscode folder will not be included in your repo.
Now ESLint and Prettier constantly scan all the ts files and show errors whenever something is not matching any of the configured rules.
Here is an example of a missing function return type and how it was fixed.


Conclusion
TypeScript makes it much easier to develop backward compatible JavaScript. But it is not the easiest thing to set up. On the other hand, it helps a lot when one has set it up, so it is most definitely worth it.
I hope this article helped you. Feel free to contact me if you have any questions. I am always happy to help.
Summary
Open the project folder with VS Code, cd into the ts folder with the Terminal within VS Code and execute the following commands.
npm init npm install --save-dev typescript @types/xrm ts-loader webpack webpack-cli webpack-merge clean-webpack-plugin eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier eslint-loader tsc --init npm install -g eslint prettier
Add the following files, or if present change the content to the following.
package.json
Add the following scripts
"build-dev": "webpack --config webpack.config.dev.js", "build": "webpack --config webpack.config.prod.js", "format": "prettier ./src/code/**/*.ts --write", "lint": "eslint ./src/code/**/*.ts --fix"
tsconfig.json
{ "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Basic Options */ "target": "es6", "module": "es6", "outDir": "../Webresources/js", /* Strict Type-Checking Options */ "strict": true, "esModuleInterop": true, /* Advanced Options */ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowJs": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "resolveJsonModule": true, }, "include": ["./src/code/**/*"] }
webpack.config.shared.js
Change the first library within the output definition to your prefix
const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { devtool: 'source-map', entry: { demo: './src/code/demo.ts' }, output: { filename: '[name].js', sourceMapFilename: 'maps/[name].js.map', path: path.resolve(__dirname, '../Webresources/js'), library: ['bebe', '[name]'], libraryTarget: 'var' }, module: { rules: [ { test: /\.(ts|tsx)$/, loader: 'eslint-loader', include: path.resolve(process.cwd(), 'src'), enforce: 'pre', options: { fix: true, } }, { test: /\.(ts|tsx)$/, use: 'ts-loader', exclude: /node_modules/, } ], }, plugins: [ new CleanWebpackPlugin(), ], resolve: { extensions: ['.ts', '.js' ], } };
webpack.config.dev.js
const { merge } = require('webpack-merge') const sharedConfig = require('./webpack.config.shared') module.exports = merge(sharedConfig,{ mode: 'development', optimization: { minimize: false }, });
webpack.config.prod.js
const { merge } = require('webpack-merge') const sharedConfig = require('./webpack.config.shared') module.exports = merge(sharedConfig, { mode: 'production' });
.eslintrc.js
module.exports = { "env": { "browser": true, "commonjs": true, "es6": true }, "extends": [ "plugin:@typescript-eslint/recommended", "prettier/@typescript-eslint", "plugin:prettier/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json", "sourceType": "module" }, "plugins": [ "@typescript-eslint", "prettier" ], "rules": { "@typescript-eslint/no-explicit-any": "off" } }
.prettierrc.js
module.exports = { "semi": true, "trailingComma": "all", "singleQuote": false, "printWidth": 120, "tabWidth": 4, "endOfLine":"auto" }
.vscode/settings.json in the project root folder
{ "eslint.workingDirectories": [ "ts" ] }
The post Setting up a TyeScript project for Dataverse appeared first on Benedikt's Power Platform Blog.
This was originally posted here.
*This post is locked for comments