@craft-ng/dev-tools 0.1.5 → 0.1.7

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.
@@ -100,12 +100,39 @@ type ProvidedDependencyExtractionResult = {
100
100
  warnings: string[];
101
101
  };
102
102
 
103
+ type InjectedDependencyDescriptor = {
104
+ dependencyText: string;
105
+ entry: GeneratedDependencyEntry;
106
+ };
107
+
108
+ type InjectCallDependencyExtractionResult = DependencyExtractionResult & {
109
+ generatedDependencies: InjectedDependencyDescriptor[];
110
+ };
111
+
103
112
  type DependencyReferenceResolution = {
104
113
  kind: 'class' | 'enum' | 'function' | 'variable' | 'namespace' | 'unknown';
105
114
  classDeclaration?: ClassDeclaration;
106
115
  moduleSpecifier?: string;
107
116
  };
108
117
 
118
+ type TrackedHelperResolution = {
119
+ serviceName: string;
120
+ scope: string;
121
+ };
122
+
123
+ type HelperExposureBinding = {
124
+ localName: string;
125
+ sourceKey: string;
126
+ };
127
+
128
+ type HelperExposureTracking = {
129
+ usedProperties: string[];
130
+ exposedProperties: Array<{
131
+ exposedKey: string;
132
+ sourceKey: string;
133
+ }>;
134
+ };
135
+
109
136
  type NormalizedOptions = Required<AngularBrandCodemodOptions>;
110
137
 
111
138
  type InjectDecoratorTokenResult =
