@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.
@@ -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 { basename, extname, isAbsolute, join, resolve } from 'node:path';
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 NormalizedOptions = Required<AngularBrandCodemodOptions>;
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 injectCallDeps = extractInjectCallDeps(classDeclaration);
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
- ...injectCallDeps.warnings,
856
+ ...propertyDeps.warnings,
330
857
  );
331
858
  result.dependencies = mergeDeps(
332
859
  constructorDeps.dependencies,
333
- injectCallDeps.dependencies,
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
- injectCallDeps.dependencies,
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
- ...constructorDeps.dependencies.map((dependencyText) => ({
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
- injectedDependencies: InjectedDependencyDescriptor[],
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
- injectedDependencies.map((dependency) => dependency.entry),
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
- injectedDependencies
635
- .filter((dependency) => {
636
- const dependencyKey = dependency.entry.key;
637
- return (
638
- !providedKeys.has(dependencyKey) &&
639
- shouldGenerateLocalMissingProvider(
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
- if (!entries.has(entry.key)) {
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 = `GetInjectedServiceDependencies<typeof ${dependencyText}>`;
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 generatedDependencyGroups.deps) {
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
- [...importNames].map((name) => ({ name, isTypeOnly: true })),
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, options);
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 { kind: 'namespace', moduleSpecifier };
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 { kind: 'enum', moduleSpecifier };
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 { kind: 'function', moduleSpecifier };
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 { kind: 'variable', moduleSpecifier };
3089
+ return {
3090
+ kind: 'variable',
3091
+ importedName: namedImport.getNameNode().getText(),
3092
+ moduleSpecifier,
3093
+ };
2259
3094
  }
2260
3095
 
2261
- return { kind: 'unknown', moduleSpecifier };
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.