@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craft-ng/dev-tools",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Development tools for ng-craft: ESLint configs, ESLint rules, and codemods",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -57,6 +57,13 @@ export type ExistingDependencyGroupsResult = {
57
57
  depsObjectNode?: Node;
58
58
  propertyNodes: Partial<Record<keyof DependencyGroups, Node>>;
59
59
  };
60
+ type InjectedDependencyDescriptor = {
61
+ dependencyText: string;
62
+ entry: GeneratedDependencyEntry;
63
+ };
64
+ type InjectCallDependencyExtractionResult = DependencyExtractionResult & {
65
+ generatedDependencies: InjectedDependencyDescriptor[];
66
+ };
60
67
  type RunFileReport = TransformResult & {
61
68
  filePath: string;
62
69
  };
@@ -74,7 +81,7 @@ export declare function getAngularKind(classDeclaration: ClassDeclaration): Angu
74
81
  export declare function isStandaloneDeclarable(classDeclaration: ClassDeclaration): boolean;
75
82
  export declare function extractDecoratorMetadataDeps(classDeclaration: ClassDeclaration, angularKind?: AngularKind | undefined, options?: AngularBrandCodemodOptions): DependencyExtractionResult;
76
83
  export declare function extractConstructorDeps(classDeclaration: ClassDeclaration): DependencyExtractionResult;
77
- export declare function extractInjectCallDeps(classDeclaration: ClassDeclaration): DependencyExtractionResult;
84
+ export declare function extractInjectCallDeps(classDeclaration: ClassDeclaration): InjectCallDependencyExtractionResult;
78
85
  export declare function mergeDeps(...dependencyGroups: readonly string[][]): string[];
79
86
  export declare function writeGeneratedDepsTypeAlias(sourceFile: SourceFile, className: string, generatedDependencyGroups: GeneratedDependencyGroups): void;
80
87
  export declare function migrateLegacyBrandExport(sourceFile: SourceFile, classDeclaration: ClassDeclaration, className: string): void;
@@ -127,7 +127,13 @@ export function analyzeSourceFileDependencies(sourceFile, options = {}) {
127
127
  providers: mergeDeps(metadataDeps.providers, metadataDeps.viewProviders),
128
128
  };
129
129
  result.generatedTypeName = getGeneratedDepsTypeName(className);
130
- result.generatedDependencyGroups = createGeneratedDependencyGroups(sourceFile, result.dependencyGroups.importDeps, result.dependencyGroups.injected, providedDeps.entries);
130
+ result.generatedDependencyGroups = createGeneratedDependencyGroups(sourceFile, result.dependencyGroups.importDeps, [
131
+ ...constructorDeps.dependencies.map((dependencyText) => ({
132
+ dependencyText,
133
+ entry: createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject'),
134
+ })),
135
+ ...injectCallDeps.generatedDependencies,
136
+ ], providedDeps.entries);
131
137
  return result;
132
138
  }
133
139
  export function findAngularDecoratedClass(sourceFile) {
@@ -233,6 +239,8 @@ export function extractInjectCallDeps(classDeclaration) {
233
239
  var _a;
234
240
  const warnings = [];
235
241
  const dependencies = [];
242
+ const generatedDependencies = [];
243
+ const sourceFile = classDeclaration.getSourceFile();
236
244
  for (const callExpression of classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression)) {
237
245
  const expression = callExpression.getExpression();
238
246
  const injectMethodName = getInjectMethodName(expression);
@@ -247,8 +255,18 @@ export function extractInjectCallDeps(classDeclaration) {
247
255
  continue;
248
256
  }
249
257
  dependencies.push(dependency);
258
+ generatedDependencies.push({
259
+ dependencyText: dependency,
260
+ entry: injectMethodName === 'inject'
261
+ ? createGeneratedDependencyEntry(sourceFile, dependency, 'inject')
262
+ : createGeneratedInjectHelperDependencyEntry(sourceFile, callExpression, expression, dependency),
263
+ });
250
264
  }
251
- return { dependencies: mergeDeps(dependencies), warnings };
265
+ return {
266
+ dependencies: mergeDeps(dependencies),
267
+ warnings,
268
+ generatedDependencies,
269
+ };
252
270
  }
