iTwin.js Extensions

An iTwin.js Extension is a separate JavaScript module that can load on demand into an iTwin.js frontend application. The separate deliverable enables Extensions to provide extensibility of an iTwin.js application without having to re-bundle the application. Extensions have access to a limited set of iTwin.js functionality through @itwin/core-extension to enable seamless integration with the host app.

What can extensions do?

Extensions can be used for many different purposes, such as:

  • Add new a new tool or decorator to an existing application to better support your custom workflows.
  • Write event based processing, i.e., subscribe to an iModel Event, or Unified Selection Event, and process that change.

How to get started

An iTwin.js Extension at a minimum is a single JavaScript file and a manifest (i.e., a package.json file with some additional properties). To get started, create a new directory for the Extension.

Setup the Manifest

The first step to creating an extension is to create a manifest. Create the package.json by running, in the directory created above:

npm init --yes

The following properties must be added to the package.json file:

  • Name: the name of the Extension.
  • Version: the version of the Extension in the format x.x.x.
  • Main: where to find the javascript file.
  • ActivationEvents: events that define when the iTwin.js application should execute your Extension. Currently, we only support onStartup, which will execute the Extension as soon as it is added to the application.

Here is a minimal example:

// package.json
{
  "name": "my-new-extension",
  "version": "0.0.1",
  "main": "./dist/index.js",
  "type": "module",
  "activationEvents": [
    "onStartup"
  ]
}

Next, you'll want to add TypeScript, and the required dependencies for developing with the iTwin.js shared libraries:

  // package.json
  "dependencies": {
    "@itwin/core-extension": "^3.2.0"
  },
  "devDependencies": {
    "typescript": "~4.4.0",
    "@itwin/build-tools": "^3.2.0",
  },

A basic tsconfig.json file needs to be setup for development. Create a new tsconfig.json file next to the package.json with the following contents:

// tsconfig.json
{
  "extends": "./node_modules/@itwin/build-tools/tsconfig-base.json",
  "include": ["./*.ts", "./*.tsx"]
}

Next, add your favorite JavaScript tool to bundle your code together. Bundling is the process of combining multiple small source files into a single file. Bundling is necessary because when Extensions are loaded by the host iTwin.js application, they can only load and execute one file at a time. It is also a good idea to make the file as small as possible, a process known as minification. For JavaScript, popular bundlers are rollup.js, esbuild, and webpack. Here is an example using esbuild:

npm i --save-dev esbuild @esbuild-plugins/node-modules-polyfill @esbuild-plugins/node-globals-polyfill

Add the following entry into the scripts section in package.json to build the final bundle:

// package.json
  "scripts": {
    "build": "node esbuild.js"
  }

And finally, an esbuild configuration file (esbuild.js) should be placed next to the package.json. The configuration tells esbuild to bundle and minify the files, as well as adds some necessary polyfills:

// esbuild.js
import { NodeModulesPolyfillPlugin } from "@esbuild-plugins/node-modules-polyfill";
import { NodeGlobalsPolyfillPlugin } from "@esbuild-plugins/node-globals-polyfill";
import path from "path";
import esbuild from "esbuild";
import { fileURLToPath } from "url";
import { argv } from "process";

const dir = path.dirname(fileURLToPath(import.meta.url)).replace(/\\/g, "/");
const arg = argv.length > 2 ? argv[2] : undefined;

esbuild
  .build({
    entryPoints: ["src/index.ts"],
    bundle: true,
    minify: true,
    define: { global: "window", __dirname: `"${dir}"` },
    outfile: "dist/index.js",
    plugins: [new NodeGlobalsPolyfillPlugin(), new NodeModulesPolyfillPlugin()],
    format: "esm",
  })
  .catch(() => process.exit(1));

Creating the Extension

The only requirement when creating an Extension is to define a default function. The default function will execute on the Extension's activation event. The function would run immediately if onStartup was specified as the Activation Event. For example:

// src/index.ts
export default function main() {
  console.log("Hello from Extension!");
}

The above would print "Hello from Extension!". We can extend functionality by adding, for example, a tool to print to the console whenever it is clicked:

// src/tool.ts
import { PrimitiveTool } from "@itwin/core-extension";

export class ExtensionTool extends PrimitiveTool {
  public static override hidden = false;
  public static override toolId = "ExtensionTool";
  public static override namespace = "Extensions";
  public static override iconSpec = "icon-select-single";
  public async onRestartTool(): Promise<void> {
    return this.exitTool();
  }
  public override async run(): Promise<boolean> {
    console.log("Extension tool clicked!");
    return super.run();
  }
}
// src/index.ts

import { registerTool } from "@itwin/core-extension";
import { ExtensionTool } from "./tool";

export default function main() {
  console.log("Hello from Extension!");
  void registerTool(ExtensionTool);
  console.log("Tool Registered");
}

The final file structure should look something like this:

my-itwin-extension
│   package.json
│   esbuild.js
│   tsconfig.json
│
└───src
│   │   index.ts
│   │   tool.ts
│
└───dist
    │   index.js

Loading an Extension into an iTwin.js Application

Extensions need to be served somewhere so that the iTwin.js application can load the Extension at runtime.

A useful way to serve JavaScript locally is to add serve as a dev dependency npm i --save-dev serve, then adding a script to your package.json: "serve": "serve . -p 3001 --cors".

By default, every IModelApp has an instance of the ExtensionAdmin. The ExtensionAdmin controls the loading and execution of Extensions. An Extension must be added to ExtensionAdmin through an ExtensionProvider before it can be executed.

In the following example we add an Extension served at localhost:3001 through a RemoteExtensionProvider. You can also load Extensions locally as if they were npm packages through the LocalExtensionProvider.

const extensionProvider = new RemoteExtensionProvider({
  jsUrl: "http://localhost:3001/dist/index.js",
  manifestUrl: "http://localhost:3001/package.json",
});

The next step is to register your host with the ExtensionAdmin. The ExtensionAdmin will only load Extensions from registered hosts.

IModelApp.extensionAdmin.registerHost("localhost:3001");

The last step is to add the Extension to the ExtensionAdmin. Once the Extension has been added, its default function will immediately execute if the onStartup Activation Event was defined in the manifest.

IModelApp.extensionAdmin.addExtension(extensionProvider)
  .catch((e) => console.log(e));

Last Updated: 21 November, 2022