@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
package/README.md
CHANGED
|
@@ -10,6 +10,66 @@ Development tools for ng-craft: ESLint configs, ESLint rules, and codemods.
|
|
|
10
10
|
npm install -D @craft-ng/dev-tools
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## Angular Brand Codemod Config
|
|
14
|
+
|
|
15
|
+
`craft-brand` can load a typed project config from `craft-brand.config.ts`.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { defineAngularBrandConfig } from '@craft-ng/dev-tools';
|
|
19
|
+
|
|
20
|
+
export default defineAngularBrandConfig({
|
|
21
|
+
importAugmentations: [
|
|
22
|
+
{
|
|
23
|
+
match: {
|
|
24
|
+
module: '@ngx-translate/core',
|
|
25
|
+
symbols: ['TranslatePipe'],
|
|
26
|
+
metadata: ['imports'],
|
|
27
|
+
},
|
|
28
|
+
deps: [{ key: 'TranslateService', symbol: 'TranslateService' }],
|
|
29
|
+
missingProvider: [
|
|
30
|
+
{ key: 'TranslateService', symbol: 'TranslateService' },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Rule semantics:
|
|
38
|
+
|
|
39
|
+
- `match.module`: module specifier to match
|
|
40
|
+
- `match.symbols`: optional exported symbol names that trigger the rule
|
|
41
|
+
- `match.metadata`: `imports` and/or `hostDirectives`
|
|
42
|
+
- `deps`: synthetic entries added to generated `GenDeps`
|
|
43
|
+
- `missingProvider`: synthetic entries added to generated `missingProvider`
|
|
44
|
+
|
|
45
|
+
Entry semantics:
|
|
46
|
+
|
|
47
|
+
- `key`: property name generated in `GenDeps`
|
|
48
|
+
- `symbol`: imported type name used in the generated type
|
|
49
|
+
- `module`: optional import source override, defaults to `match.module`
|
|
50
|
+
|
|
51
|
+
Discovery behavior:
|
|
52
|
+
|
|
53
|
+
- `craft-brand` auto-discovers `craft-brand.config.ts` by walking upward from `--root`
|
|
54
|
+
- the ESLint rules `brand-angular-gen-deps-required` and `brand-angular-deps-match` use the same upward discovery from the analyzed file, bounded by `context.cwd`
|
|
55
|
+
- `--config <path>` overrides auto-discovery for the CLI
|
|
56
|
+
- `brand-angular-gen-deps-required` can generate a missing `GenDeps_*` alias in the current file
|
|
57
|
+
- `brand-angular-deps-match` can autofix an existing `GenDeps_*` alias in the current file
|
|
58
|
+
|
|
59
|
+
Current scope:
|
|
60
|
+
|
|
61
|
+
- `TypeScript` config file only
|
|
62
|
+
- declarative rules only
|
|
63
|
+
- matching only from Angular `imports` and `hostDirectives`
|
|
64
|
+
- no arbitrary `typeText` generation and no helper-local `typeof injectX` expressions
|
|
65
|
+
|
|
66
|
+
Example CLI usage:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
craft-brand --root apps/demo/src
|
|
70
|
+
craft-brand --root apps/demo/src --config ./craft-brand.config.ts
|
|
71
|
+
```
|
|
72
|
+
|
|
13
73
|
```bash
|
|
14
74
|
import craftRules from '@craft-ng/dev-tools/eslint-rules';
|
|
15
75
|
|
|
@@ -19,9 +79,13 @@ export default [
|
|
|
19
79
|
'craft-ng': craftRules
|
|
20
80
|
},
|
|
21
81
|
rules: {
|
|
82
|
+
// Adds a Quick Fix in VS Code through the ESLint extension
|
|
83
|
+
'craft-ng/brand-angular-gen-deps-required': 'error',
|
|
22
84
|
'craft-ng/brand-angular-deps-match': 'error',
|
|
23
85
|
'craft-ng/no-angular-inject': 'error',
|
|
24
|
-
'craft-ng/
|
|
86
|
+
'craft-ng/prefer-craft-service': 'error',
|
|
87
|
+
'craft-ng/prefer-craft-http-client': 'error',
|
|
88
|
+
'craft-ng/prefer-browser-boundaries': 'error',
|
|
25
89
|
}
|
|
26
90
|
}
|
|
27
91
|
];
|
|
@@ -61,10 +125,14 @@ module.exports = defineConfig([
|
|
|
61
125
|
},
|
|
62
126
|
],
|
|
63
127
|
'@typescript-eslint/consistent-type-definitions': 'off',
|
|
64
|
-
//
|
|
128
|
+
// `brand-angular-gen-deps-required` generates missing GenDeps aliases with ESLint autofix
|
|
129
|
+
'craft-ng/brand-angular-gen-deps-required': 'error',
|
|
130
|
+
// `brand-angular-deps-match` refreshes existing GenDeps aliases with ESLint autofix
|
|
65
131
|
'craft-ng/brand-angular-deps-match': 'error',
|
|
66
132
|
'craft-ng/no-angular-inject': 'error',
|
|
67
|
-
'craft-ng/
|
|
133
|
+
'craft-ng/prefer-craft-service': 'error',
|
|
134
|
+
'craft-ng/prefer-craft-http-client': 'error',
|
|
135
|
+
'craft-ng/prefer-browser-boundaries': 'error',
|
|
68
136
|
},
|
|
69
137
|
},
|
|
70
138
|
{
|
|
@@ -74,3 +142,28 @@ module.exports = defineConfig([
|
|
|
74
142
|
},
|
|
75
143
|
]);
|
|
76
144
|
```
|
|
145
|
+
|
|
146
|
+
## Editor / AI Refresh
|
|
147
|
+
|
|
148
|
+
`GenDeps_* = GetDeps<...>` remains a source artifact generated in your `.ts` files.
|
|
149
|
+
|
|
150
|
+
Use two refresh flows:
|
|
151
|
+
|
|
152
|
+
- Current file without `GenDeps_*`: trigger the VS Code ESLint Quick Fix on `craft-ng/brand-angular-gen-deps-required`, or run `eslint --fix path/to/file.ts`
|
|
153
|
+
- Current file with `GenDeps_*`: trigger the VS Code ESLint Quick Fix on `craft-ng/brand-angular-deps-match`, or run `eslint --fix path/to/file.ts`
|
|
154
|
+
- Bulk refresh: run `craft-brand --root <source-root>`
|
|
155
|
+
|
|
156
|
+
Recommended workflow:
|
|
157
|
+
|
|
158
|
+
- after changing `inject(...)`, constructor injection, component `imports`, `providers`, or `viewProviders`, run the Quick Fix for the current file
|
|
159
|
+
- when doing a larger refactor or upgrading a whole app/lib, run `craft-brand --root <source-root>`
|
|
160
|
+
- when a browser API already exists in `@craft-ng/core/browser-boundaries`, enable `craft-ng/prefer-browser-boundaries` to prevent direct access to `window`, `document`, `localStorage`, `console`, and similar globals
|
|
161
|
+
|
|
162
|
+
Notes:
|
|
163
|
+
|
|
164
|
+
- the ESLint Quick Fix can generate a missing alias or refresh an existing one, but only for the current file
|
|
165
|
+
- the same flow works well for AI agents: file-local updates via `eslint --fix`, bulk updates via `craft-brand --root`
|
|
166
|
+
- `craft-ng/no-angular-inject` now targets raw Angular `inject()` only
|
|
167
|
+
- `craft-ng/prefer-craft-service` forbids authored Angular `@Injectable()` / `@Service()` classes in favor of `craftService(...)`
|
|
168
|
+
- `craft-ng/prefer-craft-http-client` forbids Angular `HttpClient` in favor of `CraftHttpClient`
|
|
169
|
+
- `craft-ng/prefer-craft-service` and `craft-ng/prefer-craft-http-client` also expose a VS Code Quick Fix suggestion that inserts a temporary local disable comment annotated with the intended migration target
|
package/package.json
CHANGED
|
@@ -0,0 +1,357 @@
|
|
|
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 craftService definitions using onAppStart are registered in CraftAppStartRegistry.',
|
|
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('onAppStart')) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sourceFile = getProjectSourceFile(
|
|
38
|
+
getProject(getCwd(context)),
|
|
39
|
+
filePath,
|
|
40
|
+
text,
|
|
41
|
+
);
|
|
42
|
+
const requiredRegistrations = collectRequiredRegistrations(sourceFile);
|
|
43
|
+
if (requiredRegistrations.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const registryState = analyzeRegistryState(
|
|
48
|
+
sourceFile,
|
|
49
|
+
requiredRegistrations,
|
|
50
|
+
);
|
|
51
|
+
if (
|
|
52
|
+
registryState.missing.length === 0 &&
|
|
53
|
+
registryState.outOfDate.length === 0
|
|
54
|
+
) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ensureRegistryEntries(sourceFile, requiredRegistrations);
|
|
59
|
+
const fixedText = sourceFile.getFullText();
|
|
60
|
+
if (fixedText === text) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
context.report({
|
|
65
|
+
loc: getNodeLoc(sourceCode, registryState.reportNode),
|
|
66
|
+
message: formatRegistryMessage(registryState),
|
|
67
|
+
fix(fixer) {
|
|
68
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function getProject(cwd) {
|
|
77
|
+
let project = projectCache.get(cwd);
|
|
78
|
+
if (project) {
|
|
79
|
+
return project;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const manipulationSettings = {
|
|
83
|
+
indentationText: IndentationText.TwoSpaces,
|
|
84
|
+
quoteKind: QuoteKind.Single,
|
|
85
|
+
};
|
|
86
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
87
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
88
|
+
? new Project({
|
|
89
|
+
tsConfigFilePath,
|
|
90
|
+
manipulationSettings,
|
|
91
|
+
})
|
|
92
|
+
: new Project({
|
|
93
|
+
compilerOptions: {
|
|
94
|
+
experimentalDecorators: true,
|
|
95
|
+
target: 9,
|
|
96
|
+
},
|
|
97
|
+
manipulationSettings,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
projectCache.set(cwd, project);
|
|
101
|
+
return project;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
105
|
+
const normalizedPath = path.resolve(filePath);
|
|
106
|
+
const existingSourceFile = project.getSourceFile(normalizedPath);
|
|
107
|
+
if (existingSourceFile) {
|
|
108
|
+
existingSourceFile.replaceWithText(text);
|
|
109
|
+
return existingSourceFile;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sourceFile = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
113
|
+
if (sourceFile) {
|
|
114
|
+
sourceFile.replaceWithText(text);
|
|
115
|
+
return sourceFile;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getFilePath(context) {
|
|
122
|
+
const filePath = context.filename ?? context.getFilename();
|
|
123
|
+
if (!filePath || filePath === '<input>') {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return filePath;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getCwd(context) {
|
|
131
|
+
return context.cwd ?? process.cwd();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function collectRequiredRegistrations(sourceFile) {
|
|
135
|
+
const registrations = new Map();
|
|
136
|
+
|
|
137
|
+
for (const declaration of sourceFile.getVariableDeclarations()) {
|
|
138
|
+
const initializer = declaration.getInitializerIfKind(
|
|
139
|
+
SyntaxKind.CallExpression,
|
|
140
|
+
);
|
|
141
|
+
if (!initializer || getCallExpressionName(initializer) !== 'craftService') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!containsOnAppStartCall(initializer)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const tag = readCraftServiceName(initializer);
|
|
150
|
+
const injectReference = readInjectReferenceName(declaration);
|
|
151
|
+
if (!tag || !injectReference || registrations.has(tag)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
registrations.set(tag, {
|
|
156
|
+
tag,
|
|
157
|
+
injectReference,
|
|
158
|
+
reportNode: initializer,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return [...registrations.values()];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getCallExpressionName(callExpression) {
|
|
166
|
+
const expression = callExpression.getExpression();
|
|
167
|
+
return Node.isIdentifier(expression) ? expression.getText() : undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function containsOnAppStartCall(callExpression) {
|
|
171
|
+
return callExpression
|
|
172
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
173
|
+
.some((innerCall) => getCallExpressionName(innerCall) === 'onAppStart');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readCraftServiceName(callExpression) {
|
|
177
|
+
const [definitionArgument] = callExpression.getArguments();
|
|
178
|
+
if (
|
|
179
|
+
!definitionArgument ||
|
|
180
|
+
!Node.isObjectLiteralExpression(definitionArgument)
|
|
181
|
+
) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const nameProperty = definitionArgument
|
|
186
|
+
.getProperties()
|
|
187
|
+
.find(
|
|
188
|
+
(property) =>
|
|
189
|
+
Node.isPropertyAssignment(property) && property.getName() === 'name',
|
|
190
|
+
);
|
|
191
|
+
if (!nameProperty || !Node.isPropertyAssignment(nameProperty)) {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const initializer = nameProperty.getInitializer();
|
|
196
|
+
if (
|
|
197
|
+
!initializer ||
|
|
198
|
+
(!Node.isStringLiteral(initializer) &&
|
|
199
|
+
!Node.isNoSubstitutionTemplateLiteral(initializer))
|
|
200
|
+
) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return initializer.getLiteralText();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readInjectReferenceName(declaration) {
|
|
208
|
+
const nameNode = declaration.getNameNode();
|
|
209
|
+
if (!Node.isObjectBindingPattern(nameNode)) {
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const bindingElement =
|
|
214
|
+
nameNode
|
|
215
|
+
.getElements()
|
|
216
|
+
.find((element) =>
|
|
217
|
+
element.getNameNode().getText().startsWith('inject'),
|
|
218
|
+
) ??
|
|
219
|
+
(nameNode.getElements().length === 1
|
|
220
|
+
? nameNode.getElements()[0]
|
|
221
|
+
: undefined);
|
|
222
|
+
|
|
223
|
+
return bindingElement?.getNameNode().getText();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function analyzeRegistryState(sourceFile, requiredRegistrations) {
|
|
227
|
+
const moduleDeclaration = getCraftCoreModuleDeclaration(sourceFile);
|
|
228
|
+
const registryInterface = moduleDeclaration?.getInterface(
|
|
229
|
+
'CraftAppStartRegistry',
|
|
230
|
+
);
|
|
231
|
+
const propertiesByName = new Map(
|
|
232
|
+
(registryInterface?.getProperties() ?? []).map((property) => [
|
|
233
|
+
readPropertyName(property),
|
|
234
|
+
property,
|
|
235
|
+
]),
|
|
236
|
+
);
|
|
237
|
+
const missing = [];
|
|
238
|
+
const outOfDate = [];
|
|
239
|
+
|
|
240
|
+
for (const registration of requiredRegistrations) {
|
|
241
|
+
const property = propertiesByName.get(registration.tag);
|
|
242
|
+
if (!property) {
|
|
243
|
+
missing.push(registration.tag);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const currentType = normalizeText(property.getTypeNode()?.getText() ?? '');
|
|
248
|
+
const expectedType = normalizeText(
|
|
249
|
+
`typeof ${registration.injectReference}`,
|
|
250
|
+
);
|
|
251
|
+
if (currentType !== expectedType) {
|
|
252
|
+
outOfDate.push(registration.tag);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
missing,
|
|
258
|
+
outOfDate,
|
|
259
|
+
reportNode: requiredRegistrations[0].reportNode,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getCraftCoreModuleDeclaration(sourceFile) {
|
|
264
|
+
return sourceFile.getModules().find((moduleDeclaration) => {
|
|
265
|
+
const nameNode = moduleDeclaration.getNameNode();
|
|
266
|
+
return (
|
|
267
|
+
Node.isStringLiteral(nameNode) &&
|
|
268
|
+
nameNode.getLiteralText() === '@craft-ng/core'
|
|
269
|
+
);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readPropertyName(property) {
|
|
274
|
+
const nameNode = property.getNameNode();
|
|
275
|
+
if (Node.isIdentifier(nameNode) || Node.isPrivateIdentifier(nameNode)) {
|
|
276
|
+
return nameNode.getText();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (Node.isStringLiteral(nameNode) || Node.isNumericLiteral(nameNode)) {
|
|
280
|
+
return nameNode.getLiteralText();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return property.getName();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function ensureRegistryEntries(sourceFile, requiredRegistrations) {
|
|
287
|
+
const moduleDeclaration =
|
|
288
|
+
getCraftCoreModuleDeclaration(sourceFile) ??
|
|
289
|
+
sourceFile.addModule({
|
|
290
|
+
name: "'@craft-ng/core'",
|
|
291
|
+
hasDeclareKeyword: true,
|
|
292
|
+
});
|
|
293
|
+
const registryInterface =
|
|
294
|
+
moduleDeclaration.getInterface('CraftAppStartRegistry') ??
|
|
295
|
+
moduleDeclaration.addInterface({
|
|
296
|
+
name: 'CraftAppStartRegistry',
|
|
297
|
+
});
|
|
298
|
+
const propertiesByName = new Map(
|
|
299
|
+
registryInterface
|
|
300
|
+
.getProperties()
|
|
301
|
+
.map((property) => [readPropertyName(property), property]),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
for (const registration of requiredRegistrations) {
|
|
305
|
+
const expectedType = `typeof ${registration.injectReference}`;
|
|
306
|
+
const property = propertiesByName.get(registration.tag);
|
|
307
|
+
if (!property) {
|
|
308
|
+
registryInterface.addProperty({
|
|
309
|
+
name: formatPropertyName(registration.tag),
|
|
310
|
+
type: expectedType,
|
|
311
|
+
});
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
property.setType(expectedType);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function formatPropertyName(name) {
|
|
320
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : `'${name}'`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getNodeLoc(sourceCode, node) {
|
|
324
|
+
return {
|
|
325
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
326
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function formatRegistryMessage({ missing, outOfDate }) {
|
|
331
|
+
const segments = [];
|
|
332
|
+
if (missing.length > 0) {
|
|
333
|
+
segments.push(`missing ${formatNameList(missing)}`);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (outOfDate.length > 0) {
|
|
337
|
+
segments.push(`out of date for ${formatNameList(outOfDate)}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return `CraftAppStartRegistry is ${segments.join(' and ')}. Run ESLint --fix on this file to register app-start services.`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function formatNameList(names) {
|
|
344
|
+
if (names.length === 1) {
|
|
345
|
+
return names[0];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (names.length === 2) {
|
|
349
|
+
return `${names[0]} and ${names[1]}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return `${names.slice(0, -1).join(', ')}, and ${names[names.length - 1]}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeText(text) {
|
|
356
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
357
|
+
}
|