@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.
@@ -1,7 +1,8 @@
1
1
  import { __awaiter } from "tslib";
2
2
  import { Node, Project, QuoteKind, SyntaxKind, ts, } from 'ts-morph';
3
- import { existsSync, readdirSync, statSync } from 'node:fs';
4
- import { basename, extname, isAbsolute, join, resolve } from 'node:path';
3
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
4
+ import { createRequire } from 'node:module';
5
+ import { basename, dirname, extname, isAbsolute, join, resolve, } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  const SUPPORTED_DECORATORS = {
7
8
  Component: 'component',
@@ -14,7 +15,53 @@ const DEFAULT_OPTIONS = {
14
15
  transformOnlyStandaloneDeclarables: false,
15
16
  includeProviders: true,
16
17
  includeViewProviders: true,
18
+ config: undefined,
19
+ configFilePath: undefined,
17
20
  };
21
+ const ANGULAR_BRAND_CONFIG_FILE_NAME = 'craft-brand.config.ts';
22
+ const DEFAULT_ANGULAR_BRAND_METADATA_CONTEXTS = [
23
+ 'imports',
24
+ 'hostDirectives',
25
+ ];
26
+ const DEFAULT_ANGULAR_BRAND_CONFIG = defineAngularBrandConfig({
27
+ importAugmentations: [
28
+ {
29
+ match: {
30
+ module: '@angular/router',
31
+ },
32
+ deps: [
33
+ {
34
+ key: 'Router',
35
+ symbol: 'Router',
36
+ module: '@angular/router',
37
+ },
38
+ ],
39
+ missingProvider: [
40
+ {
41
+ key: 'Router',
42
+ symbol: 'Router',
43
+ module: '@angular/router',
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ match: {
49
+ module: '@angular/forms/signals',
50
+ symbols: ['FormField'],
51
+ metadata: ['imports'],
52
+ },
53
+ deps: [
54
+ {
55
+ key: 'FormField',
56
+ symbol: 'FormField',
57
+ typeText: 'FormField<any>',
58
+ module: '@angular/forms/signals',
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ });
64
+ const angularBrandConfigCache = new Map();
18
65
  const PRIMITIVE_TYPE_TEXTS = new Set([
19
66
  'any',
20
67
  'bigint',
@@ -56,6 +103,194 @@ const IGNORED_DIRECTORIES = new Set([
56
103
  'out-tsc',
57
104
  'tmp',
58
105
  ]);
106
+ export function defineAngularBrandConfig(config) {
107
+ return config;
108
+ }
109
+ export function discoverAngularBrandConfigFilePath(searchFromDir, stopDir) {
110
+ const resolvedStopDir = stopDir ? resolve(stopDir) : undefined;
111
+ let currentDir = resolve(searchFromDir);
112
+ while (true) {
113
+ const candidatePath = join(currentDir, ANGULAR_BRAND_CONFIG_FILE_NAME);
114
+ if (existsSync(candidatePath) && statSync(candidatePath).isFile()) {
115
+ return candidatePath;
116
+ }
117
+ if (resolvedStopDir && currentDir === resolvedStopDir) {
118
+ return undefined;
119
+ }
120
+ const parentDir = dirname(currentDir);
121
+ if (parentDir === currentDir) {
122
+ return undefined;
123
+ }
124
+ currentDir = parentDir;
125
+ }
126
+ }
127
+ export function loadAngularBrandConfigFromFile(configFilePath) {
128
+ var _a;
129
+ const resolvedConfigFilePath = resolve(configFilePath);
130
+ const cachedConfig = angularBrandConfigCache.get(resolvedConfigFilePath);
131
+ if (cachedConfig) {
132
+ return cachedConfig;
133
+ }
134
+ if (!existsSync(resolvedConfigFilePath) ||
135
+ !statSync(resolvedConfigFilePath).isFile()) {
136
+ throw new Error(`Angular brand config file not found at "${resolvedConfigFilePath}".`);
137
+ }
138
+ try {
139
+ const sourceText = readFileSync(resolvedConfigFilePath, 'utf8');
140
+ const transpiled = ts.transpileModule(sourceText, {
141
+ compilerOptions: {
142
+ module: ts.ModuleKind.CommonJS,
143
+ target: ts.ScriptTarget.ES2022,
144
+ esModuleInterop: true,
145
+ allowSyntheticDefaultImports: true,
146
+ },
147
+ fileName: resolvedConfigFilePath,
148
+ });
149
+ const module = { exports: {} };
150
+ const moduleRequire = createRequire(resolvedConfigFilePath);
151
+ const compiledModule = new Function('exports', 'require', 'module', '__filename', '__dirname', transpiled.outputText);
152
+ compiledModule(module.exports, (specifier) => {
153
+ if (specifier === '@craft-ng/dev-tools') {
154
+ return { defineAngularBrandConfig };
155
+ }
156
+ return moduleRequire(specifier);
157
+ }, module, resolvedConfigFilePath, dirname(resolvedConfigFilePath));
158
+ const exportedConfig = (_a = module.exports['default']) !== null && _a !== void 0 ? _a : (module.exports['__esModule']
159
+ ? module.exports['default']
160
+ : module.exports);
161
+ const validatedConfig = validateAngularBrandConfig(exportedConfig, resolvedConfigFilePath);
162
+ angularBrandConfigCache.set(resolvedConfigFilePath, validatedConfig);
163
+ return validatedConfig;
164
+ }
165
+ catch (error) {
166
+ throw new Error(`Invalid Angular brand config at "${resolvedConfigFilePath}": ${getErrorMessage(error)}`);
167
+ }
168
+ }
169
+ function resolveAngularBrandConfig(sourceFile, options) {
170
+ var _a;
171
+ const explicitConfig = (_a = options.config) !== null && _a !== void 0 ? _a : (options.configFilePath
172
+ ? loadAngularBrandConfigFromFile(options.configFilePath)
173
+ : loadDiscoveredAngularBrandConfig(dirname(sourceFile.getFilePath())));
174
+ return normalizeAngularBrandConfig(explicitConfig);
175
+ }
176
+ function loadDiscoveredAngularBrandConfig(searchFromDir, stopDir) {
177
+ const discoveredConfigPath = discoverAngularBrandConfigFilePath(searchFromDir, stopDir);
178
+ return discoveredConfigPath
179
+ ? loadAngularBrandConfigFromFile(discoveredConfigPath)
180
+ : undefined;
181
+ }
182
+ function normalizeAngularBrandConfig(config) {
183
+ var _a, _b;
184
+ const builtInRules = ((_a = DEFAULT_ANGULAR_BRAND_CONFIG.importAugmentations) !== null && _a !== void 0 ? _a : []).map(normalizeAngularBrandImportAugmentationRule);
185
+ const configuredRules = ((_b = config === null || config === void 0 ? void 0 : config.importAugmentations) !== null && _b !== void 0 ? _b : []).map(normalizeAngularBrandImportAugmentationRule);
186
+ return {
187
+ importAugmentations: [...builtInRules, ...configuredRules],
188
+ };
189
+ }
190
+ function normalizeAngularBrandImportAugmentationRule(rule) {
191
+ return {
192
+ match: {
193
+ module: rule.match.module,
194
+ symbols: rule.match.symbols ? [...rule.match.symbols] : undefined,
195
+ metadata: rule.match.metadata
196
+ ? [...rule.match.metadata]
197
+ : [...DEFAULT_ANGULAR_BRAND_METADATA_CONTEXTS],
198
+ },
199
+ deps: rule.deps ? [...rule.deps] : [],
200
+ missingProvider: rule.missingProvider ? [...rule.missingProvider] : [],
201
+ };
202
+ }
203
+ function validateAngularBrandConfig(value, configFilePath) {
204
+ var _a;
205
+ if (!isPlainObject(value)) {
206
+ throw new Error('Expected the default export to be an object.');
207
+ }
208
+ const importAugmentations = (_a = readOptionalArray(value['importAugmentations'], 'importAugmentations', configFilePath)) === null || _a === void 0 ? void 0 : _a.map((entry, index) => validateAngularBrandImportAugmentationRule(entry, `importAugmentations[${index}]`, configFilePath));
209
+ return {
210
+ importAugmentations: importAugmentations !== null && importAugmentations !== void 0 ? importAugmentations : [],
211
+ };
212
+ }
213
+ function validateAngularBrandImportAugmentationRule(value, pathLabel, configFilePath) {
214
+ var _a, _b, _c, _d, _e, _f;
215
+ if (!isPlainObject(value)) {
216
+ throw new Error(`${pathLabel} must be an object.`);
217
+ }
218
+ if (!isPlainObject(value['match'])) {
219
+ throw new Error(`${pathLabel}.match must be an object.`);
220
+ }
221
+ const match = value['match'];
222
+ const moduleSpecifier = readRequiredString(match['module'], `${pathLabel}.match.module`, configFilePath);
223
+ const symbols = readOptionalStringArray(match['symbols'], `${pathLabel}.match.symbols`, configFilePath);
224
+ const metadata = (_b = (_a = readOptionalStringArray(match['metadata'], `${pathLabel}.match.metadata`, configFilePath)) === null || _a === void 0 ? void 0 : _a.map((context) => validateAngularBrandMetadataContext(context, pathLabel))) !== null && _b !== void 0 ? _b : undefined;
225
+ return {
226
+ match: {
227
+ module: moduleSpecifier,
228
+ symbols,
229
+ metadata,
230
+ },
231
+ deps: (_d = (_c = readOptionalArray(value['deps'], `${pathLabel}.deps`, configFilePath)) === null || _c === void 0 ? void 0 : _c.map((entry, index) => validateAngularBrandConfigEntry(entry, `${pathLabel}.deps[${index}]`, configFilePath))) !== null && _d !== void 0 ? _d : [],
232
+ missingProvider: (_f = (_e = readOptionalArray(value['missingProvider'], `${pathLabel}.missingProvider`, configFilePath)) === null || _e === void 0 ? void 0 : _e.map((entry, index) => validateAngularBrandConfigEntry(entry, `${pathLabel}.missingProvider[${index}]`, configFilePath))) !== null && _f !== void 0 ? _f : [],
233
+ };
234
+ }
235
+ function validateAngularBrandConfigEntry(value, pathLabel, configFilePath) {
236
+ if (!isPlainObject(value)) {
237
+ throw new Error(`${pathLabel} must be an object.`);
238
+ }
239
+ const key = readRequiredString(value['key'], `${pathLabel}.key`, configFilePath);
240
+ const symbol = readRequiredString(value['symbol'], `${pathLabel}.symbol`, configFilePath);
241
+ const typeText = readOptionalString(value['typeText'], `${pathLabel}.typeText`, configFilePath);
242
+ const moduleSpecifier = readOptionalString(value['module'], `${pathLabel}.module`, configFilePath);
243
+ return {
244
+ key,
245
+ symbol,
246
+ typeText,
247
+ module: moduleSpecifier,
248
+ };
249
+ }
250
+ function validateAngularBrandMetadataContext(value, pathLabel) {
251
+ if (value === 'imports' || value === 'hostDirectives') {
252
+ return value;
253
+ }
254
+ throw new Error(`${pathLabel}.match.metadata must contain only "imports" or "hostDirectives".`);
255
+ }
256
+ function readOptionalArray(value, pathLabel, configFilePath) {
257
+ if (value === undefined) {
258
+ return undefined;
259
+ }
260
+ if (!Array.isArray(value)) {
261
+ throw new Error(`${pathLabel} in "${configFilePath}" must be an array.`);
262
+ }
263
+ return value;
264
+ }
265
+ function readRequiredString(value, pathLabel, configFilePath) {
266
+ const stringValue = readOptionalString(value, pathLabel, configFilePath);
267
+ if (!stringValue) {
268
+ throw new Error(`${pathLabel} in "${configFilePath}" must be a string.`);
269
+ }
270
+ return stringValue;
271
+ }
272
+ function readOptionalString(value, pathLabel, configFilePath) {
273
+ if (value === undefined) {
274
+ return undefined;
275
+ }
276
+ if (typeof value !== 'string') {
277
+ throw new Error(`${pathLabel} in "${configFilePath}" must be a string.`);
278
+ }
279
+ return value;
280
+ }
281
+ function readOptionalStringArray(value, pathLabel, configFilePath) {
282
+ const arrayValue = readOptionalArray(value, pathLabel, configFilePath);
283
+ if (!arrayValue) {
284
+ return undefined;
285
+ }
286
+ return arrayValue.map((entry, index) => readRequiredString(entry, `${pathLabel}[${index}]`, configFilePath));
287
+ }
288
+ function isPlainObject(value) {
289
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
290
+ }
291
+ function getErrorMessage(error) {
292
+ return error instanceof Error ? error.message : String(error);
293
+ }
59
294
  export function transformSourceFile(sourceFile, options = {}) {
60
295
  const normalizedOptions = normalizeOptions(options);
61
296
  const analysis = analyzeSourceFileDependencies(sourceFile, options);
@@ -79,6 +314,7 @@ export function transformSourceFile(sourceFile, options = {}) {
79
314
  }
80
315
  export function analyzeSourceFileDependencies(sourceFile, options = {}) {
81
316
  const normalizedOptions = normalizeOptions(options);
317
+ const angularBrandConfig = resolveAngularBrandConfig(sourceFile, normalizedOptions);
82
318
  const result = {
83
319
  skipped: false,
84
320
  warnings: [],
@@ -118,22 +354,18 @@ export function analyzeSourceFileDependencies(sourceFile, options = {}) {
118
354
  ? extractProvidedDependencies(sourceFile, classDeclaration, angularKind, normalizedOptions)
119
355
  : { entries: [], warnings: [] };
120
356
  const constructorDeps = extractConstructorDeps(classDeclaration);
121
- const injectCallDeps = extractInjectCallDeps(classDeclaration);
122
- result.warnings.push(...metadataDeps.warnings, ...providedDeps.warnings, ...constructorDeps.warnings, ...injectCallDeps.warnings);
123
- result.dependencies = mergeDeps(constructorDeps.dependencies, injectCallDeps.dependencies, metadataDeps.imports, metadataDeps.hostDirectives, metadataDeps.providers, metadataDeps.viewProviders);
357
+ const propertyDeps = extractPropertyDependencies(sourceFile, classDeclaration, className);
358
+ const generatedDependencyAugmentation = createGeneratedDependencyGroupAugmentation(sourceFile, metadataDeps.occurrences, angularBrandConfig);
359
+ const syntheticDependencyTexts = mergeDeps(generatedDependencyAugmentation.deps.map((dependency) => dependency.dependencyText), generatedDependencyAugmentation.legacyInjectedDependencies.map((dependency) => dependency.dependencyText));
360
+ result.warnings.push(...metadataDeps.warnings, ...providedDeps.warnings, ...constructorDeps.warnings, ...propertyDeps.warnings);
361
+ result.dependencies = mergeDeps(constructorDeps.dependencies, propertyDeps.dependencies, metadataDeps.imports, metadataDeps.hostDirectives, metadataDeps.providers, metadataDeps.viewProviders, syntheticDependencyTexts);
124
362
  result.dependencyGroups = {
125
- injected: mergeDeps(constructorDeps.dependencies, injectCallDeps.dependencies),
363
+ injected: mergeDeps(constructorDeps.dependencies, propertyDeps.dependencies, generatedDependencyAugmentation.legacyInjectedDependencies.map((dependency) => dependency.dependencyText)),
126
364
  importDeps: mergeDeps(metadataDeps.imports, metadataDeps.hostDirectives),
127
365
  providers: mergeDeps(metadataDeps.providers, metadataDeps.viewProviders),
128
366
  };
129
367
  result.generatedTypeName = getGeneratedDepsTypeName(className);
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);
368
+ result.generatedDependencyGroups = createGeneratedDependencyGroups(sourceFile, className, result.dependencyGroups.importDeps, constructorDeps.dependencies, propertyDeps, providedDeps.entries, generatedDependencyAugmentation);
137
369
  return result;
138
370
  }
139
371
  export function findAngularDecoratedClass(sourceFile) {
@@ -268,6 +500,53 @@ export function extractInjectCallDeps(classDeclaration) {
268
500
  generatedDependencies,
269
501
  };
270
502
  }
503
+ function extractPropertyDependencies(sourceFile, classDeclaration, className) {
504
+ const warnings = [];
505
+ const dependencies = [];
506
+ const entries = [];
507
+ const missingProvider = [];
508
+ for (const property of classDeclaration.getProperties()) {
509
+ if (property.isStatic()) {
510
+ continue;
511
+ }
512
+ const propertyName = getStaticPropertyName(property.getNameNode());
513
+ if (!propertyName) {
514
+ warnings.push(`Skipped property dependency tracking for ${property.getText()} because the property name is not static.`);
515
+ continue;
516
+ }
517
+ const initializer = property.getInitializer();
518
+ if (Node.isCallExpression(initializer)) {
519
+ const expression = initializer.getExpression();
520
+ const injectMethodName = getInjectMethodName(expression);
521
+ if (injectMethodName === null || injectMethodName === void 0 ? void 0 : injectMethodName.startsWith('inject')) {
522
+ const dependencyText = injectMethodName === 'inject'
523
+ ? getAngularInjectCallDependency(initializer)
524
+ : getInjectionHelperDependency(expression);
525
+ if (!dependencyText) {
526
+ warnings.push(`Skipped ${injectMethodName}() property dependency tracking for "${propertyName}" because the dependency is not static.`);
527
+ }
528
+ else {
529
+ dependencies.push(dependencyText);
530
+ const dependencyEntry = injectMethodName === 'inject'
531
+ ? createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject')
532
+ : createGeneratedInjectHelperDependencyEntry(sourceFile, initializer, expression, dependencyText);
533
+ entries.push(createGeneratedPropertyDependencyEntry(propertyName, createSingleDependencyMapTypeText(dependencyEntry)));
534
+ if (shouldGenerateLocalMissingProvider(sourceFile, dependencyText)) {
535
+ missingProvider.push(createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject'));
536
+ }
537
+ continue;
538
+ }
539
+ }
540
+ }
541
+ entries.push(createGeneratedPropertyDependencyEntry(propertyName, createExtractDepsTypeText(className, propertyName)));
542
+ }
543
+ return {
544
+ dependencies: mergeDeps(dependencies),
545
+ warnings,
546
+ entries,
547
+ missingProvider: mergeGeneratedDependencyEntries(missingProvider),
548
+ };
549
+ }
271
550
  export function mergeDeps(...dependencyGroups) {
272
551
  const seen = new Set();
273
552
  const dependencies = [];
@@ -292,44 +571,83 @@ function emptyDependencyGroups() {
292
571
  function emptyGeneratedDependencyGroups() {
293
572
  return {
294
573
  deps: [],
574
+ propertiesDeps: [],
295
575
  provided: [],
296
576
  missingProvider: [],
297
577
  };
298
578
  }
579
+ function emptyGeneratedDependencyGroupAugmentation() {
580
+ return {
581
+ deps: [],
582
+ missingProvider: [],
583
+ legacyInjectedDependencies: [],
584
+ };
585
+ }
299
586
  function getGeneratedDepsTypeName(className) {
300
587
  return `GenDeps_${className}`;
301
588
  }
302
- function createGeneratedDependencyGroups(sourceFile, importDependencies, injectedDependencies, providedEntries) {
303
- const deps = mergeGeneratedDependencyEntries(importDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'import')), injectedDependencies.map((dependency) => dependency.entry));
589
+ function createGeneratedDependencyGroups(sourceFile, className, importDependencies, constructorDependencies, propertyDependencies, providedEntries, augmentation) {
590
+ const constructorEntries = constructorDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'inject'));
591
+ const deps = mergeGeneratedDependencyEntries(importDependencies.map((dependency) => createGeneratedDependencyEntry(sourceFile, dependency, 'import')), constructorEntries, augmentation.deps.map((dependency) => dependency.entry));
592
+ const propertiesDeps = mergeGeneratedDependencyEntries(propertyDependencies.entries);
304
593
  const provided = mergeGeneratedDependencyEntries(providedEntries);
305
594
  const providedKeys = new Set(provided.map((entry) => entry.key));
306
- const injectedEntriesByKey = new Map(injectedDependencies.map((dependency) => [
307
- dependency.entry.key,
308
- dependency.entry,
309
- ]));
310
- const missingProvider = mergeGeneratedDependencyEntries(injectedDependencies
311
- .filter((dependency) => {
312
- const dependencyKey = dependency.entry.key;
313
- return (!providedKeys.has(dependencyKey) &&
314
- shouldGenerateLocalMissingProvider(sourceFile, dependency.dependencyText));
315
- })
316
- .map((dependency) => {
317
- var _a;
318
- return (_a = injectedEntriesByKey.get(dependency.entry.key)) !== null && _a !== void 0 ? _a : createGeneratedDependencyEntry(sourceFile, dependency.dependencyText, 'inject');
319
- }));
595
+ const missingProvider = mergeGeneratedDependencyEntries(constructorDependencies
596
+ .filter((dependencyText) => shouldGenerateLocalMissingProvider(sourceFile, dependencyText))
597
+ .map((dependencyText) => createGeneratedDependencyEntry(sourceFile, dependencyText, 'inject')), propertyDependencies.missingProvider, augmentation.missingProvider
598
+ .map((dependency) => dependency.entry)
599
+ .filter((entry) => !providedKeys.has(entry.key)));
320
600
  return {
321
601
  deps,
602
+ propertiesDeps,
322
603
  provided,
323
604
  missingProvider,
324
605
  };
325
606
  }
607
+ function createGeneratedDependencyGroupAugmentation(sourceFile, metadataOccurrences, config) {
608
+ if (metadataOccurrences.length === 0) {
609
+ return emptyGeneratedDependencyGroupAugmentation();
610
+ }
611
+ const deps = [];
612
+ const missingProvider = [];
613
+ const legacyInjectedDependencies = [];
614
+ for (const rule of config.importAugmentations) {
615
+ if (!metadataOccurrences.some((occurrence) => ruleMatchesMetadataOccurrence(rule, occurrence))) {
616
+ continue;
617
+ }
618
+ const generatedDeps = rule.deps.map((entry) => createConfiguredGeneratedDependencyDescriptor(sourceFile, entry, rule.match.module));
619
+ const generatedMissingProviders = rule.missingProvider.map((entry) => createConfiguredGeneratedDependencyDescriptor(sourceFile, entry, rule.match.module));
620
+ deps.push(...generatedDeps);
621
+ missingProvider.push(...generatedMissingProviders);
622
+ legacyInjectedDependencies.push(...generatedMissingProviders);
623
+ }
624
+ return {
625
+ deps,
626
+ missingProvider,
627
+ legacyInjectedDependencies,
628
+ };
629
+ }
630
+ function ruleMatchesMetadataOccurrence(rule, occurrence) {
631
+ if (occurrence.moduleSpecifier !== rule.match.module) {
632
+ return false;
633
+ }
634
+ if (!rule.match.metadata.includes(occurrence.metadataContext)) {
635
+ return false;
636
+ }
637
+ return (!rule.match.symbols || rule.match.symbols.includes(occurrence.symbolName));
638
+ }
639
+ function createConfiguredGeneratedDependencyDescriptor(sourceFile, entry, defaultModuleSpecifier) {
640
+ var _a;
641
+ return {
642
+ dependencyText: entry.symbol,
643
+ entry: createSyntheticGeneratedDependencyEntry(sourceFile, entry.symbol, 'inject', (_a = entry.module) !== null && _a !== void 0 ? _a : defaultModuleSpecifier, entry.key, entry.typeText),
644
+ };
645
+ }
326
646
  function mergeGeneratedDependencyEntries(...groups) {
327
647
  const entries = new Map();
328
648
  for (const group of groups) {
329
649
  for (const entry of group) {
330
- if (!entries.has(entry.key)) {
331
- entries.set(entry.key, entry);
332
- }
650
+ entries.set(entry.key, entry);
333
651
  }
334
652
  }
335
653
  return [...entries.values()];
@@ -356,6 +674,12 @@ function createGeneratedDependencyEntry(sourceFile, dependencyText, context) {
356
674
  typeText: createGeneratedDependencyTypeText(sourceFile, dependencyText),
357
675
  };
358
676
  }
677
+ function createSyntheticGeneratedDependencyEntry(sourceFile, dependencyText, context, moduleSpecifier, key = createGeneratedDependencyKey(dependencyText), typeText) {
678
+ return Object.assign(Object.assign({}, createGeneratedDependencyEntry(sourceFile, dependencyText, context)), { key, typeText: typeText !== null && typeText !== void 0 ? typeText : createGeneratedDependencyTypeText(sourceFile, dependencyText), typeImport: {
679
+ moduleSpecifier,
680
+ name: dependencyText,
681
+ } });
682
+ }
359
683
  function createGeneratedInjectHelperDependencyEntry(sourceFile, callExpression, expression, dependencyText) {
360
684
  const trackedHelper = resolveTrackedInjectHelper(expression);
361
685
  if (!trackedHelper) {
@@ -364,11 +688,11 @@ function createGeneratedInjectHelperDependencyEntry(sourceFile, callExpression,
364
688
  const exposureTracking = extractHelperExposureTracking(callExpression);
365
689
  return {
366
690
  key: trackedHelper.serviceName,
367
- typeText: createTrackedInjectHelperTypeText(dependencyText, exposureTracking),
691
+ typeText: createTrackedInjectHelperTypeText(dependencyText, trackedHelper.serviceName, exposureTracking),
368
692
  };
369
693
  }
370
- function createTrackedInjectHelperTypeText(dependencyText, exposureTracking) {
371
- const baseType = `GetInjectedServiceDependencies<typeof ${dependencyText}>`;
694
+ function createTrackedInjectHelperTypeText(dependencyText, serviceName, exposureTracking) {
695
+ const baseType = `ExtractDeps<typeof ${dependencyText}>[${JSON.stringify(serviceName)}]`;
372
696
  if (!exposureTracking) {
373
697
  return baseType;
374
698
  }
@@ -385,6 +709,18 @@ function createTrackedInjectHelperTypeText(dependencyText, exposureTracking) {
385
709
  '}>',
386
710
  ].join('\n');
387
711
  }
712
+ function createGeneratedPropertyDependencyEntry(propertyName, typeText) {
713
+ return {
714
+ key: propertyName,
715
+ typeText,
716
+ };
717
+ }
718
+ function createExtractDepsTypeText(className, propertyName) {
719
+ return `ExtractDeps<${className}[${JSON.stringify(propertyName)}]>`;
720
+ }
721
+ function createSingleDependencyMapTypeText(entry) {
722
+ return formatGeneratedDependencyObject([entry]);
723
+ }
388
724
  function formatHelperExposurePropertiesType(dependencyText, properties) {
389
725
  if (properties.length === 0) {
390
726
  return '{}';
@@ -629,7 +965,10 @@ function resolveAngularDeclarableDependency(sourceFile, dependencyText) {
629
965
  function ensureGeneratedDependencyTypeImports(sourceFile, generatedDependencyGroups) {
630
966
  var _a;
631
967
  const importsToAdd = new Map();
632
- for (const entry of generatedDependencyGroups.deps) {
968
+ for (const entry of [
969
+ ...generatedDependencyGroups.deps,
970
+ ...generatedDependencyGroups.missingProvider,
971
+ ]) {
633
972
  if (!entry.typeImport) {
634
973
  continue;
635
974
  }
@@ -640,9 +979,18 @@ function ensureGeneratedDependencyTypeImports(sourceFile, generatedDependencyGro
640
979
  for (const [moduleSpecifier, importNames] of importsToAdd) {
641
980
  const existingImport = sourceFile
642
981
  .getImportDeclarations()
643
- .find((importDeclaration) => importDeclaration.getModuleSpecifierValue() === moduleSpecifier);
982
+ .find((importDeclaration) => importDeclaration.getModuleSpecifierValue() === moduleSpecifier &&
983
+ !importDeclaration.getNamespaceImport());
644
984
  if (existingImport) {
645
- existingImport.addNamedImports([...importNames].map((name) => ({ name, isTypeOnly: true })));
985
+ const existingImportNames = new Set(existingImport.getNamedImports().map((namedImport) => {
986
+ var _a, _b;
987
+ return ((_b = (_a = namedImport.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : namedImport.getNameNode().getText());
988
+ }));
989
+ const namesToAdd = [...importNames].filter((name) => !existingImportNames.has(name));
990
+ if (namesToAdd.length === 0) {
991
+ continue;
992
+ }
993
+ existingImport.addNamedImports(namesToAdd.map((name) => ({ name, isTypeOnly: true })));
646
994
  continue;
647
995
  }
648
996
  sourceFile.addImportDeclaration({
@@ -658,6 +1006,11 @@ function formatGeneratedDependencyType(angularKind, className, generatedDependen
658
1006
  return [
659
1007
  '{',
660
1008
  ` deps: ${formatGeneratedDependencyObject(generatedDependencyGroups.deps)};`,
1009
+ ...(angularKind === 'component'
1010
+ ? [
1011
+ ` propertiesDeps: ${formatGeneratedDependencyObject(generatedDependencyGroups.propertiesDeps)};`,
1012
+ ]
1013
+ : []),
661
1014
  ` provided: ${formatGeneratedDependencyObject(generatedDependencyGroups.provided)};`,
662
1015
  ...(angularKind === 'component'
663
1016
  ? [` publicProperties: GetPublicComponentProperties<${className}>;`]
@@ -781,8 +1134,8 @@ function getInjectionHelperDependency(expression) {
781
1134
  export function ensureHelperImports(sourceFile, helperImportPath) {
782
1135
  const missingImports = [
783
1136
  'DerivedService',
1137
+ 'ExtractDeps',
784
1138
  'GetDeps',
785
- 'GetInjectedServiceDependencies',
786
1139
  'GetPublicComponentProperties',
787
1140
  'GetServiceOutput',
788
1141
  ].filter((importName) => !hasLocalNamedImport(sourceFile, helperImportPath, importName));
@@ -884,8 +1237,11 @@ export function formatDependencyGroups(dependencyGroups) {
884
1237
  }
885
1238
  export function runAngularBrandCodemod() {
886
1239
  return __awaiter(this, arguments, void 0, function* (options = {}) {
887
- var _a, _b, _c;
1240
+ var _a, _b, _c, _d;
888
1241
  const rootDir = resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
1242
+ const config = (_b = options.config) !== null && _b !== void 0 ? _b : (options.configFilePath
1243
+ ? loadAngularBrandConfigFromFile(options.configFilePath)
1244
+ : loadDiscoveredAngularBrandConfig(rootDir));
889
1245
  const tsConfigFilePath = options.tsConfigFilePath
890
1246
  ? resolve(options.tsConfigFilePath)
891
1247
  : getDefaultTsConfigPath(rootDir);
@@ -909,7 +1265,7 @@ export function runAngularBrandCodemod() {
909
1265
  if (!isWithinRoot(sourceFile.getFilePath(), rootDir)) {
910
1266
  continue;
911
1267
  }
912
- const result = transformSourceFile(sourceFile, options);
1268
+ const result = transformSourceFile(sourceFile, Object.assign(Object.assign({}, options), { config }));
913
1269
  const report = Object.assign(Object.assign({}, result), { filePath: sourceFile.getFilePath() });
914
1270
  summary.files.push(report);
915
1271
  summary.warnings += result.warnings.length;
@@ -926,9 +1282,9 @@ export function runAngularBrandCodemod() {
926
1282
  if (result.skipped) {
927
1283
  summary.skippedFiles += 1;
928
1284
  }
929
- logFileResult(report, (_b = options.log) !== null && _b !== void 0 ? _b : console.log);
1285
+ logFileResult(report, (_c = options.log) !== null && _c !== void 0 ? _c : console.log);
930
1286
  }
931
- logSummary(summary, (_c = options.log) !== null && _c !== void 0 ? _c : console.log);
1287
+ logSummary(summary, (_d = options.log) !== null && _d !== void 0 ? _d : console.log);
932
1288
  return summary;
933
1289
  });
934
1290
  }
@@ -939,6 +1295,8 @@ function normalizeOptions(options) {
939
1295
  transformOnlyStandaloneDeclarables: (_b = options.transformOnlyStandaloneDeclarables) !== null && _b !== void 0 ? _b : DEFAULT_OPTIONS.transformOnlyStandaloneDeclarables,
940
1296
  includeProviders: (_c = options.includeProviders) !== null && _c !== void 0 ? _c : DEFAULT_OPTIONS.includeProviders,
941
1297
  includeViewProviders: (_d = options.includeViewProviders) !== null && _d !== void 0 ? _d : DEFAULT_OPTIONS.includeViewProviders,
1298
+ config: options.config,
1299
+ configFilePath: options.configFilePath,
942
1300
  };
943
1301
  }
944
1302
  function skip(result, warnings) {
@@ -1008,6 +1366,7 @@ function getObjectPropertyAssignment(objectLiteral, propertyName) {
1008
1366
  return Node.isPropertyAssignment(property) ? property : undefined;
1009
1367
  }
1010
1368
  function extractDecoratorMetadataDepGroups(classDeclaration, angularKind, options) {
1369
+ const sourceFile = classDeclaration.getSourceFile();
1011
1370
  const metadata = getDecoratorMetadataObject(classDeclaration);
1012
1371
  const groups = emptyMetadataDependencyGroups();
1013
1372
  if (!metadata) {
@@ -1021,6 +1380,10 @@ function extractDecoratorMetadataDepGroups(classDeclaration, angularKind, option
1021
1380
  if (options.includeViewProviders && angularKind === 'component') {
1022
1381
  groups.viewProviders = extractMetadataArrayProperty(metadata, 'viewProviders', 'viewProviders', groups.warnings);
1023
1382
  }
1383
+ groups.occurrences = [
1384
+ ...groups.imports.map((dependencyText) => createMetadataDependencyOccurrence(sourceFile, dependencyText, 'imports')),
1385
+ ...groups.hostDirectives.map((dependencyText) => createMetadataDependencyOccurrence(sourceFile, dependencyText, 'hostDirectives')),
1386
+ ];
1024
1387
  return groups;
1025
1388
  }
1026
1389
  function emptyMetadataDependencyGroups() {
@@ -1029,9 +1392,20 @@ function emptyMetadataDependencyGroups() {
1029
1392
  hostDirectives: [],
1030
1393
  providers: [],
1031
1394
  viewProviders: [],
1395
+ occurrences: [],
1032
1396
  warnings: [],
1033
1397
  };
1034
1398
  }
1399
+ function createMetadataDependencyOccurrence(sourceFile, dependencyText, metadataContext) {
1400
+ var _a, _b;
1401
+ const resolution = resolveDependencyReference(sourceFile, dependencyText);
1402
+ return {
1403
+ dependencyText,
1404
+ symbolName: (_b = (_a = resolution.importedName) !== null && _a !== void 0 ? _a : dependencyText.split('.').pop()) !== null && _b !== void 0 ? _b : dependencyText,
1405
+ moduleSpecifier: resolution.moduleSpecifier,
1406
+ metadataContext,
1407
+ };
1408
+ }
1035
1409
  function extractProvidedDependencies(sourceFile, classDeclaration, angularKind, options) {
1036
1410
  const metadata = getDecoratorMetadataObject(classDeclaration);
1037
1411
  const warnings = [];
@@ -1276,10 +1650,11 @@ function resolveImportedDependencyReference(sourceFile, dependencyText, rootIden
1276
1650
  return {
1277
1651
  kind: 'class',
1278
1652
  classDeclaration: declaration,
1653
+ importedName: rootIdentifier,
1279
1654
  moduleSpecifier,
1280
1655
  };
1281
1656
  }
1282
- return { kind: 'unknown', moduleSpecifier };
1657
+ return { kind: 'unknown', importedName: rootIdentifier, moduleSpecifier };
1283
1658
  }
1284
1659
  if (((_b = importDeclaration.getNamespaceImport()) === null || _b === void 0 ? void 0 : _b.getText()) === rootIdentifier) {
1285
1660
  const declaration = getNamespaceImportClassDeclaration(importDeclaration, dependencyText);
@@ -1287,10 +1662,15 @@ function resolveImportedDependencyReference(sourceFile, dependencyText, rootIden
1287
1662
  return {
1288
1663
  kind: 'class',
1289
1664
  classDeclaration: declaration,
1665
+ importedName: dependencyText.split('.').pop(),
1290
1666
  moduleSpecifier,
1291
1667
  };
1292
1668
  }
1293
- return { kind: 'namespace', moduleSpecifier };
1669
+ return {
1670
+ kind: 'namespace',
1671
+ importedName: dependencyText.split('.').pop(),
1672
+ moduleSpecifier,
1673
+ };
1294
1674
  }
1295
1675
  const namedImport = importDeclaration
1296
1676
  .getNamedImports()
@@ -1311,19 +1691,36 @@ function resolveImportedDependencyReference(sourceFile, dependencyText, rootIden
1311
1691
  return {
1312
1692
  kind: 'class',
1313
1693
  classDeclaration,
1694
+ importedName: namedImport.getNameNode().getText(),
1314
1695
  moduleSpecifier,
1315
1696
  };
1316
1697
  }
1317
1698
  if (declarations.some((declaration) => Node.isEnumDeclaration(declaration))) {
1318
- return { kind: 'enum', moduleSpecifier };
1699
+ return {
1700
+ kind: 'enum',
1701
+ importedName: namedImport.getNameNode().getText(),
1702
+ moduleSpecifier,
1703
+ };
1319
1704
  }
1320
1705
  if (declarations.some((declaration) => Node.isFunctionDeclaration(declaration))) {
1321
- return { kind: 'function', moduleSpecifier };
1706
+ return {
1707
+ kind: 'function',
1708
+ importedName: namedImport.getNameNode().getText(),
1709
+ moduleSpecifier,
1710
+ };
1322
1711
  }
1323
1712
  if (declarations.some((declaration) => Node.isVariableDeclaration(declaration))) {
1324
- return { kind: 'variable', moduleSpecifier };
1713
+ return {
1714
+ kind: 'variable',
1715
+ importedName: namedImport.getNameNode().getText(),
1716
+ moduleSpecifier,
1717
+ };
1325
1718
  }
1326
- return { kind: 'unknown', moduleSpecifier };
1719
+ return {
1720
+ kind: 'unknown',
1721
+ importedName: namedImport.getNameNode().getText(),
1722
+ moduleSpecifier,
1723
+ };
1327
1724
  }
1328
1725
  return undefined;
1329
1726
  }
@@ -1789,6 +2186,9 @@ function parseCliArgs(argv) {
1789
2186
  case '--tsconfig':
1790
2187
  options.tsConfigFilePath = argv[++index];
1791
2188
  break;
2189
+ case '--config':
2190
+ options.configFilePath = argv[++index];
2191
+ break;
1792
2192
  case '--helper-import':
1793
2193
  options.helperImportPath = argv[++index];
1794
2194
  break;
@@ -1819,6 +2219,7 @@ function printHelpAndExit() {
1819
2219
  Options:
1820
2220
  --root <dir> Project root. Defaults to cwd.
1821
2221
  --tsconfig <path> tsconfig path. Defaults to <root>/tsconfig.json.
2222
+ --config <path> Explicit angular brand config file path.
1822
2223
  --helper-import <path> Import path for GetDeps.
1823
2224
  --transform-only-standalone-declarables Only transform standalone components/directives.
1824
2225
  --no-providers Do not include metadata providers in generated types.