@xrmforge/typegen 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -489,6 +489,10 @@ interface SystemFormMetadata {
489
489
  formxml: string;
490
490
  description: string | null;
491
491
  isdefault: boolean;
492
+ /** Form type (systemform_type): 2 = Main, 7 = Quick Create, ... */
493
+ type: number;
494
+ /** Activation state (systemform_formactivationstate): 0 = Inactive, 1 = Active */
495
+ formactivationstate: number;
492
496
  }
493
497
  /** Parsed data-bound control from FormXml (bound to an attribute) */
494
498
  interface FormControl {
@@ -541,6 +545,8 @@ interface ParsedForm {
541
545
  name: string;
542
546
  formId: string;
543
547
  isDefault: boolean;
548
+ /** Form type (systemform_type): 2 = Main, 7 = Quick Create */
549
+ type: number;
544
550
  tabs: FormTab[];
545
551
  /** All data-bound controls across all tabs/sections (flattened) */
546
552
  allControls: FormControl[];
@@ -712,10 +718,14 @@ declare class MetadataClient {
712
718
  */
713
719
  getStateAttributes(logicalName: string): Promise<StateAttributeMetadata[]>;
714
720
  /**
715
- * Get and parse Main forms (type=2) for an entity.
716
- * Returns parsed form structures with tabs, sections, and controls.
721
+ * Get and parse the form types relevant for type generation (Main type=2 and
722
+ * Quick Create type=7), restricted to ACTIVE forms (formactivationstate=1).
723
+ * Inactive forms are leftovers that no app surfaces, so they get no interface -
724
+ * this applies to both Main and Quick Create forms.
725
+ *
726
+ * Returns parsed form structures with tabs, sections, controls, and the form type.
717
727
  */
718
- getMainForms(logicalName: string): Promise<ParsedForm[]>;
728
+ getForms(logicalName: string): Promise<ParsedForm[]>;
719
729
  /**
720
730
  * Get a global OptionSet by its exact name.
721
731
  */
@@ -1202,14 +1212,12 @@ declare function disambiguateEnumMembers(members: Array<{
1202
1212
  * Generates TypeScript declaration files (.d.ts) for Dataverse entity interfaces.
1203
1213
  * These interfaces represent the data types returned by the Web API.
1204
1214
  *
1205
- * Output pattern:
1215
+ * Output pattern (flat ES module, one file per entity):
1206
1216
  * ```typescript
1207
- * declare namespace XrmForge.Entities {
1208
- * interface Account {
1209
- * accountid: string | null;
1210
- * name: string | null;
1211
- * // ...
1212
- * }
1217
+ * export interface Account {
1218
+ * accountid: string;
1219
+ * name: string | null;
1220
+ * // ...
1213
1221
  * }
1214
1222
  * ```
1215
1223
  */
@@ -1235,13 +1243,11 @@ declare function generateEntityInterface(info: EntityTypeInfo, options?: EntityG
1235
1243
  * Uses const enum because D365 form scripts have no module system at runtime,
1236
1244
  * so enum values must be inlined at compile time.
1237
1245
  *
1238
- * Output pattern:
1246
+ * Output pattern (flat ES module, all OptionSets of an entity in one file):
1239
1247
  * ```typescript
1240
- * declare namespace XrmForge.OptionSets {
1241
- * const enum AccountCategoryCode {
1242
- * PreferredCustomer = 1,
1243
- * Standard = 2,
1244
- * }
1248
+ * export const enum AccountCategoryCode {
1249
+ * PreferredCustomer = 1,
1250
+ * Standard = 2,
1245
1251
  * }
1246
1252
  * ```
1247
1253
  */
@@ -1291,38 +1297,37 @@ declare function generateEntityOptionSets(picklistAttributes: Array<{
1291
1297
  * 4. Fields const enum: provides autocomplete with dual-language labels
1292
1298
  * 5. NO fallback getAttribute(name: string): unknown fields are compile errors
1293
1299
  *
1294
- * Output pattern:
1300
+ * Output pattern (flat ES module, one file per entity, all forms combined):
1295
1301
  * ```typescript
1296
- * declare namespace XrmForge.Forms.Account {
1297
- * type AccountMainFormFields = "name" | "telephone1" | "revenue";
1298
- *
1299
- * type AccountMainFormAttributeMap = {
1300
- * name: Xrm.Attributes.StringAttribute;
1301
- * telephone1: Xrm.Attributes.StringAttribute;
1302
- * revenue: Xrm.Attributes.NumberAttribute;
1303
- * };
1304
- *
1305
- * type AccountMainFormControlMap = {
1306
- * name: Xrm.Controls.StringControl;
1307
- * telephone1: Xrm.Controls.StringControl;
1308
- * revenue: Xrm.Controls.NumberControl;
1309
- * };
1310
- *
1311
- * const enum AccountMainFormFields {
1312
- * Name = 'name',
1313
- * Telephone1 = 'telephone1',
1314
- * Revenue = 'revenue',
1315
- * }
1316
- *
1317
- * interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
1318
- * getAttribute<K extends AccountMainFormFields>(name: K): AccountMainFormAttributeMap[K];
1319
- * getAttribute(index: number): Xrm.Attributes.Attribute;
1320
- * getAttribute(): Xrm.Attributes.Attribute[];
1321
- * getControl<K extends AccountMainFormFields>(name: K): AccountMainFormControlMap[K];
1322
- * getControl(index: number): Xrm.Controls.Control;
1323
- * getControl(): Xrm.Controls.Control[];
1324
- * }
1302
+ * export type AccountMainFormFields = "name" | "telephone1" | "revenue";
1303
+ *
1304
+ * export type AccountMainFormAttributeMap = {
1305
+ * name: Xrm.Attributes.StringAttribute;
1306
+ * telephone1: Xrm.Attributes.StringAttribute;
1307
+ * revenue: Xrm.Attributes.NumberAttribute;
1308
+ * };
1309
+ *
1310
+ * export type AccountMainFormControlMap = {
1311
+ * name: Xrm.Controls.StringControl;
1312
+ * telephone1: Xrm.Controls.StringControl;
1313
+ * revenue: Xrm.Controls.NumberControl;
1314
+ * };
1315
+ *
1316
+ * export const enum AccountMainFormFieldsEnum {
1317
+ * Name = 'name',
1318
+ * Telephone1 = 'telephone1',
1319
+ * Revenue = 'revenue',
1320
+ * }
1321
+ *
1322
+ * export interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
1323
+ * getAttribute<K extends AccountMainFormFields>(name: K): AccountMainFormAttributeMap[K];
1324
+ * getAttribute(index: number): Xrm.Attributes.Attribute;
1325
+ * getAttribute(): Xrm.Attributes.Attribute[];
1326
+ * getControl<K extends AccountMainFormFields>(name: K): AccountMainFormControlMap[K];
1327
+ * getControl(index: number): Xrm.Controls.Control;
1328
+ * getControl(): Xrm.Controls.Control[];
1325
1329
  * }
1330
+ * // plus ...FormTabs/...Sections/...FormSubgrids enums, ...FormTypeInfo, ...FormMockValues
1326
1331
  * ```
1327
1332
  */
1328
1333
 
@@ -1364,15 +1369,13 @@ declare function generateEntityForms(forms: ParsedForm[], entityLogicalName: str
1364
1369
  * Generates a const enum with ALL entity fields for use with Xrm.WebApi.
1365
1370
  * Unlike form-specific Fields enums, this contains every readable attribute.
1366
1371
  *
1367
- * Output pattern:
1372
+ * Output pattern (flat ES module):
1368
1373
  * ```typescript
1369
- * declare namespace XrmForge.Entities {
1370
- * const enum AccountFields {
1371
- * /** Account Name | Firmenname *\/
1372
- * Name = 'name',
1373
- * /** Main Phone | Haupttelefon *\/
1374
- * Telephone1 = 'telephone1',
1375
- * }
1374
+ * export const enum AccountFields {
1375
+ * /** Account Name | Firmenname *\/
1376
+ * Name = 'name',
1377
+ * /** Main Phone | Haupttelefon *\/
1378
+ * Telephone1 = 'telephone1',
1376
1379
  * }
1377
1380
  * ```
1378
1381
  */
@@ -1422,14 +1425,12 @@ declare function generateEntityNavigationProperties(info: EntityTypeInfo, option
1422
1425
  * Generates a single const enum with all entity logical names.
1423
1426
  * Eliminates raw strings in Xrm.WebApi calls.
1424
1427
  *
1425
- * Output pattern:
1428
+ * Output pattern (flat ES module):
1426
1429
  * ```typescript
1427
- * declare namespace XrmForge {
1428
- * const enum EntityNames {
1429
- * Account = 'account',
1430
- * Contact = 'contact',
1431
- * Lead = 'lead',
1432
- * }
1430
+ * export const enum EntityNames {
1431
+ * Account = 'account',
1432
+ * Contact = 'contact',
1433
+ * Lead = 'lead',
1433
1434
  * }
1434
1435
  * ```
1435
1436
  */
@@ -1454,17 +1455,14 @@ declare function generateEntityNamesEnum(entityNames: string[], _options?: Entit
1454
1455
  * Input: CustomApiTypeInfo[] (from fixture JSON or live Dataverse query)
1455
1456
  * Output: Grouped by entity (bound) or "global" (unbound)
1456
1457
  *
1457
- * @example Generated output for markant_NormalizePhone (unbound action):
1458
+ * @example Generated output for markant_NormalizePhone (unbound action) in actions/global.ts:
1458
1459
  * ```typescript
1459
- * // global.d.ts
1460
- * declare namespace XrmForge.Actions {
1461
- * interface NormalizePhoneParams { Input: string; AllowSuspicious?: boolean; }
1462
- * interface NormalizePhoneResult { Normalized: string; Status: number; Message: string; }
1463
- * }
1464
- *
1465
- * // global.ts
1466
1460
  * import { createUnboundAction } from '@xrmforge/helpers';
1467
- * export const NormalizePhone = createUnboundAction<...>('markant_NormalizePhone', { ... });
1461
+ *
1462
+ * export type NormalizePhoneParams = { Input: string; AllowSuspicious?: boolean; };
1463
+ * export type NormalizePhoneResult = { Normalized: string; Status: number; Message: string; };
1464
+ * export const NormalizePhone =
1465
+ * createUnboundAction<NormalizePhoneParams, NormalizePhoneResult>('markant_NormalizePhone', { ... });
1468
1466
  * ```
1469
1467
  */
1470
1468
 
package/dist/index.js CHANGED
@@ -774,6 +774,7 @@ function parseForm(form, parser = defaultXmlParser) {
774
774
  name: form.name,
775
775
  formId: form.formid,
776
776
  isDefault: form.isdefault,
777
+ type: form.type,
777
778
  tabs,
778
779
  allControls,
779
780
  allSpecialControls
@@ -918,13 +919,15 @@ function extractParameter(controlElement, paramName) {
918
919
  // src/metadata/client.ts
919
920
  var log4 = createLogger("metadata");
920
921
  var FORM_TYPE_MAIN = 2;
922
+ var FORM_TYPE_QUICK_CREATE = 7;
923
+ var FORM_ACTIVATION_ACTIVE = 1;
921
924
  var COMPONENT_TYPE_ENTITY = 1;
922
925
  function byUniqueName(a, b) {
923
926
  return a.uniquename < b.uniquename ? -1 : a.uniquename > b.uniquename ? 1 : 0;
924
927
  }
925
928
  var ENTITY_SELECT = "LogicalName,SchemaName,EntitySetName,DisplayName,PrimaryIdAttribute,PrimaryNameAttribute,OwnershipType,IsCustomEntity,LogicalCollectionName,MetadataId";
926
929
  var ATTRIBUTE_SELECT = "LogicalName,SchemaName,AttributeType,AttributeTypeName,DisplayName,IsPrimaryId,IsPrimaryName,RequiredLevel,IsValidForRead,IsValidForCreate,IsValidForUpdate,MetadataId";
927
- var FORM_SELECT = "name,formid,formxml,description,isdefault";
930
+ var FORM_SELECT = "name,formid,formxml,description,isdefault,type,formactivationstate";
928
931
  var MetadataClient = class {
929
932
  http;
930
933
  constructor(httpClient) {
@@ -1001,16 +1004,22 @@ var MetadataClient = class {
1001
1004
  }
1002
1005
  // ─── Form Metadata ────────────────────────────────────────────────────
1003
1006
  /**
1004
- * Get and parse Main forms (type=2) for an entity.
1005
- * Returns parsed form structures with tabs, sections, and controls.
1007
+ * Get and parse the form types relevant for type generation (Main type=2 and
1008
+ * Quick Create type=7), restricted to ACTIVE forms (formactivationstate=1).
1009
+ * Inactive forms are leftovers that no app surfaces, so they get no interface -
1010
+ * this applies to both Main and Quick Create forms.
1011
+ *
1012
+ * Returns parsed form structures with tabs, sections, controls, and the form type.
1006
1013
  */
1007
- async getMainForms(logicalName) {
1014
+ async getForms(logicalName) {
1008
1015
  const safeName = DataverseHttpClient.sanitizeIdentifier(logicalName);
1009
- log4.info(`Fetching Main forms for: ${safeName}`);
1016
+ log4.info(`Fetching active Main + Quick Create forms for: ${safeName}`);
1010
1017
  const forms = await this.http.getAll(
1011
- `/systemforms?$filter=objecttypecode eq '${safeName}' and type eq ${FORM_TYPE_MAIN}&$select=${FORM_SELECT}`
1018
+ `/systemforms?$filter=objecttypecode eq '${safeName}' and (type eq ${FORM_TYPE_MAIN} or type eq ${FORM_TYPE_QUICK_CREATE}) and formactivationstate eq ${FORM_ACTIVATION_ACTIVE}&$select=${FORM_SELECT}`
1012
1019
  );
1013
- log4.info(`Found ${forms.length} Main form(s) for "${safeName}"`);
1020
+ const mainCount = forms.filter((f) => f.type === FORM_TYPE_MAIN).length;
1021
+ const qcCount = forms.filter((f) => f.type === FORM_TYPE_QUICK_CREATE).length;
1022
+ log4.info(`Found ${mainCount} Main + ${qcCount} Quick Create active form(s) for "${safeName}"`);
1014
1023
  return forms.map((form) => {
1015
1024
  try {
1016
1025
  return parseForm(form);
@@ -1024,6 +1033,7 @@ var MetadataClient = class {
1024
1033
  name: form.name,
1025
1034
  formId: form.formid,
1026
1035
  isDefault: form.isdefault,
1036
+ type: form.type,
1027
1037
  tabs: [],
1028
1038
  allControls: [],
1029
1039
  allSpecialControls: []
@@ -1152,7 +1162,7 @@ var MetadataClient = class {
1152
1162
  this.getLookupAttributes(safeName),
1153
1163
  this.getStatusAttributes(safeName),
1154
1164
  this.getStateAttributes(safeName),
1155
- this.getMainForms(safeName),
1165
+ this.getForms(safeName),
1156
1166
  this.getRelationships(safeName)
1157
1167
  ]);
1158
1168
  const result = {
@@ -1937,6 +1947,7 @@ function generateEntityOptionSets(picklistAttributes, entityLogicalName, options
1937
1947
  }
1938
1948
 
1939
1949
  // src/generators/form-generator.ts
1950
+ var FORM_TYPE_QUICK_CREATE2 = 7;
1940
1951
  function specialControlToXrmType(controlType) {
1941
1952
  switch (controlType) {
1942
1953
  case "subgrid":
@@ -2239,8 +2250,8 @@ function generateEntityForms(forms, entityLogicalName, attributes, options = {})
2239
2250
  const entityPascal = toPascalCase(entityLogicalName);
2240
2251
  const validForms = forms.filter((f) => f.allControls.length > 0);
2241
2252
  const baseNames = validForms.map((form) => {
2242
- const safeFormName = toSafeFormName(form.name);
2243
- return buildFormBaseName(entityPascal, safeFormName);
2253
+ const base = buildFormBaseName(entityPascal, toSafeFormName(form.name));
2254
+ return form.type === FORM_TYPE_QUICK_CREATE2 ? `${base}QuickCreate` : base;
2244
2255
  });
2245
2256
  const baseNameCounts = /* @__PURE__ */ new Map();
2246
2257
  for (const name of baseNames) {
@@ -2569,11 +2580,14 @@ function groupCustomApis(apis) {
2569
2580
  // src/orchestrator/file-writer.ts
2570
2581
  import { mkdir, writeFile, readFile, unlink, readdir, access } from "fs/promises";
2571
2582
  import { join as join2, dirname } from "path";
2583
+ function normalizeLineEndings(content) {
2584
+ return content.replace(/\r\n?/g, "\n");
2585
+ }
2572
2586
  async function writeGeneratedFile(outputDir, file) {
2573
2587
  const absolutePath = join2(outputDir, file.relativePath);
2574
2588
  try {
2575
2589
  const existing = await readFile(absolutePath, "utf-8");
2576
- if (existing === file.content) {
2590
+ if (normalizeLineEndings(existing) === normalizeLineEndings(file.content)) {
2577
2591
  return false;
2578
2592
  }
2579
2593
  } catch {
@@ -2630,7 +2644,7 @@ async function checkGeneratedFile(outputDir, file) {
2630
2644
  const absolutePath = join2(outputDir, file.relativePath);
2631
2645
  try {
2632
2646
  const existing = await readFile(absolutePath, "utf-8");
2633
- return existing === file.content ? "unchanged" : "changed";
2647
+ return normalizeLineEndings(existing) === normalizeLineEndings(file.content) ? "unchanged" : "changed";
2634
2648
  } catch (error) {
2635
2649
  if (error.code === "ENOENT") {
2636
2650
  return "missing";