@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
|
@@ -15,11 +15,41 @@ import {
|
|
|
15
15
|
TypeNode,
|
|
16
16
|
ts,
|
|
17
17
|
} from 'ts-morph';
|
|
18
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
-
import {
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
import {
|
|
21
|
+
basename,
|
|
22
|
+
dirname,
|
|
23
|
+
extname,
|
|
24
|
+
isAbsolute,
|
|
25
|
+
join,
|
|
26
|
+
resolve,
|
|
27
|
+
} from 'node:path';
|
|
20
28
|
import { fileURLToPath } from 'node:url';
|
|
21
29
|
|
|
22
30
|
export type AngularKind = 'component' | 'directive' | 'pipe' | 'injectable';
|
|
31
|
+
export type AngularBrandMetadataContext = 'imports' | 'hostDirectives';
|
|
32
|
+
|
|
33
|
+
export type AngularBrandConfigEntry = {
|
|
34
|
+
key: string;
|
|
35
|
+
symbol: string;
|
|
36
|
+
typeText?: string;
|
|
37
|
+
module?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type AngularBrandImportAugmentationRule = {
|
|
41
|
+
match: {
|
|
42
|
+
module: string;
|
|
43
|
+
symbols?: readonly string[];
|
|
44
|
+
metadata?: readonly AngularBrandMetadataContext[];
|
|
45
|
+
};
|
|
46
|
+
deps?: readonly AngularBrandConfigEntry[];
|
|
47
|
+
missingProvider?: readonly AngularBrandConfigEntry[];
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type AngularBrandConfig = {
|
|
51
|
+
importAugmentations?: readonly AngularBrandImportAugmentationRule[];
|
|
52
|
+
};
|
|
23
53
|
|
|
24
54
|
export type TransformResult = {
|
|
25
55
|
changed: boolean;
|
|
@@ -38,6 +68,8 @@ export type AngularBrandCodemodOptions = {
|
|
|
38
68
|
transformOnlyStandaloneDeclarables?: boolean;
|
|
39
69
|
includeProviders?: boolean;
|
|
40
70
|
includeViewProviders?: boolean;
|
|
71
|
+
config?: AngularBrandConfig;
|
|
72
|
+
configFilePath?: string;
|
|
41
73
|
};
|
|
42
74
|
|
|
43
75
|
export type AngularClassSearchResult = {
|
|
@@ -70,10 +102,17 @@ export type GeneratedDependencyEntry = {
|
|
|
70
102
|
|
|
71
103
|
export type GeneratedDependencyGroups = {
|
|
72
104
|
deps: GeneratedDependencyEntry[];
|
|
105
|
+
propertiesDeps: GeneratedDependencyEntry[];
|
|
73
106
|
provided: GeneratedDependencyEntry[];
|
|
74
107
|
missingProvider: GeneratedDependencyEntry[];
|
|
75
108
|
};
|
|
76
109
|
|
|
110
|
+
type GeneratedDependencyGroupAugmentation = {
|
|
111
|
+
deps: GeneratedDependencyDescriptor[];
|
|
112
|
+
missingProvider: GeneratedDependencyDescriptor[];
|
|
113
|
+
legacyInjectedDependencies: GeneratedDependencyDescriptor[];
|
|
114
|
+
};
|
|
115
|
+
|
|
77
116
|
export type DependencyAnalysisResult = Omit<TransformResult, 'changed'> & {
|
|
78
117
|
classDeclaration?: ClassDeclaration;
|
|
79
118
|
};
|
|
@@ -92,6 +131,7 @@ type MetadataDependencyGroups = {
|
|
|
92
131
|
hostDirectives: string[];
|
|
93
132
|
providers: string[];
|
|
94
133
|
viewProviders: string[];
|
|
134
|
+
occurrences: MetadataDependencyOccurrence[];
|
|
95
135
|
warnings: string[];
|
|
96
136
|
};
|
|
97
137
|
|
|
@@ -105,14 +145,25 @@ type InjectedDependencyDescriptor = {
|
|
|
105
145
|
entry: GeneratedDependencyEntry;
|
|
106
146
|
};
|
|
107
147
|
|
|
148
|
+
type GeneratedDependencyDescriptor = {
|
|
149
|
+
dependencyText: string;
|
|
150
|
+
entry: GeneratedDependencyEntry;
|
|
151
|
+
};
|
|
152
|
+
|
|
108
153
|
type InjectCallDependencyExtractionResult = DependencyExtractionResult & {
|
|
109
154
|
generatedDependencies: InjectedDependencyDescriptor[];
|
|
110
155
|
};
|
|
111
156
|
|
|
157
|
+
type PropertyDependencyExtractionResult = DependencyExtractionResult & {
|
|
158
|
+
entries: GeneratedDependencyEntry[];
|
|
159
|
+
missingProvider: GeneratedDependencyEntry[];
|
|
160
|
+
};
|
|
161
|
+
|
|
112
162
|
type DependencyReferenceResolution = {
|
|
113
163
|
kind: 'class' | 'enum' | 'function' | 'variable' | 'namespace' | 'unknown';
|
|
114
164
|
classDeclaration?: ClassDeclaration;
|
|
115
165
|
moduleSpecifier?: string;
|
|
166
|
+
importedName?: string;
|
|
116
167
|
};
|
|
117
168
|
|
|
118
169
|
type TrackedHelperResolution = {
|
|
@@ -133,7 +184,35 @@ type HelperExposureTracking = {
|
|
|
133
184
|
}>;
|
|
134
185
|
};
|
|
135
186
|
|
|
136
|
-
type
|
|
187
|
+
type MetadataDependencyOccurrence = {
|
|
188
|
+
dependencyText: string;
|
|
189
|
+
symbolName: string;
|
|
190
|
+
moduleSpecifier?: string;
|
|
191
|
+
metadataContext: AngularBrandMetadataContext;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
type NormalizedAngularBrandImportAugmentationRule = {
|
|
195
|
+
match: {
|
|
196
|
+
module: string;
|
|
197
|
+
symbols?: string[];
|
|
198
|
+
metadata: AngularBrandMetadataContext[];
|
|
199
|
+
};
|
|
200
|
+
deps: AngularBrandConfigEntry[];
|
|
201
|
+
missingProvider: AngularBrandConfigEntry[];
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
type NormalizedAngularBrandConfig = {
|
|
205
|
+
importAugmentations: NormalizedAngularBrandImportAugmentationRule[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
type NormalizedOptions = {
|
|
209
|
+
helperImportPath: string;
|
|
210
|
+
transformOnlyStandaloneDeclarables: boolean;
|
|
211
|
+
includeProviders: boolean;
|
|
212
|
+
includeViewProviders: boolean;
|
|
213
|
+
config?: AngularBrandConfig;
|
|
214
|
+
configFilePath?: string;
|
|
215
|
+
};
|
|
137
216
|
|
|
138
217
|
type InjectDecoratorTokenResult =
|
|
139
218
|
| { found: false }
|
|
@@ -163,8 +242,55 @@ const DEFAULT_OPTIONS: NormalizedOptions = {
|
|
|
163
242
|
transformOnlyStandaloneDeclarables: false,
|
|
164
243
|
includeProviders: true,
|
|
165
244
|
includeViewProviders: true,
|
|
245
|
+
config: undefined,
|
|
246
|
+
configFilePath: undefined,
|
|
166
247
|
};
|
|
167
248
|
|
|
249
|
+
const ANGULAR_BRAND_CONFIG_FILE_NAME = 'craft-brand.config.ts';
|
|
250
|
+
const DEFAULT_ANGULAR_BRAND_METADATA_CONTEXTS: AngularBrandMetadataContext[] = [
|
|
251
|
+
'imports',
|
|
252
|
+
'hostDirectives',
|
|
253
|
+
];
|
|
254
|
+
const DEFAULT_ANGULAR_BRAND_CONFIG = defineAngularBrandConfig({
|
|
255
|
+
importAugmentations: [
|
|
256
|
+
{
|
|
257
|
+
match: {
|
|
258
|
+
module: '@angular/router',
|
|
259
|
+
},
|
|
260
|
+
deps: [
|
|
261
|
+
{
|
|
262
|
+
key: 'Router',
|
|
263
|
+
symbol: 'Router',
|
|
264
|
+
module: '@angular/router',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
missingProvider: [
|
|
268
|
+
{
|
|
269
|
+
key: 'Router',
|
|
270
|
+
symbol: 'Router',
|
|
271
|
+
module: '@angular/router',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
match: {
|
|
277
|
+
module: '@angular/forms/signals',
|
|
278
|
+
symbols: ['FormField'],
|
|
279
|
+
metadata: ['imports'],
|
|
280
|
+
},
|
|
281
|
+
deps: [
|
|
282
|
+
{
|
|
283
|
+
key: 'FormField',
|
|
284
|
+
symbol: 'FormField',
|
|
285
|
+
typeText: 'FormField<any>',
|
|
286
|
+
module: '@angular/forms/signals',
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
const angularBrandConfigCache = new Map<string, AngularBrandConfig>();
|
|
293
|
+
|
|
168
294
|
const PRIMITIVE_TYPE_TEXTS = new Set([
|
|
169
295
|
'any',
|
|
170
296
|
'bigint',
|
|
@@ -209,6 +335,386 @@ const IGNORED_DIRECTORIES = new Set([
|
|
|
209
335
|
'tmp',
|
|
210
336
|
]);
|
|
211
337
|
|
|
338
|
+
export function defineAngularBrandConfig<Config extends AngularBrandConfig>(
|
|
339
|
+
config: Config,
|
|
340
|
+
): Config {
|
|
341
|
+
return config;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function discoverAngularBrandConfigFilePath(
|
|
345
|
+
searchFromDir: string,
|
|
346
|
+
stopDir?: string,
|
|
347
|
+
): string | undefined {
|
|
348
|
+
const resolvedStopDir = stopDir ? resolve(stopDir) : undefined;
|
|
349
|
+
let currentDir = resolve(searchFromDir);
|
|
350
|
+
|
|
351
|
+
while (true) {
|
|
352
|
+
const candidatePath = join(currentDir, ANGULAR_BRAND_CONFIG_FILE_NAME);
|
|
353
|
+
if (existsSync(candidatePath) && statSync(candidatePath).isFile()) {
|
|
354
|
+
return candidatePath;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (resolvedStopDir && currentDir === resolvedStopDir) {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const parentDir = dirname(currentDir);
|
|
362
|
+
if (parentDir === currentDir) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
currentDir = parentDir;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function loadAngularBrandConfigFromFile(
|
|
371
|
+
configFilePath: string,
|
|
372
|
+
): AngularBrandConfig {
|
|
373
|
+
const resolvedConfigFilePath = resolve(configFilePath);
|
|
374
|
+
const cachedConfig = angularBrandConfigCache.get(resolvedConfigFilePath);
|
|
375
|
+
if (cachedConfig) {
|
|
376
|
+
return cachedConfig;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
!existsSync(resolvedConfigFilePath) ||
|
|
381
|
+
!statSync(resolvedConfigFilePath).isFile()
|
|
382
|
+
) {
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Angular brand config file not found at "${resolvedConfigFilePath}".`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const sourceText = readFileSync(resolvedConfigFilePath, 'utf8');
|
|
390
|
+
const transpiled = ts.transpileModule(sourceText, {
|
|
391
|
+
compilerOptions: {
|
|
392
|
+
module: ts.ModuleKind.CommonJS,
|
|
393
|
+
target: ts.ScriptTarget.ES2022,
|
|
394
|
+
esModuleInterop: true,
|
|
395
|
+
allowSyntheticDefaultImports: true,
|
|
396
|
+
},
|
|
397
|
+
fileName: resolvedConfigFilePath,
|
|
398
|
+
});
|
|
399
|
+
const module = { exports: {} as Record<string, unknown> };
|
|
400
|
+
const moduleRequire = createRequire(resolvedConfigFilePath);
|
|
401
|
+
const compiledModule = new Function(
|
|
402
|
+
'exports',
|
|
403
|
+
'require',
|
|
404
|
+
'module',
|
|
405
|
+
'__filename',
|
|
406
|
+
'__dirname',
|
|
407
|
+
transpiled.outputText,
|
|
408
|
+
) as (
|
|
409
|
+
exports: Record<string, unknown>,
|
|
410
|
+
require: (specifier: string) => unknown,
|
|
411
|
+
module: { exports: Record<string, unknown> },
|
|
412
|
+
__filename: string,
|
|
413
|
+
__dirname: string,
|
|
414
|
+
) => void;
|
|
415
|
+
|
|
416
|
+
compiledModule(
|
|
417
|
+
module.exports,
|
|
418
|
+
(specifier: string) => {
|
|
419
|
+
if (specifier === '@craft-ng/dev-tools') {
|
|
420
|
+
return { defineAngularBrandConfig };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return moduleRequire(specifier);
|
|
424
|
+
},
|
|
425
|
+
module,
|
|
426
|
+
resolvedConfigFilePath,
|
|
427
|
+
dirname(resolvedConfigFilePath),
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const exportedConfig =
|
|
431
|
+
module.exports['default'] ??
|
|
432
|
+
(module.exports['__esModule']
|
|
433
|
+
? module.exports['default']
|
|
434
|
+
: module.exports);
|
|
435
|
+
const validatedConfig = validateAngularBrandConfig(
|
|
436
|
+
exportedConfig,
|
|
437
|
+
resolvedConfigFilePath,
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
angularBrandConfigCache.set(resolvedConfigFilePath, validatedConfig);
|
|
441
|
+
return validatedConfig;
|
|
442
|
+
} catch (error: unknown) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Invalid Angular brand config at "${resolvedConfigFilePath}": ${getErrorMessage(error)}`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function resolveAngularBrandConfig(
|
|
450
|
+
sourceFile: SourceFile,
|
|
451
|
+
options: NormalizedOptions,
|
|
452
|
+
): NormalizedAngularBrandConfig {
|
|
453
|
+
const explicitConfig =
|
|
454
|
+
options.config ??
|
|
455
|
+
(options.configFilePath
|
|
456
|
+
? loadAngularBrandConfigFromFile(options.configFilePath)
|
|
457
|
+
: loadDiscoveredAngularBrandConfig(dirname(sourceFile.getFilePath())));
|
|
458
|
+
|
|
459
|
+
return normalizeAngularBrandConfig(explicitConfig);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function loadDiscoveredAngularBrandConfig(
|
|
463
|
+
searchFromDir: string,
|
|
464
|
+
stopDir?: string,
|
|
465
|
+
): AngularBrandConfig | undefined {
|
|
466
|
+
const discoveredConfigPath = discoverAngularBrandConfigFilePath(
|
|
467
|
+
searchFromDir,
|
|
468
|
+
stopDir,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
return discoveredConfigPath
|
|
472
|
+
? loadAngularBrandConfigFromFile(discoveredConfigPath)
|
|
473
|
+
: undefined;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function normalizeAngularBrandConfig(
|
|
477
|
+
config?: AngularBrandConfig,
|
|
478
|
+
): NormalizedAngularBrandConfig {
|
|
479
|
+
const builtInRules = (
|
|
480
|
+
DEFAULT_ANGULAR_BRAND_CONFIG.importAugmentations ?? []
|
|
481
|
+
).map(normalizeAngularBrandImportAugmentationRule);
|
|
482
|
+
const configuredRules = (config?.importAugmentations ?? []).map(
|
|
483
|
+
normalizeAngularBrandImportAugmentationRule,
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
importAugmentations: [...builtInRules, ...configuredRules],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function normalizeAngularBrandImportAugmentationRule(
|
|
492
|
+
rule: AngularBrandImportAugmentationRule,
|
|
493
|
+
): NormalizedAngularBrandImportAugmentationRule {
|
|
494
|
+
return {
|
|
495
|
+
match: {
|
|
496
|
+
module: rule.match.module,
|
|
497
|
+
symbols: rule.match.symbols ? [...rule.match.symbols] : undefined,
|
|
498
|
+
metadata: rule.match.metadata
|
|
499
|
+
? [...rule.match.metadata]
|
|
500
|
+
: [...DEFAULT_ANGULAR_BRAND_METADATA_CONTEXTS],
|
|
501
|
+
},
|
|
502
|
+
deps: rule.deps ? [...rule.deps] : [],
|
|
503
|
+
missingProvider: rule.missingProvider ? [...rule.missingProvider] : [],
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function validateAngularBrandConfig(
|
|
508
|
+
value: unknown,
|
|
509
|
+
configFilePath: string,
|
|
510
|
+
): AngularBrandConfig {
|
|
511
|
+
if (!isPlainObject(value)) {
|
|
512
|
+
throw new Error('Expected the default export to be an object.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const importAugmentations = readOptionalArray(
|
|
516
|
+
value['importAugmentations'],
|
|
517
|
+
'importAugmentations',
|
|
518
|
+
configFilePath,
|
|
519
|
+
)?.map((entry, index) =>
|
|
520
|
+
validateAngularBrandImportAugmentationRule(
|
|
521
|
+
entry,
|
|
522
|
+
`importAugmentations[${index}]`,
|
|
523
|
+
configFilePath,
|
|
524
|
+
),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
importAugmentations: importAugmentations ?? [],
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function validateAngularBrandImportAugmentationRule(
|
|
533
|
+
value: unknown,
|
|
534
|
+
pathLabel: string,
|
|
535
|
+
configFilePath: string,
|
|
536
|
+
): AngularBrandImportAugmentationRule {
|
|
537
|
+
if (!isPlainObject(value)) {
|
|
538
|
+
throw new Error(`${pathLabel} must be an object.`);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (!isPlainObject(value['match'])) {
|
|
542
|
+
throw new Error(`${pathLabel}.match must be an object.`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const match = value['match'];
|
|
546
|
+
|
|
547
|
+
const moduleSpecifier = readRequiredString(
|
|
548
|
+
match['module'],
|
|
549
|
+
`${pathLabel}.match.module`,
|
|
550
|
+
configFilePath,
|
|
551
|
+
);
|
|
552
|
+
const symbols = readOptionalStringArray(
|
|
553
|
+
match['symbols'],
|
|
554
|
+
`${pathLabel}.match.symbols`,
|
|
555
|
+
configFilePath,
|
|
556
|
+
);
|
|
557
|
+
const metadata =
|
|
558
|
+
readOptionalStringArray(
|
|
559
|
+
match['metadata'],
|
|
560
|
+
`${pathLabel}.match.metadata`,
|
|
561
|
+
configFilePath,
|
|
562
|
+
)?.map((context) =>
|
|
563
|
+
validateAngularBrandMetadataContext(context, pathLabel),
|
|
564
|
+
) ?? undefined;
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
match: {
|
|
568
|
+
module: moduleSpecifier,
|
|
569
|
+
symbols,
|
|
570
|
+
metadata,
|
|
571
|
+
},
|
|
572
|
+
deps:
|
|
573
|
+
readOptionalArray(
|
|
574
|
+
value['deps'],
|
|
575
|
+
`${pathLabel}.deps`,
|
|
576
|
+
configFilePath,
|
|
577
|
+
)?.map((entry, index) =>
|
|
578
|
+
validateAngularBrandConfigEntry(
|
|
579
|
+
entry,
|
|
580
|
+
`${pathLabel}.deps[${index}]`,
|
|
581
|
+
configFilePath,
|
|
582
|
+
),
|
|
583
|
+
) ?? [],
|
|
584
|
+
missingProvider:
|
|
585
|
+
readOptionalArray(
|
|
586
|
+
value['missingProvider'],
|
|
587
|
+
`${pathLabel}.missingProvider`,
|
|
588
|
+
configFilePath,
|
|
589
|
+
)?.map((entry, index) =>
|
|
590
|
+
validateAngularBrandConfigEntry(
|
|
591
|
+
entry,
|
|
592
|
+
`${pathLabel}.missingProvider[${index}]`,
|
|
593
|
+
configFilePath,
|
|
594
|
+
),
|
|
595
|
+
) ?? [],
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function validateAngularBrandConfigEntry(
|
|
600
|
+
value: unknown,
|
|
601
|
+
pathLabel: string,
|
|
602
|
+
configFilePath: string,
|
|
603
|
+
): AngularBrandConfigEntry {
|
|
604
|
+
if (!isPlainObject(value)) {
|
|
605
|
+
throw new Error(`${pathLabel} must be an object.`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const key = readRequiredString(
|
|
609
|
+
value['key'],
|
|
610
|
+
`${pathLabel}.key`,
|
|
611
|
+
configFilePath,
|
|
612
|
+
);
|
|
613
|
+
const symbol = readRequiredString(
|
|
614
|
+
value['symbol'],
|
|
615
|
+
`${pathLabel}.symbol`,
|
|
616
|
+
configFilePath,
|
|
617
|
+
);
|
|
618
|
+
const typeText = readOptionalString(
|
|
619
|
+
value['typeText'],
|
|
620
|
+
`${pathLabel}.typeText`,
|
|
621
|
+
configFilePath,
|
|
622
|
+
);
|
|
623
|
+
const moduleSpecifier = readOptionalString(
|
|
624
|
+
value['module'],
|
|
625
|
+
`${pathLabel}.module`,
|
|
626
|
+
configFilePath,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
return {
|
|
630
|
+
key,
|
|
631
|
+
symbol,
|
|
632
|
+
typeText,
|
|
633
|
+
module: moduleSpecifier,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function validateAngularBrandMetadataContext(
|
|
638
|
+
value: string,
|
|
639
|
+
pathLabel: string,
|
|
640
|
+
): AngularBrandMetadataContext {
|
|
641
|
+
if (value === 'imports' || value === 'hostDirectives') {
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
throw new Error(
|
|
646
|
+
`${pathLabel}.match.metadata must contain only "imports" or "hostDirectives".`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function readOptionalArray(
|
|
651
|
+
value: unknown,
|
|
652
|
+
pathLabel: string,
|
|
653
|
+
configFilePath: string,
|
|
654
|
+
): unknown[] | undefined {
|
|
655
|
+
if (value === undefined) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (!Array.isArray(value)) {
|
|
660
|
+
throw new Error(`${pathLabel} in "${configFilePath}" must be an array.`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return value;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function readRequiredString(
|
|
667
|
+
value: unknown,
|
|
668
|
+
pathLabel: string,
|
|
669
|
+
configFilePath: string,
|
|
670
|
+
): string {
|
|
671
|
+
const stringValue = readOptionalString(value, pathLabel, configFilePath);
|
|
672
|
+
if (!stringValue) {
|
|
673
|
+
throw new Error(`${pathLabel} in "${configFilePath}" must be a string.`);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return stringValue;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function readOptionalString(
|
|
680
|
+
value: unknown,
|
|
681
|
+
pathLabel: string,
|
|
682
|
+
configFilePath: string,
|
|
683
|
+
): string | undefined {
|
|
684
|
+
if (value === undefined) {
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (typeof value !== 'string') {
|
|
689
|
+
throw new Error(`${pathLabel} in "${configFilePath}" must be a string.`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return value;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function readOptionalStringArray(
|
|
696
|
+
value: unknown,
|
|
697
|
+
pathLabel: string,
|
|
698
|
+
configFilePath: string,
|
|
699
|
+
): string[] | undefined {
|
|
700
|
+
const arrayValue = readOptionalArray(value, pathLabel, configFilePath);
|
|
701
|
+
if (!arrayValue) {
|
|
702
|
+
return undefined;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return arrayValue.map((entry, index) =>
|
|
706
|
+
readRequiredString(entry, `${pathLabel}[${index}]`, configFilePath),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
711
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function getErrorMessage(error: unknown): string {
|
|
715
|
+
return error instanceof Error ? error.message : String(error);
|
|
716
|
+
}
|
|
717
|
+
|
|
212
718
|
export function transformSourceFile(
|
|
213
719
|
sourceFile: SourceFile,
|
|
214
720
|
options: AngularBrandCodemodOptions = {},
|
|
@@ -259,6 +765,10 @@ export function analyzeSourceFileDependencies(
|
|
|
259
765
|
options: AngularBrandCodemodOptions = {},
|
|
260
766
|
): DependencyAnalysisResult {
|
|
261
767
|
const normalizedOptions = normalizeOptions(options);
|
|
768
|
+
const angularBrandConfig = resolveAngularBrandConfig(
|
|
769
|
+
sourceFile,
|
|
770
|
+
normalizedOptions,
|
|
771
|
+
);
|
|
262
772
|
const result: DependencyAnalysisResult = {
|
|
263
773
|
skipped: false,
|
|
264
774
|
warnings: [],
|
|
@@ -320,26 +830,47 @@ export function analyzeSourceFileDependencies(
|
|
|
320
830
|
)
|
|
321
831
|
: { entries: [], warnings: [] };
|
|
322
832
|
const constructorDeps = extractConstructorDeps(classDeclaration);
|
|
323
|
-
const
|
|
324
|
-
|
|
833
|
+
const propertyDeps = extractPropertyDependencies(
|
|
834
|
+
sourceFile,
|
|
835
|
+
classDeclaration,
|
|
836
|
+
className,
|
|
837
|
+
);
|
|
838
|
+
const generatedDependencyAugmentation =
|
|
839
|
+
createGeneratedDependencyGroupAugmentation(
|
|
840
|
+
sourceFile,
|
|
841
|
+
metadataDeps.occurrences,
|
|
842
|
+
angularBrandConfig,
|
|
843
|
+
);
|
|
844
|
+
const syntheticDependencyTexts = mergeDeps(
|
|
845
|
+
generatedDependencyAugmentation.deps.map(
|
|
846
|
+
(dependency) => dependency.dependencyText,
|
|
847
|
+
),
|
|
848
|
+
generatedDependencyAugmentation.legacyInjectedDependencies.map(
|
|
849
|
+
(dependency) => dependency.dependencyText,
|
|
850
|
+
),
|
|
851
|
+
);
|
|
325
852
|
result.warnings.push(
|
|
326
853
|
...metadataDeps.warnings,
|
|
327
854
|
...providedDeps.warnings,
|
|
328
855
|
...constructorDeps.warnings,
|
|
329
|
-
...
|
|
856
|
+
...propertyDeps.warnings,
|
|
330
857
|
);
|
|
331
858
|
result.dependencies = mergeDeps(
|
|
332
859
|
constructorDeps.dependencies,
|
|
333
|
-
|
|
860
|
+
propertyDeps.dependencies,
|
|
334
861
|
metadataDeps.imports,
|
|
335
862
|
metadataDeps.hostDirectives,
|
|
336
863
|
metadataDeps.providers,
|
|
337
864
|
metadataDeps.viewProviders,
|
|
865
|
+
syntheticDependencyTexts,
|
|
338
866
|
);
|
|
339
867
|
result.dependencyGroups = {
|
|
340
868
|
injected: mergeDeps(
|
|
341
869
|
constructorDeps.dependencies,
|
|
342
|
-
|
|
870
|
+
propertyDeps.dependencies,
|
|
871
|
+
generatedDependencyAugmentation.legacyInjectedDependencies.map(
|
|
872
|
+
(dependency) => dependency.dependencyText,
|
|
873
|
+
),
|
|
343
874
|
),
|
|
344
875
|
importDeps: mergeDeps(metadataDeps.imports, metadataDeps.hostDirectives),
|
|
345
876
|
providers: mergeDeps(metadataDeps.providers, metadataDeps.viewProviders),
|
|
@@ -347,19 +878,12 @@ export function analyzeSourceFileDependencies(
|
|
|
347
878
|
result.generatedTypeName = getGeneratedDepsTypeName(className);
|
|
348
879
|
result.generatedDependencyGroups = createGeneratedDependencyGroups(
|
|
349
880
|
sourceFile,
|
|
881
|
+
className,
|
|
350
882
|
result.dependencyGroups.importDeps,
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
dependencyText,
|
|
354
|
-
entry: createGeneratedDependencyEntry(
|
|
355
|
-
sourceFile,
|
|
356
|
-
dependencyText,
|
|
357
|
-
'inject',
|
|
358
|
-
),
|
|
359
|
-
})),
|
|
360
|
-
...injectCallDeps.generatedDependencies,
|
|
361
|
-
],
|
|
883
|
+
constructorDeps.dependencies,
|
|
884
|
+
propertyDeps,
|
|
362
885
|
providedDeps.entries,
|
|
886
|
+
generatedDependencyAugmentation,
|
|
363
887
|
);
|
|
364
888
|
|
|
365
889
|
return result;
|
|
@@ -572,6 +1096,99 @@ export function extractInjectCallDeps(
|
|
|
572
1096
|
};
|
|
573
1097
|
}
|
|
574
1098
|
|
|
1099
|
+
function extractPropertyDependencies(
|
|
1100
|
+
sourceFile: SourceFile,
|
|
1101
|
+
classDeclaration: ClassDeclaration,
|
|
1102
|
+
className: string,
|
|
1103
|
+
): PropertyDependencyExtractionResult {
|
|
1104
|
+
const warnings: string[] = [];
|
|
1105
|
+
const dependencies: string[] = [];
|
|
1106
|
+
const entries: GeneratedDependencyEntry[] = [];
|
|
1107
|
+
const missingProvider: GeneratedDependencyEntry[] = [];
|
|
1108
|
+
|
|
1109
|
+
for (const property of classDeclaration.getProperties()) {
|
|
1110
|
+
if (property.isStatic()) {
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const propertyName = getStaticPropertyName(property.getNameNode());
|
|
1115
|
+
if (!propertyName) {
|
|
1116
|
+
warnings.push(
|
|
1117
|
+
`Skipped property dependency tracking for ${property.getText()} because the property name is not static.`,
|
|
1118
|
+
);
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const initializer = property.getInitializer();
|
|
1123
|
+
if (Node.isCallExpression(initializer)) {
|
|
1124
|
+
const expression = initializer.getExpression();
|
|
1125
|
+
const injectMethodName = getInjectMethodName(expression);
|
|
1126
|
+
|
|
1127
|
+
if (injectMethodName?.startsWith('inject')) {
|
|
1128
|
+
const dependencyText =
|
|
1129
|
+
injectMethodName === 'inject'
|
|
1130
|
+
? getAngularInjectCallDependency(initializer)
|
|
1131
|
+
: getInjectionHelperDependency(expression);
|
|
1132
|
+
|
|
1133
|
+
if (!dependencyText) {
|
|
1134
|
+
warnings.push(
|
|
1135
|
+
`Skipped ${injectMethodName}() property dependency tracking for "${propertyName}" because the dependency is not static.`,
|
|
1136
|
+
);
|
|
1137
|
+
} else {
|
|
1138
|
+
dependencies.push(dependencyText);
|
|
1139
|
+
|
|
1140
|
+
const dependencyEntry =
|
|
1141
|
+
injectMethodName === 'inject'
|
|
1142
|
+
? createGeneratedDependencyEntry(
|
|
1143
|
+
sourceFile,
|
|
1144
|
+
dependencyText,
|
|
1145
|
+
'inject',
|
|
1146
|
+
)
|
|
1147
|
+
: createGeneratedInjectHelperDependencyEntry(
|
|
1148
|
+
sourceFile,
|
|
1149
|
+
initializer,
|
|
1150
|
+
expression,
|
|
1151
|
+
dependencyText,
|
|
1152
|
+
);
|
|
1153
|
+
|
|
1154
|
+
entries.push(
|
|
1155
|
+
createGeneratedPropertyDependencyEntry(
|
|
1156
|
+
propertyName,
|
|
1157
|
+
createSingleDependencyMapTypeText(dependencyEntry),
|
|
1158
|
+
),
|
|
1159
|
+
);
|
|
1160
|
+
|
|
1161
|
+
if (shouldGenerateLocalMissingProvider(sourceFile, dependencyText)) {
|
|
1162
|
+
missingProvider.push(
|
|
1163
|
+
createGeneratedDependencyEntry(
|
|
1164
|
+
sourceFile,
|
|
1165
|
+
dependencyText,
|
|
1166
|
+
'inject',
|
|
1167
|
+
),
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
entries.push(
|
|
1177
|
+
createGeneratedPropertyDependencyEntry(
|
|
1178
|
+
propertyName,
|
|
1179
|
+
createExtractDepsTypeText(className, propertyName),
|
|
1180
|
+
),
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
return {
|
|
1185
|
+
dependencies: mergeDeps(dependencies),
|
|
1186
|
+
warnings,
|
|
1187
|
+
entries,
|
|
1188
|
+
missingProvider: mergeGeneratedDependencyEntries(missingProvider),
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
|
|
575
1192
|
export function mergeDeps(...dependencyGroups: readonly string[][]): string[] {
|
|
576
1193
|
const seen = new Set<string>();
|
|
577
1194
|
const dependencies: string[] = [];
|
|
@@ -601,65 +1218,154 @@ function emptyDependencyGroups(): DependencyGroups {
|
|
|
601
1218
|
function emptyGeneratedDependencyGroups(): GeneratedDependencyGroups {
|
|
602
1219
|
return {
|
|
603
1220
|
deps: [],
|
|
1221
|
+
propertiesDeps: [],
|
|
604
1222
|
provided: [],
|
|
605
1223
|
missingProvider: [],
|
|
606
1224
|
};
|
|
607
1225
|
}
|
|
608
1226
|
|
|
1227
|
+
function emptyGeneratedDependencyGroupAugmentation(): GeneratedDependencyGroupAugmentation {
|
|
1228
|
+
return {
|
|
1229
|
+
deps: [],
|
|
1230
|
+
missingProvider: [],
|
|
1231
|
+
legacyInjectedDependencies: [],
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
609
1235
|
function getGeneratedDepsTypeName(className: string): string {
|
|
610
1236
|
return `GenDeps_${className}`;
|
|
611
1237
|
}
|
|
612
1238
|
|
|
613
1239
|
function createGeneratedDependencyGroups(
|
|
614
1240
|
sourceFile: SourceFile,
|
|
1241
|
+
className: string,
|
|
615
1242
|
importDependencies: string[],
|
|
616
|
-
|
|
1243
|
+
constructorDependencies: string[],
|
|
1244
|
+
propertyDependencies: PropertyDependencyExtractionResult,
|
|
617
1245
|
providedEntries: GeneratedDependencyEntry[],
|
|
1246
|
+
augmentation: GeneratedDependencyGroupAugmentation,
|
|
618
1247
|
): GeneratedDependencyGroups {
|
|
1248
|
+
const constructorEntries = constructorDependencies.map((dependency) =>
|
|
1249
|
+
createGeneratedDependencyEntry(sourceFile, dependency, 'inject'),
|
|
1250
|
+
);
|
|
619
1251
|
const deps = mergeGeneratedDependencyEntries(
|
|
620
1252
|
importDependencies.map((dependency) =>
|
|
621
1253
|
createGeneratedDependencyEntry(sourceFile, dependency, 'import'),
|
|
622
1254
|
),
|
|
623
|
-
|
|
1255
|
+
constructorEntries,
|
|
1256
|
+
augmentation.deps.map((dependency) => dependency.entry),
|
|
1257
|
+
);
|
|
1258
|
+
const propertiesDeps = mergeGeneratedDependencyEntries(
|
|
1259
|
+
propertyDependencies.entries,
|
|
624
1260
|
);
|
|
625
1261
|
const provided = mergeGeneratedDependencyEntries(providedEntries);
|
|
626
1262
|
const providedKeys = new Set(provided.map((entry) => entry.key));
|
|
627
|
-
const injectedEntriesByKey = new Map(
|
|
628
|
-
injectedDependencies.map((dependency) => [
|
|
629
|
-
dependency.entry.key,
|
|
630
|
-
dependency.entry,
|
|
631
|
-
]),
|
|
632
|
-
);
|
|
633
1263
|
const missingProvider = mergeGeneratedDependencyEntries(
|
|
634
|
-
|
|
635
|
-
.filter((
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
sourceFile,
|
|
641
|
-
dependency.dependencyText,
|
|
642
|
-
)
|
|
643
|
-
);
|
|
644
|
-
})
|
|
645
|
-
.map(
|
|
646
|
-
(dependency) =>
|
|
647
|
-
injectedEntriesByKey.get(dependency.entry.key) ??
|
|
648
|
-
createGeneratedDependencyEntry(
|
|
649
|
-
sourceFile,
|
|
650
|
-
dependency.dependencyText,
|
|
651
|
-
'inject',
|
|
652
|
-
),
|
|
1264
|
+
constructorDependencies
|
|
1265
|
+
.filter((dependencyText) =>
|
|
1266
|
+
shouldGenerateLocalMissingProvider(sourceFile, dependencyText),
|
|
1267
|
+
)
|
|
1268
|
+
.map((dependencyText) =>
|
|
1269
|
+
createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject'),
|
|
653
1270
|
),
|
|
1271
|
+
propertyDependencies.missingProvider,
|
|
1272
|
+
augmentation.missingProvider
|
|
1273
|
+
.map((dependency) => dependency.entry)
|
|
1274
|
+
.filter((entry) => !providedKeys.has(entry.key)),
|
|
654
1275
|
);
|
|
655
1276
|
|
|
656
1277
|
return {
|
|
657
1278
|
deps,
|
|
1279
|
+
propertiesDeps,
|
|
658
1280
|
provided,
|
|
659
1281
|
missingProvider,
|
|
660
1282
|
};
|
|
661
1283
|
}
|
|
662
1284
|
|
|
1285
|
+
function createGeneratedDependencyGroupAugmentation(
|
|
1286
|
+
sourceFile: SourceFile,
|
|
1287
|
+
metadataOccurrences: MetadataDependencyOccurrence[],
|
|
1288
|
+
config: NormalizedAngularBrandConfig,
|
|
1289
|
+
): GeneratedDependencyGroupAugmentation {
|
|
1290
|
+
if (metadataOccurrences.length === 0) {
|
|
1291
|
+
return emptyGeneratedDependencyGroupAugmentation();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const deps: GeneratedDependencyDescriptor[] = [];
|
|
1295
|
+
const missingProvider: GeneratedDependencyDescriptor[] = [];
|
|
1296
|
+
const legacyInjectedDependencies: GeneratedDependencyDescriptor[] = [];
|
|
1297
|
+
|
|
1298
|
+
for (const rule of config.importAugmentations) {
|
|
1299
|
+
if (
|
|
1300
|
+
!metadataOccurrences.some((occurrence) =>
|
|
1301
|
+
ruleMatchesMetadataOccurrence(rule, occurrence),
|
|
1302
|
+
)
|
|
1303
|
+
) {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const generatedDeps = rule.deps.map((entry) =>
|
|
1308
|
+
createConfiguredGeneratedDependencyDescriptor(
|
|
1309
|
+
sourceFile,
|
|
1310
|
+
entry,
|
|
1311
|
+
rule.match.module,
|
|
1312
|
+
),
|
|
1313
|
+
);
|
|
1314
|
+
const generatedMissingProviders = rule.missingProvider.map((entry) =>
|
|
1315
|
+
createConfiguredGeneratedDependencyDescriptor(
|
|
1316
|
+
sourceFile,
|
|
1317
|
+
entry,
|
|
1318
|
+
rule.match.module,
|
|
1319
|
+
),
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
deps.push(...generatedDeps);
|
|
1323
|
+
missingProvider.push(...generatedMissingProviders);
|
|
1324
|
+
legacyInjectedDependencies.push(...generatedMissingProviders);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return {
|
|
1328
|
+
deps,
|
|
1329
|
+
missingProvider,
|
|
1330
|
+
legacyInjectedDependencies,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function ruleMatchesMetadataOccurrence(
|
|
1335
|
+
rule: NormalizedAngularBrandImportAugmentationRule,
|
|
1336
|
+
occurrence: MetadataDependencyOccurrence,
|
|
1337
|
+
): boolean {
|
|
1338
|
+
if (occurrence.moduleSpecifier !== rule.match.module) {
|
|
1339
|
+
return false;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
if (!rule.match.metadata.includes(occurrence.metadataContext)) {
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
return (
|
|
1347
|
+
!rule.match.symbols || rule.match.symbols.includes(occurrence.symbolName)
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function createConfiguredGeneratedDependencyDescriptor(
|
|
1352
|
+
sourceFile: SourceFile,
|
|
1353
|
+
entry: AngularBrandConfigEntry,
|
|
1354
|
+
defaultModuleSpecifier: string,
|
|
1355
|
+
): GeneratedDependencyDescriptor {
|
|
1356
|
+
return {
|
|
1357
|
+
dependencyText: entry.symbol,
|
|
1358
|
+
entry: createSyntheticGeneratedDependencyEntry(
|
|
1359
|
+
sourceFile,
|
|
1360
|
+
entry.symbol,
|
|
1361
|
+
'inject',
|
|
1362
|
+
entry.module ?? defaultModuleSpecifier,
|
|
1363
|
+
entry.key,
|
|
1364
|
+
entry.typeText,
|
|
1365
|
+
),
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
|
|
663
1369
|
function mergeGeneratedDependencyEntries(
|
|
664
1370
|
...groups: readonly GeneratedDependencyEntry[][]
|
|
665
1371
|
): GeneratedDependencyEntry[] {
|
|
@@ -667,9 +1373,7 @@ function mergeGeneratedDependencyEntries(
|
|
|
667
1373
|
|
|
668
1374
|
for (const group of groups) {
|
|
669
1375
|
for (const entry of group) {
|
|
670
|
-
|
|
671
|
-
entries.set(entry.key, entry);
|
|
672
|
-
}
|
|
1376
|
+
entries.set(entry.key, entry);
|
|
673
1377
|
}
|
|
674
1378
|
}
|
|
675
1379
|
|
|
@@ -709,6 +1413,26 @@ function createGeneratedDependencyEntry(
|
|
|
709
1413
|
};
|
|
710
1414
|
}
|
|
711
1415
|
|
|
1416
|
+
function createSyntheticGeneratedDependencyEntry(
|
|
1417
|
+
sourceFile: SourceFile,
|
|
1418
|
+
dependencyText: string,
|
|
1419
|
+
context: 'import' | 'inject',
|
|
1420
|
+
moduleSpecifier: string,
|
|
1421
|
+
key = createGeneratedDependencyKey(dependencyText),
|
|
1422
|
+
typeText?: string,
|
|
1423
|
+
): GeneratedDependencyEntry {
|
|
1424
|
+
return {
|
|
1425
|
+
...createGeneratedDependencyEntry(sourceFile, dependencyText, context),
|
|
1426
|
+
key,
|
|
1427
|
+
typeText:
|
|
1428
|
+
typeText ?? createGeneratedDependencyTypeText(sourceFile, dependencyText),
|
|
1429
|
+
typeImport: {
|
|
1430
|
+
moduleSpecifier,
|
|
1431
|
+
name: dependencyText,
|
|
1432
|
+
},
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
712
1436
|
function createGeneratedInjectHelperDependencyEntry(
|
|
713
1437
|
sourceFile: SourceFile,
|
|
714
1438
|
callExpression: import('ts-morph').CallExpression,
|
|
@@ -726,6 +1450,7 @@ function createGeneratedInjectHelperDependencyEntry(
|
|
|
726
1450
|
key: trackedHelper.serviceName,
|
|
727
1451
|
typeText: createTrackedInjectHelperTypeText(
|
|
728
1452
|
dependencyText,
|
|
1453
|
+
trackedHelper.serviceName,
|
|
729
1454
|
exposureTracking,
|
|
730
1455
|
),
|
|
731
1456
|
};
|
|
@@ -733,9 +1458,10 @@ function createGeneratedInjectHelperDependencyEntry(
|
|
|
733
1458
|
|
|
734
1459
|
function createTrackedInjectHelperTypeText(
|
|
735
1460
|
dependencyText: string,
|
|
1461
|
+
serviceName: string,
|
|
736
1462
|
exposureTracking: HelperExposureTracking | undefined,
|
|
737
1463
|
): string {
|
|
738
|
-
const baseType = `
|
|
1464
|
+
const baseType = `ExtractDeps<typeof ${dependencyText}>[${JSON.stringify(serviceName)}]`;
|
|
739
1465
|
if (!exposureTracking) {
|
|
740
1466
|
return baseType;
|
|
741
1467
|
}
|
|
@@ -760,6 +1486,29 @@ function createTrackedInjectHelperTypeText(
|
|
|
760
1486
|
].join('\n');
|
|
761
1487
|
}
|
|
762
1488
|
|
|
1489
|
+
function createGeneratedPropertyDependencyEntry(
|
|
1490
|
+
propertyName: string,
|
|
1491
|
+
typeText: string,
|
|
1492
|
+
): GeneratedDependencyEntry {
|
|
1493
|
+
return {
|
|
1494
|
+
key: propertyName,
|
|
1495
|
+
typeText,
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function createExtractDepsTypeText(
|
|
1500
|
+
className: string,
|
|
1501
|
+
propertyName: string,
|
|
1502
|
+
): string {
|
|
1503
|
+
return `ExtractDeps<${className}[${JSON.stringify(propertyName)}]>`;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function createSingleDependencyMapTypeText(
|
|
1507
|
+
entry: GeneratedDependencyEntry,
|
|
1508
|
+
): string {
|
|
1509
|
+
return formatGeneratedDependencyObject([entry]);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
763
1512
|
function formatHelperExposurePropertiesType(
|
|
764
1513
|
dependencyText: string,
|
|
765
1514
|
properties: Array<{ propertyKey: string; sourceKey: string }>,
|
|
@@ -1146,7 +1895,10 @@ function ensureGeneratedDependencyTypeImports(
|
|
|
1146
1895
|
): void {
|
|
1147
1896
|
const importsToAdd = new Map<string, Set<string>>();
|
|
1148
1897
|
|
|
1149
|
-
for (const entry of
|
|
1898
|
+
for (const entry of [
|
|
1899
|
+
...generatedDependencyGroups.deps,
|
|
1900
|
+
...generatedDependencyGroups.missingProvider,
|
|
1901
|
+
]) {
|
|
1150
1902
|
if (!entry.typeImport) {
|
|
1151
1903
|
continue;
|
|
1152
1904
|
}
|
|
@@ -1162,12 +1914,28 @@ function ensureGeneratedDependencyTypeImports(
|
|
|
1162
1914
|
.getImportDeclarations()
|
|
1163
1915
|
.find(
|
|
1164
1916
|
(importDeclaration) =>
|
|
1165
|
-
importDeclaration.getModuleSpecifierValue() === moduleSpecifier
|
|
1917
|
+
importDeclaration.getModuleSpecifierValue() === moduleSpecifier &&
|
|
1918
|
+
!importDeclaration.getNamespaceImport(),
|
|
1166
1919
|
);
|
|
1167
1920
|
|
|
1168
1921
|
if (existingImport) {
|
|
1922
|
+
const existingImportNames = new Set(
|
|
1923
|
+
existingImport.getNamedImports().map((namedImport) => {
|
|
1924
|
+
return (
|
|
1925
|
+
namedImport.getAliasNode()?.getText() ??
|
|
1926
|
+
namedImport.getNameNode().getText()
|
|
1927
|
+
);
|
|
1928
|
+
}),
|
|
1929
|
+
);
|
|
1930
|
+
const namesToAdd = [...importNames].filter(
|
|
1931
|
+
(name) => !existingImportNames.has(name),
|
|
1932
|
+
);
|
|
1933
|
+
if (namesToAdd.length === 0) {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1169
1937
|
existingImport.addNamedImports(
|
|
1170
|
-
|
|
1938
|
+
namesToAdd.map((name) => ({ name, isTypeOnly: true })),
|
|
1171
1939
|
);
|
|
1172
1940
|
continue;
|
|
1173
1941
|
}
|
|
@@ -1190,6 +1958,11 @@ function formatGeneratedDependencyType(
|
|
|
1190
1958
|
return [
|
|
1191
1959
|
'{',
|
|
1192
1960
|
` deps: ${formatGeneratedDependencyObject(generatedDependencyGroups.deps)};`,
|
|
1961
|
+
...(angularKind === 'component'
|
|
1962
|
+
? [
|
|
1963
|
+
` propertiesDeps: ${formatGeneratedDependencyObject(generatedDependencyGroups.propertiesDeps)};`,
|
|
1964
|
+
]
|
|
1965
|
+
: []),
|
|
1193
1966
|
` provided: ${formatGeneratedDependencyObject(generatedDependencyGroups.provided)};`,
|
|
1194
1967
|
...(angularKind === 'component'
|
|
1195
1968
|
? [` publicProperties: GetPublicComponentProperties<${className}>;`]
|
|
@@ -1376,8 +2149,8 @@ export function ensureHelperImports(
|
|
|
1376
2149
|
): void {
|
|
1377
2150
|
const missingImports = [
|
|
1378
2151
|
'DerivedService',
|
|
2152
|
+
'ExtractDeps',
|
|
1379
2153
|
'GetDeps',
|
|
1380
|
-
'GetInjectedServiceDependencies',
|
|
1381
2154
|
'GetPublicComponentProperties',
|
|
1382
2155
|
'GetServiceOutput',
|
|
1383
2156
|
].filter(
|
|
@@ -1538,6 +2311,11 @@ export async function runAngularBrandCodemod(
|
|
|
1538
2311
|
} = {},
|
|
1539
2312
|
): Promise<RunSummary> {
|
|
1540
2313
|
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
2314
|
+
const config =
|
|
2315
|
+
options.config ??
|
|
2316
|
+
(options.configFilePath
|
|
2317
|
+
? loadAngularBrandConfigFromFile(options.configFilePath)
|
|
2318
|
+
: loadDiscoveredAngularBrandConfig(rootDir));
|
|
1541
2319
|
const tsConfigFilePath = options.tsConfigFilePath
|
|
1542
2320
|
? resolve(options.tsConfigFilePath)
|
|
1543
2321
|
: getDefaultTsConfigPath(rootDir);
|
|
@@ -1565,7 +2343,10 @@ export async function runAngularBrandCodemod(
|
|
|
1565
2343
|
continue;
|
|
1566
2344
|
}
|
|
1567
2345
|
|
|
1568
|
-
const result = transformSourceFile(sourceFile,
|
|
2346
|
+
const result = transformSourceFile(sourceFile, {
|
|
2347
|
+
...options,
|
|
2348
|
+
config,
|
|
2349
|
+
});
|
|
1569
2350
|
const report: RunFileReport = {
|
|
1570
2351
|
...result,
|
|
1571
2352
|
filePath: sourceFile.getFilePath(),
|
|
@@ -1612,6 +2393,8 @@ function normalizeOptions(
|
|
|
1612
2393
|
options.includeProviders ?? DEFAULT_OPTIONS.includeProviders,
|
|
1613
2394
|
includeViewProviders:
|
|
1614
2395
|
options.includeViewProviders ?? DEFAULT_OPTIONS.includeViewProviders,
|
|
2396
|
+
config: options.config,
|
|
2397
|
+
configFilePath: options.configFilePath,
|
|
1615
2398
|
};
|
|
1616
2399
|
}
|
|
1617
2400
|
|
|
@@ -1707,6 +2490,7 @@ function extractDecoratorMetadataDepGroups(
|
|
|
1707
2490
|
angularKind: AngularKind,
|
|
1708
2491
|
options: NormalizedOptions,
|
|
1709
2492
|
): MetadataDependencyGroups {
|
|
2493
|
+
const sourceFile = classDeclaration.getSourceFile();
|
|
1710
2494
|
const metadata = getDecoratorMetadataObject(classDeclaration);
|
|
1711
2495
|
const groups = emptyMetadataDependencyGroups();
|
|
1712
2496
|
|
|
@@ -1745,6 +2529,19 @@ function extractDecoratorMetadataDepGroups(
|
|
|
1745
2529
|
);
|
|
1746
2530
|
}
|
|
1747
2531
|
|
|
2532
|
+
groups.occurrences = [
|
|
2533
|
+
...groups.imports.map((dependencyText) =>
|
|
2534
|
+
createMetadataDependencyOccurrence(sourceFile, dependencyText, 'imports'),
|
|
2535
|
+
),
|
|
2536
|
+
...groups.hostDirectives.map((dependencyText) =>
|
|
2537
|
+
createMetadataDependencyOccurrence(
|
|
2538
|
+
sourceFile,
|
|
2539
|
+
dependencyText,
|
|
2540
|
+
'hostDirectives',
|
|
2541
|
+
),
|
|
2542
|
+
),
|
|
2543
|
+
];
|
|
2544
|
+
|
|
1748
2545
|
return groups;
|
|
1749
2546
|
}
|
|
1750
2547
|
|
|
@@ -1754,10 +2551,29 @@ function emptyMetadataDependencyGroups(): MetadataDependencyGroups {
|
|
|
1754
2551
|
hostDirectives: [],
|
|
1755
2552
|
providers: [],
|
|
1756
2553
|
viewProviders: [],
|
|
2554
|
+
occurrences: [],
|
|
1757
2555
|
warnings: [],
|
|
1758
2556
|
};
|
|
1759
2557
|
}
|
|
1760
2558
|
|
|
2559
|
+
function createMetadataDependencyOccurrence(
|
|
2560
|
+
sourceFile: SourceFile,
|
|
2561
|
+
dependencyText: string,
|
|
2562
|
+
metadataContext: AngularBrandMetadataContext,
|
|
2563
|
+
): MetadataDependencyOccurrence {
|
|
2564
|
+
const resolution = resolveDependencyReference(sourceFile, dependencyText);
|
|
2565
|
+
|
|
2566
|
+
return {
|
|
2567
|
+
dependencyText,
|
|
2568
|
+
symbolName:
|
|
2569
|
+
resolution.importedName ??
|
|
2570
|
+
dependencyText.split('.').pop() ??
|
|
2571
|
+
dependencyText,
|
|
2572
|
+
moduleSpecifier: resolution.moduleSpecifier,
|
|
2573
|
+
metadataContext,
|
|
2574
|
+
};
|
|
2575
|
+
}
|
|
2576
|
+
|
|
1761
2577
|
function extractProvidedDependencies(
|
|
1762
2578
|
sourceFile: SourceFile,
|
|
1763
2579
|
classDeclaration: ClassDeclaration,
|
|
@@ -2182,11 +2998,12 @@ function resolveImportedDependencyReference(
|
|
|
2182
2998
|
return {
|
|
2183
2999
|
kind: 'class',
|
|
2184
3000
|
classDeclaration: declaration,
|
|
3001
|
+
importedName: rootIdentifier,
|
|
2185
3002
|
moduleSpecifier,
|
|
2186
3003
|
};
|
|
2187
3004
|
}
|
|
2188
3005
|
|
|
2189
|
-
return { kind: 'unknown', moduleSpecifier };
|
|
3006
|
+
return { kind: 'unknown', importedName: rootIdentifier, moduleSpecifier };
|
|
2190
3007
|
}
|
|
2191
3008
|
|
|
2192
3009
|
if (importDeclaration.getNamespaceImport()?.getText() === rootIdentifier) {
|
|
@@ -2198,11 +3015,16 @@ function resolveImportedDependencyReference(
|
|
|
2198
3015
|
return {
|
|
2199
3016
|
kind: 'class',
|
|
2200
3017
|
classDeclaration: declaration,
|
|
3018
|
+
importedName: dependencyText.split('.').pop(),
|
|
2201
3019
|
moduleSpecifier,
|
|
2202
3020
|
};
|
|
2203
3021
|
}
|
|
2204
3022
|
|
|
2205
|
-
return {
|
|
3023
|
+
return {
|
|
3024
|
+
kind: 'namespace',
|
|
3025
|
+
importedName: dependencyText.split('.').pop(),
|
|
3026
|
+
moduleSpecifier,
|
|
3027
|
+
};
|
|
2206
3028
|
}
|
|
2207
3029
|
|
|
2208
3030
|
const namedImport = importDeclaration
|
|
@@ -2232,6 +3054,7 @@ function resolveImportedDependencyReference(
|
|
|
2232
3054
|
return {
|
|
2233
3055
|
kind: 'class',
|
|
2234
3056
|
classDeclaration,
|
|
3057
|
+
importedName: namedImport.getNameNode().getText(),
|
|
2235
3058
|
moduleSpecifier,
|
|
2236
3059
|
};
|
|
2237
3060
|
}
|
|
@@ -2239,7 +3062,11 @@ function resolveImportedDependencyReference(
|
|
|
2239
3062
|
if (
|
|
2240
3063
|
declarations.some((declaration) => Node.isEnumDeclaration(declaration))
|
|
2241
3064
|
) {
|
|
2242
|
-
return {
|
|
3065
|
+
return {
|
|
3066
|
+
kind: 'enum',
|
|
3067
|
+
importedName: namedImport.getNameNode().getText(),
|
|
3068
|
+
moduleSpecifier,
|
|
3069
|
+
};
|
|
2243
3070
|
}
|
|
2244
3071
|
|
|
2245
3072
|
if (
|
|
@@ -2247,7 +3074,11 @@ function resolveImportedDependencyReference(
|
|
|
2247
3074
|
Node.isFunctionDeclaration(declaration),
|
|
2248
3075
|
)
|
|
2249
3076
|
) {
|
|
2250
|
-
return {
|
|
3077
|
+
return {
|
|
3078
|
+
kind: 'function',
|
|
3079
|
+
importedName: namedImport.getNameNode().getText(),
|
|
3080
|
+
moduleSpecifier,
|
|
3081
|
+
};
|
|
2251
3082
|
}
|
|
2252
3083
|
|
|
2253
3084
|
if (
|
|
@@ -2255,10 +3086,18 @@ function resolveImportedDependencyReference(
|
|
|
2255
3086
|
Node.isVariableDeclaration(declaration),
|
|
2256
3087
|
)
|
|
2257
3088
|
) {
|
|
2258
|
-
return {
|
|
3089
|
+
return {
|
|
3090
|
+
kind: 'variable',
|
|
3091
|
+
importedName: namedImport.getNameNode().getText(),
|
|
3092
|
+
moduleSpecifier,
|
|
3093
|
+
};
|
|
2259
3094
|
}
|
|
2260
3095
|
|
|
2261
|
-
return {
|
|
3096
|
+
return {
|
|
3097
|
+
kind: 'unknown',
|
|
3098
|
+
importedName: namedImport.getNameNode().getText(),
|
|
3099
|
+
moduleSpecifier,
|
|
3100
|
+
};
|
|
2262
3101
|
}
|
|
2263
3102
|
|
|
2264
3103
|
return undefined;
|
|
@@ -2957,6 +3796,9 @@ function parseCliArgs(argv: string[]): AngularBrandCodemodOptions & {
|
|
|
2957
3796
|
case '--tsconfig':
|
|
2958
3797
|
options.tsConfigFilePath = argv[++index];
|
|
2959
3798
|
break;
|
|
3799
|
+
case '--config':
|
|
3800
|
+
options.configFilePath = argv[++index];
|
|
3801
|
+
break;
|
|
2960
3802
|
case '--helper-import':
|
|
2961
3803
|
options.helperImportPath = argv[++index];
|
|
2962
3804
|
break;
|
|
@@ -2989,6 +3831,7 @@ function printHelpAndExit(): never {
|
|
|
2989
3831
|
Options:
|
|
2990
3832
|
--root <dir> Project root. Defaults to cwd.
|
|
2991
3833
|
--tsconfig <path> tsconfig path. Defaults to <root>/tsconfig.json.
|
|
3834
|
+
--config <path> Explicit angular brand config file path.
|
|
2992
3835
|
--helper-import <path> Import path for GetDeps.
|
|
2993
3836
|
--transform-only-standalone-declarables Only transform standalone components/directives.
|
|
2994
3837
|
--no-providers Do not include metadata providers in generated types.
|