@xrmforge/devkit 0.7.2 → 0.7.3

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.
@@ -568,6 +568,29 @@ const customerId = formLookupId(form.customerid); // already clean: "a1b2c3d4-.
568
568
  if (customerId === otherNormalizedId) { ... } // simple ===
569
569
  ```
570
570
 
571
+ ### Typed repetition beats untyped loops
572
+
573
+ When multiple fields need the same operation (e.g. 8 address fields), write
574
+ 8 typed lines instead of 1 loop with raw strings:
575
+
576
+ ```typescript
577
+ // WRONG: DRY reflex, but raw strings bypass type safety
578
+ for (const f of ['address1_name', 'address1_line1', 'address1_city']) {
579
+ form.$unsafe(f)?.addOnChange(handler);
580
+ }
581
+
582
+ // CORRECT: more lines, but every field is compile-time validated
583
+ form.address1_name.addOnChange(handler);
584
+ form.address1_line1.addOnChange(handler);
585
+ form.address1_city.addOnChange(handler);
586
+ form.address1_postalcode.addOnChange(handler);
587
+ form.address1_country.addOnChange(handler);
588
+ ```
589
+
590
+ 8 typed lines are better than 1 loop with raw strings. The type system
591
+ catches renamed/removed fields at compile time. A loop with raw strings
592
+ only fails at runtime. DRY is a recommendation, type safety is mandatory.
593
+
571
594
  **Rule of thumb:** If a helper function just wraps a single Xrm API call with a
572
595
  string parameter, it MUST NOT exist. The typed API is shorter, safer, and provides
573
596
  IDE autocomplete. Only keep shared helpers that contain actual domain logic
@@ -87,9 +87,14 @@ function collectTsFiles(dir) {
87
87
 
88
88
  /**
89
89
  * Check a pattern rule against all files.
90
+ * @param {string} label - Description of the check
91
+ * @param {string[]} files - Files to check
92
+ * @param {RegExp} regex - Pattern to match (violation)
93
+ * @param {string[]} excludeFiles - File paths to exclude
94
+ * @param {RegExp[]} excludePatterns - Line patterns to exclude (not violations even if regex matches)
90
95
  * @returns Number of violations
91
96
  */
92
- function checkPattern(label, files, regex, excludeFiles = []) {
97
+ function checkPattern(label, files, regex, excludeFiles = [], excludePatterns = []) {
93
98
  const violations = [];
94
99
  for (const file of files) {
95
100
  const relPath = relative(process.cwd(), file);
@@ -100,6 +105,7 @@ function checkPattern(label, files, regex, excludeFiles = []) {
100
105
  const line = lines[i];
101
106
  const trimmed = line.trim();
102
107
  if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
108
+ if (excludePatterns.some((ep) => ep.test(trimmed))) continue;
103
109
  if (regex.test(line)) {
104
110
  violations.push(` ${relPath}:${i + 1}: ${trimmed}`);
105
111
  }
@@ -214,11 +220,16 @@ checkPattern(
214
220
 
215
221
  // ── Handler Pattern ──────────────────────────────────────────────────────────
216
222
 
217
- // 3l. Exported handlers without wrapHandler
223
+ // 3l. Exported handlers without wrapHandler or wrapCommand
218
224
  checkPattern(
219
- 'Exported handlers without wrapHandler',
225
+ 'Exported handlers without wrapHandler/wrapCommand',
220
226
  formFiles,
221
- /^export\s+(const|async\s+function|function)\s+\w+(?!.*wrapHandler)/,
227
+ /^export\s+(const|async\s+function|function)\s+\w+(?!.*(?:wrapHandler|wrapCommand))/,
228
+ [],
229
+ [
230
+ // Re-exports: `export const form_OnLoad = onLoad;` (alias for a wrapped handler)
231
+ /^export\s+const\s+\w+\s*=\s*[a-zA-Z][a-zA-Z0-9]*\s*;/,
232
+ ],
222
233
  );
223
234
 
224
235
  // ── Raw $select ──────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrmforge/devkit",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Build orchestration and project tooling for Dynamics 365 WebResources",
5
5
  "keywords": [
6
6
  "dynamics-365",