@craft-ng/dev-tools 0.1.9 → 0.4.0-beta.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/README.md +96 -3
- package/package.json +1 -1
- package/src/eslint-rules/app-start-registry-match.cjs +357 -0
- package/src/eslint-rules/brand-angular-deps-match.cjs +214 -80
- package/src/eslint-rules/brand-angular-gen-deps-required.cjs +175 -0
- package/src/eslint-rules/index.cjs +10 -2
- package/src/eslint-rules/no-angular-inject.cjs +1 -78
- package/src/eslint-rules/no-angular-provide-app-initializer.cjs +79 -0
- package/src/eslint-rules/prefer-browser-boundaries.cjs +92 -0
- package/src/eslint-rules/prefer-craft-http-client.cjs +120 -0
- package/src/eslint-rules/prefer-craft-service.cjs +140 -0
- package/src/scripts/angular-brand-codemod.d.ts +25 -0
- package/src/scripts/angular-brand-codemod.js +450 -49
- package/src/scripts/angular-brand-codemod.js.map +1 -1
- package/src/scripts/angular-brand-codemod.spec.ts +716 -3
- package/src/scripts/angular-brand-codemod.ts +904 -61
- package/src/eslint-rules/no-direct-angular-class-export.cjs +0 -240
|
@@ -1,27 +1,62 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
|
+
const moduleApi = require('node:module');
|
|
2
3
|
const path = require('node:path');
|
|
4
|
+
const vm = require('node:vm');
|
|
5
|
+
const ts = require('typescript');
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
__dirname,
|
|
6
|
-
'../tsconfig.codemod.json',
|
|
7
|
-
);
|
|
8
|
-
require('ts-node/register/transpile-only');
|
|
9
|
-
|
|
10
|
-
const { Project } = require('ts-morph');
|
|
7
|
+
const { Node, Project } = require('ts-morph');
|
|
11
8
|
const {
|
|
12
9
|
analyzeSourceFileDependencies,
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
discoverAngularBrandConfigFilePath,
|
|
11
|
+
transformSourceFile,
|
|
12
|
+
} = loadAngularBrandCodemod();
|
|
15
13
|
|
|
16
14
|
const projectCache = new Map();
|
|
15
|
+
const typePrinter = ts.createPrinter({ removeComments: true });
|
|
16
|
+
|
|
17
|
+
function loadAngularBrandCodemod() {
|
|
18
|
+
const filePath = path.resolve(
|
|
19
|
+
__dirname,
|
|
20
|
+
'../scripts/angular-brand-codemod.ts',
|
|
21
|
+
);
|
|
22
|
+
const compiledFilePath = `${filePath}.cjs`;
|
|
23
|
+
const sourceText = fs.readFileSync(filePath, 'utf8');
|
|
24
|
+
const librarySourceText = sourceText.replace(
|
|
25
|
+
/\n\/\/ Check if this module is being run directly[\s\S]*$/,
|
|
26
|
+
'\n',
|
|
27
|
+
);
|
|
28
|
+
const transpiled = ts.transpileModule(librarySourceText, {
|
|
29
|
+
compilerOptions: {
|
|
30
|
+
module: ts.ModuleKind.CommonJS,
|
|
31
|
+
target: ts.ScriptTarget.ES2022,
|
|
32
|
+
esModuleInterop: true,
|
|
33
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
34
|
+
},
|
|
35
|
+
fileName: filePath,
|
|
36
|
+
});
|
|
37
|
+
const wrappedSource = module.constructor.wrap(transpiled.outputText);
|
|
38
|
+
const evaluate = vm.runInThisContext(wrappedSource, {
|
|
39
|
+
filename: compiledFilePath,
|
|
40
|
+
});
|
|
41
|
+
const loadedModule = { exports: {} };
|
|
42
|
+
evaluate(
|
|
43
|
+
loadedModule.exports,
|
|
44
|
+
moduleApi.createRequire(compiledFilePath),
|
|
45
|
+
loadedModule,
|
|
46
|
+
compiledFilePath,
|
|
47
|
+
path.dirname(filePath),
|
|
48
|
+
);
|
|
49
|
+
return loadedModule.exports;
|
|
50
|
+
}
|
|
17
51
|
|
|
18
52
|
module.exports = {
|
|
19
53
|
meta: {
|
|
20
54
|
type: 'problem',
|
|
21
55
|
docs: {
|
|
22
56
|
description:
|
|
23
|
-
'Ensure
|
|
57
|
+
'Ensure generated GenDeps aliases match the Angular symbol dependencies.',
|
|
24
58
|
},
|
|
59
|
+
fixable: 'code',
|
|
25
60
|
schema: [],
|
|
26
61
|
},
|
|
27
62
|
create(context) {
|
|
@@ -34,7 +69,10 @@ module.exports = {
|
|
|
34
69
|
}
|
|
35
70
|
|
|
36
71
|
const text = sourceCode.getText();
|
|
37
|
-
if (
|
|
72
|
+
if (
|
|
73
|
+
!text.includes('export type GenDeps_') ||
|
|
74
|
+
!text.includes('GetDeps<')
|
|
75
|
+
) {
|
|
38
76
|
return;
|
|
39
77
|
}
|
|
40
78
|
|
|
@@ -43,59 +81,64 @@ module.exports = {
|
|
|
43
81
|
filePath,
|
|
44
82
|
text,
|
|
45
83
|
);
|
|
46
|
-
const
|
|
84
|
+
const configFilePath = discoverAngularBrandConfigFilePath(
|
|
85
|
+
path.dirname(filePath),
|
|
86
|
+
getCwd(context),
|
|
87
|
+
);
|
|
88
|
+
const analysis = analyzeSourceFileDependencies(sourceFile, {
|
|
89
|
+
configFilePath,
|
|
90
|
+
});
|
|
47
91
|
if (
|
|
48
92
|
!analysis.classDeclaration ||
|
|
49
93
|
analysis.skipped ||
|
|
50
|
-
!analysis.className
|
|
94
|
+
!analysis.className ||
|
|
95
|
+
!analysis.generatedTypeName
|
|
51
96
|
) {
|
|
52
97
|
return;
|
|
53
98
|
}
|
|
54
99
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
analysis.className,
|
|
100
|
+
const currentGeneratedType = sourceFile.getTypeAlias(
|
|
101
|
+
analysis.generatedTypeName,
|
|
58
102
|
);
|
|
59
|
-
if (!
|
|
103
|
+
if (!currentGeneratedType) {
|
|
60
104
|
return;
|
|
61
105
|
}
|
|
62
106
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
const currentRange = getNodeRange(currentGeneratedType);
|
|
108
|
+
const currentSnapshot = readGeneratedDepsSnapshot(currentGeneratedType);
|
|
109
|
+
|
|
110
|
+
transformSourceFile(sourceFile, { configFilePath });
|
|
111
|
+
|
|
112
|
+
const refreshedGeneratedType = sourceFile.getTypeAlias(
|
|
113
|
+
analysis.generatedTypeName,
|
|
114
|
+
);
|
|
115
|
+
if (!refreshedGeneratedType) {
|
|
72
116
|
return;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
sourceCode,
|
|
78
|
-
existing,
|
|
79
|
-
'injected',
|
|
80
|
-
analysis.dependencyGroups.injected,
|
|
81
|
-
existing.dependencyGroups.injected,
|
|
119
|
+
const refreshedSnapshot = readGeneratedDepsSnapshot(
|
|
120
|
+
refreshedGeneratedType,
|
|
82
121
|
);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
reportIfMismatch(
|
|
92
|
-
context,
|
|
93
|
-
sourceCode,
|
|
94
|
-
existing,
|
|
95
|
-
'providers',
|
|
96
|
-
analysis.dependencyGroups.providers,
|
|
97
|
-
existing.dependencyGroups.providers,
|
|
122
|
+
if (currentSnapshot.typeText === refreshedSnapshot.typeText) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const staleSections = getStaleSections(
|
|
127
|
+
currentSnapshot.sections,
|
|
128
|
+
refreshedSnapshot.sections,
|
|
98
129
|
);
|
|
130
|
+
const fixedText = sourceFile.getFullText();
|
|
131
|
+
|
|
132
|
+
context.report({
|
|
133
|
+
loc: getRangeLoc(sourceCode, currentRange),
|
|
134
|
+
message: formatOutOfDateMessage(
|
|
135
|
+
analysis.generatedTypeName,
|
|
136
|
+
staleSections,
|
|
137
|
+
),
|
|
138
|
+
fix(fixer) {
|
|
139
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
99
142
|
},
|
|
100
143
|
};
|
|
101
144
|
},
|
|
@@ -151,49 +194,140 @@ function getCwd(context) {
|
|
|
151
194
|
return context.cwd ?? process.cwd();
|
|
152
195
|
}
|
|
153
196
|
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
) {
|
|
162
|
-
|
|
163
|
-
return;
|
|
197
|
+
function readGeneratedDepsSnapshot(typeAlias) {
|
|
198
|
+
const typeNode = typeAlias.getTypeNode();
|
|
199
|
+
const snapshot = {
|
|
200
|
+
typeText: normalizeText(typeNode?.getText() ?? ''),
|
|
201
|
+
sections: undefined,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
if (!typeNode || !Node.isTypeReference(typeNode)) {
|
|
205
|
+
return snapshot;
|
|
164
206
|
}
|
|
165
207
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
208
|
+
if (typeNode.getTypeName().getText() !== 'GetDeps') {
|
|
209
|
+
return snapshot;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const [inputType] = typeNode.getTypeArguments();
|
|
213
|
+
if (!inputType || !Node.isTypeLiteral(inputType)) {
|
|
214
|
+
return snapshot;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
snapshot.sections = {
|
|
218
|
+
deps: normalizeText(readTypeLiteralPropertyTypeText(inputType, 'deps')),
|
|
219
|
+
propertiesDeps: normalizeText(
|
|
220
|
+
readTypeLiteralPropertyTypeText(inputType, 'propertiesDeps'),
|
|
221
|
+
),
|
|
222
|
+
provided: normalizeText(
|
|
223
|
+
readTypeLiteralPropertyTypeText(inputType, 'provided'),
|
|
224
|
+
),
|
|
225
|
+
missingProvider: normalizeText(
|
|
226
|
+
readTypeLiteralPropertyTypeText(inputType, 'missingProvider'),
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return snapshot;
|
|
174
231
|
}
|
|
175
232
|
|
|
176
|
-
function
|
|
177
|
-
|
|
178
|
-
loc: node ? getNodeLoc(sourceCode, node) : undefined,
|
|
179
|
-
message,
|
|
180
|
-
});
|
|
233
|
+
function getNodeRange(node) {
|
|
234
|
+
return [node.getStart(), node.getEnd()];
|
|
181
235
|
}
|
|
182
236
|
|
|
183
|
-
function
|
|
237
|
+
function getRangeLoc(sourceCode, [start, end]) {
|
|
184
238
|
return {
|
|
185
|
-
start: sourceCode.getLocFromIndex(
|
|
186
|
-
end: sourceCode.getLocFromIndex(
|
|
239
|
+
start: sourceCode.getLocFromIndex(start),
|
|
240
|
+
end: sourceCode.getLocFromIndex(end),
|
|
187
241
|
};
|
|
188
242
|
}
|
|
189
243
|
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
function readTypeLiteralPropertyTypeText(typeLiteral, propertyName) {
|
|
245
|
+
const property = typeLiteral
|
|
246
|
+
.getMembers()
|
|
247
|
+
.find(
|
|
248
|
+
(member) =>
|
|
249
|
+
Node.isPropertySignature(member) && member.getName() === propertyName,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return property?.getTypeNode()?.getText() ?? '';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getStaleSections(currentSections, refreshedSections) {
|
|
256
|
+
if (!currentSections || !refreshedSections) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return ['deps', 'propertiesDeps', 'provided', 'missingProvider'].filter(
|
|
261
|
+
(sectionName) =>
|
|
262
|
+
currentSections[sectionName] !== refreshedSections[sectionName],
|
|
194
263
|
);
|
|
195
264
|
}
|
|
196
265
|
|
|
197
|
-
function
|
|
198
|
-
|
|
266
|
+
function formatOutOfDateMessage(generatedTypeName, staleSections) {
|
|
267
|
+
if (staleSections.length === 0) {
|
|
268
|
+
return `${generatedTypeName} is out of date. Run ESLint --fix on this file or craft-brand --root <source-root> to refresh it.`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return `${generatedTypeName} is out of date for ${formatSectionList(staleSections)}. Run ESLint --fix on this file or craft-brand --root <source-root> to refresh it.`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatSectionList(sectionNames) {
|
|
275
|
+
if (sectionNames.length === 1) {
|
|
276
|
+
return sectionNames[0];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (sectionNames.length === 2) {
|
|
280
|
+
return `${sectionNames[0]} and ${sectionNames[1]}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return `${sectionNames.slice(0, -1).join(', ')}, and ${
|
|
284
|
+
sectionNames[sectionNames.length - 1]
|
|
285
|
+
}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeText(text) {
|
|
289
|
+
const collapsedText = text.replace(/\s+/g, ' ').trim();
|
|
290
|
+
if (collapsedText === '') {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const sourceFile = ts.createSourceFile(
|
|
296
|
+
'__brand_angular_deps_match__.ts',
|
|
297
|
+
`type __BrandAngularDepsMatch = ${text};`,
|
|
298
|
+
ts.ScriptTarget.Latest,
|
|
299
|
+
true,
|
|
300
|
+
ts.ScriptKind.TS,
|
|
301
|
+
);
|
|
302
|
+
const [statement] = sourceFile.statements;
|
|
303
|
+
|
|
304
|
+
if (!statement || !ts.isTypeAliasDeclaration(statement)) {
|
|
305
|
+
return collapsedText;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const transformedType = ts.transform(statement.type, [
|
|
309
|
+
(transformationContext) => (rootNode) =>
|
|
310
|
+
ts.visitNode(rootNode, function visit(node) {
|
|
311
|
+
if (ts.isStringLiteral(node)) {
|
|
312
|
+
return ts.factory.createStringLiteral(node.text);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return ts.visitEachChild(node, visit, transformationContext);
|
|
316
|
+
}),
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
const [canonicalTypeNode] = transformedType.transformed;
|
|
320
|
+
const normalizedText = typePrinter
|
|
321
|
+
.printNode(
|
|
322
|
+
ts.EmitHint.Unspecified,
|
|
323
|
+
canonicalTypeNode ?? statement.type,
|
|
324
|
+
sourceFile,
|
|
325
|
+
)
|
|
326
|
+
.replace(/\s+/g, ' ')
|
|
327
|
+
.trim();
|
|
328
|
+
transformedType.dispose();
|
|
329
|
+
return normalizedText;
|
|
330
|
+
} catch {
|
|
331
|
+
return collapsedText;
|
|
332
|
+
}
|
|
199
333
|
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const moduleApi = require('node:module');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const vm = require('node:vm');
|
|
5
|
+
const ts = require('typescript');
|
|
6
|
+
|
|
7
|
+
const { Project } = require('ts-morph');
|
|
8
|
+
const {
|
|
9
|
+
analyzeSourceFileDependencies,
|
|
10
|
+
discoverAngularBrandConfigFilePath,
|
|
11
|
+
transformSourceFile,
|
|
12
|
+
} = loadAngularBrandCodemod();
|
|
13
|
+
|
|
14
|
+
const projectCache = new Map();
|
|
15
|
+
|
|
16
|
+
function loadAngularBrandCodemod() {
|
|
17
|
+
const filePath = path.resolve(
|
|
18
|
+
__dirname,
|
|
19
|
+
'../scripts/angular-brand-codemod.ts',
|
|
20
|
+
);
|
|
21
|
+
const compiledFilePath = `${filePath}.cjs`;
|
|
22
|
+
const sourceText = fs.readFileSync(filePath, 'utf8');
|
|
23
|
+
const librarySourceText = sourceText.replace(
|
|
24
|
+
/\n\/\/ Check if this module is being run directly[\s\S]*$/,
|
|
25
|
+
'\n',
|
|
26
|
+
);
|
|
27
|
+
const transpiled = ts.transpileModule(librarySourceText, {
|
|
28
|
+
compilerOptions: {
|
|
29
|
+
module: ts.ModuleKind.CommonJS,
|
|
30
|
+
target: ts.ScriptTarget.ES2022,
|
|
31
|
+
esModuleInterop: true,
|
|
32
|
+
moduleResolution: ts.ModuleResolutionKind.NodeJs,
|
|
33
|
+
},
|
|
34
|
+
fileName: filePath,
|
|
35
|
+
});
|
|
36
|
+
const wrappedSource = module.constructor.wrap(transpiled.outputText);
|
|
37
|
+
const evaluate = vm.runInThisContext(wrappedSource, {
|
|
38
|
+
filename: compiledFilePath,
|
|
39
|
+
});
|
|
40
|
+
const loadedModule = { exports: {} };
|
|
41
|
+
evaluate(
|
|
42
|
+
loadedModule.exports,
|
|
43
|
+
moduleApi.createRequire(compiledFilePath),
|
|
44
|
+
loadedModule,
|
|
45
|
+
compiledFilePath,
|
|
46
|
+
path.dirname(filePath),
|
|
47
|
+
);
|
|
48
|
+
return loadedModule.exports;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description:
|
|
56
|
+
'Ensure Angular components, directives, and pipes expose a generated GenDeps alias.',
|
|
57
|
+
},
|
|
58
|
+
fixable: 'code',
|
|
59
|
+
schema: [],
|
|
60
|
+
},
|
|
61
|
+
create(context) {
|
|
62
|
+
return {
|
|
63
|
+
'Program:exit'() {
|
|
64
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
65
|
+
const filePath = getFilePath(context);
|
|
66
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const text = sourceCode.getText();
|
|
71
|
+
const sourceFile = getProjectSourceFile(
|
|
72
|
+
getProject(getCwd(context)),
|
|
73
|
+
filePath,
|
|
74
|
+
text,
|
|
75
|
+
);
|
|
76
|
+
const configFilePath = discoverAngularBrandConfigFilePath(
|
|
77
|
+
path.dirname(filePath),
|
|
78
|
+
getCwd(context),
|
|
79
|
+
);
|
|
80
|
+
const analysis = analyzeSourceFileDependencies(sourceFile, {
|
|
81
|
+
configFilePath,
|
|
82
|
+
});
|
|
83
|
+
if (
|
|
84
|
+
!analysis.classDeclaration ||
|
|
85
|
+
analysis.skipped ||
|
|
86
|
+
!analysis.generatedTypeName
|
|
87
|
+
) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (sourceFile.getTypeAlias(analysis.generatedTypeName)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const reportLoc = getNodeLoc(sourceCode, analysis.classDeclaration);
|
|
96
|
+
|
|
97
|
+
transformSourceFile(sourceFile, { configFilePath });
|
|
98
|
+
|
|
99
|
+
if (!sourceFile.getTypeAlias(analysis.generatedTypeName)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fixedText = sourceFile.getFullText();
|
|
104
|
+
if (fixedText === text) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
context.report({
|
|
109
|
+
loc: reportLoc,
|
|
110
|
+
message: `${analysis.generatedTypeName} is missing. Run ESLint --fix on this file or craft-brand --root <source-root> to generate it.`,
|
|
111
|
+
fix(fixer) {
|
|
112
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
function getProject(cwd) {
|
|
121
|
+
let project = projectCache.get(cwd);
|
|
122
|
+
if (project) {
|
|
123
|
+
return project;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
127
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
128
|
+
? new Project({ tsConfigFilePath })
|
|
129
|
+
: new Project({
|
|
130
|
+
compilerOptions: {
|
|
131
|
+
experimentalDecorators: true,
|
|
132
|
+
target: 9,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
projectCache.set(cwd, project);
|
|
137
|
+
return project;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
141
|
+
const normalizedPath = path.resolve(filePath);
|
|
142
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
143
|
+
if (existingSourceFile) {
|
|
144
|
+
existingSourceFile.replaceWithText(text);
|
|
145
|
+
return existingSourceFile;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
149
|
+
if (sourceFile) {
|
|
150
|
+
sourceFile.replaceWithText(text);
|
|
151
|
+
return sourceFile;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getFilePath(context) {
|
|
158
|
+
const filePath = context.filename ?? context.getFilename();
|
|
159
|
+
if (!filePath || filePath === '<input>') {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return filePath;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getCwd(context) {
|
|
167
|
+
return context.cwd ?? process.cwd();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getNodeLoc(sourceCode, node) {
|
|
171
|
+
return {
|
|
172
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
173
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
const brandAngularGenDepsRequired = require('./brand-angular-gen-deps-required.cjs');
|
|
1
2
|
const brandAngularDepsMatch = require('./brand-angular-deps-match.cjs');
|
|
3
|
+
const appStartRegistryMatch = require('./app-start-registry-match.cjs');
|
|
2
4
|
const noAngularInject = require('./no-angular-inject.cjs');
|
|
3
|
-
const
|
|
5
|
+
const preferCraftHttpClient = require('./prefer-craft-http-client.cjs');
|
|
6
|
+
const preferCraftService = require('./prefer-craft-service.cjs');
|
|
7
|
+
const preferBrowserBoundaries = require('./prefer-browser-boundaries.cjs');
|
|
4
8
|
|
|
5
9
|
module.exports = {
|
|
6
10
|
rules: {
|
|
11
|
+
'app-start-registry-match': appStartRegistryMatch,
|
|
12
|
+
'brand-angular-gen-deps-required': brandAngularGenDepsRequired,
|
|
7
13
|
'brand-angular-deps-match': brandAngularDepsMatch,
|
|
8
14
|
'no-angular-inject': noAngularInject,
|
|
9
|
-
'
|
|
15
|
+
'prefer-craft-http-client': preferCraftHttpClient,
|
|
16
|
+
'prefer-craft-service': preferCraftService,
|
|
17
|
+
'prefer-browser-boundaries': preferBrowserBoundaries,
|
|
10
18
|
},
|
|
11
19
|
};
|
|
@@ -3,14 +3,12 @@ module.exports = {
|
|
|
3
3
|
type: 'problem',
|
|
4
4
|
docs: {
|
|
5
5
|
description:
|
|
6
|
-
'Disallow Angular inject()
|
|
6
|
+
'Disallow Angular inject() usage so dependencies go through craftService or toCraftService.',
|
|
7
7
|
},
|
|
8
8
|
schema: [],
|
|
9
9
|
},
|
|
10
10
|
create(context) {
|
|
11
11
|
const angularNamespaceImports = new Set();
|
|
12
|
-
const angularInjectableImports = new Set();
|
|
13
|
-
const angularServiceImports = new Set();
|
|
14
12
|
|
|
15
13
|
return {
|
|
16
14
|
ImportDeclaration(node) {
|
|
@@ -32,24 +30,6 @@ module.exports = {
|
|
|
32
30
|
continue;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
if (
|
|
36
|
-
specifier.type === 'ImportSpecifier' &&
|
|
37
|
-
specifier.imported.type === 'Identifier' &&
|
|
38
|
-
specifier.imported.name === 'Injectable'
|
|
39
|
-
) {
|
|
40
|
-
angularInjectableImports.add(specifier.local.name);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
specifier.type === 'ImportSpecifier' &&
|
|
46
|
-
specifier.imported.type === 'Identifier' &&
|
|
47
|
-
specifier.imported.name === 'Service'
|
|
48
|
-
) {
|
|
49
|
-
angularServiceImports.add(specifier.local.name);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
33
|
if (specifier.type === 'ImportNamespaceSpecifier') {
|
|
54
34
|
angularNamespaceImports.add(specifier.local.name);
|
|
55
35
|
}
|
|
@@ -74,22 +54,6 @@ module.exports = {
|
|
|
74
54
|
'Angular inject() is forbidden. Expose a craftService/toCraftService injector instead.',
|
|
75
55
|
});
|
|
76
56
|
},
|
|
77
|
-
Decorator(node) {
|
|
78
|
-
const decoratorName = getAngularBlockedDecoratorName(
|
|
79
|
-
node.expression,
|
|
80
|
-
angularInjectableImports,
|
|
81
|
-
angularServiceImports,
|
|
82
|
-
angularNamespaceImports,
|
|
83
|
-
);
|
|
84
|
-
if (!decoratorName) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
context.report({
|
|
89
|
-
node,
|
|
90
|
-
message: `Angular @${decoratorName} is forbidden. Expose the dependency through craftService or toCraftService instead.`,
|
|
91
|
-
});
|
|
92
|
-
},
|
|
93
57
|
};
|
|
94
58
|
},
|
|
95
59
|
};
|
|
@@ -97,44 +61,3 @@ module.exports = {
|
|
|
97
61
|
function isAngularModule(sourceValue) {
|
|
98
62
|
return typeof sourceValue === 'string' && sourceValue.startsWith('@angular/');
|
|
99
63
|
}
|
|
100
|
-
|
|
101
|
-
function getAngularBlockedDecoratorName(
|
|
102
|
-
expression,
|
|
103
|
-
angularInjectableImports,
|
|
104
|
-
angularServiceImports,
|
|
105
|
-
angularNamespaceImports,
|
|
106
|
-
) {
|
|
107
|
-
if (expression.type === 'Identifier') {
|
|
108
|
-
if (angularInjectableImports.has(expression.name)) {
|
|
109
|
-
return 'Injectable';
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (angularServiceImports.has(expression.name)) {
|
|
113
|
-
return 'Service';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return undefined;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (expression.type === 'CallExpression') {
|
|
120
|
-
return getAngularBlockedDecoratorName(
|
|
121
|
-
expression.callee,
|
|
122
|
-
angularInjectableImports,
|
|
123
|
-
angularServiceImports,
|
|
124
|
-
angularNamespaceImports,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
expression.type === 'MemberExpression' &&
|
|
130
|
-
!expression.computed &&
|
|
131
|
-
expression.object.type === 'Identifier' &&
|
|
132
|
-
angularNamespaceImports.has(expression.object.name) &&
|
|
133
|
-
expression.property.type === 'Identifier' &&
|
|
134
|
-
(expression.property.name === 'Injectable' || expression.property.name === 'Service')
|
|
135
|
-
) {
|
|
136
|
-
return expression.property.name;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|