253
271
  export function mergeDeps(...dependencyGroups) {
254
272
  const seen = new Set();
@@ -282,16 +300,20 @@ function getGeneratedDepsTypeName(className) {
282
300
  return `GenDeps_${className}`;
283
301
  }
284
302
  function createGeneratedDependencyGroups(sourceFile, importDependencies, injectedDependencies, providedEntries) {
285
- const deps = mergeGeneratedDependencyEntries(importDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'import')), injectedDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'inject')));
303
+ const deps = mergeGeneratedDependencyEntries(importDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'import')), injectedDependencies.map((dependency) => dependency.entry));
286
304
  const provided = mergeGeneratedDependencyEntries(providedEntries);
287
305
  const providedKeys = new Set(provided.map((entry) => entry.key));
306
+ const injectedEntriesByKey = new Map(injectedDependencies.map((dependency) => [dependency.entry.key, dependency.entry]));
288
307
  const missingProvider = mergeGeneratedDependencyEntries(injectedDependencies
289
308
  .filter((dependency) => {
290
- const dependencyKey = createGeneratedDependencyKey(dependency);
309
+ const dependencyKey = dependency.entry.key;
291
310
  return (!providedKeys.has(dependencyKey) &&
292
- !isProvidedInInjectableTree(sourceFile, dependency));
311
+ shouldGenerateLocalMissingProvider(sourceFile, dependency.dependencyText));
293
312
  })
294
- .map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'inject')));
313
+ .map((dependency) => {
314
+ var _a;
315
+ return (_a = injectedEntriesByKey.get(dependency.entry.key)) !== null && _a !== void 0 ? _a : createGeneratedDependencyEntry(sourceFile, dependency.dependencyText, 'inject');
316
+ }));
295
317
  return {
296
318
  deps,
297
319
  provided,
@@ -331,6 +353,216 @@ function createGeneratedDependencyEntry(sourceFile, dependencyText, context) {
331
353
  typeText: createGeneratedDependencyTypeText(sourceFile, dependencyText),
332
354
  };
333
355
  }
