npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vela-client

v0.1.6

Published

Typed, testable, form-centric client scripting runtime and test harness for Dataverse model-driven forms.

Readme

Typed, testable, form-centric TypeScript for Dataverse model-driven client scripting - the browser-side half of Vela. It gives form scripts the same strongly-typed, unit-testable "firm ground" that Vela brings to server-side plugins: per-form typed models, a class-based API faithful to the Client API, and a fake-Xrm test harness so scripts are tested without a browser.

Status

Approaching the 0.1.0 vertical slice (generate -> write -> test). The form-scripting runtime ships: VelaForm(Model), the typed guaranteed-present this.fields / this.controls / this.tabs accessors, lifecycle dispatch (onLoad / onSave), and the FormModel contract type with its CONTRACT_VERSION stamp. The fake-Xrm test harness ships too, as the vela-client/testing subpath, so form scripts are unit-tested off-browser against the real dispatch. The typed Web API layer ships as the vela-client/webapi subpath: a typed query/CRUD builder and typed action/function clients over the native Xrm.WebApi, loaded only when a script queries (kept out of the base form-scripting bundle).

Still on the roadmap: the form-model generator (vela models --forms, which lives in the .NET Vela repo) and integrated deploy plus FormXML event wiring. See ROADMAP.md for the full phased plan.

Install

npm install vela-client

The fake-Xrm test harness is not a separate package - it ships as the vela-client/testing subpath of this same package.

How it works today

The VelaForm runtime and the fakeForm harness below both ship now, so a form script and its unit test are exactly what you write today. The one piece still on the roadmap is the generator that emits the form model, so for now you hand-write (or stub) the model const yourself against the FormModel contract type.

A form model

One const per form. Today you hand-write or stub this; once the generator ships (vela models --forms) it is emitted per form from FormXML plus metadata. Only the fields, tabs, sections, and subgrids actually on that form appear; each field carries its kind (which drives the typed accessor), its base-language label, its form-level required, and its place in the tab/section tree.

// generated/contact.information.form.ts  - the generator that emits this is on the roadmap
import type { FormModel } from 'vela-client';
import { contact_gendercode, contact_statuscode, contact_statecode } from './option-sets';

export const ContactInformation = {
  entity: 'contact',
  formId: '7c1e3f2a-0000-0000-0000-000000000001',
  formType: 'Main',
  fields: {
    firstname:        { kind: 'string',  label: 'First Name',   required: 'none',     at: { tab: 'general', section: 'contactInfo' } },
    lastname:         { kind: 'string',  label: 'Last Name',    required: 'required', at: { tab: 'general', section: 'contactInfo' } },
    emailaddress1:    { kind: 'string',  label: 'Email',        required: 'none',     at: { tab: 'general', section: 'contactInfo' } },
    birthdate:        { kind: 'date',    label: 'Birthday',     required: 'none',     at: { tab: 'general', section: 'contactInfo' } },
    creditlimit:      { kind: 'money',   label: 'Credit Limit', required: 'none',     at: { tab: 'details', section: 'finance' } },
    gendercode:       { kind: 'choice',  label: 'Gender',       required: 'none',     options: contact_gendercode, at: { tab: 'general', section: 'contactInfo' } },
    statuscode:       { kind: 'choice',  label: 'Status',       required: 'required', options: contact_statuscode, at: { tab: 'general', section: 'contactInfo' } },
    donotemail:       { kind: 'boolean', label: 'Do Not Email', required: 'none',     at: { tab: 'general', section: 'contactInfo' } },
    parentcustomerid: { kind: 'lookup',  label: 'Company',      required: 'none',     targets: ['account', 'contact'], at: { tab: 'general', section: 'contactInfo' } },
  },
  layout: {
    tabs: {
      general: { label: 'General', sections: { contactInfo: { label: 'Contact', fields: ['firstname', 'lastname', 'emailaddress1', 'birthdate', 'gendercode', 'statuscode', 'donotemail', 'parentcustomerid'] } } },
      details: { label: 'Details', sections: { finance:     { label: 'Finance', fields: ['creditlimit'] } } },
    },
  },
  subgrids: { Opportunities: { entity: 'opportunity', relationship: 'opportunity_parent_contact' } },
} as const satisfies FormModel;

A form script

A form script is a class extends VelaForm(Model) with lifecycle methods. Because the model comes from the form, this.fields.x / this.controls.x are guaranteed to exist - only .getValue() may be null. Method names stay faithful to the Client API, and onSave receives a typed SaveEventArgs. The last line wires the class to the global onLoad / onSave names FormXML binds to - one defineForm call per form web resource.

