@xrmforge/devkit 0.5.7 → 0.7.0
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/index.js +7 -5
- package/dist/index.js.map +1 -1
- package/dist/templates/AGENT.md +534 -450
- package/dist/templates/error-handler.ts +3 -2
- package/dist/templates/example-form.test.ts +34 -19
- package/dist/templates/example-form.ts +76 -40
- package/dist/templates/validate-form.mjs +263 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Logger } from './logger.js';
|
|
6
6
|
import { NOTIFICATION_IDS } from './constants.js';
|
|
7
|
+
import { FormNotificationLevel } from '@xrmforge/helpers';
|
|
7
8
|
|
|
8
9
|
type EventHandler = (ctx: Xrm.Events.EventContext, ...args: never[]) => unknown;
|
|
9
10
|
|
|
@@ -73,7 +74,7 @@ function logAndNotify(
|
|
|
73
74
|
const message = err instanceof Error ? err.message : String(err);
|
|
74
75
|
logger.error(`${name} failed`, { err });
|
|
75
76
|
try {
|
|
76
|
-
ctx.getFormContext().ui.setFormNotification(message,
|
|
77
|
+
ctx.getFormContext().ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
77
78
|
} catch {
|
|
78
79
|
/* ignore */
|
|
79
80
|
}
|
|
@@ -88,7 +89,7 @@ function logAndNotifyForm(
|
|
|
88
89
|
const message = err instanceof Error ? err.message : String(err);
|
|
89
90
|
logger.error(`${name} failed`, { err });
|
|
90
91
|
try {
|
|
91
|
-
formContext.ui.setFormNotification(message,
|
|
92
|
+
formContext.ui.setFormNotification(message, FormNotificationLevel.Error, NOTIFICATION_IDS.genericError);
|
|
92
93
|
} catch {
|
|
93
94
|
/* ignore */
|
|
94
95
|
}
|
|
@@ -1,19 +1,34 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Example test for the form script.
|
|
5
|
-
*
|
|
6
|
-
* Uses @xrmforge/testing for type-safe mocking once you have
|
|
7
|
-
* generated types.
|
|
8
|
-
*/
|
|
9
|
-
describe('{{namespace}}.Example', () => {
|
|
10
|
-
it('should export onLoad
|
|
11
|
-
const mod = await import('../../src/forms/example-form.js');
|
|
12
|
-
expect(typeof mod.onLoad).toBe('function');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should export onSave
|
|
16
|
-
const mod = await import('../../src/forms/example-form.js');
|
|
17
|
-
expect(typeof mod.onSave).toBe('function');
|
|
18
|
-
});
|
|
19
|
-
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example test for the form script.
|
|
5
|
+
*
|
|
6
|
+
* Uses @xrmforge/testing for type-safe mocking once you have
|
|
7
|
+
* generated types. Replace with real form tests after 'xrmforge generate'.
|
|
8
|
+
*/
|
|
9
|
+
describe('{{namespace}}.Example', () => {
|
|
10
|
+
it('should export onLoad handler', async () => {
|
|
11
|
+
const mod = await import('../../src/forms/example-form.js');
|
|
12
|
+
expect(typeof mod.onLoad).toBe('function');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should export onSave handler', async () => {
|
|
16
|
+
const mod = await import('../../src/forms/example-form.js');
|
|
17
|
+
expect(typeof mod.onSave).toBe('function');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// TODO: After running 'xrmforge generate', add real form tests:
|
|
21
|
+
//
|
|
22
|
+
// import { createFormMock } from '@xrmforge/testing';
|
|
23
|
+
// import type { ExampleFormMockValues } from '../../generated/entities/Example.js';
|
|
24
|
+
//
|
|
25
|
+
// it('should show notification on load', () => {
|
|
26
|
+
// const { executionContext, formContext } = createFormMock<ExampleFormMockValues>({
|
|
27
|
+
// name: 'Contoso Ltd',
|
|
28
|
+
// });
|
|
29
|
+
// onLoad(executionContext);
|
|
30
|
+
// expect(formContext.ui.setFormNotification).toHaveBeenCalledWith(
|
|
31
|
+
// 'Form loaded successfully', 'INFO', 'example-load-notification'
|
|
32
|
+
// );
|
|
33
|
+
// });
|
|
34
|
+
});
|
|
@@ -1,40 +1,76 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Example Form Script for Dynamics 365.
|
|
3
|
-
*
|
|
4
|
-
* Register in D365 as: {{namespace}}.Example.onLoad
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Example Form Script for Dynamics 365 using XrmForge best practices.
|
|
3
|
+
*
|
|
4
|
+
* Register in D365 as: {{namespace}}.Example.onLoad
|
|
5
|
+
*
|
|
6
|
+
* This template demonstrates the correct patterns:
|
|
7
|
+
* - typedForm() for direct typed field access
|
|
8
|
+
* - Fields Enum for addOnChange and $control
|
|
9
|
+
* - Entity-level Fields for Web API select() queries
|
|
10
|
+
* - wrapHandler for unified error handling
|
|
11
|
+
* - Logger instead of console.log
|
|
12
|
+
* - FormNotificationLevel constant instead of raw string
|
|
13
|
+
* - pickLang() for localized UI strings
|
|
14
|
+
*
|
|
15
|
+
* Replace this file with your actual form logic after running 'xrmforge generate'.
|
|
16
|
+
*/
|
|
17
|
+
import { createLogger } from '../shared/logger.js';
|
|
18
|
+
import { wrapHandler } from '../shared/error-handler.js';
|
|
19
|
+
import { MESSAGES, pickLang } from '../shared/constants.js';
|
|
20
|
+
import { typedForm, FormNotificationLevel, select, formLookupId } from '@xrmforge/helpers';
|
|
21
|
+
// TODO: After 'xrmforge generate', replace with your actual imports:
|
|
22
|
+
// import type { ExampleForm } from '../../generated/forms/example.js';
|
|
23
|
+
// import { ExampleFormFieldsEnum as Fields } from '../../generated/forms/example.js';
|
|
24
|
+
// import { ExampleFields } from '../../generated/fields/example.js';
|
|
25
|
+
// import { EntityNames } from '../../generated/entity-names.js';
|
|
26
|
+
|
|
27
|
+
const logger = createLogger('{{namespace}}.Example');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Called when the form loads.
|
|
31
|
+
*/
|
|
32
|
+
export const onLoad = wrapHandler('{{namespace}}.Example.onLoad', logger, async (ctx) => {
|
|
33
|
+
// TODO: Replace 'Xrm.FormContext' with your generated form type:
|
|
34
|
+
// const form = typedForm<ExampleForm>(ctx.getFormContext());
|
|
35
|
+
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
36
|
+
|
|
37
|
+
// Direct field access via typedForm proxy (fully typed):
|
|
38
|
+
// const name = form.name.getValue(); // string | null
|
|
39
|
+
// form.revenue.setValue(150000); // NumberAttribute
|
|
40
|
+
|
|
41
|
+
// addOnChange uses Fields Enum via $context:
|
|
42
|
+
// form.$context.getAttribute(Fields.Name).addOnChange(() => {
|
|
43
|
+
// logger.debug('Name changed', { value: form.name.getValue() });
|
|
44
|
+
// });
|
|
45
|
+
|
|
46
|
+
// Web API queries use entity-level Fields:
|
|
47
|
+
// const result = await Xrm.WebApi.retrieveRecord(
|
|
48
|
+
// EntityNames.Account, id,
|
|
49
|
+
// select(ExampleFields.Name, ExampleFields.WebsiteUrl)
|
|
50
|
+
// );
|
|
51
|
+
|
|
52
|
+
// Lookup access:
|
|
53
|
+
// const parentId = formLookupId(form.parentaccountid);
|
|
54
|
+
|
|
55
|
+
// Localized UI strings:
|
|
56
|
+
const lang = pickLang(
|
|
57
|
+
Xrm.Utility.getGlobalContext().userSettings.languageId,
|
|
58
|
+
MESSAGES,
|
|
59
|
+
);
|
|
60
|
+
logger.info('Form loaded', { language: lang });
|
|
61
|
+
|
|
62
|
+
// Form notifications use constants:
|
|
63
|
+
form.$context.ui.setFormNotification(
|
|
64
|
+
'Form loaded',
|
|
65
|
+
FormNotificationLevel.Info,
|
|
66
|
+
'example-load',
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Called when the form is saved.
|
|
72
|
+
*/
|
|
73
|
+
export const onSave = wrapHandler('{{namespace}}.Example.onSave', logger, (ctx) => {
|
|
74
|
+
const form = typedForm<Xrm.FormContext>(ctx.getFormContext());
|
|
75
|
+
form.$context.ui.clearFormNotification('example-load');
|
|
76
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate-form.mjs - Quality Gate for XrmForge Form Scripts
|
|
3
|
+
*
|
|
4
|
+
* Runs three checks in one command:
|
|
5
|
+
* 1. TypeScript Compiler (tsc --noEmit)
|
|
6
|
+
* 2. ESLint (src/ --max-warnings=0)
|
|
7
|
+
* 3. Pattern Compliance (critical rule violations via text search)
|
|
8
|
+
*
|
|
9
|
+
* Exit code: 0 = all green, 1 = errors found
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node scripts/validate-form.mjs
|
|
13
|
+
* npm run validate
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execSync } from 'node:child_process';
|
|
17
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
18
|
+
import { join, relative } from 'node:path';
|
|
19
|
+
|
|
20
|
+
const RED = '\x1b[31m';
|
|
21
|
+
const GREEN = '\x1b[32m';
|
|
22
|
+
const NC = '\x1b[0m';
|
|
23
|
+
|
|
24
|
+
let totalErrors = 0;
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// 1. TypeScript Compiler
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
console.log('=== XrmForge Quality Gate ===\n');
|
|
31
|
+
console.log('--- TypeScript ---');
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
execSync('npx tsc --noEmit', { stdio: 'pipe', encoding: 'utf-8' });
|
|
35
|
+
console.log(`${GREEN}OK${NC} tsc --noEmit`);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
38
|
+
const errorCount = (output.match(/error TS/g) || []).length;
|
|
39
|
+
console.log(`${RED}FAIL${NC} tsc --noEmit (${errorCount} errors)`);
|
|
40
|
+
console.log(output.split('\n').slice(0, 20).join('\n'));
|
|
41
|
+
totalErrors += errorCount || 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================
|
|
45
|
+
// 2. ESLint
|
|
46
|
+
// ============================================================
|
|
47
|
+
|
|
48
|
+
console.log('\n--- ESLint ---');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
execSync('npx eslint src/ --max-warnings=0', { stdio: 'pipe', encoding: 'utf-8' });
|
|
52
|
+
console.log(`${GREEN}OK${NC} eslint src/ --max-warnings=0`);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const output = (err.stdout || '') + (err.stderr || '');
|
|
55
|
+
const problemMatch = output.match(/(\d+) problems?/);
|
|
56
|
+
const count = problemMatch ? problemMatch[1] : '?';
|
|
57
|
+
console.log(`${RED}FAIL${NC} eslint (${count} problems)`);
|
|
58
|
+
console.log(output.split('\n').slice(0, 20).join('\n'));
|
|
59
|
+
totalErrors += parseInt(count) || 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============================================================
|
|
63
|
+
// 3. Pattern Compliance (critical rules)
|
|
64
|
+
// ============================================================
|
|
65
|
+
|
|
66
|
+
console.log('\n--- Pattern Compliance ---');
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Collect all .ts files recursively from a directory.
|
|
70
|
+
*/
|
|
71
|
+
function collectTsFiles(dir) {
|
|
72
|
+
const results = [];
|
|
73
|
+
try {
|
|
74
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
const fullPath = join(dir, entry.name);
|
|
76
|
+
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'dist') {
|
|
77
|
+
results.push(...collectTsFiles(fullPath));
|
|
78
|
+
} else if (entry.isFile() && entry.name.endsWith('.ts') && !entry.name.endsWith('.d.ts')) {
|
|
79
|
+
results.push(fullPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Directory does not exist
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check a pattern rule against all files.
|
|
90
|
+
* @returns Number of violations
|
|
91
|
+
*/
|
|
92
|
+
function checkPattern(label, files, regex, excludeFiles = []) {
|
|
93
|
+
const violations = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
const relPath = relative(process.cwd(), file);
|
|
96
|
+
if (excludeFiles.some((ex) => relPath.includes(ex))) continue;
|
|
97
|
+
|
|
98
|
+
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
99
|
+
for (let i = 0; i < lines.length; i++) {
|
|
100
|
+
const line = lines[i];
|
|
101
|
+
const trimmed = line.trim();
|
|
102
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*')) continue;
|
|
103
|
+
if (regex.test(line)) {
|
|
104
|
+
violations.push(` ${relPath}:${i + 1}: ${trimmed}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (violations.length === 0) {
|
|
110
|
+
console.log(`${GREEN}OK${NC} [0] ${label}`);
|
|
111
|
+
} else {
|
|
112
|
+
console.log(`${RED}FAIL${NC} [${violations.length}] ${label}`);
|
|
113
|
+
violations.slice(0, 10).forEach((v) => console.log(v));
|
|
114
|
+
if (violations.length > 10) {
|
|
115
|
+
console.log(` ... and ${violations.length - 10} more`);
|
|
116
|
+
}
|
|
117
|
+
totalErrors += violations.length;
|
|
118
|
+
}
|
|
119
|
+
return violations.length;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const formFiles = collectTsFiles('src/forms');
|
|
123
|
+
const allSrcFiles = collectTsFiles('src');
|
|
124
|
+
|
|
125
|
+
// ── Field Access ─────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
// 3a. Raw strings in getAttribute/getControl (must use Fields Enum)
|
|
128
|
+
checkPattern(
|
|
129
|
+
'Raw field strings in getAttribute/getControl',
|
|
130
|
+
formFiles,
|
|
131
|
+
/(?:getAttribute|getControl)\s*\(\s*['"][a-z]/,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// 3b. Raw strings in helper wrappers (getValue, setFieldValue, setDisabled, addOnChange, setVisible, setRequiredLevel)
|
|
135
|
+
checkPattern(
|
|
136
|
+
'Raw field strings in helper functions (use typedForm or Fields Enum)',
|
|
137
|
+
allSrcFiles,
|
|
138
|
+
/(?:getValue|setFieldValue|setDisabled|addOnChange|setVisible|setRequiredLevel|addPreSearch)\s*\(\s*\w+\s*,\s*['"][a-z]/,
|
|
139
|
+
['generated/', 'logger.ts'],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// 3c. Raw strings in select() (must use entity-level Fields Enum)
|
|
143
|
+
checkPattern(
|
|
144
|
+
'Raw field strings in select() (use entity-level Fields Enum)',
|
|
145
|
+
allSrcFiles,
|
|
146
|
+
/\bselect\s*\(\s*['"][a-z]/,
|
|
147
|
+
['generated/'],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// ── Entity Names ─────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
// 3d. Raw entity name strings in WebApi calls (must use EntityNames Enum)
|
|
153
|
+
checkPattern(
|
|
154
|
+
'Raw entity names in WebApi (must use EntityNames Enum)',
|
|
155
|
+
allSrcFiles,
|
|
156
|
+
/(?:retrieveRecord|retrieveMultipleRecords|createRecord|updateRecord|deleteRecord)\s*\(\s*['"]/,
|
|
157
|
+
['generated/'],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// 3e. SystemEntities workaround (must extend generation instead)
|
|
161
|
+
checkPattern(
|
|
162
|
+
'SystemEntities workaround (extend generation with --entities instead)',
|
|
163
|
+
allSrcFiles,
|
|
164
|
+
/SystemEntities\./,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
// ── Magic Values ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
// 3f. Magic numbers in OptionSet comparisons (must use OptionSet Enum)
|
|
170
|
+
checkPattern(
|
|
171
|
+
'Magic numbers in value comparisons (use OptionSet Enum)',
|
|
172
|
+
allSrcFiles,
|
|
173
|
+
/getValue\(\)\s*===?\s*\d{3,}/,
|
|
174
|
+
['generated/'],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// 3g. Magic number 86400000 (must use named constant MS_PER_DAY)
|
|
178
|
+
checkPattern(
|
|
179
|
+
'Magic number 86400000 (use named constant MS_PER_DAY)',
|
|
180
|
+
allSrcFiles,
|
|
181
|
+
/86400000/,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 3h. Raw notification level strings (must use FormNotificationLevel)
|
|
185
|
+
checkPattern(
|
|
186
|
+
'Raw notification level strings (use FormNotificationLevel from @xrmforge/helpers)',
|
|
187
|
+
allSrcFiles,
|
|
188
|
+
/setFormNotification\s*\([^)]*['"](?:ERROR|WARNING|INFO)['"]/,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ── Deprecated / Unsafe ──────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
// 3i. Xrm.Page (deprecated since D365 v9.0)
|
|
194
|
+
checkPattern(
|
|
195
|
+
'Xrm.Page (deprecated since D365 v9.0)',
|
|
196
|
+
allSrcFiles,
|
|
197
|
+
/\bXrm\.Page\b/,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// 3j. eval() usage
|
|
201
|
+
checkPattern(
|
|
202
|
+
'eval() usage (use Number() or JSON.parse())',
|
|
203
|
+
allSrcFiles,
|
|
204
|
+
/\beval\s*\(/,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// 3k. console.log/warn/error outside logger.ts (must use Logger)
|
|
208
|
+
checkPattern(
|
|
209
|
+
'console.* outside logger.ts (must use Logger)',
|
|
210
|
+
allSrcFiles,
|
|
211
|
+
/\bconsole\.(log|warn|error|info|debug)\b/,
|
|
212
|
+
['logger.ts'],
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// ── Handler Pattern ──────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
// 3l. Exported handlers without wrapHandler
|
|
218
|
+
checkPattern(
|
|
219
|
+
'Exported handlers without wrapHandler',
|
|
220
|
+
formFiles,
|
|
221
|
+
/^export\s+(const|async\s+function|function)\s+\w+(?!.*wrapHandler)/,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// ── Raw $select ──────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
// 3m. Raw $select strings (must use select() from @xrmforge/helpers)
|
|
227
|
+
checkPattern(
|
|
228
|
+
'Raw $select strings (must use select() from @xrmforge/helpers)',
|
|
229
|
+
allSrcFiles,
|
|
230
|
+
/['"]\?\$select=/,
|
|
231
|
+
['generated/'],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
// ── FetchXML ─────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
// 3n. Raw field names in FetchXML attribute= (should use Fields Enum interpolation)
|
|
237
|
+
checkPattern(
|
|
238
|
+
'Raw field names in FetchXML (use Fields Enum interpolation)',
|
|
239
|
+
allSrcFiles,
|
|
240
|
+
/attribute\s*=\s*'[a-z][a-z0-9_]+'/,
|
|
241
|
+
['generated/'],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// 3o. Magic numbers in FetchXML <value> (should use OptionSet Enum)
|
|
245
|
+
checkPattern(
|
|
246
|
+
'Magic numbers in FetchXML values (use OptionSet Enum)',
|
|
247
|
+
allSrcFiles,
|
|
248
|
+
/<value>\d{3,}<\/value>/,
|
|
249
|
+
['generated/'],
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// ============================================================
|
|
253
|
+
// Result
|
|
254
|
+
// ============================================================
|
|
255
|
+
|
|
256
|
+
console.log('\n=== Result ===');
|
|
257
|
+
if (totalErrors === 0) {
|
|
258
|
+
console.log(`${GREEN}All checks passed. 0 violations.${NC}`);
|
|
259
|
+
process.exit(0);
|
|
260
|
+
} else {
|
|
261
|
+
console.log(`${RED}${totalErrors} violations found.${NC}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|