@xrmforge/typegen 0.13.0 → 0.13.1

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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 XrmForge Contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 XrmForge Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @xrmforge/typegen
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@xrmforge/typegen.svg)](https://www.npmjs.com/package/@xrmforge/typegen)
4
+ [![license](https://img.shields.io/npm/l/@xrmforge/typegen.svg)](https://github.com/juergenbeck/XrmForge/blob/main/LICENSE)
5
+
6
+ **The type-generation engine of XrmForge.** Reads Dynamics 365 / Dataverse metadata and generates TypeScript declarations that *extend* `@types/xrm` (never replace it), so your generated types coexist with PCF controls and the rest of the Microsoft ecosystem.
7
+
8
+ > Most users do not call this package directly -- they use [`@xrmforge/cli`](https://www.npmjs.com/package/@xrmforge/cli) (`xrmforge generate`), which wraps this engine. Install `@xrmforge/typegen` directly only when you want to embed generation in your own Node.js tooling. For the full framework docs, see the [XrmForge repository](https://github.com/juergenbeck/XrmForge#readme).
9
+
10
+ ---
11
+
12
+ ## What it generates
13
+
14
+ For each entity, typegen emits flat ES modules (`.ts` files) -- one file per concern, imported and processed by your bundler. No `declare namespace`, no ambient `.d.ts`.
15
+
16
+ ```
17
+ generated/
18
+ entities/account.ts export interface Account { ... } // typed Web API response objects
19
+ fields/account.ts export const enum AccountFields // $select / $filter (already _value form)
20
+ export const enum AccountNavigationProperties // parseLookup / $expand / @odata.bind
21
+ optionsets/account.ts export const enum IndustryCode ... // every picklist/status/state field
22
+ forms/account.ts Union + maps + Fields enum + Form interface + FormTypeInfo + MockValues
23
+ actions/quote.ts export const WinQuote = createBoundAction(...) // typed Custom API executors
24
+ entity-names.ts export const enum EntityNames
25
+ index.ts barrel re-export
26
+ ```
27
+
28
+ Each generated artifact is a compile-time contract:
29
+
30
+ - **Entity interfaces** -- all attributes correctly typed (string, number, boolean, Lookup, OptionSet, DateTime).
31
+ - **Form interfaces** -- per-form `getAttribute()` / `getControl()` overloads. Only fields actually on the form are valid; no string fallback, no `any`.
32
+ - **OptionSet enums** -- `const enum`, inlined by TypeScript (zero runtime overhead).
33
+ - **Fields / NavigationProperties enums** -- type-safe `$select` and lookup navigation.
34
+ - **Action / Function executors** -- generated from Custom API metadata, with typed parameters and responses.
35
+ - **Dual-language JSDoc** -- `/** Account Name | Firmenname */` when `--secondary-language` is set.
36
+
37
+ ---
38
+
39
+ ## Usage via the CLI (recommended)
40
+
41
+ ```bash
42
+ npm install --save-dev @xrmforge/cli @types/xrm
43
+ npx xrmforge generate --url https://myorg.crm4.dynamics.com --auth interactive \
44
+ --tenant-id YOUR_TENANT_ID --client-id 51f81489-12ee-4a9e-aaae-a2591f45987d \
45
+ --entities account,contact --output ./generated
46
+ ```
47
+
48
+ See [`@xrmforge/cli`](https://www.npmjs.com/package/@xrmforge/cli) for every flag, authentication method, incremental caching (`--cache`), and drift detection (`--check`).
49
+
50
+ ---
51
+
52
+ ## Programmatic API
53
+
54
+ Install directly when embedding generation in your own tooling:
55
+
56
+ ```bash
57
+ npm install @xrmforge/typegen @types/xrm
58
+ ```
59
+
60
+ The high-level entry point is the orchestrator, which runs the full pipeline (authenticate, read metadata, generate, write). It takes a credential and a config:
61
+
62
+ ```typescript
63
+ import { TypeGenerationOrchestrator, createCredential } from '@xrmforge/typegen';
64
+
65
+ // 1. Build a credential from an auth config
66
+ // (method: 'interactive' | 'client-credentials' | 'device-code' | 'token')
67
+ const credential = createCredential({
68
+ method: 'interactive',
69
+ tenantId: 'YOUR_TENANT_ID',
70
+ clientId: '51f81489-12ee-4a9e-aaae-a2591f45987d',
71
+ });
72
+
73
+ // 2. Run the pipeline
74
+ const orchestrator = new TypeGenerationOrchestrator(credential, {
75
+ environmentUrl: 'https://myorg.crm4.dynamics.com',
76
+ entities: ['account', 'contact'],
77
+ outputDir: './generated',
78
+ labelConfig: { primaryLanguage: 1033, secondaryLanguage: 1031 },
79
+ });
80
+
81
+ const result = await orchestrator.generate();
82
+ console.log(`Generated ${result.totalFiles} files`);
83
+ ```
84
+
85
+ For finer control, the building blocks are exported individually:
86
+
87
+ | Area | Exports |
88
+ |------|---------|
89
+ | Orchestration | `TypeGenerationOrchestrator`, types `GenerateConfig`, `GenerationResult`, `CheckResult`, `CheckFinding`, `CacheStats` |
90
+ | Authentication | `createCredential`, types `AuthConfig`, `ClientCredentialsAuth`, `InteractiveAuth`, `DeviceCodeAuth` |
91
+ | HTTP | `DataverseHttpClient` (ReadOnly-default, retry, rate-limit), type `HttpClientOptions` |
92
+ | Metadata | `MetadataClient`, `MetadataCache`, `ChangeDetector`, `parseForm`, plus rich metadata types (`EntityMetadata`, `AttributeMetadata`, `OptionSetMetadata`, `SystemFormMetadata`, ...) |
93
+ | Code generators | `generateEntityInterface`, `generateFormInterface`, `generateOptionSetEnum`, `generateEntityFieldsEnum`, `generateActionModule`, `generateEntityNamesEnum`, ... |
94
+ | Type mapping | `getEntityPropertyType`, `getFormAttributeType`, `toSafeIdentifier`, `toPascalCase`, `isLookupType`, ... |
95
+ | Logging | `Logger`, `ConsoleLogSink`, `JsonLogSink`, `SilentLogSink`, `LogLevel`, `configureLogging` |
96
+ | Errors | `XrmForgeError`, `AuthenticationError`, `ApiRequestError`, `MetadataError`, `GenerationError`, `ConfigError`, `isXrmForgeError`, `isRateLimitError` |
97
+
98
+ > **Node.js only.** This package pulls in `@azure/identity` and Node APIs. Do **not** import it in browser/form-script code -- use [`@xrmforge/helpers`](https://www.npmjs.com/package/@xrmforge/helpers) for the browser runtime. The `@xrmforge/eslint-plugin` rule `no-typegen-import` enforces this.
99
+
100
+ ---
101
+
102
+ ## Peer dependency
103
+
104
+ `@types/xrm` (>= 9.0.0) -- the generated types build on top of it.
105
+
106
+ ## Documentation
107
+
108
+ Full guide and generated-type patterns: [XrmForge on GitHub](https://github.com/juergenbeck/XrmForge#readme).
109
+
110
+ ## License
111
+
112
+ [MIT](https://github.com/juergenbeck/XrmForge/blob/main/LICENSE) (c) XrmForge Contributors.
package/dist/index.d.ts CHANGED
@@ -1331,6 +1331,25 @@ declare function generateEntityOptionSets(picklistAttributes: Array<{
1331
1331
  * ```
1332
1332
  */
1333
1333
 
1334
+ /**
1335
+ * Machine-readable metadata for one generated form, surfaced in form-mapping.json
1336
+ * so AI agents can pick the right form by its fields without parsing the generated
1337
+ * code (F-MAR7-04).
1338
+ */
1339
+ interface FormGenerationMeta {
1340
+ /** Form display name (from FormXml) */
1341
+ formName: string;
1342
+ /** Generated interface name (e.g. AccountForm) */
1343
+ interfaceName: string;
1344
+ /** Generated Fields enum name (e.g. AccountFormFieldsEnum) */
1345
+ fieldsEnumName: string;
1346
+ /** Generated Tabs enum name, or '' if the form has no named tabs */
1347
+ tabsEnumName: string;
1348
+ /** Sorted logical names of the attributes this form binds to */
1349
+ fields: string[];
1350
+ /** True for a Main form (systemform_type 2), false for Quick Create etc. */
1351
+ isMain: boolean;
1352
+ }
1334
1353
  /** Options for form interface generation */
1335
1354
  interface FormGeneratorOptions {
1336
1355
  /** Label configuration for dual-language JSDoc comments */
@@ -1357,9 +1376,7 @@ declare function generateFormInterface(form: ParsedForm, entityLogicalName: stri
1357
1376
  * @param options - Generator options
1358
1377
  * @returns Array of { formName, interfaceName, content }
1359
1378
  */
1360
- declare function generateEntityForms(forms: ParsedForm[], entityLogicalName: string, attributes: AttributeMetadata[], options?: FormGeneratorOptions): Array<{
1361
- formName: string;
1362
- interfaceName: string;
1379
+ declare function generateEntityForms(forms: ParsedForm[], entityLogicalName: string, attributes: AttributeMetadata[], options?: FormGeneratorOptions): Array<FormGenerationMeta & {
1363
1380
  content: string;
1364
1381
  }>;
1365
1382
 
@@ -1566,6 +1583,8 @@ interface EntityGenerationResult {
1566
1583
  files: GeneratedFile[];
1567
1584
  /** Warnings (e.g. missing labels, empty forms) */
1568
1585
  warnings: string[];
1586
+ /** Per-form metadata for this entity (drives form-mapping.json, F-MAR7-04) */
1587
+ formMeta: FormGenerationMeta[];
1569
1588
  }
1570
1589
  /** A single generated file */
1571
1590
  interface GeneratedFile {
@@ -1699,10 +1718,13 @@ declare class TypeGenerationOrchestrator {
1699
1718
  */
1700
1719
  private getPicklistAttributes;
1701
1720
  /**
1702
- * Generate a form-mapping.json that maps entity names to their generated
1703
- * form interface names, fields enums, and tabs enums.
1721
+ * Generate a form-mapping.json that maps each entity to its generated forms:
1722
+ * interface name, Fields/Tabs enum names, a main-form marker (isMain), and the
1723
+ * list of fields each form binds to. This lets AI agents pick the right form by
1724
+ * its fields without guessing interface names.
1704
1725
  *
1705
- * This helps AI agents find the correct interface names without guessing.
1726
+ * Built from the structured per-form metadata collected during generation
1727
+ * (F-MAR7-04), not by parsing the generated code.
1706
1728
  */
1707
1729
  private generateFormMapping;
1708
1730
  }
package/dist/index.js CHANGED
@@ -1954,6 +1954,7 @@ function singleQuoted(value) {
1954
1954
 
1955
1955
  // src/generators/form-generator.ts
1956
1956
  var FORM_TYPE_QUICK_CREATE2 = 7;
1957
+ var FORM_TYPE_MAIN2 = 2;
1957
1958
  function specialControlToXrmType(controlType) {
1958
1959
  switch (controlType) {
1959
1960
  case "subgrid":
@@ -1996,14 +1997,7 @@ function labelToPascalMember(label) {
1996
1997
  if (/^\d/.test(pascal)) return `_${pascal}`;
1997
1998
  return pascal;
1998
1999
  }
1999
- function generateFormInterface(form, entityLogicalName, attributeMap, options = {}, baseNameOverride) {
2000
- const labelConfig = options.labelConfig || DEFAULT_LABEL_CONFIG;
2001
- const entityPascal = toPascalCase(entityLogicalName);
2002
- const baseName = baseNameOverride || buildFormBaseName(entityPascal, toSafeFormName(form.name));
2003
- const interfaceName = `${baseName}Form`;
2004
- const fieldsTypeName = `${baseName}FormFields`;
2005
- const attrMapName = `${baseName}FormAttributeMap`;
2006
- const ctrlMapName = `${baseName}FormControlMap`;
2000
+ function collectFormFieldNames(form, attributeMap) {
2007
2001
  const fieldNames = /* @__PURE__ */ new Set();
2008
2002
  for (const control of form.allControls) {
2009
2003
  if (control.datafieldname) {
@@ -2015,9 +2009,20 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2015
2009
  fieldNames.add(systemField);
2016
2010
  }
2017
2011
  }
2012
+ return [...fieldNames].sort();
2013
+ }
2014
+ function generateFormInterface(form, entityLogicalName, attributeMap, options = {}, baseNameOverride) {
2015
+ const labelConfig = options.labelConfig || DEFAULT_LABEL_CONFIG;
2016
+ const entityPascal = toPascalCase(entityLogicalName);
2017
+ const baseName = baseNameOverride || buildFormBaseName(entityPascal, toSafeFormName(form.name));
2018
+ const interfaceName = `${baseName}Form`;
2019
+ const fieldsTypeName = `${baseName}FormFields`;
2020
+ const attrMapName = `${baseName}FormAttributeMap`;
2021
+ const ctrlMapName = `${baseName}FormControlMap`;
2022
+ const sortedFieldNames = collectFormFieldNames(form, attributeMap);
2018
2023
  const fields = [];
2019
2024
  const usedEnumNames = /* @__PURE__ */ new Set();
2020
- for (const fieldName of [...fieldNames].sort()) {
2025
+ for (const fieldName of sortedFieldNames) {
2021
2026
  const attr = attributeMap.get(fieldName);
2022
2027
  if (!attr) continue;
2023
2028
  const primaryLabel = getPrimaryLabel(attr.DisplayName, labelConfig);
@@ -2211,7 +2216,7 @@ function generateFormInterface(form, entityLogicalName, attributeMap, options =
2211
2216
  const sectionNames = tab.sections.filter((s) => s.name).map((s) => s.name);
2212
2217
  if (sectionNames.length > 0) {
2213
2218
  lines.push(` get(name: "${tab.name}"): Xrm.Controls.Tab & {`);
2214
- lines.push(" sections: {");
2219
+ lines.push(" sections: Xrm.Collection.ItemCollection<Xrm.Controls.Section> & {");
2215
2220
  for (const sectionName of sectionNames) {
2216
2221
  lines.push(` get(name: "${sectionName}"): Xrm.Controls.Section;`);
2217
2222
  }
@@ -2277,7 +2282,16 @@ function generateEntityForms(forms, entityLogicalName, attributes, options = {})
2277
2282
  }
2278
2283
  const interfaceName = `${baseName}Form`;
2279
2284
  const content = generateFormInterface(form, entityLogicalName, attributeMap, options, baseName);
2280
- results.push({ formName: form.name, interfaceName, content });
2285
+ const hasNamedTabs = form.tabs.some((t) => t.name);
2286
+ results.push({
2287
+ formName: form.name,
2288
+ interfaceName,
2289
+ fieldsEnumName: `${baseName}FormFieldsEnum`,
2290
+ tabsEnumName: hasNamedTabs ? `${baseName}FormTabs` : "",
2291
+ fields: collectFormFieldNames(form, attributeMap),
2292
+ isMain: form.type === FORM_TYPE_MAIN2,
2293
+ content
2294
+ });
2281
2295
  }
2282
2296
  return results;
2283
2297
  }
@@ -2753,9 +2767,9 @@ function generateBarrelIndex(files) {
2753
2767
  lines.push("");
2754
2768
  }
2755
2769
  if (actions.length > 0) {
2756
- lines.push("// Custom API Actions & Functions");
2770
+ lines.push("// Custom API Actions & Functions - import directly from individual files to avoid name conflicts:");
2757
2771
  for (const f of actions) {
2758
- lines.push(`export * from '${toImportSpecifier(f.relativePath)}';`);
2772
+ lines.push(`// import { ... } from '${toImportSpecifier(f.relativePath)}';`);
2759
2773
  }
2760
2774
  lines.push("");
2761
2775
  }
@@ -2898,7 +2912,8 @@ var TypeGenerationOrchestrator = class {
2898
2912
  entityResults.push({
2899
2913
  entityLogicalName: entityName,
2900
2914
  files: [],
2901
- warnings: [`Failed to process: ${failedEntities.get(entityName)}`]
2915
+ warnings: [`Failed to process: ${failedEntities.get(entityName)}`],
2916
+ formMeta: []
2902
2917
  });
2903
2918
  continue;
2904
2919
  }
@@ -2921,9 +2936,9 @@ var TypeGenerationOrchestrator = class {
2921
2936
  type: "entity"
2922
2937
  });
2923
2938
  }
2924
- const formFiles = allFiles.filter((f) => f.type === "form");
2925
- if (formFiles.length > 0) {
2926
- const formMapping = this.generateFormMapping(formFiles);
2939
+ const entityFormMeta = entityResults.filter((r) => r.formMeta.length > 0).map((r) => ({ entityName: r.entityLogicalName, forms: r.formMeta }));
2940
+ if (entityFormMeta.length > 0) {
2941
+ const formMapping = this.generateFormMapping(entityFormMeta);
2927
2942
  allFiles.push({
2928
2943
  relativePath: "form-mapping.json",
2929
2944
  content: JSON.stringify(formMapping, null, 2) + "\n",
@@ -3098,6 +3113,7 @@ var TypeGenerationOrchestrator = class {
3098
3113
  generateEntityFiles(entityName, entityInfo) {
3099
3114
  const warnings = [];
3100
3115
  const files = [];
3116
+ const formMeta = [];
3101
3117
  if (this.config.generateEntities) {
3102
3118
  const entityContent = generateEntityInterface(entityInfo, {
3103
3119
  labelConfig: this.config.labelConfig
@@ -3143,6 +3159,9 @@ var TypeGenerationOrchestrator = class {
3143
3159
  content: addGeneratedHeader(combinedContent),
3144
3160
  type: "form"
3145
3161
  });
3162
+ for (const { content: _content, ...meta } of formResults) {
3163
+ formMeta.push(meta);
3164
+ }
3146
3165
  }
3147
3166
  } else {
3148
3167
  warnings.push(`No forms found for ${entityName}`);
@@ -3163,7 +3182,7 @@ ${navPropsContent}` : fieldsEnumContent;
3163
3182
  type: "fields"
3164
3183
  });
3165
3184
  }
3166
- return { entityLogicalName: entityName, files, warnings };
3185
+ return { entityLogicalName: entityName, files, warnings, formMeta };
3167
3186
  }
3168
3187
  /**
3169
3188
  * Generate Custom API Action/Function executor files.
@@ -3233,35 +3252,26 @@ ${navPropsContent}` : fieldsEnumContent;
3233
3252
  return result;
3234
3253
  }
3235
3254
  /**
3236
- * Generate a form-mapping.json that maps entity names to their generated
3237
- * form interface names, fields enums, and tabs enums.
3255
+ * Generate a form-mapping.json that maps each entity to its generated forms:
3256
+ * interface name, Fields/Tabs enum names, a main-form marker (isMain), and the
3257
+ * list of fields each form binds to. This lets AI agents pick the right form by
3258
+ * its fields without guessing interface names.
3238
3259
  *
3239
- * This helps AI agents find the correct interface names without guessing.
3260
+ * Built from the structured per-form metadata collected during generation
3261
+ * (F-MAR7-04), not by parsing the generated code.
3240
3262
  */
3241
- generateFormMapping(formFiles) {
3263
+ generateFormMapping(entityForms) {
3242
3264
  const mapping = {};
3243
- for (const file of formFiles) {
3244
- const match = file.relativePath.match(/^forms\/(.+)\.ts$/);
3245
- if (!match) continue;
3246
- const entityName = match[1];
3247
- const interfaces = [...file.content.matchAll(/export\s+interface\s+(\w+Form)\s+extends/g)].map((m) => m[1]);
3248
- const fieldsEnums = [...file.content.matchAll(/export\s+const\s+enum\s+(\w+FormFieldsEnum)\s*\{/g)].map((m) => m[1]);
3249
- const tabsEnums = [...file.content.matchAll(/export\s+const\s+enum\s+(\w+FormTabs)\s*\{/g)].map((m) => m[1]);
3250
- const forms = [];
3251
- for (let i = 0; i < interfaces.length; i++) {
3252
- const interfaceName = interfaces[i];
3253
- const jsdocMatch = file.content.match(new RegExp(`/\\*\\*\\s*(.+?)\\s*\\*/\\s*export\\s+interface\\s+${interfaceName}`));
3254
- const formName = jsdocMatch?.[1] ?? interfaceName.replace(/Form$/, "");
3255
- forms.push({
3256
- formName,
3257
- interface: interfaceName,
3258
- fieldsEnum: fieldsEnums[i] ?? "",
3259
- tabsEnum: tabsEnums[i] ?? ""
3260
- });
3261
- }
3262
- if (forms.length > 0) {
3263
- mapping[entityName] = forms;
3264
- }
3265
+ for (const { entityName, forms } of entityForms) {
3266
+ if (forms.length === 0) continue;
3267
+ mapping[entityName] = forms.map((f) => ({
3268
+ formName: f.formName,
3269
+ interface: f.interfaceName,
3270
+ fieldsEnum: f.fieldsEnumName,
3271
+ tabsEnum: f.tabsEnumName,
3272
+ isMain: f.isMain,
3273
+ fields: f.fields
3274
+ }));
3265
3275
  }
3266
3276
  return mapping;
3267
3277
  }