@craft-ng/dev-tools 0.1.4 → 0.1.5

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.
@@ -17,6 +17,7 @@ import {
17
17
  } from 'ts-morph';
18
18
  import { existsSync, readdirSync, statSync } from 'node:fs';
19
19
  import { basename, extname, isAbsolute, join, resolve } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
20
21
 
21
22
  export type AngularKind = 'component' | 'directive' | 'pipe' | 'injectable';
22
23
 
@@ -28,6 +29,8 @@ export type TransformResult = {
28
29
  className?: string;
29
30
  dependencies: string[];
30
31
  dependencyGroups: DependencyGroups;
32
+ generatedTypeName?: string;
33
+ generatedDependencyGroups: GeneratedDependencyGroups;
31
34
  };
32
35
 
33
36
  export type AngularBrandCodemodOptions = {
@@ -56,6 +59,21 @@ export type DependencyGroups = {
56
59
  providers: string[];
57
60
  };
58
61
 
62
+ export type GeneratedDependencyEntry = {
63
+ key: string;
64
+ typeText: string;
65
+ typeImport?: {
66
+ moduleSpecifier: string;
67
+ name: string;
68
+ };
69
+ };
70
+
71
+ export type GeneratedDependencyGroups = {
72
+ deps: GeneratedDependencyEntry[];
73
+ provided: GeneratedDependencyEntry[];
74
+ missingProvider: GeneratedDependencyEntry[];
75
+ };
76
+
59
77
  export type DependencyAnalysisResult = Omit<TransformResult, 'changed'> & {
60
78
  classDeclaration?: ClassDeclaration;
61
79
  };
@@ -77,6 +95,17 @@ type MetadataDependencyGroups = {
77
95
  warnings: string[];
78
96
  };
79
97
 
98
+ type ProvidedDependencyExtractionResult = {
99
+ entries: GeneratedDependencyEntry[];
100
+ warnings: string[];
101
+ };
102
+
103
+ type DependencyReferenceResolution = {
104
+ kind: 'class' | 'enum' | 'function' | 'variable' | 'namespace' | 'unknown';
105
+ classDeclaration?: ClassDeclaration;
106
+ moduleSpecifier?: string;
107
+ };
108
+
80
109
  type NormalizedOptions = Required<AngularBrandCodemodOptions>;
81
110
 
82
111
  type InjectDecoratorTokenResult =
@@ -170,15 +199,6 @@ export function transformSourceFile(
170
199
  return result;
171
200
  }
172
201
 
173
- const exportSafety = getExportRewriteSafety(
174
- sourceFile,
175
- classDeclaration,
176
- className,
177
- );
178
- if (!exportSafety.safe) {
179
- return skip(result, exportSafety.warnings);
180
- }
181
-
182
202
  const importSafety = getHelperImportSafety(
183
203
  sourceFile,
184
204
  normalizedOptions.helperImportPath,
@@ -187,11 +207,22 @@ export function transformSourceFile(
187
207
  return skip(result, importSafety.warnings);
188
208
  }
189
209
 
190
- removeConflictingExports(sourceFile, classDeclaration, className);
210
+ const beforeText = sourceFile.getFullText();
211
+
212
+ migrateLegacyBrandExport(sourceFile, classDeclaration, className);
191
213
  ensureHelperImports(sourceFile, normalizedOptions.helperImportPath);
192
- writeDefaultBrandExport(sourceFile, className, result.dependencyGroups);
214
+ ensureGeneratedDependencyTypeImports(
215
+ sourceFile,
216
+ result.generatedDependencyGroups,
217
+ );
218
+ writeGeneratedDepsTypeAlias(
219
+ sourceFile,
220
+ className,
221
+ result.generatedDependencyGroups,
222
+ );
223
+ sourceFile.organizeImports();
193
224
 
194
- result.changed = true;
225
+ result.changed = sourceFile.getFullText() !== beforeText;
195
226
  return result;
196
227
  }
197
228
 
@@ -205,6 +236,7 @@ export function analyzeSourceFileDependencies(
205
236
  warnings: [],
206
237
  dependencies: [],
207
238
  dependencyGroups: emptyDependencyGroups(),
239
+ generatedDependencyGroups: emptyGeneratedDependencyGroups(),
208
240
  };
209
241
 
210
242
  const angularClass = findAngularDecoratedClass(sourceFile);
@@ -250,11 +282,21 @@ export function analyzeSourceFileDependencies(
250
282
  normalizedOptions,
251
283
  )
252
284
  : emptyMetadataDependencyGroups();
285
+ const providedDeps =
286
+ angularKind === 'component' || angularKind === 'directive'
287
+ ? extractProvidedDependencies(
288
+ sourceFile,
289
+ classDeclaration,
290
+ angularKind,
291
+ normalizedOptions,
292
+ )
293
+ : { entries: [], warnings: [] };
253
294
  const constructorDeps = extractConstructorDeps(classDeclaration);
254
295
  const injectCallDeps = extractInjectCallDeps(classDeclaration);
255
296
 
256
297
  result.warnings.push(
257
298
  ...metadataDeps.warnings,
299
+ ...providedDeps.warnings,
258
300
  ...constructorDeps.warnings,
259
301
  ...injectCallDeps.warnings,
260
302
  );
@@ -274,6 +316,13 @@ export function analyzeSourceFileDependencies(
274
316
  importDeps: mergeDeps(metadataDeps.imports, metadataDeps.hostDirectives),
275
317
  providers: mergeDeps(metadataDeps.providers, metadataDeps.viewProviders),
276
318
  };
319
+ result.generatedTypeName = getGeneratedDepsTypeName(className);
320
+ result.generatedDependencyGroups = createGeneratedDependencyGroups(
321
+ sourceFile,
322
+ result.dependencyGroups.importDeps,
323
+ result.dependencyGroups.injected,
324
+ providedDeps.entries,
325
+ );
277
326
 
278
327
  return result;
279
328
  }