356
+ function createGeneratedInjectHelperDependencyEntry(sourceFile, callExpression, expression, dependencyText) {
357
+ const trackedHelper = resolveTrackedInjectHelper(expression);
358
+ if (!trackedHelper) {
359
+ return createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject');
360
+ }
361
+ const exposureTracking = extractHelperExposureTracking(callExpression);
362
+ return {
363
+ key: trackedHelper.serviceName,
364
+ typeText: createTrackedInjectHelperTypeText(dependencyText, exposureTracking),
365
+ };
366
+ }
367
+ function createTrackedInjectHelperTypeText(dependencyText, exposureTracking) {
368
+ const baseType = `GetInjectedServiceDependencies<typeof ${dependencyText}>`;
369
+ if (!exposureTracking) {
370
+ return baseType;
371
+ }
372
+ return [
373
+ `DerivedService<${baseType}, {`,
374
+ ` derivedPropertiesUsed: ${formatHelperExposurePropertiesType(dependencyText, exposureTracking.usedProperties.map((sourceKey) => ({
375
+ propertyKey: sourceKey,
376
+ sourceKey,
377
+ })))};`,
378
+ ` derivedPropertiesExposed: ${formatHelperExposurePropertiesType(dependencyText, exposureTracking.exposedProperties.map(({ exposedKey, sourceKey }) => ({
379
+ propertyKey: exposedKey,
380
+ sourceKey,
381
+ })))};`,
382
+ '}>',
383
+ ].join('\n');
384
+ }
385
+ function formatHelperExposurePropertiesType(dependencyText, properties) {
386
+ if (properties.length === 0) {
387
+ return '{}';
388
+ }
389
+ return [
390
+ '{',
391
+ ...properties.map(({ propertyKey, sourceKey }) => ` ${formatObjectKey(propertyKey)}: ${createTrackedHelperOutputTypeText(dependencyText, sourceKey)};`),
392
+ ' }',
393
+ ].join('\n');
394
+ }
395
+ function createTrackedHelperOutputTypeText(dependencyText, sourceKey) {
396
+ if (sourceKey === '$self') {
397
+ return `GetServiceOutput<typeof ${dependencyText}>`;
398
+ }
399
+ return `GetServiceOutput<typeof ${dependencyText}>[${JSON.stringify(sourceKey)}]`;
400
+ }
401
+ function resolveTrackedInjectHelper(expression) {
402
+ var _a, _b, _c, _d;
403
+ const symbol = (_a = expression.getSymbol()) !== null && _a !== void 0 ? _a : expression.getType().getSymbol();
404
+ const declarations = (_d = (_c = (_b = symbol === null || symbol === void 0 ? void 0 : symbol.getAliasedSymbol()) === null || _b === void 0 ? void 0 : _b.getDeclarations()) !== null && _c !== void 0 ? _c : symbol === null || symbol === void 0 ? void 0 : symbol.getDeclarations()) !== null && _d !== void 0 ? _d : [];
405
+ for (const declaration of declarations) {
406
+ const helper = resolveTrackedInjectHelperFromDeclaration(declaration);
407
+ if (helper) {
408
+ return helper;
409
+ }
410
+ }
411
+ return undefined;
412
+ }
413
+ function resolveTrackedInjectHelperFromDeclaration(declaration) {
414
+ var _a, _b;
415
+ if (!Node.isBindingElement(declaration)) {
416
+ return undefined;
417
+ }
418
+ const objectBindingPattern = declaration.getFirstAncestorByKind(SyntaxKind.ObjectBindingPattern);
419
+ const variableDeclaration = objectBindingPattern === null || objectBindingPattern === void 0 ? void 0 : objectBindingPattern.getFirstAncestorByKind(SyntaxKind.VariableDeclaration);
420
+ const initializer = variableDeclaration === null || variableDeclaration === void 0 ? void 0 : variableDeclaration.getInitializer();
421
+ if (!Node.isCallExpression(initializer) || !isServiceFactoryCall(initializer)) {
422
+ return undefined;
423
+ }
424
+ const [optionsArgument] = initializer.getArguments();
425
+ if (!Node.isObjectLiteralExpression(optionsArgument)) {
426
+ return undefined;
427
+ }
428
+ const nameInitializer = (_a = getObjectPropertyAssignment(optionsArgument, 'name')) === null || _a === void 0 ? void 0 : _a.getInitializer();
429
+ const scopeInitializer = (_b = getObjectPropertyAssignment(optionsArgument, 'scope')) === null || _b === void 0 ? void 0 : _b.getInitializer();
430
+ if (!Node.isStringLiteral(nameInitializer) ||
431
+ !Node.isStringLiteral(scopeInitializer)) {
432
+ return undefined;
433
+ }
434
+ return {
435
+ serviceName: nameInitializer.getLiteralText(),
436
+ scope: scopeInitializer.getLiteralText(),
437
+ };
438
+ }
439
+ function isServiceFactoryCall(callExpression) {
440
+ const expression = callExpression.getExpression();
441
+ if (Node.isIdentifier(expression)) {
442
+ return expression.getText() === 'craftService' || expression.getText() === 'toCraftService';
443
+ }
444
+ if (Node.isPropertyAccessExpression(expression)) {
445
+ const methodName = expression.getName();
446
+ return methodName === 'craftService' || methodName === 'toCraftService';
447
+ }
448
+ return false;
449
+ }
450
+ function extractHelperExposureTracking(callExpression) {
451
+ const exposeArgument = [...callExpression.getArguments()]
452
+ .reverse()
453
+ .find((argument) => Node.isArrowFunction(argument) || Node.isFunctionExpression(argument));
454
+ if (!exposeArgument ||
455
+ (!Node.isArrowFunction(exposeArgument) &&
456
+ !Node.isFunctionExpression(exposeArgument))) {
457
+ return undefined;
458
+ }
459
+ const parameter = exposeArgument.getParameters()[0];
460
+ const nameNode = parameter === null || parameter === void 0 ? void 0 : parameter.getNameNode();
461
+ if (!nameNode || !Node.isObjectBindingPattern(nameNode)) {
462
+ return undefined;
463
+ }
464
+ const bindings = extractHelperExposureBindings(nameNode);
465
+ if (bindings.length === 0) {
466
+ return undefined;
467
+ }
468
+ const returnObjectLiteral = getExposureReturnObjectLiteral(exposeArgument);
469
+ if (!returnObjectLiteral) {
470
+ return undefined;
471
+ }
472
+ const exposedProperties = extractHelperExposedProperties(returnObjectLiteral, bindings);
473
+ const usedProperties = mergeDeps(exposedProperties.map(({ sourceKey }) => sourceKey), extractHelperYieldedProperties(exposeArgument, bindings));
474
+ return {
475
+ usedProperties,
476
+ exposedProperties,
477
+ };
478
+ }
479
+ function extractHelperExposureBindings(bindingPattern) {
480
+ var _a;
481
+ const bindings = [];
482
+ for (const element of bindingPattern.getElements()) {
483
+ const nameNode = element.getNameNode();
484
+ if (!Node.isIdentifier(nameNode)) {
485
+ continue;
486
+ }
487
+ const propertyNameNode = element.getPropertyNameNode();
488
+ const sourceKey = (_a = getStaticPropertyName(propertyNameNode !== null && propertyNameNode !== void 0 ? propertyNameNode : nameNode)) !== null && _a !== void 0 ? _a : nameNode.getText();
489
+ bindings.push({
490
+ localName: nameNode.getText(),
491
+ sourceKey,
492
+ });
493
+ }
494
+ return bindings;
495
+ }
496
+ function getExposureReturnObjectLiteral(exposeArgument) {
497
+ const body = exposeArgument.getBody();
498
+ if (Node.isObjectLiteralExpression(body)) {
499
+ return body;
500
+ }
501
+ if (Node.isParenthesizedExpression(body)) {
502
+ const expression = body.getExpression();
503
+ return Node.isObjectLiteralExpression(expression) ? expression : undefined;
504
+ }
505
+ if (!Node.isBlock(body)) {
506
+ return undefined;
507
+ }
508
+ const returnStatement = [...body.getDescendantsOfKind(SyntaxKind.ReturnStatement)]
509
+ .reverse()
510
+ .find((statement) => Node.isObjectLiteralExpression(statement.getExpression()));
511
+ const expression = returnStatement === null || returnStatement === void 0 ? void 0 : returnStatement.getExpression();
512
+ return Node.isObjectLiteralExpression(expression) ? expression : undefined;
513
+ }
514
+ function extractHelperExposedProperties(objectLiteral, bindings) {
515
+ const bindingMap = new Map(bindings.map((binding) => [binding.localName, binding.sourceKey]));
516
+ const exposedProperties = [];
517
+ for (const property of objectLiteral.getProperties()) {
518
+ if (Node.isShorthandPropertyAssignment(property)) {
519
+ const sourceKey = bindingMap.get(property.getName());
520
+ if (sourceKey) {
521
+ exposedProperties.push({
522
+ exposedKey: property.getName(),
523
+ sourceKey,
524
+ });
525
+ }
526
+ continue;
527
+ }
528
+ if (!Node.isPropertyAssignment(property)) {
529
+ continue;
530
+ }
531
+ const exposedKey = getStaticPropertyName(property.getNameNode());
532
+ const initializer = property.getInitializer();
533
+ if (!exposedKey || !Node.isIdentifier(initializer)) {
534
+ continue;
535
+ }
536
+ const sourceKey = bindingMap.get(initializer.getText());
537
+ if (!sourceKey) {
538
+ continue;
539
+ }
540
+ exposedProperties.push({
541
+ exposedKey,
542
+ sourceKey,
543
+ });
544
+ }
545
+ return exposedProperties;
546
+ }
547
+ function extractHelperYieldedProperties(exposeArgument, bindings) {
548
+ const bindingMap = new Map(bindings.map((binding) => [binding.localName, binding.sourceKey]));
549
+ const yieldedProperties = [];
550
+ for (const yieldExpression of exposeArgument.getDescendantsOfKind(SyntaxKind.YieldExpression)) {
551
+ const expression = yieldExpression.getExpression();
552
+ if (!Node.isCallExpression(expression)) {
553
+ continue;
554
+ }
555
+ const target = expression.getExpression();
556
+ if (!Node.isIdentifier(target)) {
557
+ continue;
558
+ }
559
+ const sourceKey = bindingMap.get(target.getText());
560
+ if (sourceKey) {
561
+ yieldedProperties.push(sourceKey);
562
+ }
563
+ }
564
+ return yieldedProperties;
565
+ }
334
566
  function createGeneratedDependencyKey(dependencyText) {
335
567
  var _a;
336
568
  const lastSegment = (_a = dependencyText.split('.').pop()) !== null && _a !== void 0 ? _a : dependencyText;
@@ -420,7 +652,11 @@ function formatGeneratedDependencyType(generatedDependencyGroups) {
420
652
  '{',
421
653
  ` deps: ${formatGeneratedDependencyObject(generatedDependencyGroups.deps)};`,
422
654
  ` provided: ${formatGeneratedDependencyObject(generatedDependencyGroups.provided)};`,
423
- ` missingProvider: ${formatGeneratedDependencyObject(generatedDependencyGroups.missingProvider)};`,
655
+ ...(generatedDependencyGroups.missingProvider.length > 0
656
+ ? [
657
+ ` missingProvider: ${formatGeneratedDependencyObject(generatedDependencyGroups.missingProvider)};`,
658
+ ]
659
+ : []),
424
660
  '}',
425
661
  ].join('\n');
426
662
  }
