fix(migrations): fix provide-initializer migration when using useFactory (#58518)

Priori to this commit, initializer functions with dependencies were not migrated correctly.
With this commit, the function is executed in a injection context to allow the usage of `inject`.

PR Close #58518
This commit is contained in:
Younes Jaaidi 2024-11-06 01:39:50 +01:00 committed by Jessica Janiuk
parent a6d2d2dc10
commit 5ce10264a4
2 changed files with 91 additions and 30 deletions

View file

@ -90,36 +90,40 @@ function tryParseProviderExpression(node: ts.Node): ProviderInfo | undefined {
let deps: string[] = [];
let initializerToken: string | undefined;
let useExisting: ts.Expression | undefined;
let useFactory: ts.Expression | undefined;
let useFactoryCode: string | undefined;
let useValue: ts.Expression | undefined;
let multi = false;
for (const property of node.properties) {
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
continue;
if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.name)) {
switch (property.name.text) {
case 'deps':
if (ts.isArrayLiteralExpression(property.initializer)) {
deps = property.initializer.elements.map((el) => el.getText());
}
break;
case 'provide':
initializerToken = property.initializer.getText();
break;
case 'useExisting':
useExisting = property.initializer;
break;
case 'useFactory':
useFactoryCode = property.initializer.getText();
break;
case 'useValue':
useValue = property.initializer;
break;
case 'multi':
multi = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
break;
}
}
switch (property.name.text) {
case 'deps':
if (ts.isArrayLiteralExpression(property.initializer)) {
deps = property.initializer.elements.map((el) => el.getText());
}
break;
case 'provide':
initializerToken = property.initializer.getText();
break;
case 'useExisting':
useExisting = property.initializer;
break;
case 'useFactory':
useFactory = property.initializer;
break;
case 'useValue':
useValue = property.initializer;
break;
case 'multi':
multi = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
break;
// Handle the `useFactory() {}` shorthand case.
if (ts.isMethodDeclaration(property) && property.name.getText() === 'useFactory') {
const params = property.parameters.map((param) => param.getText()).join(', ');
useFactoryCode = `(${params}) => ${property.body?.getText()}`;
}
}
@ -146,12 +150,15 @@ function tryParseProviderExpression(node: ts.Node): ProviderInfo | undefined {
};
}
if (useFactory) {
if (useFactoryCode) {
const args = deps.map((dep) => `inject(${dep})`);
return {
...info,
importInject: deps.length > 0,
initializerCode: `(${useFactory.getText()})(${args.join(', ')})`,
initializerCode: `() => {
const initializerFn = (${useFactoryCode})(${args.join(', ')});
return initializerFn();
}`,
};
}

View file

@ -110,6 +110,29 @@ describe('Provide initializer migration', () => {
expect(content).toContain(`const providers = [provideAppInitializer(initializerFn)];`);
});
it('should transform APP_INITIALIZER + useFactory shorthand into provideAppInitializer', async () => {
const content = await migrateCode(`
import { APP_INITIALIZER } from '@angular/core';
const providers = [{
provide: APP_INITIALIZER,
useFactory() {
const service = inject(Service);
return () => service.init();
},
multi: true,
}];
`);
expect(content).toContain(`const providers = [provideAppInitializer(() => {
const initializerFn = (() => {
const service = inject(Service);
return () => service.init();
})();
return initializerFn();
})];`);
});
it('should transform APP_INITIALIZER + useFactory into provideAppInitializer', async () => {
const content = await migrateCode(`
import { APP_INITIALIZER } from '@angular/core';
@ -124,10 +147,13 @@ describe('Provide initializer migration', () => {
}];
`);
expect(content).toContain(`const providers = [provideAppInitializer((() => {
expect(content).toContain(`const providers = [provideAppInitializer(() => {
const initializerFn = (() => {
const service = inject(Service);
return () => service.init();
})())];`);
})();
return initializerFn();
})];`);
});
it('should transform APP_INITIALIZER + useExisting into provideAppInitializer', async () => {
@ -147,6 +173,31 @@ describe('Provide initializer migration', () => {
);
});
it('should transform APP_INITIALIZER + deps + useFactory shorthand into provideAppInitializer', async () => {
const content = await migrateCode(`
import { APP_INITIALIZER } from '@angular/core';
const providers = [{
provide: APP_INITIALIZER,
useFactory(a: ServiceA, b: ServiceB) {
return () => a.init();
},
deps: [ServiceA, ServiceB],
multi: true,
}];
`);
expect(content).toContain(`import { inject, provideAppInitializer } from '@angular/core';`);
expect(content).toContain(
`const providers = [provideAppInitializer(() => {
const initializerFn = ((a: ServiceA, b: ServiceB) => {
return () => a.init();
})(inject(ServiceA), inject(ServiceB));
return initializerFn();
})];`,
);
});
it('should transform APP_INITIALIZER + deps into provideAppInitializer', async () => {
const content = await migrateCode(`
import { APP_INITIALIZER } from '@angular/core';
@ -163,9 +214,12 @@ describe('Provide initializer migration', () => {
expect(content).toContain(`import { inject, provideAppInitializer } from '@angular/core';`);
expect(content).toContain(
`const providers = [provideAppInitializer(((a: ServiceA, b: ServiceB) => {
`const providers = [provideAppInitializer(() => {
const initializerFn = ((a: ServiceA, b: ServiceB) => {
return () => a.init();
})(inject(ServiceA), inject(ServiceB)))];`,
})(inject(ServiceA), inject(ServiceB));
return initializerFn();
})];`,
);
});