@@ -493,8 +542,301 @@ function emptyDependencyGroups(): DependencyGroups {
493
542
  };
494
543
  }
495
544
 
496
- function createDepsExpression(dependencyGroups: DependencyGroups): string {
497
- return `deps({ injected: [${dependencyGroups.injected.join(', ')}], importDeps: [${dependencyGroups.importDeps.join(', ')}], providers: [${dependencyGroups.providers.join(', ')}] })`;
545
+ function emptyGeneratedDependencyGroups(): GeneratedDependencyGroups {
546
+ return {
547
+ deps: [],
548
+ provided: [],
549
+ missingProvider: [],
550
+ };
551
+ }
552
+
553
+ function getGeneratedDepsTypeName(className: string): string {
554
+ return `GenDeps_${className}`;
555
+ }
556
+
557
+ function createGeneratedDependencyGroups(
558
+ sourceFile: SourceFile,
559
+ importDependencies: string[],
560
+ injectedDependencies: string[],
561
+ providedEntries: GeneratedDependencyEntry[],
562
+ ): GeneratedDependencyGroups {
563
+ const deps = mergeGeneratedDependencyEntries(
564
+ importDependencies.map((dependency) =>
565
+ createGeneratedDependencyEntry(sourceFile, dependency, 'import'),
566
+ ),
567
+ injectedDependencies.map((dependency) =>
568
+ createGeneratedDependencyEntry(sourceFile, dependency, 'inject'),
569
+ ),
570
+ );
571
+ const provided = mergeGeneratedDependencyEntries(providedEntries);
572
+ const providedKeys = new Set(provided.map((entry) => entry.key));
573
+ const missingProvider = mergeGeneratedDependencyEntries(
574
+ injectedDependencies
575
+ .filter((dependency) => {
576
+ const dependencyKey = createGeneratedDependencyKey(dependency);
577
+ return (
578
+ !providedKeys.has(dependencyKey) &&
579
+ !isProvidedInInjectableTree(sourceFile, dependency)
580
+ );
581
+ })
582
+ .map((dependency) =>
583
+ createGeneratedDependencyEntry(sourceFile, dependency, 'inject'),
584
+ ),
585
+ );
586
+
587
+ return {
588
+ deps,
589
+ provided,
590
+ missingProvider,
591
+ };
592
+ }
593
+
594
+ function mergeGeneratedDependencyEntries(
595
+ ...groups: readonly GeneratedDependencyEntry[][]
596
+ ): GeneratedDependencyEntry[] {
597
+ const entries = new Map<string, GeneratedDependencyEntry>();
598
+
599
+ for (const group of groups) {
600
+ for (const entry of group) {
601
+ if (!entries.has(entry.key)) {
602
+ entries.set(entry.key, entry);
603
+ }
604
+ }
605
+ }
606
+
607
+ return [...entries.values()];
608
+ }
609
+
610
+ function createGeneratedDependencyEntry(
611
+ sourceFile: SourceFile,
612
+ dependencyText: string,
613
+ context: 'import' | 'inject',
614
+ ): GeneratedDependencyEntry {
615
+ if (context === 'import') {
616
+ const declarableResolution = resolveAngularDeclarableDependency(
617
+ sourceFile,
618
+ dependencyText,
619
+ );
620
+ if (declarableResolution) {
621
+ const generatedTypeName = getGeneratedDepsTypeName(
622
+ declarableResolution.className,
623
+ );
624
+ return {
625
+ key: generatedTypeName,
626
+ typeText: generatedTypeName,
627
+ typeImport: declarableResolution.moduleSpecifier
628
+ ? {
629
+ moduleSpecifier: declarableResolution.moduleSpecifier,
630
+ name: generatedTypeName,
631
+ }
632
+ : undefined,
633
+ };
634
+ }
635
+ }
636
+
637
+ return {
638
+ key: createGeneratedDependencyKey(dependencyText),
639
+ typeText: createGeneratedDependencyTypeText(sourceFile, dependencyText),
640
+ };
641
+ }
642
+
643
+ function createGeneratedDependencyKey(dependencyText: string): string {
644
+ const lastSegment = dependencyText.split('.').pop() ?? dependencyText;
645
+ const helperMatch = /^(inject|provide)([A-Z].*)$/.exec(lastSegment);
646
+ if (helperMatch) {
647
+ return helperMatch[2];
648
+ }
649
+
650
+ const toYieldMatch = /^(.*)ToYield$/.exec(lastSegment);
651
+ if (toYieldMatch?.[1]) {
652
+ return toYieldMatch[1];
653
+ }
654
+
655
+ return lastSegment;
656
+ }
657
+
658
+ function createGeneratedDependencyTypeText(
659
+ sourceFile: SourceFile,
660
+ dependencyText: string,
661
+ ): string {
662
+ if (isHelperLikeDependency(dependencyText)) {
663
+ return `ReturnType<typeof ${dependencyText}>`;
664
+ }
665
+
666
+ const resolution = resolveDependencyReference(sourceFile, dependencyText);
667
+ if (resolution.kind === 'class' || resolution.kind === 'enum') {
668
+ return dependencyText;
669
+ }
670
+
671
+ if (resolution.kind === 'unknown') {
672
+ return dependencyText;
673
+ }
674
+
675
+ return `typeof ${dependencyText}`;
676
+ }
677
+
678
+ function isHelperLikeDependency(dependencyText: string): boolean {
679
+ const lastSegment = dependencyText.split('.').pop() ?? dependencyText;
680
+ return (
681
+ /^(inject|provide)[A-Z].*/.test(lastSegment) || /ToYield$/.test(lastSegment)
682
+ );
683
+ }
684
+
685
+ function resolveAngularDeclarableDependency(
686
+ sourceFile: SourceFile,
687
+ dependencyText: string,
688
+ ): { className: string; moduleSpecifier?: string } | undefined {
689
+ const resolution = resolveDependencyReference(sourceFile, dependencyText);
690
+ if (!resolution.classDeclaration) {
691
+ return undefined;
692
+ }
693
+
694
+ const angularKind = getAngularKind(resolution.classDeclaration);
695
+ if (
696
+ angularKind !== 'component' &&
697
+ angularKind !== 'directive' &&
698
+ angularKind !== 'pipe'
699
+ ) {
700
+ return undefined;
701
+ }
702
+
703
+ const className = resolution.classDeclaration.getName();
704
+ if (!className) {
705
+ return undefined;
706
+ }
707
+
708
+ const declarationSourceFile = resolution.classDeclaration.getSourceFile();
709
+ if (
710
+ declarationSourceFile === sourceFile ||
711
+ declarationSourceFile.isDeclarationFile()
712
+ ) {
713
+ return undefined;
714
+ }
715
+
716
+ return {
717
+ className,
718
+ moduleSpecifier: resolution.moduleSpecifier,
719
+ };
720
+ }
721
+
722
+ function ensureGeneratedDependencyTypeImports(
723
+ sourceFile: SourceFile,
724
+ generatedDependencyGroups: GeneratedDependencyGroups,
725
+ ): void {
726
+ const importsToAdd = new Map<string, Set<string>>();
727
+
728
+ for (const entry of generatedDependencyGroups.deps) {
729
+ if (!entry.typeImport) {
730
+ continue;
731
+ }
732
+
733
+ const importNames =
734
+ importsToAdd.get(entry.typeImport.moduleSpecifier) ?? new Set<string>();
735
+ importNames.add(entry.typeImport.name);
736
+ importsToAdd.set(entry.typeImport.moduleSpecifier, importNames);
737
+ }
738
+
739
+ for (const [moduleSpecifier, importNames] of importsToAdd) {
740
+ const existingImport = sourceFile
741
+ .getImportDeclarations()
742
+ .find(
743
+ (importDeclaration) =>
744
+ importDeclaration.getModuleSpecifierValue() === moduleSpecifier,
745
+ );
746
+
747
+ if (existingImport) {
748
+ existingImport.addNamedImports(
749
+ [...importNames].map((name) => ({ name, isTypeOnly: true })),
750
+ );
751
+ continue;
752
+ }
753
+
754
+ sourceFile.addImportDeclaration({
755
+ moduleSpecifier,
756
+ namedImports: [...importNames].map((name) => ({
757
+ name,
758
+ isTypeOnly: true,
759
+ })),
760
+ });
761
+ }
762
+ }
763
+
764
+ function formatGeneratedDependencyType(
765
+ generatedDependencyGroups: GeneratedDependencyGroups,
766
+ ): string {
767
+ return [
768
+ '{',
769
+ ` deps: ${formatGeneratedDependencyObject(generatedDependencyGroups.deps)};`,
770
+ ` provided: ${formatGeneratedDependencyObject(generatedDependencyGroups.provided)};`,
771
+ ` missingProvider: ${formatGeneratedDependencyObject(generatedDependencyGroups.missingProvider)};`,
772
+ '}',
773
+ ].join('\n');
774
+ }
775
+
776
+ function formatGeneratedDependencyObject(
777
+ entries: GeneratedDependencyEntry[],
778
+ ): string {
779
+ if (entries.length === 0) {
780
+ return '{}';
781
+ }
782
+
783
+ return [
784
+ '{',
785
+ ...entries.map(
786
+ (entry) => ` ${formatObjectKey(entry.key)}: ${entry.typeText};`,
787
+ ),
788
+ ' }',
789
+ ].join('\n');
790
+ }
791
+
792
+ function formatObjectKey(key: string): string {
793
+ return /^[$A-Z_][0-9A-Z_$]*$/i.test(key) ? key : `'${key}'`;
794
+ }
795
+
796
+ export function writeGeneratedDepsTypeAlias(
797
+ sourceFile: SourceFile,
798
+ className: string,
799
+ generatedDependencyGroups: GeneratedDependencyGroups,
800
+ ): void {
801
+ const generatedTypeName = getGeneratedDepsTypeName(className);
802
+ const generatedType = `GetDeps<${formatGeneratedDependencyType(generatedDependencyGroups)}>`;
803
+ const existingTypeAlias = sourceFile.getTypeAlias(generatedTypeName);
804
+
805
+ if (existingTypeAlias) {
806
+ existingTypeAlias.setIsExported(true);
807
+ existingTypeAlias.setType(generatedType);
808
+ return;
809
+ }
810
+
811
+ sourceFile.addTypeAlias({
812
+ isExported: true,
813
+ name: generatedTypeName,
814
+ type: generatedType,
815
+ });
816
+ }
817
+
818
+ export function migrateLegacyBrandExport(
819
+ sourceFile: SourceFile,
820
+ classDeclaration: ClassDeclaration,
821
+ className: string,
822
+ ): void {
823
+ const existing = readExistingDependencyGroups(sourceFile, className);
824
+ if (!existing.found || !existing.exportAssignmentNode) {
825
+ return;
826
+ }
827
+
828
+ const exportAssignment = existing.exportAssignmentNode.asKind(
829
+ SyntaxKind.ExportAssignment,
830
+ );
831
+ exportAssignment?.remove();
832
+
833
+ if (
834
+ !classDeclaration.isDefaultExport() &&
835
+ !classDeclaration.isExported() &&
836
+ exportAssignment
837
+ ) {
838
+ classDeclaration.setIsDefaultExport(true);
839
+ }
498
840
  }
499
841
 
500
842
  function invalidExistingDependencyGroups(
@@ -585,7 +927,7 @@ export function ensureHelperImports(
585
927
  sourceFile: SourceFile,
586
928
  helperImportPath: string,
587
929
  ): void {
588
- const missingImports = ['brandAngularSymbol', 'deps'].filter(
930
+ const missingImports = ['GetDeps'].filter(
589
931
  (importName) =>
590
932
  !hasLocalNamedImport(sourceFile, helperImportPath, importName),
591
933
  );
@@ -602,51 +944,18 @@ export function ensureHelperImports(
602
944
  );
603
945
 
604
946
  if (existingImport) {
605
- existingImport.addNamedImports(missingImports);
947
+ existingImport.addNamedImports(
948
+ missingImports.map((name) => ({ name, isTypeOnly: true })),
949
+ );
606
950
  return;
607
951
  }
608
952
 
609
953
  sourceFile.addImportDeclaration({
610
954
  moduleSpecifier: helperImportPath,
611
- namedImports: missingImports,
612
- });
613
- }
614
-
615
- export function removeConflictingExports(
616
- sourceFile: SourceFile,
617
- classDeclaration: ClassDeclaration,
618
- className: string,
619
- ): void {
620
- if (classDeclaration.isDefaultExport()) {
621
- classDeclaration.setIsDefaultExport(false);
622
- }
623
-
624
- if (classDeclaration.isExported()) {
625
- classDeclaration.setIsExported(false);
626
- }
627
-
628
- for (const exportAssignment of sourceFile.getExportAssignments()) {
629
- if (
630
- isDefaultIdentifierExport(exportAssignment, className) ||
631
- isDefaultBrandedExport(exportAssignment, className)
632
- ) {
633
- exportAssignment.remove();
634
- }
635
- }
636
-
637
- for (const exportDeclaration of sourceFile.getExportDeclarations()) {
638
- removeClassFromExportDeclaration(exportDeclaration, className);
639
- }
640
- }
641
-
642
- export function writeDefaultBrandExport(
643
- sourceFile: SourceFile,
644
- className: string,
645
- dependencyGroups: DependencyGroups,
646
- ): void {
647
- sourceFile.addExportAssignment({
648
- expression: `brandAngularSymbol(${className}, ${createDepsExpression(dependencyGroups)})`,
649
- isExportEquals: false,
955
+ namedImports: missingImports.map((name) => ({
956
+ name,
957
+ isTypeOnly: true,
958
+ })),
650
959
  });
651
960
  }
652
961
 
@@ -996,6 +1305,204 @@ function emptyMetadataDependencyGroups(): MetadataDependencyGroups {
996
1305
  };
997
1306
  }
998
1307
 
1308
+ function extractProvidedDependencies(
1309
+ sourceFile: SourceFile,
1310
+ classDeclaration: ClassDeclaration,
1311
+ angularKind: AngularKind,
1312
+ options: NormalizedOptions,
1313
+ ): ProvidedDependencyExtractionResult {
1314
+ const metadata = getDecoratorMetadataObject(classDeclaration);
1315
+ const warnings: string[] = [];
1316
+ const entries: GeneratedDependencyEntry[] = [];
1317
+
1318
+ if (!metadata) {
1319
+ return { entries, warnings };
1320
+ }
1321
+
1322
+ if (options.includeProviders) {
1323
+ entries.push(
1324
+ ...extractProvidedDependencyArrayProperty(
1325
+ sourceFile,
1326
+ metadata,
1327
+ 'providers',
1328
+ warnings,
1329
+ ),
1330
+ );
1331
+ }
1332
+
1333
+ if (options.includeViewProviders && angularKind === 'component') {
1334
+ entries.push(
1335
+ ...extractProvidedDependencyArrayProperty(
1336
+ sourceFile,
1337
+ metadata,
1338
+ 'viewProviders',
1339
+ warnings,
1340
+ ),
1341
+ );
1342
+ }
1343
+
1344
+ return {
1345
+ entries: mergeGeneratedDependencyEntries(entries),
1346
+ warnings,
1347
+ };
1348
+ }
1349
+
1350
+ function extractProvidedDependencyArrayProperty(
1351
+ sourceFile: SourceFile,
1352
+ metadata: ObjectLiteralExpression,
1353
+ propertyName: 'providers' | 'viewProviders',
1354
+ warnings: string[],
1355
+ ): GeneratedDependencyEntry[] {
1356
+ const property = getObjectPropertyAssignment(metadata, propertyName);
1357
+ if (!property) {
1358
+ return [];
1359
+ }
1360
+
1361
+ const initializer = property.getInitializer();
1362
+ if (!Node.isArrayLiteralExpression(initializer)) {
1363
+ warnings.push(
1364
+ `Skipped metadata property "${propertyName}" because it is not a static array.`,
1365
+ );
1366
+ return [];
1367
+ }
1368
+
1369
+ return extractProvidedDependencyEntriesFromArray(
1370
+ sourceFile,
1371
+ initializer,
1372
+ propertyName,
1373
+ warnings,
1374
+ );
1375
+ }
1376
+
1377
+ function extractProvidedDependencyEntriesFromArray(
1378
+ sourceFile: SourceFile,
1379
+ arrayLiteral: ArrayLiteralExpression,
1380
+ context: 'providers' | 'viewProviders',
1381
+ warnings: string[],
1382
+ ): GeneratedDependencyEntry[] {
1383
+ const entries: GeneratedDependencyEntry[] = [];
1384
+
1385
+ for (const element of arrayLiteral.getElements()) {
1386
+ if (Node.isArrayLiteralExpression(element)) {
1387
+ entries.push(
1388
+ ...extractProvidedDependencyEntriesFromArray(
1389
+ sourceFile,
1390
+ element,
1391
+ context,
1392
+ warnings,
1393
+ ),
1394
+ );
1395
+ continue;
1396
+ }
1397
+
1398
+ if (Node.isSpreadElement(element)) {
1399
+ warnings.push(
1400
+ `Skipped spread element in "${context}" metadata providers.`,
1401
+ );
1402
+ continue;
1403
+ }
1404
+
1405
+ if (Node.isCallExpression(element)) {
1406
+ const providerFactory = getStaticExpressionText(element.getExpression());
1407
+ if (!providerFactory) {
1408
+ warnings.push(
1409
+ `Skipped complex provider factory "${element.getText()}" in "${context}".`,
1410
+ );
1411
+ continue;
1412
+ }
1413
+
1414
+ entries.push({
1415
+ key: createGeneratedDependencyKey(providerFactory),
1416
+ typeText: `ReturnType<typeof ${providerFactory}>`,
1417
+ });
1418
+ continue;
1419
+ }
1420
+
1421
+ const staticExpression = getStaticExpressionText(element);
1422
+ if (staticExpression) {
1423
+ entries.push({
1424
+ key: createGeneratedDependencyKey(staticExpression),
1425
+ typeText: createGeneratedDependencyTypeText(
1426
+ sourceFile,
1427
+ staticExpression,
1428
+ ),
1429
+ });
1430
+ continue;
1431
+ }
1432
+
1433
+ if (Node.isObjectLiteralExpression(element)) {
1434
+ const providerEntry = extractProvidedDependencyEntryFromObjectLiteral(
1435
+ sourceFile,
1436
+ element,
1437
+ context,
1438
+ warnings,
1439
+ );
1440
+ if (providerEntry) {
1441
+ entries.push(providerEntry);
1442
+ }
1443
+ continue;
1444
+ }
1445
+
1446
+ warnings.push(
1447
+ `Skipped complex expression "${element.getText()}" in "${context}" metadata providers.`,
1448
+ );
1449
+ }
1450
+
1451
+ return entries;
1452
+ }
1453
+
1454
+ function extractProvidedDependencyEntryFromObjectLiteral(
1455
+ sourceFile: SourceFile,
1456
+ objectLiteral: ObjectLiteralExpression,
1457
+ context: 'providers' | 'viewProviders',
1458
+ warnings: string[],
1459
+ ): GeneratedDependencyEntry | undefined {
1460
+ const provideProperty = getObjectPropertyAssignment(objectLiteral, 'provide');
1461
+ const provideText = getStaticExpressionText(
1462
+ provideProperty?.getInitializer(),
1463
+ );
1464
+
1465
+ if (!provideText) {
1466
+ warnings.push(
1467
+ `Skipped provider object in "${context}" because "provide" is not static.`,
1468
+ );
1469
+ return undefined;
1470
+ }
1471
+
1472
+ const useClass = getStaticExpressionText(
1473
+ getObjectPropertyAssignment(objectLiteral, 'useClass')?.getInitializer(),
1474
+ );
1475
+ const useExisting = getStaticExpressionText(
1476
+ getObjectPropertyAssignment(objectLiteral, 'useExisting')?.getInitializer(),
1477
+ );
1478
+ const useValue = getStaticExpressionText(
1479
+ getObjectPropertyAssignment(objectLiteral, 'useValue')?.getInitializer(),
1480
+ );
1481
+ const useFactory = getStaticExpressionText(
1482
+ getObjectPropertyAssignment(objectLiteral, 'useFactory')?.getInitializer(),
1483
+ );
1484
+
1485
+ if (useFactory) {
1486
+ return {
1487
+ key: createGeneratedDependencyKey(provideText),
1488
+ typeText: `ReturnType<typeof ${useFactory}>`,
1489
+ };
1490
+ }
1491
+
1492
+ if (useValue) {
1493
+ return {
1494
+ key: createGeneratedDependencyKey(provideText),
1495
+ typeText: `typeof ${useValue}`,
1496
+ };
1497
+ }
1498
+
1499
+ const valueText = useClass ?? useExisting ?? provideText;
1500
+ return {
1501
+ key: createGeneratedDependencyKey(provideText),
1502
+ typeText: createGeneratedDependencyTypeText(sourceFile, valueText),
1503
+ };
1504
+ }
1505
+
999
1506
  function extractMetadataArrayProperty(
1000
1507
  metadata: ObjectLiteralExpression,
1001
1508
  propertyName: string,
@@ -1166,6 +1673,208 @@ function getStaticPropertyName(nameNode: Node): string | undefined {
1166
1673
  return undefined;
1167
1674
  }
1168
1675
 
1676
+ function resolveDependencyReference(
1677
+ sourceFile: SourceFile,
1678
+ dependencyText: string,
1679
+ ): DependencyReferenceResolution {
1680
+ const [rootIdentifier] = dependencyText.split('.');
1681
+ if (!rootIdentifier) {
1682
+ return { kind: 'unknown' };
1683
+ }
1684
+
1685
+ const importResolution = resolveImportedDependencyReference(
1686
+ sourceFile,
1687
+ dependencyText,
1688
+ rootIdentifier,
1689
+ );
1690
+ if (importResolution) {
1691
+ return importResolution;
1692
+ }
1693
+
1694
+ const localClass = sourceFile.getClass(rootIdentifier);
1695
+ if (localClass) {
1696
+ return { kind: 'class', classDeclaration: localClass };
1697
+ }
1698
+
1699
+ if (sourceFile.getEnum(rootIdentifier)) {
1700
+ return { kind: 'enum' };
1701
+ }
1702
+
1703
+ if (sourceFile.getFunction(rootIdentifier)) {
1704
+ return { kind: 'function' };
1705
+ }
1706
+
1707
+ if (
1708
+ sourceFile
1709
+ .getVariableDeclarations()
1710
+ .some((declaration) => declaration.getName() === rootIdentifier)
1711
+ ) {
1712
+ return { kind: 'variable' };
1713
+ }
1714
+
1715
+ return { kind: 'unknown' };
1716
+ }
1717
+
1718
+ function resolveImportedDependencyReference(
1719
+ sourceFile: SourceFile,
1720
+ dependencyText: string,
1721
+ rootIdentifier: string,
1722
+ ): DependencyReferenceResolution | undefined {
1723
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
1724
+ const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
1725
+
1726
+ if (importDeclaration.getDefaultImport()?.getText() === rootIdentifier) {
1727
+ const declaration = getDefaultImportClassDeclaration(importDeclaration);
1728
+ if (declaration) {
1729
+ return {
1730
+ kind: 'class',
1731
+ classDeclaration: declaration,
1732
+ moduleSpecifier,
1733
+ };
1734
+ }
1735
+
1736
+ return { kind: 'unknown', moduleSpecifier };
1737
+ }
1738
+
1739
+ if (importDeclaration.getNamespaceImport()?.getText() === rootIdentifier) {
1740
+ const declaration = getNamespaceImportClassDeclaration(
1741
+ importDeclaration,
1742
+ dependencyText,
1743
+ );
1744
+ if (declaration) {
1745
+ return {
1746
+ kind: 'class',
1747
+ classDeclaration: declaration,
1748
+ moduleSpecifier,
1749
+ };
1750
+ }
1751
+
1752
+ return { kind: 'namespace', moduleSpecifier };
1753
+ }
1754
+
1755
+ const namedImport = importDeclaration
1756
+ .getNamedImports()
1757
+ .find((specifier) => {
1758
+ const localName =
1759
+ specifier.getAliasNode()?.getText() ??
1760
+ specifier.getNameNode().getText();
1761
+ return localName === rootIdentifier;
1762
+ });
1763
+
1764
+ if (!namedImport) {
1765
+ continue;
1766
+ }
1767
+
1768
+ const localIdentifier =
1769
+ namedImport.getAliasNode() ?? namedImport.getNameNode();
1770
+ const symbol =
1771
+ localIdentifier.getSymbol() ?? localIdentifier.getType().getSymbol();
1772
+ const aliasedSymbol = symbol?.getAliasedSymbol() ?? symbol;
1773
+ const declarations = aliasedSymbol?.getDeclarations() ?? [];
1774
+ const classDeclaration = declarations.find((declaration) =>
1775
+ Node.isClassDeclaration(declaration),
1776
+ );
1777
+
1778
+ if (classDeclaration && Node.isClassDeclaration(classDeclaration)) {
1779
+ return {
1780
+ kind: 'class',
1781
+ classDeclaration,
1782
+ moduleSpecifier,
1783
+ };
1784
+ }
1785
+
1786
+ if (
1787
+ declarations.some((declaration) => Node.isEnumDeclaration(declaration))
1788
+ ) {
1789
+ return { kind: 'enum', moduleSpecifier };
1790
+ }
1791
+
1792
+ if (
1793
+ declarations.some((declaration) =>
1794
+ Node.isFunctionDeclaration(declaration),
1795
+ )
1796
+ ) {
1797
+ return { kind: 'function', moduleSpecifier };
1798
+ }
1799
+
1800
+ if (
1801
+ declarations.some((declaration) =>
1802
+ Node.isVariableDeclaration(declaration),
1803
+ )
1804
+ ) {
1805
+ return { kind: 'variable', moduleSpecifier };
1806
+ }
1807
+
1808
+ return { kind: 'unknown', moduleSpecifier };
1809
+ }
1810
+
1811
+ return undefined;
1812
+ }
1813
+
1814
+ function getDefaultImportClassDeclaration(
1815
+ importDeclaration: import('ts-morph').ImportDeclaration,
1816
+ ): ClassDeclaration | undefined {
1817
+ const moduleSourceFile = importDeclaration.getModuleSpecifierSourceFile();
1818
+ if (!moduleSourceFile) {
1819
+ return undefined;
1820
+ }
1821
+
1822
+ const defaultExportSymbol = moduleSourceFile.getDefaultExportSymbol();
1823
+ const declarations = defaultExportSymbol?.getDeclarations() ?? [];
1824
+ return declarations.find((declaration) =>
1825
+ Node.isClassDeclaration(declaration),
1826
+ ) as ClassDeclaration | undefined;
1827
+ }
1828
+
1829
+ function getNamespaceImportClassDeclaration(
1830
+ importDeclaration: import('ts-morph').ImportDeclaration,
1831
+ dependencyText: string,
1832
+ ): ClassDeclaration | undefined {
1833
+ const moduleSourceFile = importDeclaration.getModuleSpecifierSourceFile();
1834
+ if (!moduleSourceFile) {
1835
+ return undefined;
1836
+ }
1837
+
1838
+ const leafIdentifier = dependencyText.split('.').pop();
1839
+ if (!leafIdentifier) {
1840
+ return undefined;
1841
+ }
1842
+
1843
+ const exportedDeclarations =
1844
+ moduleSourceFile.getExportedDeclarations().get(leafIdentifier) ?? [];
1845
+ return exportedDeclarations.find((declaration) =>
1846
+ Node.isClassDeclaration(declaration),
1847
+ ) as ClassDeclaration | undefined;
1848
+ }
1849
+
1850
+ function isProvidedInInjectableTree(
1851
+ sourceFile: SourceFile,
1852
+ dependencyText: string,
1853
+ ): boolean {
1854
+ const resolution = resolveDependencyReference(sourceFile, dependencyText);
1855
+ const classDeclaration = resolution.classDeclaration;
1856
+
1857
+ if (!classDeclaration || getAngularKind(classDeclaration) !== 'injectable') {
1858
+ return false;
1859
+ }
1860
+
1861
+ const metadata = getDecoratorMetadataObject(classDeclaration);
1862
+ const providedInProperty = metadata
1863
+ ? getObjectPropertyAssignment(metadata, 'providedIn')
1864
+ : undefined;
1865
+ const initializer = providedInProperty?.getInitializer();
1866
+
1867
+ if (!initializer) {
1868
+ return false;
1869
+ }
1870
+
1871
+ return (
1872
+ !Node.isNullLiteral(initializer) &&
1873
+ !Node.isFalseLiteral(initializer) &&
1874
+ !(Node.isIdentifier(initializer) && initializer.getText() === 'undefined')
1875
+ );
1876
+ }
1877
+
1169
1878
  function getDependencyTextFromTypeNode(typeNode: TypeNode): string | undefined {
1170
1879
  if (isPrimitiveTypeNode(typeNode)) {
1171
1880
  return undefined;
@@ -1415,7 +2124,7 @@ function getHelperImportSafety(
1415
2124
  sourceFile: SourceFile,
1416
2125
  helperImportPath: string,
1417
2126
  ): { safe: true; warnings: [] } | { safe: false; warnings: string[] } {
1418
- const warnings = ['brandAngularSymbol', 'deps']
2127
+ const warnings = ['GetDeps']
1419
2128
  .filter((helperName) =>
1420
2129
  hasConflictingTopLevelBinding(sourceFile, helperImportPath, helperName),
1421
2130
  )
@@ -1680,9 +2389,9 @@ function logFileResult(
1680
2389
  `file=${report.filePath}`,
1681
2390
  report.angularKind ? `kind=${report.angularKind}` : undefined,
1682
2391
  report.className ? `class=${report.className}` : undefined,
1683
- `injected=[${report.dependencyGroups.injected.join(', ')}]`,
1684
- `importDeps=[${report.dependencyGroups.importDeps.join(', ')}]`,
1685
- `providers=[${report.dependencyGroups.providers.join(', ')}]`,
2392
+ `deps=[${report.generatedDependencyGroups.deps.map((entry) => entry.key).join(', ')}]`,
2393
+ `provided=[${report.generatedDependencyGroups.provided.map((entry) => entry.key).join(', ')}]`,
2394
+ `missingProvider=[${report.generatedDependencyGroups.missingProvider.map((entry) => entry.key).join(', ')}]`,
1686
2395
  `status=${status}`,
1687
2396
  ].filter(Boolean);
1688
2397
 
@@ -1760,17 +2469,22 @@ function printHelpAndExit(): never {
1760
2469
  Options:
1761
2470
  --root <dir> Project root. Defaults to cwd.
1762
2471
  --tsconfig <path> tsconfig path. Defaults to <root>/tsconfig.json.
1763
- --helper-import <path> Import path for brandAngularSymbol and deps.
2472
+ --helper-import <path> Import path for GetDeps.
1764
2473
  --transform-only-standalone-declarables Only transform standalone components/directives.
1765
- --no-providers Do not include metadata providers in deps().
1766
- --no-view-providers Do not include component viewProviders in deps().
2474
+ --no-providers Do not include metadata providers in generated types.
2475
+ --no-view-providers Do not include component viewProviders in generated types.
1767
2476
  --dry-run Print results without writing files.
1768
2477
  --help Show this help.
1769
2478
  `);
1770
2479
  process.exit(0);
1771
2480
  }
1772
2481
 
1773
- if (require.main === module) {
2482
+ // Check if this module is being run directly (ESM equivalent of require.main === module)
2483
+ if (
2484
+ import.meta.url === `file://${process.argv[1]}` ||
2485
+ (import.meta.url.startsWith('file:') &&
2486
+ process.argv[1] === fileURLToPath(import.meta.url))
2487
+ ) {
1774
2488
  runAngularBrandCodemod(parseCliArgs(process.argv.slice(2))).catch(
1775
2489
  (error: unknown) => {
1776
2490
  console.error(error);