-
Notifications
You must be signed in to change notification settings - Fork 2
Webresources
Build Dataverse web resources in TypeScript, with strong typing, bundling, testing, and one-click deploy:
- Write web resources as TypeScript classes and bundle them to one JavaScript library with webpack.
- Deploy straight to Dataverse via the Web API — no external tooling.
- Generate strongly-typed
Xrmtypings with XrmDefinitelyTyped. - Unit test with Jest and xrm-mock.

Steps: Prerequisites → Generate typings → Create a class → Build → Deploy → Register on a form → Test.
- Node.js and the .NET SDK — see Getting Started. Everything else (webpack, jest, TypeScript) is installed per project as local dev dependencies; no global npm installs are needed.
- A connected environment.
Cross-platform: everything here — including Generate Typings — works on Windows, macOS, and Linux. Typings run a bundled cross-platform build of XrmDefinitelyTyped via
dotnet, authenticated with the extension's own connection (service principal or interactive sign-in).
Run Dataverse PowerTools: Generate Typings first. It runs XrmDefinitelyTyped to
produce, into a typings folder:
- Strongly-typed definitions for the Web API and each form's
FormContext(so you get IntelliSense for the exact fields on a form). - The bundled XrmQuery helper library (
webresources_src/lib/dg.xrmquery.web.min.js) that the webpack build references.
Re-run it whenever the schema or a form's fields change.
Verify: a typings/XRM folder appears and webresources_src/lib/ contains
dg.xrmquery.web.min.js.
Generate typings before your first build — the webpack config imports the XrmQuery library that this step produces.
Right-click the webresources_src folder (or use Create Web Resource Class). You'll
be asked for the class name, the Dataverse table, and the form the code runs
on. The library.ts entry point is updated to export the new class so webpack bundles
it, and a matching test is scaffolded.
export class Account {
static async OnLoad(executionContext: Xrm.ExecutionContext<unknown, unknown>): Promise<void> {
const form = <Form.account.Main.Information>executionContext.getFormContext();
this.BindEvents(form);
this.OnLoadLogic(form);
}
static async OnLoadLogic(form: Form.account.Main.Information) {
switch (form.ui.getFormType().valueOf()) {
case Xrm.FormType.Create: /* new records */ break;
case Xrm.FormType.Update: /* existing records */ break;
}
}
static async BindEvents(form: Form.account.Main.Information) {
form.data.entity.addOnSave(() => this.OnSave(form));
}
static async OnSave(form: Form.account.Main.Information) { /* on save */ }
}Casting the form as Form.account.Main.Information gives full IntelliSense for the
fields on that form — that type comes from Step 1.
Run Dataverse PowerTools: Build Webresources. webpack bundles the TypeScript into
bin/<prefix>_library.js (where <prefix> is your solution's publisher prefix), with a
source map for debugging.
Verify: bin/<prefix>_library.js exists.
Click Deploy to <org> in the Actions panel (or run Dataverse PowerTools: Build
and Deploy Webresources). It builds, upserts every file in bin/ directly to
Dataverse via the Web API, registers your decorated form events (next step), and
publishes everything once at the end. If a solution is configured
(webresourceSolutionName), the resources are added to it.
Verify: the log shows Deployed webresource: <prefix>_library.js and
Webresource deployment complete; the file appears in the maker portal under Web
Resources.
You can register the handler on a form in two ways:
Let the extension do it — add a PowerTools.RegisterEvent block with Add Form
Registration; it is applied automatically on every deploy (deploy-then-register is
the order that always works — the web resource must exist before a form can reference
it). The panel's Form Registrations card lists every decoration in your source —
click a row to jump to it. To re-apply registrations without deploying, run
Dataverse PowerTools: Register Form Events from the Command Palette:
<PowerTools.RegisterEvent[]>[
{
formId: "f182e4c6-6bff-ed11-8f6d-00224897cd92",
event: "onload",
executionContext: true,
triggerId: "d8b98c6a-d2a3-4465-bfec-aa75adf73c15",
function: "prefix.Account.OnLoad",
},
];Or do it manually in the form editor — add the web resource and register the function:
-
Library:
<prefix>_library.js -
Function:
<prefix>.ClassName.OnLoad - Pass the execution context as the first parameter.
See Configure a form to use a web resource.
Debug Web Resources (local) launches a dedicated browser where the live
model-driven app runs your local bundle: edits rebuild via webpack --watch
and hot-reload the form.
Your build, on the real form — the output panel reports it served the local bundle, and the form runs it:

Edit the source and the form picks it up, with no redeploy:
Breakpoints set in your TypeScript bind through the
dev build's inline source maps (new projects scaffold
devtool: "inline-source-map"; older projects can switch it in
webpack.dev.js). Re-running the command restarts the session cleanly, and
stopping the debugger from VS Code's toolbar tears the whole session down —
browser, watch build and interception. The panel shows a live session card with
a Stop button while it runs.
By default every class bundles into a single <prefix>_library.js. New
projects can instead choose one JS web resource per TypeScript file
(webresourceOutput: "perFile" in dataverse-powertools.json): each
webresources_src/*.ts builds to bin/<prefix>_<name>.js, with exports
merged onto the <prefix> global so forms still call
<prefix>.Class.Function. Deploy pushes every built file. (Local debug
interception currently targets the bundled library.)
Use the Form Intersects view to generate typings that target multiple similar forms at once (see the XrmDefinitelyTyped Form intersections docs). Click + to add an intersect, add the tables/forms to it, then re-run Generate Typings.
Create tests with Create New Test. Tests use Jest with xrm-mock as a fake Xrm
implementation, so web resources run locally without Dataverse:
import { Account } from "../Account";
import { XrmMockGenerator } from "xrm-mock";
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("someattribute", "somevalue");
const form = XrmMockGenerator.eventContext.getFormContext() as Form.account.Main.Information;
Account.OnLoad(XrmMockGenerator.eventContext);
it("keeps the value", () => {
expect(form.getAttribute("someattribute").getValue()).toBe("somevalue");
});Alongside form typings, the project bundles XrmQuery — a fluent, strongly-typed wrapper over the Web API:
const accounts = await XrmQuery.retrieveMultiple(x => x.accounts)
.select(x => [x.accountnumber])
.filter(x => Filter.equals(x.name, "Contoso"))
.promise();| Symptom | Fix |
|---|---|
Build fails on a missing dg.xrmquery.web.min module |
Run Generate Typings first — it produces that file. |
Generate Typings reports the bundled tool is missing |
The cross-platform typings tool ships inside the extension — reinstall/update the extension. (Older projects' paket-based XrmDefinitelyTyped.exe is no longer used.) |
Type errors like Namespace 'Form' has no exported member '<table>'
|
Re-run Generate Typings so the form/table types exist. |
| Deploy can't connect | Re-connect — see Refreshing a stale connection. |
Older web resource projects used spkl for deployment. If a spkl.json is present, run
Upgrade from Spkl — it reads the solution name into dataverse-powertools.json and
removes the old spkl config. Deployment then uses the built-in Web API upsert above.