@@ -320,7 +347,13 @@ export function analyzeSourceFileDependencies(
320
347
  result.generatedDependencyGroups = createGeneratedDependencyGroups(
321
348
  sourceFile,
322
349
  result.dependencyGroups.importDeps,
323
- result.dependencyGroups.injected,
350
+ [
351
+ ...constructorDeps.dependencies.map((dependencyText) => ({
352
+ dependencyText,
353
+ entry: createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject'),
354
+ })),
355
+ ...injectCallDeps.generatedDependencies,
356
+ ],
324
357
  providedDeps.entries,
325
358
  );
326
359
 
@@ -485,9 +518,11 @@ export function extractConstructorDeps(
485
518
 
486
519
  export function extractInjectCallDeps(
487
520
  classDeclaration: ClassDeclaration,
488
- ): DependencyExtractionResult {
521
+ ): InjectCallDependencyExtractionResult {
489
522
  const warnings: string[] = [];
490
523
  const dependencies: string[] = [];
524
+ const generatedDependencies: InjectedDependencyDescriptor[] = [];
525
+ const sourceFile = classDeclaration.getSourceFile();
491
526
 
492
527
  for (const callExpression of classDeclaration.getDescendantsOfKind(
493
528
  SyntaxKind.CallExpression,
@@ -511,9 +546,25 @@ export function extractInjectCallDeps(
511
546
  }
512
547
 
513
548
  dependencies.push(dependency);
549
+ generatedDependencies.push({
550
+ dependencyText: dependency,
551
+ entry:
552
+ injectMethodName === 'inject'
553
+ ? createGeneratedDependencyEntry(sourceFile, dependency, 'inject')
554
+ : createGeneratedInjectHelperDependencyEntry(
555
+ sourceFile,
556
+ callExpression,
557
+ expression,
558
+ dependency,
559
+ ),
560
+ });
514
561
  }
515
562
 
516
- return { dependencies: mergeDeps(dependencies), warnings };
563
+ return {
564
+ dependencies: mergeDeps(dependencies),
565
+ warnings,
566
+ generatedDependencies,
567
+ };
517
568
  }
518
569
 
519
570
  export function mergeDeps(...dependencyGroups: readonly string[][]): string[] {
@@ -557,30 +608,39 @@ function getGeneratedDepsTypeName(className: string): string {
557
608
  function createGeneratedDependencyGroups(
558
609
  sourceFile: SourceFile,
559
610
  importDependencies: string[],
560
- injectedDependencies: string[],
611
+ injectedDependencies: InjectedDependencyDescriptor[],
561
612
  providedEntries: GeneratedDependencyEntry[],
562
613
  ): GeneratedDependencyGroups {
563
614
  const deps = mergeGeneratedDependencyEntries(
564
615
  importDependencies.map((dependency) =>
565
616
  createGeneratedDependencyEntry(sourceFile, dependency, 'import'),
566
617
  ),
567
- injectedDependencies.map((dependency) =>
568
- createGeneratedDependencyEntry(sourceFile, dependency, 'inject'),
569
- ),
618
+ injectedDependencies.map((dependency) => dependency.entry),
570
619
  );
571
620
  const provided = mergeGeneratedDependencyEntries(providedEntries);
572
621
  const providedKeys = new Set(provided.map((entry) => entry.key));
622
+ const injectedEntriesByKey = new Map(
623
+ injectedDependencies.map((dependency) => [dependency.entry.key, dependency.entry]),
624
+ );
573
625
  const missingProvider = mergeGeneratedDependencyEntries(
574
626
  injectedDependencies
575
627
  .filter((dependency) => {
576
- const dependencyKey = createGeneratedDependencyKey(dependency);
628
+ const dependencyKey = dependency.entry.key;
577
629
  return (
578
630
  !providedKeys.has(dependencyKey) &&
579
- !isProvidedInInjectableTree(sourceFile, dependency)
631
+ shouldGenerateLocalMissingProvider(
632
+ sourceFile,
633
+ dependency.dependencyText,
634
+ )
580
635
  );
581
636
  })
582
637
  .map((dependency) =>
583
- createGeneratedDependencyEntry(sourceFile, dependency, 'inject'),
638
+ injectedEntriesByKey.get(dependency.entry.key) ??
639
+ createGeneratedDependencyEntry(
640
+ sourceFile,
641
+ dependency.dependencyText,
642
+ 'inject',
643
+ ),
584
644
  ),
585
645
  );
586
646
 
@@ -640,6 +700,342 @@ function createGeneratedDependencyEntry(
640
700
  };
641
701
  }
642
702
 
703
+ function createGeneratedInjectHelperDependencyEntry(
704
+ sourceFile: SourceFile,
705
+ callExpression: import('ts-morph').CallExpression,
706
+ expression: Node,
707
+ dependencyText: string,
708
+ ): GeneratedDependencyEntry {
709
+ const trackedHelper = resolveTrackedInjectHelper(expression);
710
+ if (!trackedHelper) {
711
+ return createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject');
712
+ }
713
+
714
+ const exposureTracking = extractHelperExposureTracking(callExpression);
715
+
716
+ return {
717
+ key: trackedHelper.serviceName,
718
+ typeText: createTrackedInjectHelperTypeText(
719
+ dependencyText,
720
+ exposureTracking,
721
+ ),
722
+ };
723
+ }
724
+
725
+ function createTrackedInjectHelperTypeText(
726
+ dependencyText: string,
727
+ exposureTracking: HelperExposureTracking | undefined,
728
+ ): string {
729
+ const baseType = `GetInjectedServiceDependencies<typeof ${dependencyText}>`;
730
+ if (!exposureTracking) {
731
+ return baseType;
732
+ }
733
+
734
+ return [
735
+ `DerivedService<${baseType}, {`,
736
+ ` derivedPropertiesUsed: ${formatHelperExposurePropertiesType(
737
+ dependencyText,
738
+ exposureTracking.usedProperties.map((sourceKey) => ({
739
+ propertyKey: sourceKey,
740
+ sourceKey,
741
+ })),
742
+ )};`,
743
+ ` derivedPropertiesExposed: ${formatHelperExposurePropertiesType(
744
+ dependencyText,
745
+ exposureTracking.exposedProperties.map(
746
+ ({ exposedKey, sourceKey }) => ({
747
+ propertyKey: exposedKey,
748
+ sourceKey,
749
+ }),
750
+ ),
751
+ )};`,
752
+ '}>',
753
+ ].join('\n');
754
+ }
755
+
756
+ function formatHelperExposurePropertiesType(
757
+ dependencyText: string,
758
+ properties: Array<{ propertyKey: string; sourceKey: string }>,
759
+ ): string {
760
+ if (properties.length === 0) {
761
+ return '{}';
762
+ }
763
+
764
+ return [
765
+ '{',
766
+ ...properties.map(
767
+ ({ propertyKey, sourceKey }) =>
768
+ ` ${formatObjectKey(propertyKey)}: ${createTrackedHelperOutputTypeText(
769
+ dependencyText,
770
+ sourceKey,
771
+ )};`,
772
+ ),
773
+ ' }',
774
+ ].join('\n');
775
+ }
776
+
777
+ function createTrackedHelperOutputTypeText(
778
+ dependencyText: string,
779
+ sourceKey: string,
780
+ ): string {
781
+ if (sourceKey === '$self') {
782
+ return `GetServiceOutput<typeof ${dependencyText}>`;
783
+ }
784
+
785
+ return `GetServiceOutput<typeof ${dependencyText}>[${JSON.stringify(sourceKey)}]`;
786
+ }
787
+
788
+ function resolveTrackedInjectHelper(
789
+ expression: Node,
790
+ ): TrackedHelperResolution | undefined {
791
+ const symbol = expression.getSymbol() ?? expression.getType().getSymbol();
792
+ const declarations =
793
+ symbol?.getAliasedSymbol()?.getDeclarations() ?? symbol?.getDeclarations() ?? [];
794
+
795
+ for (const declaration of declarations) {
796
+ const helper = resolveTrackedInjectHelperFromDeclaration(declaration);
797
+ if (helper) {
798
+ return helper;
799
+ }
800
+ }
801
+
802
+ return undefined;
803
+ }
804
+
805
+ function resolveTrackedInjectHelperFromDeclaration(
806
+ declaration: Node,
807
+ ): TrackedHelperResolution | undefined {
808
+ if (!Node.isBindingElement(declaration)) {
809
+ return undefined;
810
+ }
811
+
812
+ const objectBindingPattern = declaration.getFirstAncestorByKind(
813
+ SyntaxKind.ObjectBindingPattern,
814
+ );
815
+ const variableDeclaration =
816
+ objectBindingPattern?.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
817
+ const initializer = variableDeclaration?.getInitializer();
818
+ if (!Node.isCallExpression(initializer) || !isServiceFactoryCall(initializer)) {
819
+ return undefined;
820
+ }
821
+
822
+ const [optionsArgument] = initializer.getArguments();
823
+ if (!Node.isObjectLiteralExpression(optionsArgument)) {
824
+ return undefined;
825
+ }
826
+
827
+ const nameInitializer = getObjectPropertyAssignment(
828
+ optionsArgument,
829
+ 'name',
830
+ )?.getInitializer();
831
+ const scopeInitializer = getObjectPropertyAssignment(
832
+ optionsArgument,
833
+ 'scope',
834
+ )?.getInitializer();
835
+
836
+ if (
837
+ !Node.isStringLiteral(nameInitializer) ||
838
+ !Node.isStringLiteral(scopeInitializer)
839
+ ) {
840
+ return undefined;
841
+ }
842
+
843
+ return {
844
+ serviceName: nameInitializer.getLiteralText(),
845
+ scope: scopeInitializer.getLiteralText(),
846
+ };
847
+ }
848
+
849
+ function isServiceFactoryCall(
850
+ callExpression: import('ts-morph').CallExpression,
851
+ ): boolean {
852
+ const expression = callExpression.getExpression();
853
+ if (Node.isIdentifier(expression)) {
854
+ return expression.getText() === 'craftService' || expression.getText() === 'toCraftService';
855
+ }
856
+
857
+ if (Node.isPropertyAccessExpression(expression)) {
858
+ const methodName = expression.getName();
859
+ return methodName === 'craftService' || methodName === 'toCraftService';
860
+ }
861
+
862
+ return false;
863
+ }
864
+
865
+ function extractHelperExposureTracking(
866
+ callExpression: import('ts-morph').CallExpression,
867
+ ): HelperExposureTracking | undefined {
868
+ const exposeArgument = [...callExpression.getArguments()]
869
+ .reverse()
870
+ .find(
871
+ (argument) =>
872
+ Node.isArrowFunction(argument) || Node.isFunctionExpression(argument),
873
+ );
874
+
875
+ if (
876
+ !exposeArgument ||
877
+ (!Node.isArrowFunction(exposeArgument) &&
878
+ !Node.isFunctionExpression(exposeArgument))
879
+ ) {
880
+ return undefined;
881
+ }
882
+
883
+ const parameter = exposeArgument.getParameters()[0];
884
+ const nameNode = parameter?.getNameNode();
885
+ if (!nameNode || !Node.isObjectBindingPattern(nameNode)) {
886
+ return undefined;
887
+ }
888
+
889
+ const bindings = extractHelperExposureBindings(nameNode);
890
+ if (bindings.length === 0) {
891
+ return undefined;
892
+ }
893
+
894
+ const returnObjectLiteral = getExposureReturnObjectLiteral(exposeArgument);
895
+ if (!returnObjectLiteral) {
896
+ return undefined;
897
+ }
898
+
899
+ const exposedProperties = extractHelperExposedProperties(
900
+ returnObjectLiteral,
901
+ bindings,
902
+ );
903
+ const usedProperties = mergeDeps(
904
+ exposedProperties.map(({ sourceKey }) => sourceKey),
905
+ extractHelperYieldedProperties(exposeArgument, bindings),
906
+ );
907
+
908
+ return {
909
+ usedProperties,
910
+ exposedProperties,
911
+ };
912
+ }
913
+
914
+ function extractHelperExposureBindings(
915
+ bindingPattern: import('ts-morph').ObjectBindingPattern,
916
+ ): HelperExposureBinding[] {
917
+ const bindings: HelperExposureBinding[] = [];
918
+
919
+ for (const element of bindingPattern.getElements()) {
920
+ const nameNode = element.getNameNode();
921
+ if (!Node.isIdentifier(nameNode)) {
922
+ continue;
923
+ }
924
+
925
+ const propertyNameNode = element.getPropertyNameNode();
926
+ const sourceKey =
927
+ getStaticPropertyName(propertyNameNode ?? nameNode) ?? nameNode.getText();
928
+
929
+ bindings.push({
930
+ localName: nameNode.getText(),
931
+ sourceKey,
932
+ });
933
+ }
934
+
935
+ return bindings;
936
+ }
937
+
938
+ function getExposureReturnObjectLiteral(
939
+ exposeArgument: import('ts-morph').ArrowFunction | import('ts-morph').FunctionExpression,
940
+ ): ObjectLiteralExpression | undefined {
941
+ const body = exposeArgument.getBody();
942
+ if (Node.isObjectLiteralExpression(body)) {
943
+ return body;
944
+ }
945
+
946
+ if (Node.isParenthesizedExpression(body)) {
947
+ const expression = body.getExpression();
948
+ return Node.isObjectLiteralExpression(expression) ? expression : undefined;
949
+ }
950
+
951
+ if (!Node.isBlock(body)) {
952
+ return undefined;
953
+ }
954
+
955
+ const returnStatement = [...body.getDescendantsOfKind(SyntaxKind.ReturnStatement)]
956
+ .reverse()
957
+ .find((statement) => Node.isObjectLiteralExpression(statement.getExpression()));
958
+
959
+ const expression = returnStatement?.getExpression();
960
+ return Node.isObjectLiteralExpression(expression) ? expression : undefined;
961
+ }
962
+
963
+ function extractHelperExposedProperties(
964
+ objectLiteral: ObjectLiteralExpression,
965
+ bindings: HelperExposureBinding[],
966
+ ): Array<{ exposedKey: string; sourceKey: string }> {
967
+ const bindingMap = new Map(
968
+ bindings.map((binding) => [binding.localName, binding.sourceKey]),
969
+ );
970
+ const exposedProperties: Array<{ exposedKey: string; sourceKey: string }> = [];
971
+
972
+ for (const property of objectLiteral.getProperties()) {
973
+ if (Node.isShorthandPropertyAssignment(property)) {
974
+ const sourceKey = bindingMap.get(property.getName());
975
+ if (sourceKey) {
976
+ exposedProperties.push({
977
+ exposedKey: property.getName(),
978
+ sourceKey,
979
+ });
980
+ }
981
+ continue;
982
+ }
983
+
984
+ if (!Node.isPropertyAssignment(property)) {
985
+ continue;
986
+ }
987
+
988
+ const exposedKey = getStaticPropertyName(property.getNameNode());
989
+ const initializer = property.getInitializer();
990
+ if (!exposedKey || !Node.isIdentifier(initializer)) {
991
+ continue;
992
+ }
993
+
994
+ const sourceKey = bindingMap.get(initializer.getText());
995
+ if (!sourceKey) {
996
+ continue;
997
+ }
998
+
999
+ exposedProperties.push({
1000
+ exposedKey,
1001
+ sourceKey,
1002
+ });
1003
+ }
1004
+
1005
+ return exposedProperties;
1006
+ }
1007
+
1008
+ function extractHelperYieldedProperties(
1009
+ exposeArgument: import('ts-morph').ArrowFunction | import('ts-morph').FunctionExpression,
1010
+ bindings: HelperExposureBinding[],
1011
+ ): string[] {
1012
+ const bindingMap = new Map(
1013
+ bindings.map((binding) => [binding.localName, binding.sourceKey]),
1014
+ );
1015
+ const yieldedProperties: string[] = [];
1016
+
1017
+ for (const yieldExpression of exposeArgument.getDescendantsOfKind(
1018
+ SyntaxKind.YieldExpression,
1019
+ )) {
1020
+ const expression = yieldExpression.getExpression();
1021
+ if (!Node.isCallExpression(expression)) {
1022
+ continue;
1023
+ }
1024
+
1025
+ const target = expression.getExpression();
1026
+ if (!Node.isIdentifier(target)) {
1027
+ continue;
1028
+ }
1029
+
1030
+ const sourceKey = bindingMap.get(target.getText());
1031
+ if (sourceKey) {
1032
+ yieldedProperties.push(sourceKey);
1033
+ }
1034
+ }
1035
+
1036
+ return yieldedProperties;
1037
+ }
1038
+
643
1039
  function createGeneratedDependencyKey(dependencyText: string): string {
644
1040
  const lastSegment = dependencyText.split('.').pop() ?? dependencyText;
645
1041
  const helperMatch = /^(inject|provide)([A-Z].*)$/.exec(lastSegment);
@@ -768,7 +1164,13 @@ function formatGeneratedDependencyType(
768
1164
  '{',
769
1165
  ` deps: ${formatGeneratedDependencyObject(generatedDependencyGroups.deps)};`,
770
1166
  ` provided: ${formatGeneratedDependencyObject(generatedDependencyGroups.provided)};`,
771
- ` missingProvider: ${formatGeneratedDependencyObject(generatedDependencyGroups.missingProvider)};`,
1167
+ ...(generatedDependencyGroups.missingProvider.length > 0
1168
+ ? [
1169
+ ` missingProvider: ${formatGeneratedDependencyObject(
1170
+ generatedDependencyGroups.missingProvider,
1171
+ )};`,
1172
+ ]
1173
+ : []),
772
1174
  '}',
773
1175
  ].join('\n');
774
1176
  }
@@ -782,13 +1184,21 @@ function formatGeneratedDependencyObject(
782
1184
 
783
1185
  return [
784
1186
  '{',
785
- ...entries.map(
786
- (entry) => ` ${formatObjectKey(entry.key)}: ${entry.typeText};`,
787
- ),
1187
+ ...entries.map((entry) => formatGeneratedDependencyEntry(entry)),
788
1188
  ' }',
789
1189
  ].join('\n');
790
1190
  }
791
1191
 
1192
+ function formatGeneratedDependencyEntry(entry: GeneratedDependencyEntry): string {
1193
+ const typeLines = entry.typeText.split('\n');
1194
+ const lines = [
1195
+ ` ${formatObjectKey(entry.key)}: ${typeLines[0]}`,
1196
+ ...typeLines.slice(1).map((line) => ` ${line}`),
1197
+ ];
1198
+ lines[lines.length - 1] += ';';
1199
+ return lines.join('\n');
1200
+ }
1201
+
792
1202
  function formatObjectKey(key: string): string {
793
1203
  return /^[$A-Z_][0-9A-Z_$]*$/i.test(key) ? key : `'${key}'`;
794
1204
  }
@@ -927,7 +1337,12 @@ export function ensureHelperImports(
927
1337
  sourceFile: SourceFile,
928
1338
  helperImportPath: string,
929
1339
  ): void {
930
- const missingImports = ['GetDeps'].filter(
1340
+ const missingImports = [
1341
+ 'DerivedService',
1342
+ 'GetDeps',
1343
+ 'GetInjectedServiceDependencies',
1344
+ 'GetServiceOutput',
1345
+ ].filter(
931
1346
  (importName) =>
932
1347
  !hasLocalNamedImport(sourceFile, helperImportPath, importName),
933
1348
  );
@@ -1875,6 +2290,64 @@ function isProvidedInInjectableTree(
1875
2290
  );
1876
2291
  }
1877
2292
 
2293
+ function shouldGenerateLocalMissingProvider(
2294
+ sourceFile: SourceFile,
2295
+ dependencyText: string,
2296
+ ): boolean {
2297
+ return (
2298
+ !resolveTrackedInjectHelperByName(sourceFile, dependencyText) &&
2299
+ !isProvidedInInjectableTree(sourceFile, dependencyText)
2300
+ );
2301
+ }
2302
+
2303
+ function resolveTrackedInjectHelperByName(
2304
+ sourceFile: SourceFile,
2305
+ dependencyText: string,
2306
+ ): TrackedHelperResolution | undefined {
2307
+ const [rootIdentifier] = dependencyText.split('.');
2308
+ if (!rootIdentifier) {
2309
+ return undefined;
2310
+ }
2311
+
2312
+ for (const declaration of resolveDependencyIdentifierDeclarations(
2313
+ sourceFile,
2314
+ rootIdentifier,
2315
+ )) {
2316
+ const helper = resolveTrackedInjectHelperFromDeclaration(declaration);
2317
+ if (helper) {
2318
+ return helper;
2319
+ }
2320
+ }
2321
+
2322
+ return undefined;
2323
+ }
2324
+
2325
+ function resolveDependencyIdentifierDeclarations(
2326
+ sourceFile: SourceFile,
2327
+ rootIdentifier: string,
2328
+ ): Node[] {
2329
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
2330
+ const namedImport = importDeclaration.getNamedImports().find((specifier) => {
2331
+ const localName =
2332
+ specifier.getAliasNode()?.getText() ?? specifier.getNameNode().getText();
2333
+ return localName === rootIdentifier;
2334
+ });
2335
+
2336
+ if (!namedImport) {
2337
+ continue;
2338
+ }
2339
+
2340
+ const identifier = namedImport.getAliasNode() ?? namedImport.getNameNode();
2341
+ const symbol = identifier.getSymbol() ?? identifier.getType().getSymbol();
2342
+ const aliasedSymbol = symbol?.getAliasedSymbol() ?? symbol;
2343
+ return aliasedSymbol?.getDeclarations() ?? [namedImport];
2344
+ }
2345
+
2346
+ return sourceFile
2347
+ .getDescendantsOfKind(SyntaxKind.BindingElement)
2348
+ .filter((bindingElement) => bindingElement.getName() === rootIdentifier);
2349
+ }
2350
+
1878
2351
  function getDependencyTextFromTypeNode(typeNode: TypeNode): string | undefined {
1879
2352
  if (isPrimitiveTypeNode(typeNode)) {
1880
2353
  return undefined;
@@ -2124,7 +2597,12 @@ function getHelperImportSafety(
2124
2597
  sourceFile: SourceFile,
2125
2598
  helperImportPath: string,
2126
2599
  ): { safe: true; warnings: [] } | { safe: false; warnings: string[] } {
2127
- const warnings = ['GetDeps']
2600
+ const warnings = [
2601
+ 'DerivedService',
2602
+ 'GetDeps',
2603
+ 'GetInjectedServiceDependencies',
2604
+ 'GetServiceOutput',
2605
+ ]
2128
2606
  .filter((helperName) =>
2129
2607
  hasConflictingTopLevelBinding(sourceFile, helperImportPath, helperName),
2130
2608
  )