// forms/ContactInformationForm.ts
import { VelaForm, defineForm, SaveEventArgs } from 'vela-client';
import { ContactInformation } from '../generated/contact.information.form';
import { contact_gendercode } from '../generated/option-sets';

export class ContactForm extends VelaForm(ContactInformation) {
  onLoad() {
    this.fields.gendercode.addOnChange(this.applyGenderRules); // real Client API name, auto-bound
    this.applyGenderRules();
  }

  applyGenderRules() {
    const female = this.fields.gendercode.getValue() === contact_gendercode.Female;
    this.tabs.details.setVisible(female);
    this.fields.lastname.setRequiredLevel(female ? 'none' : 'required');
    this.controls.creditlimit.setVisible(!female); // control (UI), not attribute
  }

  async onSave(e: SaveEventArgs) {
    if (!this.fields.emailaddress1.getValue()) {
      e.preventDefault();
      this.setFormNotification('Email is required', 'ERROR'); // real ui.setFormNotification
    }
  }
}

// FormXML binds these global names; one defineForm call is the whole wiring.
export const { onLoad, onSave } = defineForm(ContactInformation, ContactForm);

A unit test

The harness reuses the same typed field API as the runtime and runs the real dispatch off-browser. The only harness-specific verbs are fakeForm(Model, FormClass), form.load(), and form.triggerSave().

// tests/ContactInformationForm.test.ts
import { fakeForm } from 'vela-client/testing';
import { ContactInformation } from '../generated/contact.information.form';
import { contact_gendercode } from '../generated/option-sets';
import { ContactForm } from '../forms/ContactInformationForm';

test('gender rules toggle the details tab and the last-name requirement', async () => {
  const form = fakeForm(ContactInformation, ContactForm);
  await form.load();

  form.fields.gendercode.setValue(contact_gendercode.Female);
  form.fields.gendercode.fireOnChange(); // real Client API - triggers the attached handler

  expect(form.tabs.details.getVisible()).toBe(true);
  expect(form.fields.lastname.getRequiredLevel()).toBe('none');
});

A typed query

The vela-client/webapi subpath is a typed layer over the native Xrm.WebApi. Column names are typed keys, $select narrows the result, and $filter is a typed builder. See docs/guides/client-queries.md.

import { createWebApi } from 'vela-client/webapi';

const { entities } = await createWebApi() // no argument: binds to the ambient Xrm.WebApi
  .retrieveMultiple<Account>('account')
  .select('name', 'revenue')
  .filter((f) => f.and(f.eq('statecode', 0), f.gt('revenue', 1000)))
  .orderBy('name')
  .top(20)
  .execute(); // entities: Pick<Account, 'name' | 'revenue'>[]

Design principles

  • Model the form, not the table. At runtime the Client API only sees the columns and controls on the loaded form, so a typed model is generated per form, not per table.
  • Guaranteed-present accessors. Because the model comes from the form, this.fields.x / this.controls.x are guaranteed to exist; only .getValue() may be null. Only tryField('offModel') returns nullable.
  • Names faithful to the Client API. No renaming of getValue / setVisible / addOnChange and friends, so experienced Dataverse developers are immediately at home. The attribute (data) vs control (UI) split is kept exactly as the Client API has it.
  • Own our Client API typings. No @types/xrm; only the small, stable surface we wrap is typed, which bounds the maintenance cost. Everything else stays reachable through the escape hatch.
  • Tiny runtime. The library loads on every form open, so the core stays aggressively tree-shakeable, and the Web API query helper is lazy-loaded rather than pulled into the base form-scripting path.
  • A raw escape hatch is always available. this.form (raw formContext) and this.executionContext (raw ExecutionContext) are always there, so anything Vela has not wrapped can still be done with the real Client API.

Two-repo layout

This repository is the TypeScript runtime plus the fake-Xrm harness. The C# CLI that generates the typed form models (vela models --forms) and deploys the web resources (vela apply) lives in the main Vela repo at https://github.com/allandecastro/vela. The two are linked by a versioned contract fixture - the exact shape of the generated .ts - stamped with CONTRACT_VERSION and checked at runtime, so the generator and the library can never silently desync. See AGENTS.md for how the pieces fit together and docs/design-examples.md for the concrete target shapes.

Development

npm run build          # bundle with tsup (ESM, per-entry chunks)
npm run typecheck      # tsc --noEmit (strict)
npm run test           # Vitest
npm run test:coverage  # Vitest with v8 coverage thresholds
npm run size           # size-limit budget for each entry point
npm run mutation       # Stryker-JS mutation testing (form, runtime, webapi)

Quality gates run in CI: coverage thresholds, per-entry bundle-size budgets, and (on changes to the correctness code) Stryker-JS mutation testing. All TypeScript jobs run on hosted Linux.

License

MIT