@@ -430,10 +666,19 @@ function formatGeneratedDependencyObject(entries) {
430
666
  }
431
667
  return [
432
668
  '{',
433
- ...entries.map((entry) => ` ${formatObjectKey(entry.key)}: ${entry.typeText};`),
669
+ ...entries.map((entry) => formatGeneratedDependencyEntry(entry)),
434
670
  ' }',
435
671
  ].join('\n');
436
672
  }
673
+ function formatGeneratedDependencyEntry(entry) {
674
+ const typeLines = entry.typeText.split('\n');
675
+ const lines = [
676
+ ` ${formatObjectKey(entry.key)}: ${typeLines[0]}`,
677
+ ...typeLines.slice(1).map((line) => ` ${line}`),
678
+ ];
679
+ lines[lines.length - 1] += ';';
680
+ return lines.join('\n');
681
+ }
437
682
  function formatObjectKey(key) {
438
683
  return /^[$A-Z_][0-9A-Z_$]*$/i.test(key) ? key : `'${key}'`;
439
684
  }
@@ -524,7 +769,12 @@ function getInjectionHelperDependency(expression) {
524
769
  return undefined;
525
770
  }
526
771
  export function ensureHelperImports(sourceFile, helperImportPath) {
527
- const missingImports = ['GetDeps'].filter((importName) => !hasLocalNamedImport(sourceFile, helperImportPath, importName));
772
+ const missingImports = [
773
+ 'DerivedService',
774
+ 'GetDeps',
775
+ 'GetInjectedServiceDependencies',
776
+ 'GetServiceOutput',
777
+ ].filter((importName) => !hasLocalNamedImport(sourceFile, helperImportPath, importName));
528
778
  if (missingImports.length === 0) {
529
779
  return;
530
780
  }
@@ -1107,6 +1357,43 @@ function isProvidedInInjectableTree(sourceFile, dependencyText) {
1107
1357
  !Node.isFalseLiteral(initializer) &&
1108
1358
  !(Node.isIdentifier(initializer) && initializer.getText() === 'undefined'));
1109
1359
  }
