@craft-ng/dev-tools 0.5.0-beta.0 → 0.5.0-beta.2
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/package.json +1 -1
- package/src/eslint-rules/brand-angular-deps-match.cjs +11 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +125 -0
- package/src/eslint-rules/craft-method-name-match.cjs +166 -0
- package/src/eslint-rules/index.cjs +10 -0
- package/src/eslint-rules/prefer-craft-computed.cjs +105 -0
- package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
- package/src/eslint-rules/require-component-monitoring.cjs +212 -0
- package/src/scripts/angular-brand-codemod.js +40 -6
- package/src/scripts/angular-brand-codemod.js.map +1 -1
- package/src/scripts/angular-brand-codemod.spec.ts +76 -0
- package/src/scripts/angular-brand-codemod.ts +51 -12
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
IndentationText,
|
|
5
|
+
Node,
|
|
6
|
+
Project,
|
|
7
|
+
QuoteKind,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
} = require('ts-morph');
|
|
10
|
+
|
|
11
|
+
const projectCache = new Map();
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
"Ensure Angular @Component and @Directive classes provide provideHostName('ClassName') in providers.",
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
schema: [],
|
|
22
|
+
},
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
'Program:exit'() {
|
|
26
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
27
|
+
const filePath = getFilePath(context);
|
|
28
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const text = sourceCode.getText();
|
|
33
|
+
if (!text.includes('@Component') && !text.includes('@Directive')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sourceFile = getProjectSourceFile(
|
|
38
|
+
getProject(getCwd(context)),
|
|
39
|
+
filePath,
|
|
40
|
+
text,
|
|
41
|
+
);
|
|
42
|
+
const issues = collectHostNameIssues(sourceFile);
|
|
43
|
+
if (issues.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const reportLoc = getNodeLoc(sourceCode, issues[0].reportNode);
|
|
48
|
+
|
|
49
|
+
for (const issue of issues) {
|
|
50
|
+
applyIssueFix(issue);
|
|
51
|
+
}
|
|
52
|
+
ensureProvideHostNameImport(sourceFile);
|
|
53
|
+
|
|
54
|
+
const fixedText = sourceFile.getFullText();
|
|
55
|
+
if (fixedText === text) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
context.report({
|
|
60
|
+
loc: reportLoc,
|
|
61
|
+
message: formatIssueMessage(issues),
|
|
62
|
+
fix(fixer) {
|
|
63
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function getProject(cwd) {
|
|
72
|
+
let project = projectCache.get(cwd);
|
|
73
|
+
if (project) {
|
|
74
|
+
return project;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const manipulationSettings = {
|
|
78
|
+
indentationText: IndentationText.TwoSpaces,
|
|
79
|
+
quoteKind: QuoteKind.Single,
|
|
80
|
+
};
|
|
81
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
82
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
83
|
+
? new Project({
|
|
84
|
+
tsConfigFilePath,
|
|
85
|
+
manipulationSettings,
|
|
86
|
+
})
|
|
87
|
+
: new Project({
|
|
88
|
+
compilerOptions: {
|
|
89
|
+
experimentalDecorators: true,
|
|
90
|
+
target: 9,
|
|
91
|
+
},
|
|
92
|
+
manipulationSettings,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
projectCache.set(cwd, project);
|
|
96
|
+
return project;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
100
|
+
const normalizedPath = path.resolve(filePath);
|
|
101
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
102
|
+
if (existingSourceFile) {
|
|
103
|
+
existingSourceFile.replaceWithText(text);
|
|
104
|
+
return existingSourceFile;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
108
|
+
if (sourceFile) {
|
|
109
|
+
sourceFile.replaceWithText(text);
|
|
110
|
+
return sourceFile;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getFilePath(context) {
|
|
117
|
+
const filePath = context.filename ?? context.getFilename();
|
|
118
|
+
if (!filePath || filePath === '<input>') {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCwd(context) {
|
|
126
|
+
return context.cwd ?? process.cwd();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getNodeLoc(sourceCode, node) {
|
|
130
|
+
return {
|
|
131
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
132
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectHostNameIssues(sourceFile) {
|
|
137
|
+
const angularDecorators = collectAngularDecoratorNames(sourceFile);
|
|
138
|
+
if (
|
|
139
|
+
angularDecorators.componentNames.size === 0 &&
|
|
140
|
+
angularDecorators.directiveNames.size === 0 &&
|
|
141
|
+
angularDecorators.namespaceNames.size === 0
|
|
142
|
+
) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const issues = [];
|
|
147
|
+
|
|
148
|
+
for (const classDeclaration of sourceFile.getClasses()) {
|
|
149
|
+
const className = classDeclaration.getName();
|
|
150
|
+
if (!className) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const decoratorMatch = findAngularDecorator(
|
|
155
|
+
classDeclaration,
|
|
156
|
+
angularDecorators,
|
|
157
|
+
);
|
|
158
|
+
if (!decoratorMatch) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const metadata = getDecoratorMetadata(decoratorMatch.decorator);
|
|
163
|
+
if (!metadata) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const providersProperty = getObjectProperty(metadata, 'providers');
|
|
168
|
+
if (!providersProperty) {
|
|
169
|
+
issues.push({
|
|
170
|
+
kind: 'missing-providers',
|
|
171
|
+
decoratorKind: decoratorMatch.kind,
|
|
172
|
+
className,
|
|
173
|
+
metadata,
|
|
174
|
+
reportNode: classDeclaration,
|
|
175
|
+
});
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const providersArray = getArrayInitializer(providersProperty);
|
|
180
|
+
if (!providersArray) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const hostNameCalls = findProvideHostNameCalls(providersArray);
|
|
185
|
+
if (hostNameCalls.length === 0) {
|
|
186
|
+
issues.push({
|
|
187
|
+
kind: 'missing-call',
|
|
188
|
+
decoratorKind: decoratorMatch.kind,
|
|
189
|
+
className,
|
|
190
|
+
providersArray,
|
|
191
|
+
reportNode: classDeclaration,
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const expectedHostName = `${getHostNamePrefix(decoratorMatch.kind)}${className}`;
|
|
197
|
+
const matchingCall = hostNameCalls.find((callExpression) =>
|
|
198
|
+
isMatchingHostNameCall(callExpression, expectedHostName),
|
|
199
|
+
);
|
|
200
|
+
if (matchingCall) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
issues.push({
|
|
205
|
+
kind: 'mismatched-call',
|
|
206
|
+
decoratorKind: decoratorMatch.kind,
|
|
207
|
+
className,
|
|
208
|
+
callExpression: hostNameCalls[0],
|
|
209
|
+
reportNode: classDeclaration,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return issues;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function collectAngularDecoratorNames(sourceFile) {
|
|
217
|
+
const componentNames = new Set();
|
|
218
|
+
const directiveNames = new Set();
|
|
219
|
+
const namespaceNames = new Set();
|
|
220
|
+
|
|
221
|
+
for (const importDeclaration of sourceFile.getImportDeclarations()) {
|
|
222
|
+
if (importDeclaration.getModuleSpecifierValue() !== '@angular/core') {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const namedImport of importDeclaration.getNamedImports()) {
|
|
227
|
+
const importedName = namedImport.getName();
|
|
228
|
+
const localName = namedImport.getAliasNode()?.getText() ?? importedName;
|
|
229
|
+
|
|
230
|
+
if (importedName === 'Component') {
|
|
231
|
+
componentNames.add(localName);
|
|
232
|
+
}
|
|
233
|
+
if (importedName === 'Directive') {
|
|
234
|
+
directiveNames.add(localName);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const namespaceImport = importDeclaration.getNamespaceImport();
|
|
239
|
+
if (namespaceImport) {
|
|
240
|
+
namespaceNames.add(namespaceImport.getText());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
componentNames,
|
|
246
|
+
directiveNames,
|
|
247
|
+
namespaceNames,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function findAngularDecorator(classDeclaration, angularDecorators) {
|
|
252
|
+
for (const decorator of classDeclaration.getDecorators()) {
|
|
253
|
+
const expression = decorator.getExpression();
|
|
254
|
+
if (!Node.isCallExpression(expression)) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const callee = expression.getExpression();
|
|
259
|
+
if (Node.isIdentifier(callee)) {
|
|
260
|
+
const decoratorName = callee.getText();
|
|
261
|
+
if (angularDecorators.componentNames.has(decoratorName)) {
|
|
262
|
+
return { decorator, kind: 'component' };
|
|
263
|
+
}
|
|
264
|
+
if (angularDecorators.directiveNames.has(decoratorName)) {
|
|
265
|
+
return { decorator, kind: 'directive' };
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (!Node.isPropertyAccessExpression(callee)) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const objectExpression = callee.getExpression();
|
|
275
|
+
const propertyName = callee.getName();
|
|
276
|
+
if (
|
|
277
|
+
Node.isIdentifier(objectExpression) &&
|
|
278
|
+
angularDecorators.namespaceNames.has(objectExpression.getText())
|
|
279
|
+
) {
|
|
280
|
+
if (propertyName === 'Component') {
|
|
281
|
+
return { decorator, kind: 'component' };
|
|
282
|
+
}
|
|
283
|
+
if (propertyName === 'Directive') {
|
|
284
|
+
return { decorator, kind: 'directive' };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getHostNamePrefix(kind) {
|
|
293
|
+
return kind === 'directive' ? 'directive:' : 'component:';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function getDecoratorMetadata(decorator) {
|
|
297
|
+
const expression = decorator.getExpression();
|
|
298
|
+
if (!Node.isCallExpression(expression)) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const [metadataArgument] = expression.getArguments();
|
|
303
|
+
if (!metadataArgument || !Node.isObjectLiteralExpression(metadataArgument)) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return metadataArgument;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getObjectProperty(objectLiteralExpression, name) {
|
|
311
|
+
const property = objectLiteralExpression
|
|
312
|
+
.getProperties()
|
|
313
|
+
.find(
|
|
314
|
+
(candidate) =>
|
|
315
|
+
Node.isPropertyAssignment(candidate) && candidate.getName() === name,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
return Node.isPropertyAssignment(property) ? property : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function getArrayInitializer(propertyAssignment) {
|
|
322
|
+
const initializer = propertyAssignment.getInitializer();
|
|
323
|
+
if (!initializer || !Node.isArrayLiteralExpression(initializer)) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return initializer;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function findProvideHostNameCalls(arrayLiteralExpression) {
|
|
331
|
+
return arrayLiteralExpression
|
|
332
|
+
.getElements()
|
|
333
|
+
.filter((element) => Node.isCallExpression(element))
|
|
334
|
+
.filter((callExpression) => isProvideHostNameCall(callExpression));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isProvideHostNameCall(callExpression) {
|
|
338
|
+
const expression = callExpression.getExpression();
|
|
339
|
+
if (Node.isIdentifier(expression)) {
|
|
340
|
+
return expression.getText() === 'provideHostName';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (Node.isPropertyAccessExpression(expression)) {
|
|
344
|
+
return expression.getName() === 'provideHostName';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isMatchingHostNameCall(callExpression, expectedHostName) {
|
|
351
|
+
const [nameArgument] = callExpression.getArguments();
|
|
352
|
+
if (
|
|
353
|
+
!nameArgument ||
|
|
354
|
+
(!Node.isStringLiteral(nameArgument) &&
|
|
355
|
+
!Node.isNoSubstitutionTemplateLiteral(nameArgument))
|
|
356
|
+
) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return nameArgument.getLiteralText() === expectedHostName;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function applyIssueFix(issue) {
|
|
364
|
+
const expectedHostName = `${getHostNamePrefix(issue.decoratorKind)}${issue.className}`;
|
|
365
|
+
const expectedCall = `provideHostName('${expectedHostName}')`;
|
|
366
|
+
|
|
367
|
+
if (issue.kind === 'missing-providers') {
|
|
368
|
+
issue.metadata.addPropertyAssignment({
|
|
369
|
+
name: 'providers',
|
|
370
|
+
initializer: `[${expectedCall}]`,
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (issue.kind === 'missing-call') {
|
|
376
|
+
issue.providersArray.addElement(expectedCall);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (issue.kind === 'mismatched-call') {
|
|
381
|
+
issue.callExpression.replaceWithText(expectedCall);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function ensureProvideHostNameImport(sourceFile) {
|
|
386
|
+
const coreImports = sourceFile
|
|
387
|
+
.getImportDeclarations()
|
|
388
|
+
.filter(
|
|
389
|
+
(importDeclaration) =>
|
|
390
|
+
importDeclaration.getModuleSpecifierValue() === '@craft-ng/core',
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
for (const importDeclaration of coreImports) {
|
|
394
|
+
const namedImports = importDeclaration.getNamedImports();
|
|
395
|
+
const directImport = namedImports.find(
|
|
396
|
+
(namedImport) =>
|
|
397
|
+
namedImport.getName() === 'provideHostName' &&
|
|
398
|
+
!namedImport.getAliasNode(),
|
|
399
|
+
);
|
|
400
|
+
if (directImport) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
for (const importDeclaration of coreImports) {
|
|
406
|
+
for (const namedImport of importDeclaration.getNamedImports()) {
|
|
407
|
+
if (
|
|
408
|
+
namedImport.getName() === 'provideHostName' &&
|
|
409
|
+
namedImport.getAliasNode()
|
|
410
|
+
) {
|
|
411
|
+
namedImport.remove();
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const targetImport = coreImports.find(
|
|
417
|
+
(importDeclaration) => !importDeclaration.getNamespaceImport(),
|
|
418
|
+
);
|
|
419
|
+
if (targetImport) {
|
|
420
|
+
const hasNamedImport = targetImport
|
|
421
|
+
.getNamedImports()
|
|
422
|
+
.some((namedImport) => namedImport.getName() === 'provideHostName');
|
|
423
|
+
if (!hasNamedImport) {
|
|
424
|
+
targetImport.addNamedImport('provideHostName');
|
|
425
|
+
}
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const insertIndex = sourceFile
|
|
430
|
+
.getStatements()
|
|
431
|
+
.findIndex(
|
|
432
|
+
(statement) => statement.getKind() !== SyntaxKind.ImportDeclaration,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
sourceFile.insertImportDeclaration(
|
|
436
|
+
insertIndex < 0 ? sourceFile.getImportDeclarations().length : insertIndex,
|
|
437
|
+
{
|
|
438
|
+
moduleSpecifier: '@craft-ng/core',
|
|
439
|
+
namedImports: ['provideHostName'],
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatIssueMessage(issues) {
|
|
445
|
+
if (issues.length === 1) {
|
|
446
|
+
const issue = issues[0];
|
|
447
|
+
const expectedHostName = `${getHostNamePrefix(issue.decoratorKind)}${issue.className}`;
|
|
448
|
+
return `${issue.className} must include provideHostName('${expectedHostName}') in providers.`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const classNames = [...new Set(issues.map((issue) => issue.className))].join(
|
|
452
|
+
', ',
|
|
453
|
+
);
|
|
454
|
+
return `Classes must include provideHostName('<kind>:<ClassName>') in providers: ${classNames}.`;
|
|
455
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const {
|
|
4
|
+
IndentationText,
|
|
5
|
+
Project,
|
|
6
|
+
QuoteKind,
|
|
7
|
+
Scope,
|
|
8
|
+
SyntaxKind,
|
|
9
|
+
} = require('ts-morph');
|
|
10
|
+
|
|
11
|
+
const projectCache = new Map();
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description:
|
|
18
|
+
"Ensure Angular @Component and @Directive classes declare 'private readonly _monitoring = componentMonitoring()'.",
|
|
19
|
+
},
|
|
20
|
+
fixable: 'code',
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
missingMonitoring:
|
|
24
|
+
"Component/Directive '{{className}}' must declare 'private readonly _monitoring = componentMonitoring()'.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
'Program:exit'() {
|
|
30
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
31
|
+
const filePath = getFilePath(context);
|
|
32
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const text = sourceCode.getText();
|
|
37
|
+
if (!text.includes('@Component') && !text.includes('@Directive')) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sourceFile = getProjectSourceFile(
|
|
42
|
+
getProject(getCwd(context)),
|
|
43
|
+
filePath,
|
|
44
|
+
text,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const issues = collectMonitoringIssues(sourceFile);
|
|
48
|
+
if (issues.length === 0) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const reportLoc = getNodeLoc(sourceCode, issues[0].classDeclaration);
|
|
53
|
+
|
|
54
|
+
for (const issue of issues) {
|
|
55
|
+
applyMonitoringFix(issue);
|
|
56
|
+
}
|
|
57
|
+
ensureComponentMonitoringImport(sourceFile);
|
|
58
|
+
|
|
59
|
+
const fixedText = sourceFile.getFullText();
|
|
60
|
+
if (fixedText === text) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const classNames = issues.map((i) => i.className).join(', ');
|
|
65
|
+
|
|
66
|
+
context.report({
|
|
67
|
+
loc: reportLoc,
|
|
68
|
+
message: `Component(s) missing componentMonitoring(): ${classNames}`,
|
|
69
|
+
fix(fixer) {
|
|
70
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
function getProject(cwd) {
|
|
79
|
+
let project = projectCache.get(cwd);
|
|
80
|
+
if (project) {
|
|
81
|
+
return project;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const manipulationSettings = {
|
|
85
|
+
indentationText: IndentationText.TwoSpaces,
|
|
86
|
+
quoteKind: QuoteKind.Single,
|
|
87
|
+
};
|
|
88
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
89
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
90
|
+
? new Project({ tsConfigFilePath, manipulationSettings })
|
|
91
|
+
: new Project({
|
|
92
|
+
compilerOptions: { experimentalDecorators: true, target: 9 },
|
|
93
|
+
manipulationSettings,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
projectCache.set(cwd, project);
|
|
97
|
+
return project;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
101
|
+
const normalizedPath = path.resolve(filePath);
|
|
102
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
103
|
+
if (existingSourceFile) {
|
|
104
|
+
existingSourceFile.replaceWithText(text);
|
|
105
|
+
return existingSourceFile;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
109
|
+
if (sourceFile) {
|
|
110
|
+
sourceFile.replaceWithText(text);
|
|
111
|
+
return sourceFile;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getFilePath(context) {
|
|
118
|
+
const filePath = context.filename ?? context.getFilename();
|
|
119
|
+
if (!filePath || filePath === '<input>') {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
return filePath;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getCwd(context) {
|
|
126
|
+
return context.cwd ?? process.cwd();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function getNodeLoc(sourceCode, node) {
|
|
130
|
+
return {
|
|
131
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
132
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isAngularComponentOrDirective(classDeclaration) {
|
|
137
|
+
for (const decorator of classDeclaration.getDecorators()) {
|
|
138
|
+
const name = decorator.getName();
|
|
139
|
+
if (name === 'Component' || name === 'Directive') {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function hasMonitoringProperty(classDeclaration) {
|
|
147
|
+
for (const prop of classDeclaration.getProperties()) {
|
|
148
|
+
if (
|
|
149
|
+
prop.getName() === '_monitoring' &&
|
|
150
|
+
prop.hasModifier(SyntaxKind.PrivateKeyword) &&
|
|
151
|
+
prop.isReadonly()
|
|
152
|
+
) {
|
|
153
|
+
const initializer = prop.getInitializer();
|
|
154
|
+
if (
|
|
155
|
+
initializer &&
|
|
156
|
+
initializer.getKindName() === 'CallExpression' &&
|
|
157
|
+
initializer.getText().startsWith('componentMonitoring(')
|
|
158
|
+
) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectMonitoringIssues(sourceFile) {
|
|
167
|
+
const issues = [];
|
|
168
|
+
|
|
169
|
+
for (const classDeclaration of sourceFile.getClasses()) {
|
|
170
|
+
const className = classDeclaration.getName();
|
|
171
|
+
if (!className) continue;
|
|
172
|
+
if (!isAngularComponentOrDirective(classDeclaration)) continue;
|
|
173
|
+
if (hasMonitoringProperty(classDeclaration)) continue;
|
|
174
|
+
|
|
175
|
+
issues.push({ className, classDeclaration });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return issues;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyMonitoringFix(issue) {
|
|
182
|
+
const { classDeclaration } = issue;
|
|
183
|
+
|
|
184
|
+
// Insert as first property of the class
|
|
185
|
+
classDeclaration.insertProperty(0, {
|
|
186
|
+
name: '_monitoring',
|
|
187
|
+
scope: Scope.Private,
|
|
188
|
+
isReadonly: true,
|
|
189
|
+
initializer: 'componentMonitoring()',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ensureComponentMonitoringImport(sourceFile) {
|
|
194
|
+
const craftNgCoreImport = sourceFile
|
|
195
|
+
.getImportDeclarations()
|
|
196
|
+
.find((imp) => imp.getModuleSpecifierValue() === '@craft-ng/core');
|
|
197
|
+
|
|
198
|
+
if (craftNgCoreImport) {
|
|
199
|
+
const namedImports = craftNgCoreImport.getNamedImports();
|
|
200
|
+
const alreadyImported = namedImports.some(
|
|
201
|
+
(ni) => ni.getName() === 'componentMonitoring',
|
|
202
|
+
);
|
|
203
|
+
if (!alreadyImported) {
|
|
204
|
+
craftNgCoreImport.addNamedImport('componentMonitoring');
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
sourceFile.addImportDeclaration({
|
|
208
|
+
moduleSpecifier: '@craft-ng/core',
|
|
209
|
+
namedImports: ['componentMonitoring'],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|