-
-
Notifications
You must be signed in to change notification settings - Fork 0
08 WebResources Project
flowline clone and flowline init scaffold a WebResources/ project alongside your Plugins/
project for your JavaScript or TypeScript source, images, and other web resources.
It has as-simple-as-possible Rollup build pipeline to get you started.
The dist/ folder it produces is what flowline push deploys to Dataverse — see
05-Push-WebResources for how push wires up form event handlers and web resource dependencies
automatically as it goes, no Maker Portal visits required.
WebResources/
├── 📂 src/
│ ├── 📂 modules/ ← shared code, imported by entry points
│ │ └── 📜 common.ts
│ ├── 📜 account.ts ← entry point (one per form/ribbon)
│ └── 📜 contact.ts
├── 📂 public/ ← static files copied to dist/ unchanged
│ ├── 📂 images/
│ └── 📜 *.html
├── 📂 dist/ ← build output (gitignored) — synced by 'flowline push'
├── 📜 eslint.config.mjs ← ESLint config
├── 📜 package.json ← npm config, packages, run scripts
├── 📜 rollup.config.mjs ← Rollup config
├── 📜 tsconfig.json ← TypeScript config
└── 📜 <SolutionName>.WebResources.csproj ← .NET project file that runs 'npm run build'
This structure is just an easy starting point but it is up to you if you want to modify it. Flowline
only looks in the dist/ folder to push the web resources. You can change
anything about the structure or build process, see Using a different build tool.
The name is not how Flowline finds it.
You don't configure this. push reads the solution file and figures out which project is the
WebResources project automatically — by ruling out plugin projects, PCF controls, and test projects,
then picking the one that looks most like a web resources project. If it genuinely can't tell two
projects apart, it stops and asks you to clarify rather than guessing.
That means both the folder and project name are yours to rename. WebResources/<SolutionName>.WebResources.csproj
is just what clone scaffolds, not what push requires — as long as the solution file references the
project, Flowline will find it.
Warning
Power Apps Portals and Code Apps projects aren't excluded yet. Both use the same TypeScript/React tooling as a WebResources project, so Flowline can't tell them apart. This only matters if one sits beside a WebResources project in the same solution — a solo WebResources project still resolves safely — but it's a known gap if you add one later.
Node.js must be installed. Restore packages once after cloning:
npm installTo update packages to their latest versions:
npm updatenpm run buildOr via .NET — the WebResources project wires Rollup into MSBuild, so dotnet build triggers
npm run build automatically. Rollup decides what to rebuild:
dotnet buildnpm run build does two things in sequence:
-
build:static— copiespublic/todist/unchanged (images, HTML) -
build:js— runs Rollup, compiles every.ts/.jsinsrc/to an IIFE indist/
flowline push --scope webresources builds the project by calling dotnet build, then syncs dist/ to Dataverse.
If you already built and tested, flowline push --scope webresources --no-build skips the build and
pushes the existing dist/.
Either way, push refuses to run when dist/ is missing or empty — an empty folder
would delete every web resource in the solution.
You can write scripts in TypeScript or plain JavaScript — whichever you prefer. The scaffold
uses TypeScript by default, but Rollup handles .js files identically. Mix and match freely.
Write each script as an ES module.
ES modules are the modern JavaScript way to split code across files: you export what should be
accessible from outside, and import what you need from other files. Functions without export are
private to that file.
For Dataverse, any function you want to register as an event handler (like onLoad, onSave,
onChange) must be exported (use export function) so Rollup can expose it in the IIFE bundle. Functions:
// src/account.ts
import { hide } from './modules/common'
export function onLoad(executionContext: Xrm.Events.EventContext): void {
const formContext = executionContext.getFormContext();
hide('fax', formContext);
var myName = formatName(formContext.getAttribute('name')?.getValue() ?? '');
}
// not exported — only used internally
function formatName(name: string): string {
return name.trim();
}The same in plain JavaScript:
// src/account.js
import { hide } from './modules/common.js'
export function onLoad(executionContext) {
const formContext = executionContext.getFormContext();
hide('fax', formContext);
var myName = formatName(formContext.getAttribute('name')?.getValue() ?? '');
}
// not exported — only used internally
function formatName(name) {
return name.trim();
}Register Account.onLoad as the event handler. Use auto-wiring form event handlers for that.
Every .ts or .js file directly in src/ is treated as an entry point — Rollup compiles it to
a separate .js file in dist/:
src/account.ts → dist/account.js
src/account-ribbon.ts → dist/account-ribbon.js
src/contact.ts → dist/contact.js
Files in src/modules/ are shared code — they are imported by entry points but not compiled as
standalone files.
Put utilities in src/modules/. They are plain ES modules — import and export normally:
// src/modules/common.ts
export function hide(field: string, formContext: Xrm.FormContext): void {
formContext.getControl(field)?.setVisible(false);
}Dataverse loads JavaScript web resource libraries as classic, global-scope scripts, not ES modules —
it invokes your registered event handler by name off a global object (LibraryName.functionName).
Native import/export doesn't work in that context. Rollup solves this by bundling your ES module
source into an IIFE (Immediately Invoked
Function Expression): a self-contained bundle that exposes your exported functions as a global
variable, without polluting the rest of the page.
Rollup derives the global variable name from the filename, converted to PascalCase:
| File | Global name | Dataverse event handler |
|---|---|---|
account.ts |
Account |
Account.onLoad |
account-ribbon.ts |
AccountRibbon |
AccountRibbon.onSave |
contact.ts |
Contact |
Contact.onLoad |
To add a namespace prefix (MyCompany.Account.onLoad), set namespacePrefix in rollup.config.mjs:
const namespacePrefix = 'MyCompany.'; // don't forget the trailing dotThe template ships with strict TypeScript (strict: true, target ES2022) and
@microsoft/eslint-plugin-power-apps —
a Microsoft ESLint plugin that flags deprecated and unsupported Dataverse API usage. Violations
print during build but don't block it, so you get a fast feedback loop by default; tighten this in
rollup.config.mjs (eslint({ throwOnError: true })) once you want violations to fail the build.
If you write plain JavaScript and don't want TypeScript, remove @rollup/plugin-typescript and
tsconfig.json and update rollup.config.mjs to drop the TypeScript plugin.
Place images, HTML, and other static assets in public/. They are copied to dist/ unchanged on
every build, preserving the folder structure:
public/images/logo.png → dist/images/logo.png
public/help.html → dist/help.html
If you prefer plain JavaScript the old-fashioned way — no modules — just put your .js files
in public/ and ignore src/ entirely. They are copied to dist/ as-is and deployed by flowline push.
See 05-Push-WebResources for how flowline push wires up form events and dependencies, and
deploys dist/ to Dataverse.
Rollup is a good fit for Dataverse web resources for a few specific reasons:
-
Named IIFE global per entry point, auto-discovered. Matches exactly what Dataverse expects
(
Account.onLoadoff a global).rollup.config.mjsscanssrc/and turns each file into its own build — adding a newcontact.tsfile is picked up automatically, no config change. Vite can't do this for multiple entries — it's Rollup underneath, and Rollup itself refusesiife/umdoutput for more than one entry point. esbuild can do a named global, but only one per build call, so multiple files need the same manual looping this config already does. - Minimal config. No dev server or HMR needed — you test inside a live Dataverse form, not a previewed page, so that machinery would be dead weight.
You can replace Rollup with any tool — Vite, esbuild, Webpack, or plain tsc. The only contract
is that npm run build populates dist/ with the files to deploy. The WebResources project calls
npm run build; it does not care what runs inside it.
To switch:
- Replace
rollup.config.mjsand updatedevDependenciesinpackage.jsonwith your preferred tool. - Update the
build:jsscript inpackage.jsonto call your tool instead ofrollup --config. - Ensure your tool outputs IIFE bundles to
dist/with PascalCase global names, or adjust your Dataverse event handler registrations to match whatever output format you choose.
The build:static script (copies public/ to dist/) is independent and can stay as-is or be
folded into your build tool's config.