@xrmforge/devkit 0.5.2 → 0.5.4
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 +21 -21
- package/dist/index.d.ts +35 -4
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/templates/AGENT.md +357 -255
- package/dist/templates/azure-pipelines.yml +32 -32
- package/dist/templates/constants.ts +32 -0
- package/dist/templates/error-handler.ts +95 -0
- package/dist/templates/eslint.config.js +21 -0
- package/dist/templates/example-form.test.ts +19 -19
- package/dist/templates/example-form.ts +40 -40
- package/dist/templates/github-actions-ci.yml +36 -36
- package/dist/templates/gitignore +19 -19
- package/dist/templates/logger.ts +67 -0
- package/dist/templates/self-check.sh +106 -0
- package/dist/templates/vitest.config.ts +8 -8
- package/package.json +3 -2
package/dist/templates/AGENT.md
CHANGED
|
@@ -1,255 +1,357 @@
|
|
|
1
|
-
# XrmForge - AI Agent Instructions
|
|
2
|
-
|
|
3
|
-
This file helps AI coding assistants write optimal Dynamics 365 form scripts.
|
|
4
|
-
|
|
5
|
-
## Packages
|
|
6
|
-
|
|
7
|
-
- `@xrmforge/typegen` - Generates typed declarations from Dataverse metadata
|
|
8
|
-
- `@xrmforge/helpers` - Browser-safe runtime: select(), parseLookup(), typedForm(), Xrm constants, Action executors
|
|
9
|
-
- `@xrmforge/testing` - Type-safe form mocks: createFormMock(), fireOnChange()
|
|
10
|
-
- `@xrmforge/devkit` - esbuild IIFE bundles via xrmforge build
|
|
11
|
-
- `@xrmforge/eslint-plugin` - D365-specific ESLint rules
|
|
12
|
-
|
|
13
|
-
## Generated Types (generated/ directory)
|
|
14
|
-
|
|
15
|
-
Run `xrmforge generate` to create:
|
|
16
|
-
- `generated/forms/{entity}.ts` - Form interface + Fields/Tabs/Sections/Subgrids enums
|
|
17
|
-
- `generated/optionsets/{entity}.ts` - OptionSet const enums
|
|
18
|
-
- `generated/entities/{entity}.ts` - Entity interface
|
|
19
|
-
- `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
|
|
20
|
-
- `generated/entity-names.ts` - EntityNames const enum
|
|
21
|
-
- `generated/index.ts` - Barrel file with `export * from` re-exports
|
|
22
|
-
|
|
23
|
-
## Rules: MANDATORY (every violation is a bug)
|
|
24
|
-
|
|
25
|
-
1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
|
|
26
|
-
```typescript
|
|
27
|
-
import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
|
|
28
|
-
form.getAttribute(Fields.Name) // CORRECT
|
|
29
|
-
form.getAttribute("name") // BUG - raw string
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
2. **OptionSet Enum** for ALL value comparisons. Never magic numbers.
|
|
33
|
-
```typescript
|
|
34
|
-
import { StatusCode } from '../generated/optionsets/invoice.js';
|
|
35
|
-
if (status === StatusCode.Active) // CORRECT
|
|
36
|
-
if (status === 0) // BUG - magic number
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
3. **FormContext Cast** to generated form interface in every onLoad:
|
|
40
|
-
```typescript
|
|
41
|
-
import type { AccountMainForm } from '../generated/forms/account.js';
|
|
42
|
-
const form = ctx.getFormContext() as AccountMainForm;
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
4. **EntityNames Enum** in ALL Xrm.WebApi calls:
|
|
46
|
-
```typescript
|
|
47
|
-
import { EntityNames } from '../generated/entity-names.js';
|
|
48
|
-
Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
5. **
|
|
52
|
-
```typescript
|
|
53
|
-
import { parseLookup } from '@xrmforge/helpers';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
- Never `
|
|
94
|
-
- Never
|
|
95
|
-
- Never `
|
|
96
|
-
- Never
|
|
97
|
-
- Never `
|
|
98
|
-
- Never `
|
|
99
|
-
- Never
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
|
174
|
-
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
1
|
+
# XrmForge - AI Agent Instructions
|
|
2
|
+
|
|
3
|
+
This file helps AI coding assistants write optimal Dynamics 365 form scripts.
|
|
4
|
+
|
|
5
|
+
## Packages
|
|
6
|
+
|
|
7
|
+
- `@xrmforge/typegen` - Generates typed declarations from Dataverse metadata
|
|
8
|
+
- `@xrmforge/helpers` - Browser-safe runtime: select(), parseLookup(), typedForm(), Xrm constants, Action executors
|
|
9
|
+
- `@xrmforge/testing` - Type-safe form mocks: createFormMock(), fireOnChange()
|
|
10
|
+
- `@xrmforge/devkit` - esbuild IIFE bundles via xrmforge build
|
|
11
|
+
- `@xrmforge/eslint-plugin` - D365-specific ESLint rules
|
|
12
|
+
|
|
13
|
+
## Generated Types (generated/ directory)
|
|
14
|
+
|
|
15
|
+
Run `xrmforge generate` to create:
|
|
16
|
+
- `generated/forms/{entity}.ts` - Form interface + Fields/Tabs/Sections/Subgrids enums
|
|
17
|
+
- `generated/optionsets/{entity}.ts` - OptionSet const enums
|
|
18
|
+
- `generated/entities/{entity}.ts` - Entity interface
|
|
19
|
+
- `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
|
|
20
|
+
- `generated/entity-names.ts` - EntityNames const enum
|
|
21
|
+
- `generated/index.ts` - Barrel file with `export * from` re-exports
|
|
22
|
+
|
|
23
|
+
## Rules: MANDATORY (every violation is a bug)
|
|
24
|
+
|
|
25
|
+
1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
|
|
26
|
+
```typescript
|
|
27
|
+
import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
|
|
28
|
+
form.getAttribute(Fields.Name) // CORRECT
|
|
29
|
+
form.getAttribute("name") // BUG - raw string
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
2. **OptionSet Enum** for ALL value comparisons. Never magic numbers.
|
|
33
|
+
```typescript
|
|
34
|
+
import { StatusCode } from '../generated/optionsets/invoice.js';
|
|
35
|
+
if (status === StatusCode.Active) // CORRECT
|
|
36
|
+
if (status === 0) // BUG - magic number
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
3. **FormContext Cast** to generated form interface in every onLoad:
|
|
40
|
+
```typescript
|
|
41
|
+
import type { AccountMainForm } from '../generated/forms/account.js';
|
|
42
|
+
const form = ctx.getFormContext() as AccountMainForm;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. **EntityNames Enum** in ALL Xrm.WebApi calls:
|
|
46
|
+
```typescript
|
|
47
|
+
import { EntityNames } from '../generated/entity-names.js';
|
|
48
|
+
Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
5. **Lookup helpers** from @xrmforge/helpers for ALL lookup value access:
|
|
52
|
+
```typescript
|
|
53
|
+
import { formLookup, formLookupId, parseLookup } from '@xrmforge/helpers';
|
|
54
|
+
// Form lookups (getAttribute on FormContext):
|
|
55
|
+
const customer = formLookup(form.getAttribute(Fields.CustomerId));
|
|
56
|
+
const customerId = formLookupId(form.getAttribute(Fields.CustomerId));
|
|
57
|
+
// Web API response lookups (_fieldname_value + OData annotations):
|
|
58
|
+
const parent = parseLookup(apiResponse, 'parentaccountid');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
6. **select()** from @xrmforge/helpers for ALL $select queries:
|
|
62
|
+
```typescript
|
|
63
|
+
import { select } from '@xrmforge/helpers';
|
|
64
|
+
Xrm.WebApi.retrieveRecord(EntityNames.Account, id, select(Fields.Name, Fields.Revenue))
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
7. **wrapHandler()** around EVERY exported async event handler:
|
|
68
|
+
```typescript
|
|
69
|
+
import { createLogger } from '../shared/logger';
|
|
70
|
+
import { wrapHandler } from '../shared/error-handler';
|
|
71
|
+
const logger = createLogger('Namespace.Entity');
|
|
72
|
+
export const onLoad = wrapHandler('Namespace.Entity.onLoad', logger, async (ctx) => {
|
|
73
|
+
// handler code
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
8. **createFormMock()** from @xrmforge/testing for ALL form tests:
|
|
78
|
+
```typescript
|
|
79
|
+
import { createFormMock, fireOnChange, setupXrmMock } from '@xrmforge/testing';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
9. **Module exports** (not window/global assignments). esbuild globalName handles namespacing.
|
|
83
|
+
|
|
84
|
+
10. **Structured Logger** instead of console.* (except in logger.ts itself):
|
|
85
|
+
```typescript
|
|
86
|
+
import { createLogger } from '../shared/logger';
|
|
87
|
+
const logger = createLogger('Namespace.Entity');
|
|
88
|
+
logger.info('Form loaded', { recordId });
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Rules: NEVER (every occurrence is a bug)
|
|
92
|
+
|
|
93
|
+
- Never `getAttribute("raw_string")` when Fields enum exists
|
|
94
|
+
- Never magic numbers for OptionSet values (use OptionSet enums)
|
|
95
|
+
- Never `Xrm.Page` (deprecated since D365 v9.0)
|
|
96
|
+
- Never synchronous XMLHttpRequest
|
|
97
|
+
- Never `eval()`
|
|
98
|
+
- Never `window.X = ...` (use module exports)
|
|
99
|
+
- Never `console.log/warn/error` in form scripts (use shared logger)
|
|
100
|
+
- Never export async handlers without wrapHandler()
|
|
101
|
+
- Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames)
|
|
102
|
+
- Never `"?$select=name,revenue"` as raw string (use select() from @xrmforge/helpers)
|
|
103
|
+
- Never `.getValue()[0].id.replace(...)` for lookups (use formLookup/formLookupId from @xrmforge/helpers)
|
|
104
|
+
- Never `import ... from '@xrmforge/typegen'` in browser code. @xrmforge/typegen is a Node.js CLI tool. Use `@xrmforge/helpers` for browser-safe runtime functions (select, parseLookup, formLookup, createUnboundAction, etc.)
|
|
105
|
+
|
|
106
|
+
## Mandatory Shared Utilities
|
|
107
|
+
|
|
108
|
+
Every XrmForge project MUST have these in `src/shared/`:
|
|
109
|
+
|
|
110
|
+
### logger.ts
|
|
111
|
+
```typescript
|
|
112
|
+
export interface Logger { debug(msg: string, data?: unknown): void; info(...); warn(...); error(...); }
|
|
113
|
+
export function createLogger(namespace: string): Logger;
|
|
114
|
+
// Only file allowed to use console.*
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### error-handler.ts
|
|
118
|
+
```typescript
|
|
119
|
+
export function wrapHandler<T>(name: string, logger: Logger, handler: T): T;
|
|
120
|
+
// Catches sync+async errors, shows form notification, never rethrows
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### constants.ts
|
|
124
|
+
```typescript
|
|
125
|
+
export const NOTIFICATION_IDS = { ... } as const;
|
|
126
|
+
export const MESSAGES = { ... } as const;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Before/After Examples
|
|
130
|
+
|
|
131
|
+
### Field Access
|
|
132
|
+
```typescript
|
|
133
|
+
// BEFORE: formContext.getAttribute("name").getValue()
|
|
134
|
+
// AFTER:
|
|
135
|
+
import { AccountMainFormFieldsEnum as Fields } from '../generated/forms/account.js';
|
|
136
|
+
import type { AccountMainForm } from '../generated/forms/account.js';
|
|
137
|
+
const form = ctx.getFormContext() as AccountMainForm;
|
|
138
|
+
form.getAttribute(Fields.AccountName).getValue(); // StringAttribute, typed
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### OptionSet Comparison
|
|
142
|
+
```typescript
|
|
143
|
+
// BEFORE: if (status.getValue() === 595300002) { ... }
|
|
144
|
+
// AFTER:
|
|
145
|
+
import { StatusCode } from '../generated/optionsets/invoice.js';
|
|
146
|
+
if (status.getValue() === StatusCode.Gebucht) { ... }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Testing
|
|
150
|
+
```typescript
|
|
151
|
+
import { createFormMock } from '@xrmforge/testing';
|
|
152
|
+
const mock = createFormMock<AccountMainForm>({
|
|
153
|
+
name: 'Test', statuscode: 0
|
|
154
|
+
});
|
|
155
|
+
onLoad(mock.asEventContext());
|
|
156
|
+
expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## File Structure
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
src/forms/{entity}-form.ts - Form scripts (one per entity)
|
|
163
|
+
src/shared/{name}.ts - Shared utilities
|
|
164
|
+
generated/ - Generated types (do not edit manually)
|
|
165
|
+
tests/forms/{entity}.test.ts - Tests
|
|
166
|
+
xrmforge.config.json - Build config
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Pattern Recognition: Legacy to XrmForge
|
|
170
|
+
|
|
171
|
+
When you see these patterns in legacy code, apply the XrmForge replacement:
|
|
172
|
+
|
|
173
|
+
| Legacy Pattern | XrmForge Replacement |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `getAttribute("name")` | `getAttribute(Fields.Name)` |
|
|
176
|
+
| `getControl("name")` | `getControl(Fields.Name)` |
|
|
177
|
+
| `getValue() === 595300000` | `getValue() === OptionSets.StatusCode.Active` |
|
|
178
|
+
| `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
|
|
179
|
+
| `"?$select=name,revenue"` | `select(Fields.Name, Fields.Revenue)` (from @xrmforge/helpers) |
|
|
180
|
+
| `value[0].id.replace("{","")...` | `parseLookup(form.getAttribute(Fields.X))` (from @xrmforge/helpers) |
|
|
181
|
+
| `Xrm.Page.getAttribute(...)` | `formContext.getAttribute(...)` |
|
|
182
|
+
| `var formContext` (global) | `const form = ctx.getFormContext()` (parameter) |
|
|
183
|
+
| `function form_OnLoad(ctx)` | `export function onLoad(ctx: Xrm.Events.EventContext)` |
|
|
184
|
+
| `.then(success, error)` | `async/await with try/catch` |
|
|
185
|
+
|
|
186
|
+
### Creating OptionSet Enums from Legacy Magic Numbers
|
|
187
|
+
|
|
188
|
+
When you find magic numbers like `getValue() === 105710002` in legacy code:
|
|
189
|
+
1. Search the file for ALL numeric comparisons with getValue()
|
|
190
|
+
2. Create a const enum in generated/optionsets/ with descriptive names
|
|
191
|
+
3. Import and use the enum instead of the number
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
```typescript
|
|
195
|
+
// generated/optionsets/invoice.ts
|
|
196
|
+
export const enum InvoiceStatusCode {
|
|
197
|
+
Neu = 1,
|
|
198
|
+
Versendet = 105710000,
|
|
199
|
+
Abgeschlossen = 105710001,
|
|
200
|
+
Gebucht = 105710002,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// In the form script:
|
|
204
|
+
import { InvoiceStatusCode } from '../../generated/optionsets/invoice.js';
|
|
205
|
+
if (status.getValue() === InvoiceStatusCode.Gebucht) { ... }
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Testing with Global Xrm Mock
|
|
209
|
+
|
|
210
|
+
Use `setupXrmMock()` from @xrmforge/testing to mock the global Xrm namespace:
|
|
211
|
+
```typescript
|
|
212
|
+
import { createFormMock, setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
|
|
213
|
+
|
|
214
|
+
beforeEach(() => setupXrmMock());
|
|
215
|
+
afterEach(() => teardownXrmMock());
|
|
216
|
+
|
|
217
|
+
// Override specific WebApi methods:
|
|
218
|
+
setupXrmMock({
|
|
219
|
+
webApiOverrides: {
|
|
220
|
+
retrieveMultipleRecords: async () => ({ entities: [{ name: 'Test' }] }),
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Build
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx xrmforge build # IIFE bundles for D365
|
|
229
|
+
npx xrmforge build --watch # Watch mode (~10ms rebuilds)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## @types/xrm Pitfalls (known issues)
|
|
233
|
+
|
|
234
|
+
When creating manual typings without `xrmforge generate`:
|
|
235
|
+
|
|
236
|
+
1. **Form Interface:** Do NOT use `interface extends Xrm.FormContext` (getAttribute overload conflicts).
|
|
237
|
+
Use `Omit` pattern instead:
|
|
238
|
+
```typescript
|
|
239
|
+
interface AccountMainForm extends Omit<Xrm.FormContext, 'getAttribute' | 'getControl'> {
|
|
240
|
+
getAttribute(name: Fields.AccountName): Xrm.Attributes.StringAttribute;
|
|
241
|
+
getAttribute(name: string): Xrm.Attributes.Attribute;
|
|
242
|
+
// ...
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
2. **AlertDialogResponse** does NOT exist in @types/xrm. Use `Xrm.Async.PromiseLike<void>`.
|
|
247
|
+
|
|
248
|
+
3. **ConfirmDialogResponse** does NOT exist. Use `Xrm.Navigation.ConfirmResult`.
|
|
249
|
+
|
|
250
|
+
4. **setNotification()** requires 2 arguments: (message, uniqueId).
|
|
251
|
+
|
|
252
|
+
5. **openFile()** requires `fileSize` property in FileDetails.
|
|
253
|
+
|
|
254
|
+
6. **const enum in .d.ts files** cannot be imported at runtime by test frameworks.
|
|
255
|
+
Since v0.8.0, XrmForge generates `.ts` files, so this is no longer an issue.
|
|
256
|
+
For manual typings, use regular `enum` in `.ts` files (not `.d.ts`).
|
|
257
|
+
|
|
258
|
+
## Self-Check (MANDATORY before Tests)
|
|
259
|
+
|
|
260
|
+
After converting ALL scripts, run these checks. Fix every violation before proceeding to tests.
|
|
261
|
+
Document results in SESSION-GEDAECHTNIS.md (violation count per category).
|
|
262
|
+
|
|
263
|
+
### Pattern Compliance (all must be 0, or documented exception)
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
# 1. Raw field strings in getAttribute/getControl (must use Fields Enum)
|
|
267
|
+
grep -rn "getAttribute('" src/forms/ --include="*.ts" | grep -v "Fields\."
|
|
268
|
+
grep -rn "getControl('" src/forms/ --include="*.ts" | grep -v "Fields\."
|
|
269
|
+
|
|
270
|
+
# 2. Magic numbers in OptionSet comparisons (must use OptionSet Enum)
|
|
271
|
+
grep -rn "getValue() ===" src/ --include="*.ts" | grep -E "[0-9]{3,}"
|
|
272
|
+
|
|
273
|
+
# 3. Direct _value access instead of parseLookup (in Web API responses)
|
|
274
|
+
grep -rn "_value\b" src/ --include="*.ts" | grep -v "generated/" | grep -v "parseLookup" | grep -v "getValue"
|
|
275
|
+
|
|
276
|
+
# 4. Raw entity names in WebApi calls (must use EntityNames)
|
|
277
|
+
grep -rn "retrieveRecord\|retrieveMultipleRecords\|deleteRecord\|createRecord\|updateRecord" src/ --include="*.ts" | grep "'[a-z]" | grep -v "EntityNames"
|
|
278
|
+
|
|
279
|
+
# 5. Missing select() - no raw "$select=" strings anywhere in src/
|
|
280
|
+
grep -rn '\$select' src/ --include="*.ts" | grep -v "select(" | grep -v "generated/"
|
|
281
|
+
|
|
282
|
+
# 6. Missing FormContext Cast in onLoad (must have "as <Generated>Form")
|
|
283
|
+
grep -rn "getFormContext()" src/forms/ --include="*.ts" | grep -v " as "
|
|
284
|
+
|
|
285
|
+
# 7. Exported handlers without wrapHandler
|
|
286
|
+
grep -rn "^export const\|^export async function\|^export function" src/forms/ --include="*.ts" | grep -v "wrapHandler"
|
|
287
|
+
|
|
288
|
+
# 8. Entity-level FieldsEnums not used (generated/fields/ should be imported)
|
|
289
|
+
echo "Fields imports from generated/fields/:"
|
|
290
|
+
grep -rn "from.*generated/fields/" src/ --include="*.ts" | wc -l
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Code Quality (all must be 0)
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
# console.* outside logger.ts
|
|
297
|
+
grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"
|
|
298
|
+
|
|
299
|
+
# Xrm.Page (deprecated since D365 v9.0)
|
|
300
|
+
grep -rn "Xrm\.Page" src/ --include="*.ts"
|
|
301
|
+
|
|
302
|
+
# var declarations
|
|
303
|
+
grep -rnE "^\s*var " src/ --include="*.ts"
|
|
304
|
+
|
|
305
|
+
# eval()
|
|
306
|
+
grep -rn "\beval(" src/ --include="*.ts"
|
|
307
|
+
|
|
308
|
+
# XMLHttpRequest
|
|
309
|
+
grep -rn "XMLHttpRequest" src/ --include="*.ts"
|
|
310
|
+
|
|
311
|
+
# as any without eslint-disable comment explaining why
|
|
312
|
+
grep -rn "as any" src/ --include="*.ts" | grep -v "eslint-disable"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Documentation (all must pass)
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
# Files without JSDoc header (first line must be /**)
|
|
319
|
+
for f in src/forms/*.ts src/shared/*.ts; do
|
|
320
|
+
head -1 "$f" | grep -q "^/\*\*" || echo "No header: $f"
|
|
321
|
+
done
|
|
322
|
+
|
|
323
|
+
# Exported functions without JSDoc
|
|
324
|
+
grep -rn -B1 "^export " src/ --include="*.ts" | grep -E "^[^*]*export" | grep -v "/\*\*"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Test Completeness
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
# Every form script needs a test file
|
|
331
|
+
for f in src/forms/*.ts; do
|
|
332
|
+
base=$(basename "$f" .ts)
|
|
333
|
+
test -f "tests/forms/${base}.test.ts" || echo "No test: $f"
|
|
334
|
+
done
|
|
335
|
+
|
|
336
|
+
# Every test file must use setupXrmMock
|
|
337
|
+
for f in tests/**/*.test.ts; do
|
|
338
|
+
grep -q "setupXrmMock" "$f" || echo "No setupXrmMock: $f"
|
|
339
|
+
done
|
|
340
|
+
|
|
341
|
+
# Every test file needs at least 2 test cases
|
|
342
|
+
for f in tests/**/*.test.ts; do
|
|
343
|
+
count=$(grep -c "it(" "$f" 2>/dev/null || echo 0)
|
|
344
|
+
[ "$count" -lt 2 ] && echo "Only $count tests: $f"
|
|
345
|
+
done
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Exceptions
|
|
349
|
+
|
|
350
|
+
Some checks have legitimate exceptions:
|
|
351
|
+
- **Raw field strings in helpers**: Generic helper functions that accept `fieldName: string` parameters cannot use Fields Enums. Document these.
|
|
352
|
+
- **System entities not in EntityNames**: Entities not in the Solution (e.g. `annotation`, `transactioncurrency`, `systemuser`) may use string literals. Document which ones.
|
|
353
|
+
- **as any for Grid.refresh()**: `@types/xrm` does not type `Grid.refresh()`. Requires eslint-disable with explanation.
|
|
354
|
+
|
|
355
|
+
## Full Migration Guide
|
|
356
|
+
|
|
357
|
+
See: https://www.npmjs.com/package/@xrmforge/typegen (MIGRATION.md)
|