@xrmforge/devkit 0.5.4 → 0.5.7
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/templates/AGENT.md +108 -15
- package/dist/templates/self-check.sh +2 -2
- package/package.json +1 -1
package/dist/templates/AGENT.md
CHANGED
|
@@ -18,8 +18,18 @@ Run `xrmforge generate` to create:
|
|
|
18
18
|
- `generated/entities/{entity}.ts` - Entity interface
|
|
19
19
|
- `generated/fields/{entity}.ts` - Entity Fields enum for type-safe $select queries
|
|
20
20
|
- `generated/entity-names.ts` - EntityNames const enum
|
|
21
|
+
- `generated/form-mapping.json` - Entity to form interface mapping (read after generate!)
|
|
21
22
|
- `generated/index.ts` - Barrel file with `export * from` re-exports
|
|
22
23
|
|
|
24
|
+
**After generate:** Read `generated/form-mapping.json` for the mapping of entity logical
|
|
25
|
+
names to form interface names. Do NOT guess interface names from entity names.
|
|
26
|
+
Fields enum member names are based on the **primary language label** (often German),
|
|
27
|
+
not the logical field name. Always read the generated files to get correct names.
|
|
28
|
+
|
|
29
|
+
**Large projects (50+ entities):** `form-mapping.json` can be large. Do NOT grep through
|
|
30
|
+
all generated files to find interface names. Read `form-mapping.json` once, then use
|
|
31
|
+
the mapping for all imports. This saves time and avoids wrong guesses.
|
|
32
|
+
|
|
23
33
|
## Rules: MANDATORY (every violation is a bug)
|
|
24
34
|
|
|
25
35
|
1. **Fields Enum** for ALL getAttribute/getControl calls. Never raw strings.
|
|
@@ -42,11 +52,12 @@ Run `xrmforge generate` to create:
|
|
|
42
52
|
const form = ctx.getFormContext() as AccountMainForm;
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
4. **EntityNames Enum** in ALL Xrm.WebApi calls:
|
|
55
|
+
4. **EntityNames Enum** in ALL Xrm.WebApi calls — no exceptions, even for system entities:
|
|
46
56
|
```typescript
|
|
47
57
|
import { EntityNames } from '../generated/entity-names.js';
|
|
48
58
|
Xrm.WebApi.retrieveRecord(EntityNames.Account, id)
|
|
49
59
|
```
|
|
60
|
+
If an entity is not in EntityNames, extend the generation (`--entities` flag) to include it.
|
|
50
61
|
|
|
51
62
|
5. **Lookup helpers** from @xrmforge/helpers for ALL lookup value access:
|
|
52
63
|
```typescript
|
|
@@ -57,11 +68,28 @@ Run `xrmforge generate` to create:
|
|
|
57
68
|
// Web API response lookups (_fieldname_value + OData annotations):
|
|
58
69
|
const parent = parseLookup(apiResponse, 'parentaccountid');
|
|
59
70
|
```
|
|
71
|
+
**NEVER** access lookup values directly:
|
|
72
|
+
```typescript
|
|
73
|
+
// BUG - never do this:
|
|
74
|
+
form.getAttribute(Fields.CustomerId).getValue()[0].id.replace('{','')
|
|
75
|
+
// CORRECT:
|
|
76
|
+
formLookupId(form.getAttribute(Fields.CustomerId))
|
|
77
|
+
```
|
|
60
78
|
|
|
61
79
|
6. **select()** from @xrmforge/helpers for ALL $select queries:
|
|
62
80
|
```typescript
|
|
63
81
|
import { select } from '@xrmforge/helpers';
|
|
82
|
+
// Simple query:
|
|
64
83
|
Xrm.WebApi.retrieveRecord(EntityNames.Account, id, select(Fields.Name, Fields.Revenue))
|
|
84
|
+
// Combined with $filter:
|
|
85
|
+
Xrm.WebApi.retrieveMultipleRecords(EntityNames.Account,
|
|
86
|
+
`${select(Fields.Name, Fields.Revenue)}&$filter=statecode eq 0`)
|
|
87
|
+
```
|
|
88
|
+
**Signature:** `select(...fields: string[])` takes REST parameters, NOT an array.
|
|
89
|
+
```typescript
|
|
90
|
+
select(Fields.Name, Fields.Revenue) // CORRECT
|
|
91
|
+
select([Fields.Name, Fields.Revenue]) // WRONG - array argument
|
|
92
|
+
select('account', Fields.Name) // WRONG - first arg is not entity name
|
|
65
93
|
```
|
|
66
94
|
|
|
67
95
|
7. **wrapHandler()** around EVERY exported async event handler:
|
|
@@ -98,11 +126,26 @@ Run `xrmforge generate` to create:
|
|
|
98
126
|
- Never `window.X = ...` (use module exports)
|
|
99
127
|
- Never `console.log/warn/error` in form scripts (use shared logger)
|
|
100
128
|
- Never export async handlers without wrapHandler()
|
|
101
|
-
- Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames)
|
|
129
|
+
- Never `Xrm.WebApi.retrieveRecord("account", ...)` with raw entity name (use EntityNames, no exceptions even for system entities)
|
|
102
130
|
- 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)
|
|
131
|
+
- Never `.getValue()[0].id` or `.getValue()[0].id.replace(...)` for lookups (use formLookup/formLookupId from @xrmforge/helpers)
|
|
132
|
+
- Never build your own lookup helper (getLookupValueId, firstLookupValue, etc.) when formLookup/formLookupId exists
|
|
104
133
|
- 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
134
|
|
|
135
|
+
## Subagent Handoff (when delegating to sub-agents)
|
|
136
|
+
|
|
137
|
+
Copy these MANDATORY rules into every sub-agent prompt:
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
1. Fields Enum for ALL getAttribute/getControl (never raw strings)
|
|
141
|
+
2. OptionSet Enum for ALL value comparisons (never magic numbers)
|
|
142
|
+
3. EntityNames for ALL Xrm.WebApi calls (never raw entity names, no exceptions)
|
|
143
|
+
4. formLookup/formLookupId for ALL lookup access (never .getValue()[0].id)
|
|
144
|
+
5. select() for ALL $select queries (never raw "$select=" strings)
|
|
145
|
+
6. wrapHandler() around EVERY exported async handler
|
|
146
|
+
7. createLogger() instead of console.* (except logger.ts)
|
|
147
|
+
```
|
|
148
|
+
|
|
106
149
|
## Mandatory Shared Utilities
|
|
107
150
|
|
|
108
151
|
Every XrmForge project MUST have these in `src/shared/`:
|
|
@@ -146,16 +189,34 @@ import { StatusCode } from '../generated/optionsets/invoice.js';
|
|
|
146
189
|
if (status.getValue() === StatusCode.Gebucht) { ... }
|
|
147
190
|
```
|
|
148
191
|
|
|
149
|
-
### Testing
|
|
192
|
+
### Testing (onLoad + onChange)
|
|
150
193
|
```typescript
|
|
151
|
-
import { createFormMock } from '@xrmforge/testing';
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
194
|
+
import { createFormMock, setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
|
|
195
|
+
|
|
196
|
+
beforeEach(() => setupXrmMock());
|
|
197
|
+
afterEach(() => teardownXrmMock());
|
|
198
|
+
|
|
199
|
+
// onLoad test:
|
|
200
|
+
const mock = createFormMock<AccountMainForm>({ name: 'Test', statuscode: 0 });
|
|
155
201
|
onLoad(mock.asEventContext());
|
|
156
|
-
expect(mock.
|
|
202
|
+
expect(mock.getControl(Fields.Revenue).getVisible()).toBe(true);
|
|
203
|
+
|
|
204
|
+
// onChange test (MANDATORY for every onChange handler):
|
|
205
|
+
mock.setValue(Fields.Revenue, 500000);
|
|
206
|
+
mock.fireOnChange(Fields.Revenue);
|
|
207
|
+
expect(mock.getControl(Fields.CreditLimit).getVisible()).toBe(true);
|
|
208
|
+
expect(mock.getValue(Fields.CreditOnHold)).toBe(true);
|
|
157
209
|
```
|
|
158
210
|
|
|
211
|
+
**Test quality rule:** At least 30% of tests MUST use `fireOnChange` or WebApi mock
|
|
212
|
+
assertions. Pure smoke tests (`onLoad` + `not.toThrow`) do NOT count as behavior tests.
|
|
213
|
+
Tests that only check `getOnChangeHandlers().length > 0` are registration tests, not
|
|
214
|
+
behavior tests. Every onChange handler MUST have a `fireOnChange` test.
|
|
215
|
+
|
|
216
|
+
**attr.controls:** Since @xrmforge/testing 0.2.3, `createFormMock()` automatically links
|
|
217
|
+
each attribute to its control. `mock.getControl(Fields.Name)` works out of the box.
|
|
218
|
+
No need to mock controls separately.
|
|
219
|
+
|
|
159
220
|
## File Structure
|
|
160
221
|
|
|
161
222
|
```
|
|
@@ -177,7 +238,7 @@ When you see these patterns in legacy code, apply the XrmForge replacement:
|
|
|
177
238
|
| `getValue() === 595300000` | `getValue() === OptionSets.StatusCode.Active` |
|
|
178
239
|
| `Xrm.WebApi.retrieveRecord("account", id)` | `Xrm.WebApi.retrieveRecord(EntityNames.Account, id)` |
|
|
179
240
|
| `"?$select=name,revenue"` | `select(Fields.Name, Fields.Revenue)` (from @xrmforge/helpers) |
|
|
180
|
-
| `value[0].id.replace("{","")...` | `
|
|
241
|
+
| `value[0].id.replace("{","")...` | `formLookupId(form.getAttribute(Fields.X))` for forms, `parseLookup(response, 'field')` for Web API |
|
|
181
242
|
| `Xrm.Page.getAttribute(...)` | `formContext.getAttribute(...)` |
|
|
182
243
|
| `var formContext` (global) | `const form = ctx.getFormContext()` (parameter) |
|
|
183
244
|
| `function form_OnLoad(ctx)` | `export function onLoad(ctx: Xrm.Events.EventContext)` |
|
|
@@ -260,6 +321,13 @@ When creating manual typings without `xrmforge generate`:
|
|
|
260
321
|
After converting ALL scripts, run these checks. Fix every violation before proceeding to tests.
|
|
261
322
|
Document results in SESSION-GEDAECHTNIS.md (violation count per category).
|
|
262
323
|
|
|
324
|
+
**Platform note:** The checks below use bash/grep syntax. On Windows PowerShell, use
|
|
325
|
+
`Select-String` instead of `grep`. Example:
|
|
326
|
+
```powershell
|
|
327
|
+
# PowerShell equivalent of: grep -rn "getAttribute('" src/forms/ --include="*.ts"
|
|
328
|
+
Get-ChildItem -Recurse src/forms -Filter *.ts | Select-String "getAttribute\('" | Where-Object { $_.Line -notmatch 'Fields\.' }
|
|
329
|
+
```
|
|
330
|
+
|
|
263
331
|
### Pattern Compliance (all must be 0, or documented exception)
|
|
264
332
|
|
|
265
333
|
```bash
|
|
@@ -273,8 +341,9 @@ grep -rn "getValue() ===" src/ --include="*.ts" | grep -E "[0-9]{3,}"
|
|
|
273
341
|
# 3. Direct _value access instead of parseLookup (in Web API responses)
|
|
274
342
|
grep -rn "_value\b" src/ --include="*.ts" | grep -v "generated/" | grep -v "parseLookup" | grep -v "getValue"
|
|
275
343
|
|
|
276
|
-
# 4. Raw entity names in WebApi calls (must use EntityNames)
|
|
344
|
+
# 4. Raw entity names in WebApi calls (must use EntityNames, no exceptions)
|
|
277
345
|
grep -rn "retrieveRecord\|retrieveMultipleRecords\|deleteRecord\|createRecord\|updateRecord" src/ --include="*.ts" | grep "'[a-z]" | grep -v "EntityNames"
|
|
346
|
+
# Every match is a violation. If entity is missing from EntityNames, extend generation.
|
|
278
347
|
|
|
279
348
|
# 5. Missing select() - no raw "$select=" strings anywhere in src/
|
|
280
349
|
grep -rn '\$select' src/ --include="*.ts" | grep -v "select(" | grep -v "generated/"
|
|
@@ -293,11 +362,11 @@ grep -rn "from.*generated/fields/" src/ --include="*.ts" | wc -l
|
|
|
293
362
|
### Code Quality (all must be 0)
|
|
294
363
|
|
|
295
364
|
```bash
|
|
296
|
-
# console.* outside logger.ts
|
|
297
|
-
grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"
|
|
365
|
+
# console.* outside logger.ts (exclude JSDoc comments)
|
|
366
|
+
grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts" | grep -v "^\s*\*" | grep -v "^\s*//"
|
|
298
367
|
|
|
299
|
-
# Xrm.Page (deprecated since D365 v9.0)
|
|
300
|
-
grep -rn "Xrm\.Page" src/ --include="*.ts"
|
|
368
|
+
# Xrm.Page (deprecated since D365 v9.0, exclude JSDoc comments)
|
|
369
|
+
grep -rn "Xrm\.Page" src/ --include="*.ts" | grep -v "^\s*\*" | grep -v "^\s*//"
|
|
301
370
|
|
|
302
371
|
# var declarations
|
|
303
372
|
grep -rnE "^\s*var " src/ --include="*.ts"
|
|
@@ -345,6 +414,30 @@ for f in tests/**/*.test.ts; do
|
|
|
345
414
|
done
|
|
346
415
|
```
|
|
347
416
|
|
|
417
|
+
### Test Gap Analysis (after writing tests)
|
|
418
|
+
|
|
419
|
+
Before declaring tests complete, verify coverage gaps:
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
# 1. onChange handlers without fireOnChange test
|
|
423
|
+
for f in tests/**/*.test.ts; do
|
|
424
|
+
has_onchange=$(grep -c "onChange\|on_change\|OnChange" "$(echo $f | sed 's|tests/|src/|;s|.test.ts|.ts|')" 2>/dev/null || echo 0)
|
|
425
|
+
has_fire=$(grep -c "fireOnChange" "$f" 2>/dev/null || echo 0)
|
|
426
|
+
[ "$has_onchange" -gt 0 ] && [ "$has_fire" -eq 0 ] && echo "Missing fireOnChange: $f"
|
|
427
|
+
done
|
|
428
|
+
|
|
429
|
+
# 2. WebApi calls without mock assertions
|
|
430
|
+
grep -rln "Xrm.WebApi\|retrieveRecord\|retrieveMultiple" src/forms/ --include="*.ts" | while read f; do
|
|
431
|
+
test_f="tests/forms/$(basename "$f" .ts).test.ts"
|
|
432
|
+
[ -f "$test_f" ] && ! grep -q "retrieveRecord\|retrieveMultiple\|webApiOverrides" "$test_f" && echo "No WebApi mock: $test_f"
|
|
433
|
+
done
|
|
434
|
+
|
|
435
|
+
# 3. Behavior test ratio (target: >= 30%)
|
|
436
|
+
total=$(grep -rc "it(" tests/ --include="*.test.ts" 2>/dev/null | awk -F: '{s+=$2}END{print s}')
|
|
437
|
+
behavior=$(grep -rc "fireOnChange\|retrieveRecord\|retrieveMultiple\|expect.*getValue\|expect.*getVisible" tests/ --include="*.test.ts" 2>/dev/null | awk -F: '{s+=$2}END{print s}')
|
|
438
|
+
echo "Behavior tests: $behavior / $total (target: >= 30%)"
|
|
439
|
+
```
|
|
440
|
+
|
|
348
441
|
### Exceptions
|
|
349
442
|
|
|
350
443
|
Some checks have legitimate exceptions:
|
|
@@ -57,10 +57,10 @@ echo ""
|
|
|
57
57
|
echo "--- Code Quality ---"
|
|
58
58
|
|
|
59
59
|
check "console.* outside logger.ts" \
|
|
60
|
-
bash -c 'grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts"'
|
|
60
|
+
bash -c 'grep -rn "console\." src/ --include="*.ts" | grep -v "logger.ts" | grep -v "^\s*\*" | grep -v "^\s*//"'
|
|
61
61
|
|
|
62
62
|
check "Xrm.Page (deprecated since D365 v9.0)" \
|
|
63
|
-
bash -c 'grep -rn "Xrm\.Page" src/ --include="*.ts"'
|
|
63
|
+
bash -c 'grep -rn "Xrm\.Page" src/ --include="*.ts" | grep -v "^\s*\*" | grep -v "^\s*//"'
|
|
64
64
|
|
|
65
65
|
check "var declarations" \
|
|
66
66
|
bash -c 'grep -rnE "^\s*var " src/ --include="*.ts"'
|