@xrmforge/typegen 0.6.0 → 0.7.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/MIGRATION.md +14 -3
- package/dist/index.d.ts +203 -2
- package/docs/architecture/00-README.md +26 -0
- package/docs/architecture/01-executive-summary.md +11 -0
- package/docs/architecture/02-packages.md +110 -0
- package/docs/architecture/03-generated-types.md +176 -0
- package/docs/architecture/04-cli.md +58 -0
- package/docs/architecture/05-build.md +50 -0
- package/docs/architecture/06-incremental.md +42 -0
- package/docs/architecture/07-http-client.md +59 -0
- package/docs/architecture/08-authentication.md +18 -0
- package/docs/architecture/09-testing.md +55 -0
- package/docs/architecture/10-eslint-plugin.md +82 -0
- package/docs/architecture/11-agent-md.md +38 -0
- package/docs/architecture/12-xrm-pitfalls.md +14 -0
- package/docs/architecture/13-helpers.md +50 -0
- package/docs/architecture/14-showcases.md +21 -0
- package/docs/architecture/15-ci-cd.md +49 -0
- package/docs/architecture/16-technical-debt.md +17 -0
- package/docs/architecture/17-roadmap.md +25 -0
- package/docs/architecture/18-design-principles.md +22 -0
- package/docs/architektur/00-README.md +26 -0
- package/docs/architektur/01-zusammenfassung.md +11 -0
- package/docs/architektur/02-packages.md +110 -0
- package/docs/architektur/03-generierte-typen.md +176 -0
- package/docs/architektur/04-cli.md +58 -0
- package/docs/architektur/05-build.md +50 -0
- package/docs/architektur/06-inkrementell.md +42 -0
- package/docs/architektur/07-http-client.md +59 -0
- package/docs/architektur/08-authentifizierung.md +18 -0
- package/docs/architektur/09-testing.md +55 -0
- package/docs/architektur/10-eslint-plugin.md +82 -0
- package/docs/architektur/11-agent-md.md +38 -0
- package/docs/architektur/12-xrm-fallstricke.md +14 -0
- package/docs/architektur/13-helpers.md +50 -0
- package/docs/architektur/14-showcases.md +21 -0
- package/docs/architektur/15-ci-cd.md +49 -0
- package/docs/architektur/16-technische-schulden.md +17 -0
- package/docs/architektur/17-roadmap.md +25 -0
- package/docs/architektur/18-designprinzipien.md +22 -0
- package/package.json +4 -8
- package/dist/helpers.d.ts +0 -203
- package/dist/helpers.js +0 -120
- package/dist/helpers.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Build Architecture
|
|
2
|
+
|
|
3
|
+
### 5.1 esbuild IIFE Bundles
|
|
4
|
+
|
|
5
|
+
XrmForge uses esbuild to create IIFE (Immediately Invoked Function Expression) bundles for Dynamics 365. D365 requires scripts to be registered as namespace.function (e.g. `Contoso.Account.onLoad`).
|
|
6
|
+
|
|
7
|
+
**esbuild configuration per entry:**
|
|
8
|
+
```
|
|
9
|
+
format: 'iife'
|
|
10
|
+
bundle: true
|
|
11
|
+
globalName: entry.namespace // e.g. 'Contoso.Account'
|
|
12
|
+
target: config.target // default: 'es2020'
|
|
13
|
+
minify: config.minify
|
|
14
|
+
sourcemap: config.sourcemap
|
|
15
|
+
external: config.external // e.g. ['fs', 'path'] for Node.js deps
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
All entries are built in parallel using `Promise.allSettled()`, allowing partial success.
|
|
19
|
+
|
|
20
|
+
### 5.2 xrmforge.config.json Schema
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"build": {
|
|
25
|
+
"outDir": "./dist/prefix_/JS",
|
|
26
|
+
"target": "es2020",
|
|
27
|
+
"sourcemap": true,
|
|
28
|
+
"minify": true,
|
|
29
|
+
"external": [],
|
|
30
|
+
"entries": {
|
|
31
|
+
"entry_name": {
|
|
32
|
+
"input": "./src/forms/account-form.ts",
|
|
33
|
+
"namespace": "Contoso.Account",
|
|
34
|
+
"out": "Account/OnLoad.js"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 5.3 globalName Handling
|
|
42
|
+
|
|
43
|
+
esbuild automatically creates nested globals from dotted namespaces:
|
|
44
|
+
```javascript
|
|
45
|
+
// namespace: "Contoso.Account" produces:
|
|
46
|
+
var Contoso = Contoso || {};
|
|
47
|
+
Contoso.Account = (() => { return { onLoad, onSave }; })();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
D365 event registration: `Contoso.Account.onLoad`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Incremental Generation
|
|
2
|
+
|
|
3
|
+
### 6.1 Overview
|
|
4
|
+
|
|
5
|
+
Incremental generation uses the Dataverse `RetrieveMetadataChanges` function to detect which entities have changed since the last generation. This reduces generation time from seconds to milliseconds (measured: 4720ms to 473ms, 10x improvement).
|
|
6
|
+
|
|
7
|
+
### 6.2 Components
|
|
8
|
+
|
|
9
|
+
**ChangeDetector** (`src/metadata/change-detector.ts`):
|
|
10
|
+
- `getInitialVersionStamp()` - First run: fetches the initial ServerVersionStamp
|
|
11
|
+
- `detectChanges(clientVersionStamp)` - Subsequent runs: returns changedEntityNames, deletedEntityNames, newVersionStamp
|
|
12
|
+
|
|
13
|
+
**MetadataCache** (`src/metadata/cache.ts`):
|
|
14
|
+
- Filesystem-based: `.xrmforge/cache/metadata.json`
|
|
15
|
+
- Stores: manifest (version, environment URL, ServerVersionStamp, last refreshed, entity list) + entityTypeInfos per entity
|
|
16
|
+
- Validation: checks cache version, environment URL match, file existence
|
|
17
|
+
|
|
18
|
+
### 6.3 Flow
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
First run (no cache):
|
|
22
|
+
1. Fetch all entity metadata
|
|
23
|
+
2. getInitialVersionStamp()
|
|
24
|
+
3. Save cache with ServerVersionStamp
|
|
25
|
+
|
|
26
|
+
Subsequent run (cache exists):
|
|
27
|
+
1. Load cache, validate environment URL
|
|
28
|
+
2. detectChanges(cachedVersionStamp)
|
|
29
|
+
3. Fetch only changed entities
|
|
30
|
+
4. Remove deleted entities from cache
|
|
31
|
+
5. Save cache with new ServerVersionStamp
|
|
32
|
+
|
|
33
|
+
Expired stamp (>90 days):
|
|
34
|
+
Error code 0x80044352 detected
|
|
35
|
+
Fall back to full refresh
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 6.4 RetrieveMetadataChanges API
|
|
39
|
+
|
|
40
|
+
- **Type:** OData Function (GET, not POST)
|
|
41
|
+
- **URL:** `/RetrieveMetadataChanges(Query=@q,ClientVersionStamp=@s)?@q={...}&@s='...'`
|
|
42
|
+
- **Response:** EntityMetadata[] with HasChanged flag, ServerVersionStamp, DeletedMetadata
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# HTTP Client
|
|
2
|
+
|
|
3
|
+
### 7.1 DataverseHttpClient
|
|
4
|
+
|
|
5
|
+
The core HTTP client (`src/http/client.ts`) provides resilient communication with the Dataverse Web API.
|
|
6
|
+
|
|
7
|
+
**Key methods:**
|
|
8
|
+
- `get<T>(path, signal?)` - Single GET request with retry
|
|
9
|
+
- `getAll<T>(path, signal?)` - GET with automatic @odata.nextLink paging (max 100 pages)
|
|
10
|
+
|
|
11
|
+
### 7.2 Read-Only Default
|
|
12
|
+
|
|
13
|
+
The client defaults to `readOnly: true`, blocking POST/PATCH/PUT/DELETE requests. This prevents accidental data mutations during type generation. Write access requires explicit `readOnly: false`.
|
|
14
|
+
|
|
15
|
+
### 7.3 Retry with Exponential Backoff
|
|
16
|
+
|
|
17
|
+
- **Base delay:** 1000ms (configurable)
|
|
18
|
+
- **Max backoff:** 60 seconds
|
|
19
|
+
- **Jitter:** Random delay up to base delay
|
|
20
|
+
- **Formula:** `min(baseDelay * 2^(attempt-1) + jitter, 60000)`
|
|
21
|
+
- **Max retries:** configurable (default: 3)
|
|
22
|
+
|
|
23
|
+
### 7.4 Rate Limiting (HTTP 429)
|
|
24
|
+
|
|
25
|
+
- **Separate counter** from standard retries (not mixed)
|
|
26
|
+
- **Retry-After header** respected (seconds converted to milliseconds)
|
|
27
|
+
- **Max 10 consecutive 429 retries** (DEFAULT_MAX_RATE_LIMIT_RETRIES)
|
|
28
|
+
- 429 responses do NOT increment the standard retry counter
|
|
29
|
+
|
|
30
|
+
### 7.5 Concurrency Control
|
|
31
|
+
|
|
32
|
+
Non-recursive semaphore pattern:
|
|
33
|
+
- **maxConcurrency:** 5 (default)
|
|
34
|
+
- Wait queue with FIFO ordering
|
|
35
|
+
- All retries happen inside a single slot (prevents slot exhaustion)
|
|
36
|
+
|
|
37
|
+
### 7.6 Token Caching
|
|
38
|
+
|
|
39
|
+
- In-memory only (never persisted to disk)
|
|
40
|
+
- 5-minute buffer before expiry (TOKEN_BUFFER_MS = 300000)
|
|
41
|
+
- Pending token refresh promise prevents concurrent token requests
|
|
42
|
+
|
|
43
|
+
### 7.7 Input Sanitization
|
|
44
|
+
|
|
45
|
+
OData injection prevention:
|
|
46
|
+
- `sanitizeIdentifier()` - Regex `[a-zA-Z_][a-zA-Z0-9_]*`
|
|
47
|
+
- `sanitizeGuid()` - GUID format validation
|
|
48
|
+
- `escapeODataString()` - Single quote doubling
|
|
49
|
+
|
|
50
|
+
### 7.8 Error Handling
|
|
51
|
+
|
|
52
|
+
| HTTP Status | Behavior | Retried |
|
|
53
|
+
|-------------|----------|---------|
|
|
54
|
+
| 2xx | Success | No |
|
|
55
|
+
| 401 | Clear token cache, retry once | Yes (1x) |
|
|
56
|
+
| 429 | Respect Retry-After, separate counter | Yes (up to 10x) |
|
|
57
|
+
| 5xx | Exponential backoff | Yes (up to maxRetries) |
|
|
58
|
+
| 404, 403 | Non-retryable | No |
|
|
59
|
+
| Network error | Exponential backoff | Yes |
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Authentication
|
|
2
|
+
|
|
3
|
+
### 8.1 Credential Factory
|
|
4
|
+
|
|
5
|
+
`createCredential(config: AuthConfig)` returns a `TokenCredential` (from @azure/identity) based on the auth method:
|
|
6
|
+
|
|
7
|
+
### 8.2 Four Auth Flows
|
|
8
|
+
|
|
9
|
+
| Method | Config | @azure/identity Class | Use Case |
|
|
10
|
+
|--------|--------|-----------------------|----------|
|
|
11
|
+
| client-credentials | tenantId, clientId, clientSecret | ClientSecretCredential | CI/CD, automated pipelines |
|
|
12
|
+
| interactive | tenantId, clientId? | InteractiveBrowserCredential | Developer workstation |
|
|
13
|
+
| device-code | tenantId, clientId? | DeviceCodeCredential | Headless CLI environments |
|
|
14
|
+
| token | token (string) | StaticTokenCredential | Pre-acquired tokens (e.g. from TokenVault) |
|
|
15
|
+
|
|
16
|
+
### 8.3 Token Scope
|
|
17
|
+
|
|
18
|
+
All auth flows request the scope: `{environmentUrl}/.default`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Testing Framework
|
|
2
|
+
|
|
3
|
+
### 9.1 createFormMock
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { createFormMock } from '@xrmforge/testing';
|
|
7
|
+
import type { AccountMainForm, AccountMainFormMockValues } from '../typings/forms/account';
|
|
8
|
+
|
|
9
|
+
const mock = createFormMock<AccountMainForm, AccountMainFormMockValues>({
|
|
10
|
+
name: 'Contoso Ltd',
|
|
11
|
+
statuscode: 0,
|
|
12
|
+
revenue: 1000000,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Use in tests:
|
|
16
|
+
onLoad(mock.executionContext);
|
|
17
|
+
expect(mock.formContext.getControl('revenue').getVisible()).toBe(true);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**What it mocks:**
|
|
21
|
+
- Attributes: MockAttribute instances with getValue/setValue, dirty tracking, onChange handlers, required level, submit mode
|
|
22
|
+
- Controls: MockControl instances with visible/disabled/label/notification state
|
|
23
|
+
- UI: Form notifications, tab/section stubs
|
|
24
|
+
- Entity: ID, entity name, primary attribute
|
|
25
|
+
- Data: refresh(), save() stubs returning Promise-like
|
|
26
|
+
- Navigation: openForm/openAlertDialog stubs
|
|
27
|
+
|
|
28
|
+
**Lazy initialization:** Attributes accessed via `getAttribute()` that were not in the initial values are created on-the-fly with null value.
|
|
29
|
+
|
|
30
|
+
### 9.2 fireOnChange
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
mock.fireOnChange('statuscode');
|
|
34
|
+
// Triggers all handlers registered via getAttribute('statuscode').addOnChange(handler)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Creates a MockEventContext with the attribute as event source.
|
|
38
|
+
|
|
39
|
+
### 9.3 setupXrmMock / teardownXrmMock
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { setupXrmMock, teardownXrmMock } from '@xrmforge/testing';
|
|
43
|
+
|
|
44
|
+
beforeEach(() => setupXrmMock());
|
|
45
|
+
afterEach(() => teardownXrmMock());
|
|
46
|
+
|
|
47
|
+
// With WebApi overrides:
|
|
48
|
+
setupXrmMock({
|
|
49
|
+
webApiOverrides: {
|
|
50
|
+
retrieveMultipleRecords: async () => ({ entities: [{ name: 'Test' }] }),
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Sets up a global `Xrm` object on `globalThis` with minimal WebApi, Navigation, and Utility stubs.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ESLint Plugin
|
|
2
|
+
|
|
3
|
+
### 10.1 Installation
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
// eslint.config.js (flat config, ESLint v9)
|
|
7
|
+
import xrmforge from '@xrmforge/eslint-plugin';
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
xrmforge.configs.recommended,
|
|
11
|
+
// or pick individual rules
|
|
12
|
+
];
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 10.2 Rules
|
|
16
|
+
|
|
17
|
+
#### no-xrm-page (error)
|
|
18
|
+
|
|
19
|
+
Forbids the deprecated `Xrm.Page` API (removed in D365 v9.0+).
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// Bad
|
|
23
|
+
Xrm.Page.getAttribute("name");
|
|
24
|
+
|
|
25
|
+
// Good
|
|
26
|
+
const form = executionContext.getFormContext();
|
|
27
|
+
form.getAttribute("name");
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
#### no-magic-optionset (warn)
|
|
31
|
+
|
|
32
|
+
Forbids raw numbers (>= 2) in comparisons with `.getValue()`.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
// Bad
|
|
36
|
+
if (attr.getValue() === 595300000) { }
|
|
37
|
+
|
|
38
|
+
// Good
|
|
39
|
+
import { StatusCode } from '../typings/optionsets/account';
|
|
40
|
+
if (attr.getValue() === StatusCode.Active) { }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### no-sync-webapi (error)
|
|
44
|
+
|
|
45
|
+
Forbids synchronous XMLHttpRequest (`new XMLHttpRequest()` and `.open()` with `async=false`).
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// Bad
|
|
49
|
+
xhr.open("GET", url, false);
|
|
50
|
+
|
|
51
|
+
// Good
|
|
52
|
+
const data = await Xrm.WebApi.retrieveRecord("account", id);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### require-error-handling (warn)
|
|
56
|
+
|
|
57
|
+
Requires try/catch in exported async functions starting with "on" (event handlers).
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// Bad
|
|
61
|
+
export async function onLoad(ctx) {
|
|
62
|
+
await fetch("/api"); // no error handling
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Good
|
|
66
|
+
export async function onLoad(ctx) {
|
|
67
|
+
try { await fetch("/api"); }
|
|
68
|
+
catch (error) { console.error(error); }
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### require-namespace (warn)
|
|
73
|
+
|
|
74
|
+
Forbids direct `window.X = ...` or `globalThis.X = ...` assignments. Module exports with esbuild globalName should be used instead.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
// Bad
|
|
78
|
+
window.Contoso = { onLoad: function() {} };
|
|
79
|
+
|
|
80
|
+
// Good
|
|
81
|
+
export function onLoad(ctx: Xrm.Events.EventContext) {}
|
|
82
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# AGENT.md System
|
|
2
|
+
|
|
3
|
+
### 11.1 Purpose
|
|
4
|
+
|
|
5
|
+
The AGENT.md is a scaffolded file that teaches AI coding assistants (Claude, ChatGPT, Copilot, Cursor) how to write optimal D365 form scripts using XrmForge. It is generated by `xrmforge init` and placed in the project root.
|
|
6
|
+
|
|
7
|
+
### 11.2 Content Structure
|
|
8
|
+
|
|
9
|
+
1. **Package overview** - What each @xrmforge package does
|
|
10
|
+
2. **10 Rules: Always** - Fields Enum, OptionSet Enum, FormContext cast, EntityNames, parseLookup, select, createFormMock, module exports, Tabs/Sections enums, error handling
|
|
11
|
+
3. **Rules: Never** - Raw strings, magic numbers, Xrm.Page, sync XHR, eval, window assignments
|
|
12
|
+
4. **Before/After examples** - Field access, OptionSet comparison, testing
|
|
13
|
+
5. **Pattern Recognition table** - Legacy pattern to XrmForge replacement mapping
|
|
14
|
+
6. **OptionSet Enum creation guide** - How to create enums from magic numbers in legacy code
|
|
15
|
+
7. **Testing with setupXrmMock** - Global Xrm mock pattern
|
|
16
|
+
8. **Build commands** - xrmforge build, watch mode
|
|
17
|
+
9. **@types/xrm Pitfalls** - Known issues and workarounds
|
|
18
|
+
10. **File structure** - Expected project layout
|
|
19
|
+
|
|
20
|
+
### 11.3 Template System
|
|
21
|
+
|
|
22
|
+
The AGENT.md is stored as `src/scaffold/templates/AGENT.md` in the devkit package and loaded via `template-loader.ts` at scaffold time. No variable substitution needed (the file is static).
|
|
23
|
+
|
|
24
|
+
### 11.4 KI Comparison Test Results
|
|
25
|
+
|
|
26
|
+
Five AI models were tested converting legacy D365 JavaScript (account.js + lm_helper.js, 1,288 lines) to TypeScript with XrmForge:
|
|
27
|
+
|
|
28
|
+
| Rank | Model | Score | Tool | Strength |
|
|
29
|
+
|------|-------|-------|------|----------|
|
|
30
|
+
| 1 | Claude Opus 4.6 | 42/50 | Claude Code | Most tests (62), best code structure |
|
|
31
|
+
| 2 | Claude Sonnet 4.6 | 41/50 | Claude Code | Most bugs found (5), best DI approach |
|
|
32
|
+
| 3 | Cursor Composer 2 | 35/50 | Cursor IDE | Recognized select() Node API issue |
|
|
33
|
+
| 4 | ChatGPT GPT-4o | 30/50 | ChatGPT Web | Functional but less XrmForge-specific |
|
|
34
|
+
| 5 | MS Copilot | 12/50 | Browser Chat | No workspace access, never saw AGENT.md |
|
|
35
|
+
|
|
36
|
+
**Criteria (11, max 5 points each = 55 max):** Fields Enum usage, OptionSet Enums, FormContext typing, XrmForge helpers, module exports, tests present, test quality, error handling, code quality, bugs found, documentation.
|
|
37
|
+
|
|
38
|
+
**Key finding:** No AI consistently used `@xrmforge/typegen/helpers` imports (select, parseLookup). This remains the biggest adoption gap.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# @types/xrm Pitfalls
|
|
2
|
+
|
|
3
|
+
Known issues when working with `@types/xrm`:
|
|
4
|
+
|
|
5
|
+
| Issue | Wrong | Correct |
|
|
6
|
+
|-------|-------|---------|
|
|
7
|
+
| Form interface | `interface extends Xrm.FormContext` | `extends Omit<Xrm.FormContext, 'getAttribute' \| 'getControl'>` |
|
|
8
|
+
| AlertDialogResponse | `Xrm.Navigation.AlertDialogResponse` | `Xrm.Async.PromiseLike<void>` (type does not exist) |
|
|
9
|
+
| ConfirmDialogResponse | `Xrm.Navigation.ConfirmDialogResponse` | `Xrm.Navigation.ConfirmResult` (type does not exist) |
|
|
10
|
+
| setNotification | `setNotification(message)` | `setNotification(message, uniqueId)` (requires 2 args) |
|
|
11
|
+
| openFile | `openFile({ fileName, ... })` | Must include `fileSize` property in FileDetails |
|
|
12
|
+
| SubmitMode | `Xrm.Attributes.SubmitMode` | `Xrm.SubmitMode` |
|
|
13
|
+
| const enum in .d.ts | `const enum` in `.d.ts` files | Use regular `enum` in `.ts` files (vitest cannot import const enums from .d.ts) |
|
|
14
|
+
| Grid.refresh() | `grid.refresh()` | `(grid as any).refresh()` (not typed in @types/xrm) |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @xrmforge/helpers Package
|
|
2
|
+
|
|
3
|
+
### 13.1 Problem
|
|
4
|
+
|
|
5
|
+
The previous approach used a `/helpers` subpath export on `@xrmforge/typegen`. This was confusing because typegen is a Node.js code generation tool, while helpers are browser-safe runtime utilities. The subpath `@xrmforge/typegen/helpers` was non-obvious and AI coding assistants consistently failed to discover it.
|
|
6
|
+
|
|
7
|
+
### 13.2 Solution
|
|
8
|
+
|
|
9
|
+
A standalone `@xrmforge/helpers` package consolidates all browser-safe runtime code. Zero Node.js dependencies. Clean, discoverable import path:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// Import from the dedicated helpers package
|
|
13
|
+
import { select, parseLookup, typedForm } from '@xrmforge/helpers';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### 13.3 Exports
|
|
17
|
+
|
|
18
|
+
**Web API Helpers:**
|
|
19
|
+
- `select(...fields: string[]): string` - Builds `?$select=field1,field2`
|
|
20
|
+
- `selectExpand(fields: string[], expand: string): string` - Builds `?$select=...&$expand=...`
|
|
21
|
+
- `parseLookup(response: Record<string, unknown>, fieldName: string): LookupValue | null` - Parses `_fieldname_value` with OData annotations
|
|
22
|
+
- `parseLookups(response: Record<string, unknown>, fieldName: string): LookupValue[]` - Multi-value lookup parsing
|
|
23
|
+
- `parseFormattedValue(response: Record<string, unknown>, fieldName: string): string | null` - Extracts `@OData.Community.Display.V1.FormattedValue`
|
|
24
|
+
|
|
25
|
+
**Xrm Constants (8 const enums):**
|
|
26
|
+
- DisplayState, FormNotificationLevel, RequiredLevel, SubmitMode, SaveMode, ClientType, ClientState, OperationType
|
|
27
|
+
|
|
28
|
+
**typedForm() Proxy:**
|
|
29
|
+
- `typedForm<TForm>(formContext)` - Returns a proxy where `form.name` delegates to `getAttribute('name')`
|
|
30
|
+
- GET trap: Property access delegates to getAttribute(); `$context` returns raw FormContext; `$control(name)` returns getControl()
|
|
31
|
+
- SET trap: Throws TypeError forcing `.setValue()` usage
|
|
32
|
+
- HAS trap: Checks if attribute exists on the form
|
|
33
|
+
|
|
34
|
+
**Action/Function Runtime:**
|
|
35
|
+
- `createBoundAction(entityName, actionName)` - Creates a bound action executor
|
|
36
|
+
- `executeRequest(request)` - Executes an Organization Request via Xrm.WebApi.online.execute
|
|
37
|
+
- `withProgress(message, fn)` - Wraps an async operation with Xrm.Utility.showProgressIndicator
|
|
38
|
+
|
|
39
|
+
### 13.4 Migration
|
|
40
|
+
|
|
41
|
+
The old import path `@xrmforge/typegen/helpers` has been removed. Update all imports:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Old (removed)
|
|
45
|
+
import { select } from '@xrmforge/typegen/helpers';
|
|
46
|
+
import { typedForm } from '@xrmforge/formhelpers';
|
|
47
|
+
|
|
48
|
+
// New
|
|
49
|
+
import { select, typedForm } from '@xrmforge/helpers';
|
|
50
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Showcases
|
|
2
|
+
|
|
3
|
+
### 14.1 Markant WebResources (Production Showcase)
|
|
4
|
+
|
|
5
|
+
Located in the XrmForge-Workspace repository under `docs/07_showcase/markant-webresources/`.
|
|
6
|
+
|
|
7
|
+
- **30 WebResources** in `src/forms/` (account, contact, opportunity, lead, quote, email, task, etc.)
|
|
8
|
+
- **1 shared library** (GDPR retention UI)
|
|
9
|
+
- **9 test files** with 59 tests
|
|
10
|
+
- **79 generated typings:** 25 form interfaces, 28 entity interfaces, 22 OptionSet files, 4 action executors
|
|
11
|
+
- **esbuild build** via xrmforge.config.json (32 entries)
|
|
12
|
+
- **Deploy script** (deploy.mjs) with @azure/identity auth, incremental deployment, hash-based change detection
|
|
13
|
+
- **27 entities, 236 OptionSet enums, 95 form interfaces, 7 Custom API executors**
|
|
14
|
+
|
|
15
|
+
### 14.2 LMApp WebResources (KI Comparison Showcase)
|
|
16
|
+
|
|
17
|
+
Created during the KI comparison tests (Session 9). 18 legacy JavaScript form scripts (~8,400 lines) converted to TypeScript with XrmForge patterns.
|
|
18
|
+
|
|
19
|
+
- **19 WebResources** with Fields Enums, EntityNames, OptionSet Enums
|
|
20
|
+
- **84 tests** in 8 test files
|
|
21
|
+
- **XrmForge-optimized:** All 10 AGENT.md rules applied (FormContext cast, Fields Enum, EntityNames, OptionSet Enums, shared getLookupObject, Tab Enums)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# CI/CD
|
|
2
|
+
|
|
3
|
+
### 15.1 GitHub Actions CI (`.github/workflows/ci.yml`)
|
|
4
|
+
|
|
5
|
+
**Triggers:** Push to main, Pull Requests against main.
|
|
6
|
+
|
|
7
|
+
**Matrix:** Node 20, Node 22 on ubuntu-latest.
|
|
8
|
+
|
|
9
|
+
**Steps:**
|
|
10
|
+
1. Checkout
|
|
11
|
+
2. Setup pnpm (from packageManager field)
|
|
12
|
+
3. Setup Node.js (matrix version)
|
|
13
|
+
4. `pnpm install --frozen-lockfile`
|
|
14
|
+
5. `pnpm lint`
|
|
15
|
+
6. `pnpm -r exec tsc --noEmit` (typecheck all packages)
|
|
16
|
+
7. `pnpm build`
|
|
17
|
+
8. `pnpm test`
|
|
18
|
+
9. Coverage (Node 22 only): `npx vitest run --coverage` in typegen
|
|
19
|
+
|
|
20
|
+
### 15.2 Release Workflow (`.github/workflows/release.yml`)
|
|
21
|
+
|
|
22
|
+
**Triggers:** After successful CI on push to main.
|
|
23
|
+
|
|
24
|
+
**Steps:**
|
|
25
|
+
1. Checkout, setup pnpm, setup Node 22
|
|
26
|
+
2. `pnpm install --frozen-lockfile`
|
|
27
|
+
3. `pnpm build`
|
|
28
|
+
4. Changesets action: creates Release PR or publishes to npm
|
|
29
|
+
|
|
30
|
+
**Publish command:** `pnpm release` = `turbo run build && changeset publish`
|
|
31
|
+
|
|
32
|
+
### 15.3 Turbo Pipeline
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
build: dependsOn: [^build], outputs: [dist/**]
|
|
36
|
+
test: dependsOn: [build]
|
|
37
|
+
typecheck: dependsOn: [^build]
|
|
38
|
+
lint: (no dependencies)
|
|
39
|
+
dev: cache: false, persistent: true
|
|
40
|
+
clean: cache: false
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 15.4 Changesets
|
|
44
|
+
|
|
45
|
+
Configured for public npm access, auto-update internal dependencies on patch level. Publish requires NPM_TOKEN secret.
|
|
46
|
+
|
|
47
|
+
### 15.5 Publishing Order
|
|
48
|
+
|
|
49
|
+
Due to internal dependencies: typegen first, then devkit, then cli. Must use `pnpm publish` (not `npm publish`) to resolve `workspace:*` references to real versions.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Technical Debt
|
|
2
|
+
|
|
3
|
+
### 16.1 Known Issues
|
|
4
|
+
|
|
5
|
+
| Issue | Status | Priority |
|
|
6
|
+
|-------|--------|----------|
|
|
7
|
+
| parseLookup/select not adopted by AI assistants | Open | High |
|
|
8
|
+
| release.yml double runs (CI triggers release, release re-triggers CI) | Open | Low |
|
|
9
|
+
| No integration tests against live Dataverse | Open (OE-4) | Medium |
|
|
10
|
+
| @xrmforge/webapi has no Action/Function support | Accepted | Low |
|
|
11
|
+
| devDependency versions in scaffolded package.json are pinned to old versions | Open | Low |
|
|
12
|
+
|
|
13
|
+
### 16.2 Accepted Limitations
|
|
14
|
+
|
|
15
|
+
- **const enum limitation:** Cannot be imported at runtime by test frameworks from `.d.ts` files. Workaround: use `.ts` files with regular `enum` for manual typings.
|
|
16
|
+
- **Grid.refresh() requires `as any`:** Not typed in @types/xrm.
|
|
17
|
+
- **Single solution per entity:** If an entity appears in multiple solutions, it is only generated once.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
### 17.1 Next Steps (Priority Order)
|
|
4
|
+
|
|
5
|
+
1. **parseLookup/select Adoption** - Improve AGENT.md examples so AI assistants consistently use `/helpers` imports
|
|
6
|
+
2. **LMApp Showcase regeneration** - With latest releases (testing@0.2.0, devkit@0.4.0 with improved AGENT.md)
|
|
7
|
+
3. **KI Battle Round 3** - Re-test Sonnet vs Opus after improvements to measure progress
|
|
8
|
+
4. **Documentation website** - xrmforge.dev or xrmforge.io (OE-3)
|
|
9
|
+
|
|
10
|
+
### 17.2 Open Decisions
|
|
11
|
+
|
|
12
|
+
| ID | Decision | Status |
|
|
13
|
+
|----|----------|--------|
|
|
14
|
+
| OE-1 | npm scope availability (@xrmforge) | Open |
|
|
15
|
+
| OE-2 | GitHub org vs personal repo | Decided: personal (juergenbeck/XrmForge) |
|
|
16
|
+
| OE-3 | Documentation domain (xrmforge.dev or .io) | Open |
|
|
17
|
+
| OE-4 | Dataverse test environment for integration tests | Open |
|
|
18
|
+
| OE-5 | Publisher prefix and solution name for PCF/WebResource tests | Open |
|
|
19
|
+
|
|
20
|
+
### 17.3 Future Possibilities
|
|
21
|
+
|
|
22
|
+
- Relationship Names const enum (OE-7, low priority)
|
|
23
|
+
- @xrmforge/webapi with Action/Function support (reuse DataverseHttpClient)
|
|
24
|
+
- Plugin system for custom generators and type mappings
|
|
25
|
+
- Server-side generation (Custom API in Dataverse)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Design Principles
|
|
2
|
+
|
|
3
|
+
The 18 design principles that govern all XrmForge development:
|
|
4
|
+
|
|
5
|
+
1. **Extend, don't replace** - Types build on @types/xrm, never override them.
|
|
6
|
+
2. **TypeScript all the way** - 100% TypeScript-native. No .NET, no ADAL.
|
|
7
|
+
3. **Code must build** - Every work step ends with green build + tests.
|
|
8
|
+
4. **Research before speed** - Investigate, compare, decide, then implement. Never guess.
|
|
9
|
+
5. **No module without basics** - Error handling, logging, unit tests, JSDoc on all public APIs.
|
|
10
|
+
6. **Monorepo discipline** - Each package standalone, no circular deps, barrel exports.
|
|
11
|
+
7. **Enterprise resilience** - Retry + exponential backoff, rate-limit awareness, token caching, read-only default.
|
|
12
|
+
8. **esbuild-first, webpack-compatible** - Default: esbuild (fast). webpack stays supported. IIFE output for D365.
|
|
13
|
+
9. **MSAL-only authentication** - Only @azure/identity (no legacy ADAL). Three flows: client credentials, browser, device code.
|
|
14
|
+
10. **Review required** - After every step, immediate critical review (6 dimensions). No asking if review is wanted.
|
|
15
|
+
11. **Session state required** - session-state.md updated, changelog written, open questions tracked.
|
|
16
|
+
12. **No half measures** - Every step completed fully: green build + tests + review before next step.
|
|
17
|
+
13. **Informed architecture decisions** - Research, compare, recommend with pros/cons, get decision, persist.
|
|
18
|
+
14. **Abstraction over vendor lock-in** - External dependencies behind interfaces (parser, auth, bundler).
|
|
19
|
+
15. **Dual-language labels** - Primary language (1033/English) for identifiers, secondary in JSDoc. German umlauts transliterated.
|
|
20
|
+
16. **Review with research and live verification** - Internet research, live D365 verification, production code checks, cite sources.
|
|
21
|
+
17. **Challenge postponement** - "Later" check: Will it get harder? API contract? Real effort? Technical reasons?
|
|
22
|
+
18. **Read-only default for Dataverse access** - DataverseHttpClient defaults to readOnly: true. Write access is an explicit opt-in.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# XrmForge Architektur
|
|
2
|
+
|
|
3
|
+
> **Status:** Lebendes Dokument, das den aktuellen Implementierungsstand beschreibt.
|
|
4
|
+
> **Letztes Update:** 2026-04-04 (Session 10)
|
|
5
|
+
> **Version:** 7 Packages, 666+ Tests über alle Packages.
|
|
6
|
+
|
|
7
|
+
## Kapitel
|
|
8
|
+
|
|
9
|
+
1. [Zusammenfassung](01-zusammenfassung.md)
|
|
10
|
+
2. [Package-Architektur](02-packages.md)
|
|
11
|
+
3. [Generierte Typen](03-generierte-typen.md)
|
|
12
|
+
4. [CLI-Befehle](04-cli.md)
|
|
13
|
+
5. [Build-Architektur](05-build.md)
|
|
14
|
+
6. [Inkrementelle Generierung](06-inkrementell.md)
|
|
15
|
+
7. [HTTP-Client](07-http-client.md)
|
|
16
|
+
8. [Authentifizierung](08-authentifizierung.md)
|
|
17
|
+
9. [Test-Framework](09-testing.md)
|
|
18
|
+
10. [ESLint-Plugin](10-eslint-plugin.md)
|
|
19
|
+
11. [AGENT.md-System](11-agent-md.md)
|
|
20
|
+
12. [@types/xrm-Fallstricke](12-xrm-fallstricke.md)
|
|
21
|
+
13. [@xrmforge/helpers Package](13-helpers.md)
|
|
22
|
+
14. [Showcases](14-showcases.md)
|
|
23
|
+
15. [CI/CD](15-ci-cd.md)
|
|
24
|
+
16. [Technische Schulden](16-technische-schulden.md)
|
|
25
|
+
17. [Roadmap](17-roadmap.md)
|
|
26
|
+
18. [Designprinzipien](18-designprinzipien.md)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 1. Zusammenfassung
|
|
2
|
+
|
|
3
|
+
XrmForge ist ein quelloffenes TypeScript-Toolkit für typsichere Dynamics 365 / Dataverse WebResource-Entwicklung. Es generiert TypeScript-Deklarationen aus Live-Dataverse-Metadaten und verwandelt Laufzeit-String-Fehler in Kompilierzeit-Typfehler.
|
|
4
|
+
|
|
5
|
+
**Kernnutzenversprechen:** Jeder Feldname, OptionSet-Wert, Tab-Name, Entitätsname und Subgrid-Name wird zu einer typisierten Konstante mit IDE-Autovervollständigung und Kompilierzeit-Validierung.
|
|
6
|
+
|
|
7
|
+
**Zielgruppe:** D365-Entwickler, die Formularskripte (WebResources) in JavaScript/TypeScript schreiben und Kompilierzeit-Sicherheit, null Magic Strings und moderne Werkzeuge (esbuild, vitest, ESLint) wollen.
|
|
8
|
+
|
|
9
|
+
**Technologie-Stack:** TypeScript, pnpm-Monorepo mit Turborepo, esbuild für IIFE-Bundles, vitest für Tests, @azure/identity für Authentifizierung, fast-xml-parser für FormXml-Parsing.
|
|
10
|
+
|
|
11
|
+
**npm-Organisation:** [@xrmforge](https://www.npmjs.com/org/xrmforge)
|