TypeScript Web Resources¶
DGT-CLI-010 — Form and command-bar (ribbon) scripts are written in TypeScript and bundled with webpack. The
canonical starting point is the
webresources-template
repository — clone it rather than wiring up TypeScript, bundling, and linting from scratch.
Toolchain¶
| Tool | Role |
|---|---|
| pnpm | Package manager — enforced via a preinstall hook (only-allow pnpm), so npm/yarn are rejected. |
TypeScript (strict) |
@types/xrm provides the Xrm API typings; tsconfig.json targets ES6 with moduleResolution: bundler. |
webpack + ts-loader |
Bundles each entry point into a single web resource file. |
| Biome | Linting and formatting in one tool (replaces ESLint + Prettier). Config in biome.json. |
Jest + xrm-mock |
Unit testing against a mocked Xrm — see Client-side Testing. |
Node.js current LTS is the recommended runtime. Install dependencies with pnpm install.
Project structure¶
src/
├── form/ # form scripts — entry points named *.form.ts
├── ribbon/ # command-bar scripts — entry points named *.ribbon.ts
└── model/ # generated early-bound TypeScript model (dgtp); excluded from linting
webpack discovers entry points by globbing src/form/*.form.ts and src/ribbon/*.ribbon.ts
and emits one bundle per entry into dist/scripts/form/<name>.form.js /
dist/scripts/ribbon/<name>.ribbon.js. Each bundle's exports are attached to a single global
object, DGT (webpack library.type: assign-properties) — so a handler written as
export namespace Account { export function onLoad… } in account.form.ts is referenced in the
form designer as DGT.Account.onLoad.
One entry point per form/ribbon; shared code in plain modules
DGT-CLI-020 — Only files matching *.form.ts / *.ribbon.ts become bundles. Put shared helpers in
ordinary .ts modules and import them — they're compiled into the bundles that use them,
rather than emitted as separate web resources.
Build & test¶
| Command | Does |
|---|---|
pnpm run build |
dependency check + Biome lint + development bundle (inline source maps). |
pnpm run build:prod |
Biome lint + minified production bundle. |
pnpm run bundle / bundle:prod |
webpack only — dev / prod. |
pnpm run lint |
Biome lint. |
pnpm run test / test-watch |
Jest (--runInBand). |
Register OnLoad only — bind everything else in code¶
DGT-CLI-030 — Register only the form's OnLoad event in the form designer (pointing at, e.g.,
DGT.Account.onLoad). Register all other handlers — OnSave, field OnChange, tab/section
visibility, and so on — programmatically from inside onLoad:
export namespace Account {
export function onLoad(executionContext: Xrm.Events.EventContext): void {
const formContext = executionContext.getFormContext<XrmTable.Account.AccountMainFormContext>();
formContext.data.entity.addOnSave(onSave);
formContext.getAttribute("name")?.addOnChange(onNameChange);
}
function onSave(executionContext: Xrm.Events.SaveEventContext): void { /* ... */ }
function onNameChange(executionContext: Xrm.Events.EventContext): void { /* ... */ }
}
This keeps a form's full event wiring visible in source control, rather than split between the form XML and the script — a form's behavior shouldn't require opening the form designer to understand.
Use the generated, form-typed model¶
DGT-CLI-040 — Generate typed models with dgtp codegeneration (see
TS Model Generation) into src/model/, and type the form context against
them instead of looking attributes up by bare string:
executionContext.getFormContext<XrmTable.Account.AccountMainFormContext>()exposes only the attributes, controls, tabs, and sections that exist on that specific form — a typo in an attribute name becomes a compile error.XrmTable.Account.FormContextis the looser, whole-table variant for scripts not tied to a single form.
The template's scripts/model-generate.ps1 wraps the dgtp call:
dgtp profile select <profile>
dgtp codegeneration ./src/model/ --config ./src/model/model.json --folder dataverse
src/model/ is generated and excluded from Biome — don't hand-edit it, and keep it out of
source control the same way as server-side models (see
Source Control).
Deprecated client APIs are off limits¶
DGT-CLI-050 — Client-side code never uses deprecated platform APIs:
- no
Xrm.Page— use theformContextfrom the execution context (as in the examples above) andXrm.Utility.getGlobalContext(); - no
parent.Xrmin HTML web resources — pass the context explicitly or use a PCF control instead; - no references to scripts shipped by the platform (jQuery has been removed from the platform — a web resource that referenced it breaks);
- no OData v2 (Organization Data Service) endpoints — only the Web API.
The @types/xrm typings and the Solution Checker both flag most of these, but the rule applies
regardless of whether a checker catches it. Track upcoming removals via
Microsoft's deprecation announcements.
Formatting & line endings¶
DGT-CLI-060 — Biome owns both formatting (4-space indent, single quotes, LF line endings) and linting —
run pnpm run lint and don't hand-tune style. Because Biome normalizes to LF, mixed line
endings are a non-issue here; the repo-wide .gitattributes
(Source Control) stays as the belt-and-braces
default.
Deploy¶
pnpm run build:prod produces dist/scripts/...; push it with dgtp push:
See dgtp push → web resource push for
--delete-obsolete and mapping-config behavior, and
Build Pipeline for where this runs in CI. The template ships a
ready-made Azure DevOps pipeline (root azure-pipelines.yml) that builds with pnpm on Node 24
and pushes via dgtp using a dgtp:xrm:connection connection string — see
dgtp CLI → Configuration.
The repo also still contains .azure-pipelines/build-push-webresources.yml, an older
npm/Node-22-based pipeline with unresolved # TODO placeholders that nothing currently
references — ignore it until it's cleaned up or removed.