di-bag-codemod 0.1.0

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.
@@ -0,0 +1,58 @@
1
+ // tools/codemod/lib/transforms/build-and-start.mjs
2
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
3
+ const propertyName = name => IDENTIFIER.test(name) ? name : JSON.stringify(name);
4
+
5
+ /**
6
+ * `builder.buildAndStart(keys, options)` becomes `builder.build().ensureServicesReady(keys, options)`.
7
+ * `signal` and `timeoutMs` are renamed. `startupOrder` becomes the mapped concurrency role:
8
+ * 'parallel' is the new default and is dropped, 'sequential' is 1, a number stays that number.
9
+ * Every emitted API name comes from the rename map.
10
+ * @returns {string | undefined} the new call, or undefined after reporting why it was left alone.
11
+ */
12
+ export default function buildAndStart(call, api) {
13
+ const { ts } = api;
14
+ const callee = call.expression;
15
+ const [keys, options, ...rest] = call.arguments;
16
+ const signal = api.nameOf('StartupOptions', 'signal');
17
+ const timeout = api.nameOf('StartupOptions', 'timeoutMs');
18
+ const concurrency = api.nameForRole('concurrency');
19
+ if (keys === undefined || rest.length > 0) {
20
+ api.manual(call, `buildAndStart is called with an unexpected number of arguments; rewrite it to ${api.nameOf('Builder', 'build')}().${api.nameOf('Builder', 'buildAndStart')}(serviceKeys, options) by hand`);
21
+ return undefined;
22
+ }
23
+ const separator = api.slice(callee.expression.end, api.start(callee.name));
24
+ const replacements = [{
25
+ start: api.start(callee.name), end: callee.name.end,
26
+ text: `${api.nameOf('Builder', 'build')}()${separator}${api.nameOf('Builder', 'buildAndStart')}`,
27
+ }];
28
+ if (options === undefined) return api.assemble(call, replacements);
29
+ if (!ts.isObjectLiteralExpression(options)) {
30
+ if (!api.provenOptionOrigin(options, 'StartupOptions')) api.manual(options, `these options are not an object literal; where they are built, rename signal to ${signal}, timeoutMs to ${timeout}, and replace startupOrder with ${concurrency}`);
31
+ return api.assemble(call, replacements);
32
+ }
33
+ const spread = options.properties.find(ts.isSpreadAssignment);
34
+ if (spread) {
35
+ api.manual(spread, 'options are spread here; rename signal, timeoutMs and startupOrder where that object is built');
36
+ return undefined;
37
+ }
38
+ const bag = api.objectLiteral(options, {
39
+ rename: key => ({ signal, timeoutMs: timeout })[key],
40
+ replace: {
41
+ startupOrder(property) {
42
+ const value = ts.isPropertyAssignment(property) ? property.initializer : undefined;
43
+ if (value && (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))) {
44
+ if (value.text === 'parallel') return null;
45
+ if (value.text === 'sequential') return `${propertyName(concurrency)}: 1`;
46
+ }
47
+ if (value && ts.isNumericLiteral(value)) return `${propertyName(concurrency)}: ${value.text}`;
48
+ api.manual(property, `startupOrder is not a literal; use ${concurrency}: omit it for 'parallel', 1 for 'sequential', or the number`);
49
+ return undefined;
50
+ },
51
+ },
52
+ });
53
+ if (bag === undefined) return undefined;
54
+ replacements.push(bag.remaining === 0
55
+ ? { start: keys.end, end: options.end, text: '' }
56
+ : { start: api.start(options), end: options.end, text: bag.text });
57
+ return api.assemble(call, replacements);
58
+ }
@@ -0,0 +1,14 @@
1
+ import { locate, tokenUse } from './collection-tokens.mjs';
2
+
3
+ export default function collectionRead(call, api) {
4
+ const callee = call.expression;
5
+ const target = api.nameOf(api.member.owner, api.member.name);
6
+ const use = tokenUse(api, call.arguments[0]);
7
+ if (use.state === 'collection') {
8
+ return api.assemble(call, [{ start: api.start(callee.name), end: callee.name.end, text: target }]);
9
+ }
10
+ api.manual(call, use.state === 'mixed'
11
+ ? `${use.name} is also used as a single service (${locate(use.otherUse)}); split it into two tokens, then write ${target}(token) here`
12
+ : `${api.member.name} is removed; create this token as a collection token, then write ${target}(token) here`);
13
+ return undefined;
14
+ }
@@ -0,0 +1,10 @@
1
+ import { locate, tokenUse } from './collection-tokens.mjs';
2
+
3
+ export default function collectionReference(call, api) {
4
+ const use = tokenUse(api, call.arguments[0]);
5
+ if (use.state === 'collection' && call.arguments.length === 1) return api.text(call.arguments[0]);
6
+ api.manual(call, use.state === 'mixed'
7
+ ? `${use.name} is also used as a single service (${locate(use.otherUse)}); split it into two tokens, then pass the collection token itself here`
8
+ : `${api.member.name} is removed; create this token as a collection token, then pass the token itself here`);
9
+ return undefined;
10
+ }
@@ -0,0 +1,20 @@
1
+ import { creationUse, locate } from './collection-tokens.mjs';
2
+
3
+ export default function collectionToken(call, api) {
4
+ const callee = call.expression;
5
+ const use = creationUse(api, call);
6
+ if (use.state === 'collection' || use.state === 'single') {
7
+ return api.assemble(call, [{
8
+ start: api.start(callee.name),
9
+ end: callee.name.end,
10
+ text: api.nameForRole(use.state),
11
+ }]);
12
+ }
13
+ if (use.state === 'mixed') {
14
+ api.manual(call, `${use.name} is used as a collection and as a single service (${locate(use.otherUse)}); create a second token with ${api.nameForRole('collection')} and keep the single token with ${api.nameForRole('single')}`);
15
+ } else {
16
+ api.manual(call, `token creation is not bound to a traceable program variable; choose ${api.nameForRole('single')} or ${api.nameForRole('collection')} by hand`);
17
+ }
18
+ // The kind stays manual; independently resolved children can still migrate.
19
+ return api.assemble(call, []);
20
+ }
@@ -0,0 +1,98 @@
1
+ // tools/codemod/lib/transforms/collection-tokens.mjs
2
+ const COLLECTION_POSITIONS = new Set(['Builder.contribute', 'DiBagApi.all', 'Bag.resolveAll', 'Bag.inspectAll']);
3
+ const analyses = new WeakMap();
4
+
5
+ function analyze(api) {
6
+ const { ts, checker, program, library } = api;
7
+ let analysis = analyses.get(program);
8
+ if (analysis) return analysis;
9
+ analysis = new Map();
10
+ analyses.set(program, analysis);
11
+ const files = program.getSourceFiles().filter(file =>
12
+ !file.isDeclarationFile && !library.isLibraryFile(file.fileName) && !file.fileName.includes('/node_modules/'));
13
+ const visitDeclarations = node => {
14
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isTokenCreation(api, node.initializer)) {
15
+ const symbol = checker.getSymbolAtLocation(node.name);
16
+ if (symbol) {
17
+ analysis.set(symbol, { declaration: node, creation: node.initializer, collectionUses: [], otherUses: [] });
18
+ }
19
+ }
20
+ ts.forEachChild(node, visitDeclarations);
21
+ };
22
+ for (const file of files) visitDeclarations(file);
23
+ const visitUses = node => {
24
+ if (ts.isIdentifier(node)) {
25
+ const entry = analysis.get(variableSymbol(api, node));
26
+ if (entry && node !== entry.declaration.name) {
27
+ const use = classify(api, node);
28
+ if (use === 'collection') entry.collectionUses.push(node);
29
+ else if (use === 'other') entry.otherUses.push(node);
30
+ }
31
+ }
32
+ ts.forEachChild(node, visitUses);
33
+ };
34
+ for (const file of files) visitUses(file);
35
+ return analysis;
36
+ }
37
+
38
+ function isTokenCreation(api, node) {
39
+ const { ts, library } = api;
40
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return false;
41
+ const coverage = library.memberCoverage(library.symbolAt(node.expression.name));
42
+ return coverage.complete && coverage.members.length > 0
43
+ && coverage.members.every(member => member.owner === 'token()' && member.name === 'of');
44
+ }
45
+
46
+ function variableSymbol(api, identifier) {
47
+ const { ts, checker } = api;
48
+ const parent = identifier.parent;
49
+ let symbol = ts.isShorthandPropertyAssignment(parent) ? checker.getShorthandAssignmentValueSymbol(parent)
50
+ : ts.isExportSpecifier(parent) ? checker.getExportSpecifierLocalTargetSymbol(parent)
51
+ : checker.getSymbolAtLocation(identifier);
52
+ if (symbol && symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
53
+ return symbol;
54
+ }
55
+
56
+ function classify(api, identifier) {
57
+ const { ts, library } = api;
58
+ const parent = identifier.parent;
59
+ if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isImportClause(parent) || ts.isTypeQueryNode(parent)) return 'neutral';
60
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === identifier && parent.name.text === 'key') return 'neutral';
61
+ if (ts.isCallExpression(parent) && parent.arguments[0] === identifier && ts.isPropertyAccessExpression(parent.expression)) {
62
+ const coverage = library.memberCoverage(library.symbolAt(parent.expression.name));
63
+ if (coverage.complete && coverage.members.length > 0
64
+ && coverage.members.every(member => COLLECTION_POSITIONS.has(`${member.owner}.${member.name}`))) {
65
+ return 'collection';
66
+ }
67
+ }
68
+ return 'other';
69
+ }
70
+
71
+ export function tokenUse(api, expression) {
72
+ const { ts } = api;
73
+ if (!ts.isIdentifier(expression)) return { state: 'untraceable' };
74
+ const entry = analyze(api).get(variableSymbol(api, expression));
75
+ if (!entry) return { state: 'untraceable' };
76
+ return describe(expression.text, entry);
77
+ }
78
+
79
+ export function creationUse(api, call) {
80
+ const { ts, checker } = api;
81
+ const declaration = call.parent;
82
+ if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== call || !ts.isIdentifier(declaration.name)) {
83
+ return { state: 'untraceable' };
84
+ }
85
+ const entry = analyze(api).get(checker.getSymbolAtLocation(declaration.name));
86
+ return entry ? describe(declaration.name.text, entry) : { state: 'untraceable' };
87
+ }
88
+
89
+ function describe(name, entry) {
90
+ if (entry.collectionUses.length === 0) return { state: 'single', name };
91
+ if (entry.otherUses.length > 0) return { state: 'mixed', name, otherUse: entry.otherUses[0] };
92
+ return { state: 'collection', name };
93
+ }
94
+
95
+ export function locate(node) {
96
+ const file = node.getSourceFile();
97
+ return `${file.fileName.split('/').slice(-2).join('/')}:${file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1}`;
98
+ }
@@ -0,0 +1,61 @@
1
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
2
+ const propertyName = name => IDENTIFIER.test(name) ? name : JSON.stringify(name);
3
+
4
+ function methodReplacement(call, api) {
5
+ const name = call.expression.name;
6
+ return { start: api.start(name), end: name.end, text: api.nameOf('Bag', name.text) };
7
+ }
8
+
9
+ function fieldNames(oldName, api) {
10
+ const names = {
11
+ keys: propertyName(api.nameForRole('keys')),
12
+ providers: propertyName(api.nameForRole('providers')),
13
+ };
14
+ return oldName === 'createScope'
15
+ ? { ...names, shared: propertyName(api.nameForRole('sharing')) }
16
+ : names;
17
+ }
18
+
19
+ function shareReplacements(options, call, api, sharedName) {
20
+ const { ts } = api;
21
+ if (!ts.isObjectLiteralExpression(options) || options.properties.length !== 1) {
22
+ api.manual(call, 'the createScope options are not an object literal; rewrite it to createChildContainer by hand');
23
+ return undefined;
24
+ }
25
+ const [property] = options.properties;
26
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === 'share') {
27
+ return [{ start: api.start(property.name), end: property.name.end, text: `${sharedName}: share` }];
28
+ }
29
+ if (ts.isPropertyAssignment(property) && !ts.isComputedPropertyName(property.name)
30
+ && (ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))
31
+ && property.name.text === 'share') {
32
+ return [{ start: api.start(property.name), end: property.name.end, text: sharedName }];
33
+ }
34
+ api.manual(call, 'the createScope options are not an object literal; rewrite it to createChildContainer by hand');
35
+ return undefined;
36
+ }
37
+
38
+ export default function containerDerivation(call, api) {
39
+ const oldName = call.expression.name.text;
40
+ const args = [...call.arguments];
41
+ const names = fieldNames(oldName, api);
42
+ const replacements = [methodReplacement(call, api)];
43
+ const lifetimeManual = api.childLifetimeManualReason(call);
44
+ if (lifetimeManual !== undefined) api.manual(call, lifetimeManual);
45
+ if (oldName === 'fork') {
46
+ if (args.length === 0 || args.length === 2) return api.assemble(call, replacements);
47
+ api.manual(call, 'fork is called with an unexpected number of arguments; rewrite it to createIndependentContainer by hand');
48
+ return undefined;
49
+ }
50
+ if (args.length === 0 || args.length === 2) return api.assemble(call, replacements);
51
+ if (args.length === 1) {
52
+ const shared = shareReplacements(args[0], call, api, names.shared);
53
+ return shared === undefined ? undefined : api.assemble(call, [...replacements, ...shared]);
54
+ }
55
+ if (args.length === 3) {
56
+ const shared = shareReplacements(args[2], call, api, names.shared);
57
+ return shared === undefined ? undefined : api.assemble(call, [...replacements, ...shared]);
58
+ }
59
+ api.manual(call, 'createScope is called with an unexpected number of arguments; rewrite it to createChildContainer by hand');
60
+ return undefined;
61
+ }
@@ -0,0 +1,19 @@
1
+ // tools/codemod/lib/transforms/index.mjs
2
+ import buildAndStart from './build-and-start.mjs';
3
+ import collectionRead from './collection-read.mjs';
4
+ import collectionReference from './collection-reference.mjs';
5
+ import collectionToken from './collection-token.mjs';
6
+ import containerDerivation from './container-derivation.mjs';
7
+ import providerFacades from './provider-facades.mjs';
8
+ import providerSources from './provider-sources.mjs';
9
+
10
+ /** Custom transforms by id. Each transform returns a whole rewritten call or reports and gives up. */
11
+ export const transforms = {
12
+ 'build-and-start': buildAndStart,
13
+ 'collection-read': collectionRead,
14
+ 'collection-reference': collectionReference,
15
+ 'collection-token': collectionToken,
16
+ 'container-derivation': containerDerivation,
17
+ 'provider-facades': providerFacades,
18
+ 'provider-sources': providerSources,
19
+ };
@@ -0,0 +1,74 @@
1
+ import { originalKind, literalBag, valueOf, nodeOf, renamedLiteral, text, textWithTrivia } from './provider-methods.mjs';
2
+
3
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
4
+ const propertyName = name => IDENTIFIER.test(name) ? name : JSON.stringify(name);
5
+ const field = (role, value, api) => `${propertyName(api.nameForRole(role))}: ${value}`;
6
+ const member = (receiver, role, api) => {
7
+ const name = api.nameForRole(role);
8
+ return IDENTIFIER.test(name) ? `${receiver}.${name}` : `${receiver}[${JSON.stringify(name)}]`;
9
+ };
10
+ const bagCall = (facade, role, fields, api) =>
11
+ `${member(facade, role, api)}({ ${fields.join(', ')} })`;
12
+ const callbackValues = new Map([['direct', 'exposed-service'], ['awaited', 'fulfilled-value']]);
13
+ const lifetimeValues = new Map([['root', 'singleton:one-per-container-tree'], ['scoped', 'scoped:one-per-container'], ['transient', 'transient:one-per-resolve']]);
14
+ const returnValues = new Map([['auto', 'auto-detect'], ['raw', 'uninspected'], ['nativePromise', 'native-promise']]);
15
+
16
+ export default function providerFacades(call, api) {
17
+ const oldName = call.expression.name.text;
18
+ const registration = call.arguments[0];
19
+ if (originalKind(registration, api) === 'manual') {
20
+ api.manual(call, 'the decorated value is any, unknown, invalid, or a factory/provider union; wrap a factory with createProvider or supply a provider by hand');
21
+ return undefined;
22
+ }
23
+ const facade = text(call.expression.expression, api);
24
+ const receiver = text(registration, api);
25
+ if (oldName === 'withDisposal') return bagCall(facade, 'method', [field('provider', receiver, api), field('disposeService', textWithTrivia(call.arguments[1], api).trimStart(), api)], api);
26
+ if (oldName === 'withLifetime') {
27
+ const oldLifetime = call.arguments[1];
28
+ const adjustment = api.childLifetimeAdjustment(call);
29
+ const lifetime = adjustment
30
+ ? api.quote(oldLifetime, adjustment.lifetime)
31
+ : renamedLiteral(oldLifetime, lifetimeValues, call, 'withLifetime uses a nonliteral lifetime; rewrite it to a full lifetime value by hand', api);
32
+ if (lifetime === undefined) return undefined;
33
+ const fields = [field('provider', receiver, api), field('lifetime', lifetime, api)];
34
+ let hasAllows = false;
35
+ if (call.arguments[2] !== undefined) {
36
+ const options = literalBag(call.arguments[2], new Set(['allowScopedDependencies']), call, 'the withLifetime options are not a supported object literal; rewrite the provider bag by hand', api);
37
+ if (options === undefined) return undefined;
38
+ const allows = valueOf(options, 'allowScopedDependencies', api);
39
+ if (allows !== undefined) { fields.push(field('allowsScopedDependencies', allows, api)); hasAllows = true; }
40
+ }
41
+ if (adjustment?.addAllows && !hasAllows) fields.push(field('allowsScopedDependencies', 'true', api));
42
+ return bagCall(facade, 'method', fields, api);
43
+ }
44
+ const allowed = oldName === 'transformService' ? new Set(['mode', 'transform', 'acquisitionMode']) : new Set(['static', 'dynamic']);
45
+ const options = literalBag(call.arguments[1], allowed, call, `the ${oldName} options are not a supported object literal; rewrite the provider bag by hand`, api);
46
+ if (options === undefined) return undefined;
47
+ if (oldName === 'transformService') {
48
+ const transform = valueOf(options, 'transform', api);
49
+ const receives = renamedLiteral(nodeOf(options, 'mode', api), callbackValues, call, 'transformService mode is nonliteral; choose callbackReceives by hand', api);
50
+ const returnNode = nodeOf(options, 'acquisitionMode', api);
51
+ const returnKind = returnNode === undefined ? undefined : renamedLiteral(returnNode, returnValues, call, 'transformService acquisitionMode is nonliteral; choose transformReturnKind by hand', api);
52
+ if (transform === undefined || receives === undefined || (returnNode !== undefined && returnKind === undefined)) return undefined;
53
+ const fields = [field('provider', receiver, api), field('transformService', transform, api), field('callbackReceives', receives, api)];
54
+ if (returnKind !== undefined) fields.push(field('transformReturnKind', returnKind, api));
55
+ return bagCall(facade, 'method', fields, api);
56
+ }
57
+ const registrationMetadata = valueOf(options, 'static', api);
58
+ const dynamicProperty = options.get('dynamic');
59
+ if (registrationMetadata !== undefined && dynamicProperty !== undefined) {
60
+ api.manual(call, 'combined static and dynamic metadata can change evaluation order when split; rewrite the two provider bags by hand');
61
+ return undefined;
62
+ }
63
+ if (registrationMetadata !== undefined) return bagCall(facade, 'registrationFacade', [field('provider', receiver, api), field('registrationMetadata', registrationMetadata, api)], api);
64
+ if (!dynamicProperty || !api.ts.isPropertyAssignment(dynamicProperty)) {
65
+ api.manual(call, 'withMetadata dynamic options are opaque; rewrite the provider bag by hand');
66
+ return undefined;
67
+ }
68
+ const dynamic = literalBag(dynamicProperty.initializer, new Set(['mode', 'describe']), call, 'withMetadata dynamic options are not a supported object literal; rewrite the provider bag by hand', api);
69
+ if (dynamic === undefined) return undefined;
70
+ const describe = valueOf(dynamic, 'describe', api);
71
+ const receives = renamedLiteral(nodeOf(dynamic, 'mode', api), callbackValues, call, 'withMetadata dynamic mode is nonliteral; choose callbackReceives by hand', api);
72
+ if (describe === undefined || receives === undefined) return undefined;
73
+ return bagCall(facade, 'acquisitionFacade', [field('provider', receiver, api), field('describeAcquisition', describe, api), field('callbackReceives', receives, api)], api);
74
+ }
@@ -0,0 +1,100 @@
1
+ export function originalKind(node, api) {
2
+ const type = api.checker.getTypeAtLocation(node);
3
+ if (type.flags & (api.ts.TypeFlags.Any | api.ts.TypeFlags.Unknown | api.ts.TypeFlags.TypeParameter)) return 'manual';
4
+ const members = type.isUnion() ? type.types : [type];
5
+ const kinds = members.map(member => {
6
+ if (api.checker.getSignaturesOfType(member, api.ts.SignatureKind.Call).length > 0) return 'factory';
7
+ const unresolved = member.aliasSymbol ?? member.getSymbol();
8
+ const symbol = unresolved?.flags & api.ts.SymbolFlags.Alias ? api.checker.getAliasedSymbol(unresolved) : unresolved;
9
+ const bases = member.getBaseTypes?.() ?? [];
10
+ const authentic = candidate => candidate?.declarations?.some(declaration => api.library.isLibraryFile(declaration.getSourceFile().fileName));
11
+ if (authentic(symbol) && (symbol?.name === 'FactoryWithDisposal' || symbol?.name === 'Provider' || symbol?.name === 'ProviderBase')) return 'provider';
12
+ if (bases.some(base => { const unresolvedBase = base.aliasSymbol ?? base.getSymbol(); const baseSymbol = unresolvedBase?.flags & api.ts.SymbolFlags.Alias ? api.checker.getAliasedSymbol(unresolvedBase) : unresolvedBase; return authentic(baseSymbol) && baseSymbol?.name === 'ProviderBase'; })) return 'provider';
13
+ return 'invalid';
14
+ });
15
+ return kinds.every(kind => kind === 'factory') ? 'factory'
16
+ : kinds.every(kind => kind === 'provider') ? 'provider'
17
+ : 'manual';
18
+ }
19
+ export const text = (node, api) => api.text(node);
20
+ export const textWithTrivia = (node, api) => `${api.slice(node.pos, api.start(node))}${api.text(node)}`;
21
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
22
+ const propertyName = name => IDENTIFIER.test(name) ? name : JSON.stringify(name);
23
+ const field = (role, value, api) => `${propertyName(api.nameForRole(role))}: ${value}`;
24
+ const roleMethod = (receiver, role, args, api) => {
25
+ const name = api.nameForRole(role);
26
+ const access = IDENTIFIER.test(name) ? `.${name}` : `[${JSON.stringify(name)}]`;
27
+ return `(${receiver})${access}(${args.join(', ')})`;
28
+ };
29
+ const prop = (property, api) => (api.ts.isPropertyAssignment(property) || api.ts.isShorthandPropertyAssignment(property)) && !api.ts.isComputedPropertyName(property.name) ? property.name.text : undefined;
30
+ export function literalBag(node, allowed, call, reason, api) {
31
+ if (!api.ts.isObjectLiteralExpression(node) || node.properties.some(item => api.ts.isSpreadAssignment(item) || api.ts.isMethodDeclaration(item) || api.ts.isGetAccessorDeclaration(item) || api.ts.isSetAccessorDeclaration(item) || prop(item, api) === undefined || !allowed.has(prop(item, api)))) { api.manual(call, reason); return undefined; }
32
+ const names = node.properties.map(item => prop(item, api));
33
+ if (new Set(names).size !== names.length || /\/\*|\/\//.test(api.slice(api.start(node), node.end))) { api.manual(call, `${reason}; duplicate keys and comments require a hand rewrite`); return undefined; }
34
+ return new Map(node.properties.map(item => [prop(item, api), item]));
35
+ }
36
+ export function valueOf(map, name, api) { const item = map.get(name); return item && api.ts.isShorthandPropertyAssignment(item) ? text(item.name, api) : item && api.ts.isPropertyAssignment(item) ? textWithTrivia(item.initializer, api).trimStart() : undefined; }
37
+ export function nodeOf(map, name, api) { const item = map.get(name); return item && api.ts.isShorthandPropertyAssignment(item) ? item.name : item && api.ts.isPropertyAssignment(item) ? item.initializer : undefined; }
38
+ export function renamedLiteral(node, values, call, reason, api) {
39
+ if (!node || (!api.ts.isStringLiteral(node) && !api.ts.isNoSubstitutionTemplateLiteral(node))) { api.manual(call, reason); return undefined; }
40
+ return api.quote(node, values.get(node.text) ?? node.text);
41
+ }
42
+ export function lifetimeOptions(node, call, api) {
43
+ if (node === undefined) return undefined;
44
+ const bag = literalBag(node, new Set(['allowScopedDependencies']), call, 'the withLifetime options are not a supported object literal; rewrite the provider chain by hand', api); if (bag === undefined) return null;
45
+ const item = bag.get('allowScopedDependencies');
46
+ if (item === undefined) return text(node, api);
47
+ if (api.ts.isShorthandPropertyAssignment(item)) return api.assemble(node, [{ start: api.start(item), end: item.end, text: field('allowsScopedDependencies', text(item.name, api), api) }]);
48
+ return api.assemble(node, [{ start: api.start(item.name), end: item.name.end, text: propertyName(api.nameForRole('allowsScopedDependencies')) }]);
49
+ }
50
+ function method(receiver, oldName, args, api) { return `(${receiver}).${api.nameOf('DiBagApi', oldName)}(${args.join(', ')})`; }
51
+ function receiverFor(call, api) {
52
+ const registration = call.arguments[0];
53
+ const kind = originalKind(registration, api);
54
+ if (kind === 'manual') { api.manual(call, 'the decorated value is any, unknown, invalid, or a factory/provider union; wrap a factory with createProvider or supply a provider by hand'); return undefined; }
55
+ const source = text(registration, api);
56
+ if (kind === 'provider') return source;
57
+ const facade = text(call.expression.expression, api);
58
+ return `${facade}.${api.nameOf('DiBagApi', 'fromFactory')}(${source})`;
59
+ }
60
+ export default function providerMethods(call, api) {
61
+ const oldName = call.expression.name.text;
62
+ const receiver = receiverFor(call, api); if (receiver === undefined) return undefined;
63
+ if (oldName === 'withDisposal') return method(receiver, oldName, [textWithTrivia(call.arguments[1], api).trimStart()], api);
64
+ if (oldName === 'withLifetime') {
65
+ const lifetime = renamedLiteral(call.arguments[1], new Map([['root', 'singleton:one-per-container-tree'], ['scoped', 'scoped:one-per-container'], ['transient', 'transient:one-per-resolve']]), call, 'withLifetime uses a nonliteral lifetime; rewrite it to a full lifetime value by hand', api);
66
+ if (lifetime === undefined) return undefined;
67
+ const options = lifetimeOptions(call.arguments[2], call, api); if (options === null) return undefined;
68
+ return method(receiver, oldName, options === undefined ? [lifetime] : [lifetime, options], api);
69
+ }
70
+ const allowed = oldName === 'transformService' ? new Set(['mode', 'transform', 'acquisitionMode']) : new Set(['static', 'dynamic']);
71
+ const options = literalBag(call.arguments[1], allowed, call, `the ${oldName} options are not a supported object literal; rewrite the provider chain by hand`, api);
72
+ if (options === undefined) return undefined;
73
+ if (oldName === 'transformService') {
74
+ const callback = valueOf(options, 'transform', api);
75
+ const receives = renamedLiteral(nodeOf(options, 'mode', api), new Map([['direct', 'exposed-service'], ['awaited', 'fulfilled-value']]), call, 'transformService mode is nonliteral; choose callbackReceives by hand', api);
76
+ const returnNode = nodeOf(options, 'acquisitionMode', api);
77
+ const returnKind = returnNode === undefined ? undefined : renamedLiteral(returnNode, new Map([['auto', 'auto-detect'], ['raw', 'uninspected'], ['nativePromise', 'native-promise']]), call, 'transformService acquisitionMode is nonliteral; choose transformReturnKind by hand', api);
78
+ if (callback === undefined || receives === undefined || (returnNode !== undefined && returnKind === undefined)) { if (callback === undefined) api.manual(call, 'transformService options must contain transform and mode; rewrite the provider chain by hand'); return undefined; }
79
+ const fields = [field('transformService', callback, api), field('callbackReceives', receives, api)];
80
+ if (returnKind !== undefined) fields.push(field('transformReturnKind', returnKind, api));
81
+ return method(receiver, oldName, [`{ ${fields.join(', ')} }`], api);
82
+ }
83
+ const staticValue = valueOf(options, 'static', api), dynamicNode = options.get('dynamic');
84
+ if (staticValue !== undefined && dynamicNode !== undefined) {
85
+ api.manual(call, 'combined static and dynamic metadata can change evaluation order when split; rewrite the two provider methods by hand');
86
+ return undefined;
87
+ }
88
+ let result = receiver;
89
+ if (staticValue !== undefined) result = method(result, oldName, [staticValue], api);
90
+ if (dynamicNode !== undefined && !api.ts.isPropertyAssignment(dynamicNode)) { api.manual(call, 'withMetadata shorthand dynamic options are opaque; rewrite the provider chain by hand'); return undefined; }
91
+ if (dynamicNode !== undefined && api.ts.isPropertyAssignment(dynamicNode)) {
92
+ const dynamic = literalBag(dynamicNode.initializer, new Set(['mode', 'describe']), call, 'withMetadata dynamic options are not a supported object literal; rewrite the provider chain by hand', api); if (dynamic === undefined) return undefined;
93
+ const describe = valueOf(dynamic, 'describe', api);
94
+ const mode = renamedLiteral(nodeOf(dynamic, 'mode', api), new Map([['direct', 'exposed-service'], ['awaited', 'fulfilled-value']]), call, 'withMetadata dynamic mode is nonliteral; choose callbackReceives by hand', api);
95
+ if (describe === undefined || mode === undefined) { api.manual(call, 'withMetadata dynamic options must contain describe and mode; rewrite the provider chain by hand'); return undefined; }
96
+ result = roleMethod(result, 'acquisitionMethod', [`{ ${field('describeAcquisition', describe, api)}, ${field('callbackReceives', mode, api)} }`], api);
97
+ }
98
+ if (staticValue === undefined && dynamicNode === undefined) { api.manual(call, 'withMetadata has neither static nor dynamic metadata; rewrite the provider chain by hand'); return undefined; }
99
+ return result;
100
+ }
@@ -0,0 +1,174 @@
1
+ const returnKind = value => ({
2
+ auto: 'auto-detect', raw: 'uninspected', nativePromise: 'native-promise',
3
+ })[value];
4
+
5
+ const modeTypeArgument = { fromFactory: 1, fromFunction: 2, fromClass: 2, fromPlugin: 2 };
6
+
7
+ function explicitKindReplacements(call, api) {
8
+ const index = modeTypeArgument[api.member.name];
9
+ const argument = index === undefined ? undefined : call.typeArguments?.[index];
10
+ if (argument === undefined) return [];
11
+ const replacements = [];
12
+ const visit = node => {
13
+ if (api.ts.isLiteralTypeNode(node) && (api.ts.isStringLiteral(node.literal) || api.ts.isNoSubstitutionTemplateLiteral(node.literal))) {
14
+ const mapped = returnKind(node.literal.text);
15
+ if (mapped === undefined) return false;
16
+ const original = api.text(node.literal);
17
+ const delimiter = original[0] === '"' ? '"' : original[0] === '`' ? '`' : "'";
18
+ replacements.push({ start: api.start(node.literal), end: node.literal.end, text: `${delimiter}${mapped}${delimiter}` });
19
+ return true;
20
+ }
21
+ return api.ts.isUnionTypeNode(node) && node.types.every(visit);
22
+ };
23
+ if (!visit(argument)) {
24
+ api.manual(argument, `${api.member.name} has a nonliteral or unsupported explicit return-kind type argument ${index}; rewrite it by hand`);
25
+ return [];
26
+ }
27
+ return [{ start: api.start(argument), end: argument.end, text: api.assemble(argument, replacements) }];
28
+ }
29
+
30
+ function literalProperties(literal, api, operation) {
31
+ const values = new Map();
32
+ for (const property of literal.properties) {
33
+ if (api.ts.isSpreadAssignment(property)) {
34
+ api.manual(operation, `${api.member.name} options contain a spread; rewrite them to ${api.nameOf('DiBagApi', api.member.name)} with explicit factoryReturnKind${api.member.name === 'fromPlugin' ? ' and isValidPluginOutput' : ''} by hand`);
35
+ return undefined;
36
+ }
37
+ if (!api.ts.isPropertyAssignment(property) && !api.ts.isShorthandPropertyAssignment(property)) {
38
+ api.manual(operation, `${api.member.name} options contain a computed or accessor property; rewrite them by hand`);
39
+ return undefined;
40
+ }
41
+ const name = property.name && (api.ts.isIdentifier(property.name) || api.ts.isStringLiteral(property.name)) ? property.name.text : undefined;
42
+ if (name === undefined) {
43
+ api.manual(operation, `${api.member.name} options contain a computed property; rewrite them by hand`);
44
+ return undefined;
45
+ }
46
+ values.set(name, api.ts.isShorthandPropertyAssignment(property) ? api.text(property.name) : api.text(property.initializer));
47
+ }
48
+ return values;
49
+ }
50
+
51
+ function optionFields(call, api, index) {
52
+ const options = call.arguments[index];
53
+ if (options === undefined) return new Map();
54
+ if (!api.ts.isObjectLiteralExpression(options)) {
55
+ api.manual(options, `${api.member.name} options are not an object literal; rewrite them to ${api.nameOf('DiBagApi', api.member.name)} options with factoryReturnKind${api.member.name === 'fromFactory' ? ' and factoryReceivesContext' : ''} by hand`);
56
+ return undefined;
57
+ }
58
+ return literalProperties(options, api, call);
59
+ }
60
+
61
+ function quotedKind(expression, api, operation) {
62
+ if (expression === undefined) return { present: false };
63
+ const node = expression.trim();
64
+ const match = /^(?:'([^']+)'|"([^"]+)")$/.exec(node);
65
+ const mapped = match ? returnKind(match[1] ?? match[2]) : undefined;
66
+ if (mapped === undefined) {
67
+ api.manual(operation, `${api.member.name} acquisitionMode is not a supported string literal; rewrite factoryReturnKind by hand`);
68
+ return undefined;
69
+ }
70
+ return { present: true, text: `'${mapped}'` };
71
+ }
72
+
73
+ function acceptsOnly(options, names, api, operation) {
74
+ const unexpected = [...options.keys()].find(name => !names.includes(name));
75
+ if (unexpected === undefined) return true;
76
+ api.manual(operation, `${api.member.name} options contain unsupported property ${unexpected}; rewrite them by hand`);
77
+ return false;
78
+ }
79
+
80
+ function property(api, role, value) {
81
+ return `${api.nameForRole(role)}: ${value}`;
82
+ }
83
+
84
+ function bagCall(call, api, fields, typeReplacements) {
85
+ const callee = call.expression;
86
+ const replacements = [
87
+ ...typeReplacements,
88
+ { start: api.start(callee.name), end: callee.name.end, text: api.nameOf('DiBagApi', api.member.name) },
89
+ { start: api.start(call.arguments[0]), end: call.arguments[call.arguments.length - 1].end, text: `{ ${fields.join(', ')} }` },
90
+ ];
91
+ return api.assemble(call, replacements);
92
+ }
93
+
94
+ export default function providerSources(call, api) {
95
+ const name = api.member.name;
96
+ const args = call.arguments;
97
+ const typeReplacements = explicitKindReplacements(call, api);
98
+ if (args.some(api.ts.isSpreadElement)) {
99
+ api.manual(call, `${name} is called with a spread argument; rewrite it by hand`);
100
+ return undefined;
101
+ }
102
+
103
+ if (name === 'fromFactory' || name === 'fromSyncFactory' || name === 'fromAsyncFactory') {
104
+ if (args.length < 1 || args.length > 2) {
105
+ api.manual(call, `${name} has an unexpected argument count; rewrite it to createProvider by hand`);
106
+ return undefined;
107
+ }
108
+ const options = optionFields(call, api, 1);
109
+ if (options === undefined) return undefined;
110
+ if (!acceptsOnly(options, name === 'fromFactory' ? ['acquisitionMode', 'context'] : ['context'], api, call)) return undefined;
111
+ const fields = [];
112
+ const selected = name === 'fromFactory' ? quotedKind(options.get('acquisitionMode'), api, call) : { present: true, text: name === 'fromSyncFactory' ? `'sync-value'` : `'native-promise'` };
113
+ if (selected === undefined) return undefined;
114
+ if (selected.present) fields.push(property(api, 'returnKind', selected.text));
115
+ const context = options.get('context');
116
+ if (context !== undefined) {
117
+ if (!/^(?:'acquisition'|"acquisition")$/.test(context.trim())) {
118
+ api.manual(call, `${name} context is not the literal 'acquisition'; rewrite factoryReceivesContext by hand`);
119
+ return undefined;
120
+ }
121
+ fields.push(property(api, 'receivesContext', 'true'));
122
+ }
123
+ const callee = call.expression;
124
+ const replacement = [...typeReplacements, { start: api.start(callee.name), end: callee.name.end, text: api.nameOf('DiBagApi', name) }];
125
+ if (fields.length === 0) {
126
+ if (args[1] !== undefined) replacement.push({ start: args[0].end, end: args[1].end, text: '' });
127
+ return api.assemble(call, replacement);
128
+ }
129
+ if (args[1] === undefined) replacement.push({ start: args[0].end, end: args[0].end, text: `, { ${fields.join(', ')} }` });
130
+ else replacement.push({ start: api.start(args[1]), end: args[1].end, text: `{ ${fields.join(', ')} }` });
131
+ return api.assemble(call, replacement);
132
+ }
133
+
134
+ if (name === 'fromFunction' || name === 'fromClass') {
135
+ if (args.length < 2 || args.length > 3) {
136
+ api.manual(call, `${name} has an unexpected argument count; rewrite it by hand`);
137
+ return undefined;
138
+ }
139
+ const options = optionFields(call, api, 2);
140
+ if (options === undefined) return undefined;
141
+ if (!acceptsOnly(options, ['acquisitionMode'], api, call)) return undefined;
142
+ const fields = [property(api, 'dependencies', api.text(args[0])), property(api, 'callable', api.text(args[1]))];
143
+ const selected = quotedKind(options.get('acquisitionMode'), api, call);
144
+ if (selected === undefined) return undefined;
145
+ if (selected.present) fields.push(property(api, 'returnKind', selected.text));
146
+ return bagCall(call, api, fields, typeReplacements);
147
+ }
148
+
149
+ if (name === 'fromPlugin') {
150
+ if (args.length !== 3) {
151
+ api.manual(call, 'fromPlugin has an unexpected argument count; rewrite it to createProviderFromPlugin by hand');
152
+ return undefined;
153
+ }
154
+ const options = optionFields(call, api, 2);
155
+ if (options === undefined) return undefined;
156
+ if (!acceptsOnly(options, ['acquisitionMode', 'validate'], api, call)) return undefined;
157
+ const selected = quotedKind(options.get('acquisitionMode'), api, call);
158
+ if (selected === undefined) return undefined;
159
+ const validator = options.get('validate');
160
+ if (!selected.present || validator === undefined) {
161
+ api.manual(call, 'fromPlugin options do not have literal acquisitionMode and validate properties; rewrite them by hand');
162
+ return undefined;
163
+ }
164
+ return bagCall(call, api, [
165
+ property(api, 'dependencies', api.text(args[0])),
166
+ property(api, 'descriptor', api.text(args[1])),
167
+ property(api, 'returnKind', selected.text),
168
+ property(api, 'validator', validator),
169
+ ], typeReplacements);
170
+ }
171
+
172
+ api.manual(call, `provider-sources does not recognize ${name}`);
173
+ return undefined;
174
+ }