1360
+ function shouldGenerateLocalMissingProvider(sourceFile, dependencyText) {
1361
+ return (!resolveTrackedInjectHelperByName(sourceFile, dependencyText) &&
1362
+ !isProvidedInInjectableTree(sourceFile, dependencyText));
1363
+ }
1364
+ function resolveTrackedInjectHelperByName(sourceFile, dependencyText) {
1365
+ const [rootIdentifier] = dependencyText.split('.');
1366
+ if (!rootIdentifier) {
1367
+ return undefined;
1368
+ }
1369
+ for (const declaration of resolveDependencyIdentifierDeclarations(sourceFile, rootIdentifier)) {
1370
+ const helper = resolveTrackedInjectHelperFromDeclaration(declaration);
1371
+ if (helper) {
1372
+ return helper;
1373
+ }
1374
+ }
1375
+ return undefined;
1376
+ }
1377
+ function resolveDependencyIdentifierDeclarations(sourceFile, rootIdentifier) {
1378
+ var _a, _b, _c, _d;
1379
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
1380
+ const namedImport = importDeclaration.getNamedImports().find((specifier) => {
1381
+ var _a, _b;
1382
+ const localName = (_b = (_a = specifier.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : specifier.getNameNode().getText();
1383
+ return localName === rootIdentifier;
1384
+ });
1385
+ if (!namedImport) {
1386
+ continue;
1387
+ }
1388
+ const identifier = (_a = namedImport.getAliasNode()) !== null && _a !== void 0 ? _a : namedImport.getNameNode();
1389
+ const symbol = (_b = identifier.getSymbol()) !== null && _b !== void 0 ? _b : identifier.getType().getSymbol();
1390
+ const aliasedSymbol = (_c = symbol === null || symbol === void 0 ? void 0 : symbol.getAliasedSymbol()) !== null && _c !== void 0 ? _c : symbol;
1391
+ return (_d = aliasedSymbol === null || aliasedSymbol === void 0 ? void 0 : aliasedSymbol.getDeclarations()) !== null && _d !== void 0 ? _d : [namedImport];
1392
+ }
1393
+ return sourceFile
1394
+ .getDescendantsOfKind(SyntaxKind.BindingElement)
1395
+ .filter((bindingElement) => bindingElement.getName() === rootIdentifier);
1396
+ }
1110
1397
  function getDependencyTextFromTypeNode(typeNode) {
1111
1398
  if (isPrimitiveTypeNode(typeNode)) {
1112
1399
  return undefined;
@@ -1268,7 +1555,12 @@ function getExportRewriteSafety(sourceFile, classDeclaration, className) {
1268
1555
  : { safe: false, warnings };
1269
1556
  }
1270
1557
  function getHelperImportSafety(sourceFile, helperImportPath) {
1271
- const warnings = ['GetDeps']
1558
+ const warnings = [
1559
+ 'DerivedService',
1560
+ 'GetDeps',
1561
+ 'GetInjectedServiceDependencies',
1562
+ 'GetServiceOutput',
1563
+ ]
1272
1564
  .filter((helperName) => hasConflictingTopLevelBinding(sourceFile, helperImportPath, helperName))
1273
1565
  .map((helperName) => `Skipped file because "${helperName}" is already bound to a different top-level symbol.`);
1274
1566
  return warnings.length === 0