@delegateas/create-dataverse-webresource
v1.0.12
Published
Scaffold a single-file HTML Dataverse web resource, deployed with XrmSync
Readme
create-dataverse-webresource
Scaffolds Dataverse HTML web resources that are one file each: TypeScript and CSS in src/,
bundled into self-contained .html files, deployed with
XrmSync.
Every folder under src/ is one web resource, so a single project can hold as many pages as you
like — see More than one web resource.
Run it inside the empty folder that should hold the project. The one positional name is used for the source folder, npm package, and deployed HTML file:
mkdir account-summary && cd account-summary
npx @delegateas/create-dataverse-webresource account-summaryYou are asked for anything you do not pass as an option:
npx @delegateas/create-dataverse-webresource account-summary \
--url https://contoso.crm4.dynamics.com \
--solution MySolution \
--prefix new \
--remote [email protected]:contoso/account-summary.git| Option | Meaning |
| --- | --- |
| -u, --url | Dataverse environment URL. Written to appsettings.json as DATAVERSE_URL. |
| -n, --solution | Unique name of the solution to deploy into. |
| -p, --prefix | Publisher prefix of that solution. Used to spell out the web resource name. |
| -r, --remote | Git remote to push the new project to. Optional — press Enter to skip when asked. |
| -y, --yes | Do not ask anything — for scripts and CI. |
The generated project is then installed, committed to a fresh git repository (and pushed, if you
gave a remote), built once, and uploaded once — so it is already live at
<url>/WebResources/<prefix>_<solution>/<name>.html, which the scaffolder prints when it
finishes.
Type generation is part of setup and runs by default. The initial upload runs when an interactive
terminal is available; npm run deploy does the same thing later.
What you get
./
src/<name>/main.ts your code — the page starts here
src/<name>/styles.css your styles
src/<name>/index.html the page shell
src/assets.d.ts lets TypeScript understand imported CSS and images
build.mjs lints src/, then bundles each web resource into ONE html file
dev.mjs build on save + sync to Dataverse, in one command
autocommit.mjs commits every build to git, so no work is ever lost
mock.mjs local Dataverse-shaped Web API for `npm run offline`
mock/metadata.json schema that does not exist in Dataverse yet
mock/data/*.json seeded records and canned server results
eslint.config.js the lint rules the build enforces
appsettings.json which environment and solution to deploy to, create/update only
AGENTS.md instructions for coding agents working in the project
.config/dotnet-tools.json the pinned XrmSync and XrmDefinitelyTyped versionsnpm run dev in that folder rebuilds on every save and keeps the environment in sync, so edits
show up in the real form after a hard refresh. npm run deploy is the one-shot equivalent.
More than one web resource
build.mjs treats every folder under src/ that holds an index.html as its own web resource,
and builds it into dist/<folder>.html. XrmSync uploads all of dist/ recursively, so that
becomes <prefix>_<solution>/<folder>.html in the solution.
Adding a page is therefore adding a folder — copy the one you have, rename it, done. There is no
list to keep in step: not in appsettings.json, not in build.mjs. AGENTS.md and the generated
README.md both say so, so an agent asked for a second page does the right thing on its own.
A folder under src/ without an index.html is not a web resource, which is where shared code
goes. The watcher works out the list when it starts, so a brand new folder needs npm run dev
restarted.
Never deletes anything
A scaffolded project is a sidecar: the solution it deploys into belongs to a real project, and
already holds web resources this one knows nothing about. XrmSync's default is to make the solution
mirror the folder it syncs, which would delete every one of them. So the template sets
"NoDelete": true on the sync item — a deploy creates and updates, and that is all it can do.
The cost is that removing a folder from src/ no longer removes the web resource from Dataverse.
That is the right trade for a sidecar, and both the generated README.md and AGENTS.md say so, so
neither the user nor an agent is surprised by it.
Why offline mode
npm run offline points XrmQuery at a real localhost /api/data/v9.2/ endpoint. Seed records are
ordinary JSON files under mock/data/, while mock/metadata.json declares tables, columns and
relationships that do not exist in Dataverse yet. The metadata generator emits a gitignored
.offline/schema.d.ts that merges with XrmDefinitelyTyped output, so UX work can begin before the
real schema exists.
The HTTP mock is tested through XrmQuery, including CRUD and upsert, alternate keys, lookup binding,
related queries, one-to-many and many-to-one expands, filtering, paging, formatted values,
associations, canned actions/functions, batch requests, basic FetchXML and configured predefined
queries. This is a lightweight table/query implementation, not XrmMockup: it does not execute
plugins, workflows, security or arbitrary server logic, and no XrmMockup settings are added to
appsettings.json.
Compared with the old implementation, generated projects keep one server file plus the data and metadata users actually edit; the browser-side engine and overlay source trees remain gone.
Why a bundler
Dataverse serves an HTML web resource as a single file — it cannot load a sibling .js, a
stylesheet, or anything from a CDN. tsc alone cannot produce one bundled file, so the template
uses esbuild (one dependency) in a small build.mjs that inlines the JavaScript, the CSS and
any imported images as data URIs. The build fails if an external reference ever survives, rather
than shipping a page that renders blank inside a form.
Every dependency of the generated project is dev-only — nothing ships to Dataverse except the HTML file.
Why lint in the build
These projects are largely written by coding agents, which do not read a lint script they were
never told to run. So build.mjs runs ESLint itself: npm run dev reports problems after every
rebuild, and npm run build and npm run deploy fail on them. That is the whole feedback loop, in
the command people already run.
Watch mode deliberately reports without blocking. If a lint error stopped dist/ from being
rewritten, XrmSync would keep syncing a stale file while you fixed it — a much worse failure than a
noisy console. One-shot builds do fail, so nothing broken is uploaded on purpose.
The rules live in templates/default/eslint.config.js, which is also the source of this package's
own config: eslint.config.js at the repository root imports the rule list from it, so the linter
that checks the scaffolder is the one it ships. They are strict on accessibility, size, complexity and unsafe DOM
writes — cheap to satisfy in a file this small, and exactly the mistakes that turn a web resource
into a blank form.
Why commit in the build
Same reasoning as the linting, aimed at a worse failure. The people these projects are for do not know git, so left to themselves they accumulate one enormous uncommitted diff and have no way back from a change that made things worse — which is exactly the change an agent is most likely to make.
Telling the agent to commit does not solve it: it is advice, and advice gets skipped. So the build
commits instead. autocommit.mjs runs git add -A and commits on every rebuild in npm run dev
and on every npm run build, whichever tool or human made the edit. Nothing to configure, nothing
agent-specific, and it keeps working when the project is opened in a different editor.
The commits are noisy and are not expected to build — a broken state is committed too, because it is
the state you most want to escape from. Failures are never fatal: no repository, no git, or no
configured identity all leave the build's exit code alone, and the identity case prints the two
git config --global commands that fix it, once per process. NO_AUTOCOMMIT=1 turns it off.
The generated AGENTS.md tells agents not to rewrite that history, since it is the user's only undo.
Requirements
- Node.js 20 or newer.
- The .NET SDK, for XrmSync. The scaffolder warns if it is missing but still creates a working project.
Developing this package
npm install
npm run lint # same rules the template ships; `npm run lint:fix` fixes what it can
npm test # builds, then runs the end-to-end tests in test/The tests scaffold into a temp folder, check the output, then install and build a real project and
assert the result is genuinely self-contained. The install-and-build tests skip themselves if the
npm registry is unreachable, and can be skipped deliberately with CDW_SKIP_BUILD_TESTS=1 — which
is what CI does on every push, since that suite installs a whole project and takes minutes.
Note that npm test runs the TypeScript tests through Node's type stripping, so developing this
package needs Node 22 or newer (see .nvmrc), even though the published CLI runs on Node 20.
Releasing
.github/workflows/publish.yml publishes to npm when a v* tag is pushed:
npm version patch # or minor / major — bumps package.json and commits
git push --follow-tagsThe workflow refuses to publish if the tag and package.json version disagree, then typechecks and
runs the full test suite before publishing. A prerelease version (0.2.0-beta.1) is published under
the next dist-tag, so npx @delegateas/create-dataverse-webresource keeps resolving to the last
stable release.
Authentication is npm trusted publishing over GitHub's
OIDC — there is no NPM_TOKEN to rotate, and every release gets a provenance attestation. This
requires one-time setup on npmjs.com: on the package's Settings → Trusted publisher, register
this repository with workflow publish.yml and environment npm. Until that exists, the publish
step fails to authenticate.
To rehearse the release path without publishing, run the Publish workflow manually from the
Actions tab with dry_run left checked.
Note: XrmSync's web resource support is not in its 1.0.0 release, so
templates/default/_dotconfig/dotnet-tools.json pins a -preview version deliberately. The pin is
1.0.0-preview.24 or later for a second reason: NoDelete arrived in that release, and the
template's appsettings.json sets it. Move the pin forward once a stable release includes both the
webresources command and NoDelete.
XrmSync 1.0.0-preview.25 also updates DataverseConnection to 1.2.4; keep that pin or a
newer listed XrmSync release so generated projects use the current Dataverse authentication layer.
The same file pins XrmDefinitelyTyped at 7.0.0-preview.3, matching the
@delegateas/xrmquery version in the template's package.json. Those two are a pair — the tool
generates the declarations the library's types merge into — so move both pins together. preview.3
is the floor: earlier previews could not generate large tables such as contact and systemuser,
and ignored the XrmDefinitelyTyped section of appsettings.json.
