Skip to content

Commit 9391d39

Browse files
committed
feat(core): add syntactic sugar for initializers
add helper functions provideAppInitializer, provideEnvironmentInitializer & providePlatformInitializer to respectively simplify and replace the use of APP_INITIALIZER, ENVIRONMENT_INITIALIZER, PLATFORM_INITIALIZER add a migration for the three initialiers
1 parent 1549afe commit 9391d39

File tree

28 files changed

+1045
-53
lines changed

28 files changed

+1045
-53
lines changed

goldens/public-api/core/index.api.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const APP_BOOTSTRAP_LISTENER: InjectionToken<readonly ((compRef: Componen
9999
// @public
100100
export const APP_ID: InjectionToken<string>;
101101

102-
// @public
102+
// @public @deprecated
103103
export const APP_INITIALIZER: InjectionToken<readonly (() => Observable<unknown> | Promise<unknown> | void)[]>;
104104

105105
// @public
@@ -653,7 +653,7 @@ export abstract class EmbeddedViewRef<C> extends ViewRef {
653653
// @public
654654
export function enableProdMode(): void;
655655

656-
// @public
656+
// @public @deprecated
657657
export const ENVIRONMENT_INITIALIZER: InjectionToken<readonly (() => void)[]>;
658658

659659
// @public
@@ -1376,7 +1376,7 @@ export interface PipeTransform {
13761376
// @public
13771377
export const PLATFORM_ID: InjectionToken<Object>;
13781378

1379-
// @public
1379+
// @public @deprecated
13801380
export const PLATFORM_INITIALIZER: InjectionToken<readonly (() => void)[]>;
13811381

13821382
// @public
@@ -1400,6 +1400,12 @@ export class PlatformRef {
14001400
// @public
14011401
export type Predicate<T> = (value: T) => boolean;
14021402

1403+
// @public
1404+
export function provideAppInitializer(initializerFn: () => Observable<unknown> | Promise<unknown> | void): EnvironmentProviders;
1405+
1406+
// @public
1407+
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;
1408+
14031409
// @public
14041410
export function provideExperimentalCheckNoChangesForDebug(options: {
14051411
interval?: number;
@@ -1410,6 +1416,9 @@ export function provideExperimentalCheckNoChangesForDebug(options: {
14101416
// @public
14111417
export function provideExperimentalZonelessChangeDetection(): EnvironmentProviders;
14121418

1419+
// @public
1420+
export function providePlatformInitializer(initializerFn: () => void): EnvironmentProviders;
1421+
14131422
// @public
14141423
export type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];
14151424

packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ rollup_bundle(
3737
"//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration",
3838
"//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag",
3939
"//packages/core/schematics/migrations/pending-tasks:index.ts": "pending-tasks",
40+
"//packages/core/schematics/migrations/provide-initializer:index.ts": "provide-initializer",
4041
},
4142
format = "cjs",
4243
link_workspace_root = True,
@@ -48,6 +49,7 @@ rollup_bundle(
4849
deps = [
4950
"//packages/core/schematics/migrations/explicit-standalone-flag",
5051
"//packages/core/schematics/migrations/pending-tasks",
52+
"//packages/core/schematics/migrations/provide-initializer",
5153
"//packages/core/schematics/ng-generate/control-flow-migration",
5254
"//packages/core/schematics/ng-generate/inject-migration",
5355
"//packages/core/schematics/ng-generate/route-lazy-loading",

packages/core/schematics/migrations.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
"version": "19.0.0",
1010
"description": "Updates ExperimentalPendingTasks to PendingTasks",
1111
"factory": "./bundles/pending-tasks#migrate"
12+
},
13+
"provide-initializer": {
14+
"version": "19.0.0",
15+
"description": "Replaces `APP_INITIALIZER`, 'ENVIRONMENT_INITIALIZER' & 'PLATFORM_INITIALIZER' respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatormInitializer`.",
16+
"factory": "./bundles/provide-initializer#migrate"
1217
}
1318
}
1419
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
package(
4+
default_visibility = [
5+
"//packages/core/schematics:__pkg__",
6+
"//packages/core/schematics/migrations/google3:__pkg__",
7+
"//packages/core/schematics/test:__pkg__",
8+
],
9+
)
10+
11+
ts_library(
12+
name = "provide-initializer",
13+
srcs = glob(["**/*.ts"]),
14+
tsconfig = "//packages/core/schematics:tsconfig.json",
15+
deps = [
16+
"//packages/core/schematics/utils",
17+
"@npm//@angular-devkit/schematics",
18+
"@npm//@types/node",
19+
"@npm//typescript",
20+
],
21+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## Replace `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER`, and `PLATFORM_INITIALIZER` with provider functions
2+
3+
Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER`, and `PLATFORM_INITIALIZER` with their respective provider functions: `provideAppInitializer`, `provideEnvironmentInitializer`, and `providePlatformInitializer`.
4+
5+
#### Before
6+
7+
```ts
8+
import {APP_INITIALIZER} from '@angular/core';
9+
10+
const providers = [
11+
{
12+
provide: APP_INITIALIZER,
13+
useValue: () => { console.log('hello'); },
14+
multi: true,
15+
}
16+
];
17+
```
18+
19+
#### After
20+
21+
```ts
22+
import {provideAppInitializer} from '@angular/core';
23+
24+
const providers = [provideAppInitializer(() => { console.log('hello'); })];
25+
```
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics';
10+
import {relative} from 'path';
11+
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
12+
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
13+
import {migrateFile} from './utils';
14+
15+
export function migrate(): Rule {
16+
return async (tree: Tree) => {
17+
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
18+
const basePath = process.cwd();
19+
const allPaths = [...buildPaths, ...testPaths];
20+
21+
if (!allPaths.length) {
22+
throw new SchematicsException(
23+
'Could not find any tsconfig file. Cannot run the provide initializer migration.',
24+
);
25+
}
26+
27+
for (const tsconfigPath of allPaths) {
28+
runMigration(tree, tsconfigPath, basePath);
29+
}
30+
};
31+
}
32+
33+
function runMigration(tree: Tree, tsconfigPath: string, basePath: string) {
34+
const program = createMigrationProgram(tree, tsconfigPath, basePath);
35+
const sourceFiles = program
36+
.getSourceFiles()
37+
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));
38+
39+
for (const sourceFile of sourceFiles) {
40+
let update: UpdateRecorder | null = null;
41+
42+
const rewriter = (startPos: number, width: number, text: string | null) => {
43+
if (update === null) {
44+
// Lazily initialize update, because most files will not require migration.
45+
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
46+
}
47+
update.remove(startPos, width);
48+
if (text !== null) {
49+
update.insertLeft(startPos, text);
50+
}
51+
};
52+
migrateFile(sourceFile, rewriter);
53+
54+
if (update !== null) {
55+
tree.commitUpdate(update);
56+
}
57+
}
58+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
import {ChangeTracker} from '../../utils/change_tracker';
12+
import {getImportSpecifier} from '../../utils/typescript/imports';
13+
import {closestNode} from '../../utils/typescript/nodes';
14+
15+
export type RewriteFn = (startPos: number, width: number, text: string) => void;
16+
17+
export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
18+
const changeTracker = new ChangeTracker(ts.createPrinter());
19+
20+
const visitNode = (node: ts.Node) => {
21+
const provider = tryParseProviderExpression(node);
22+
23+
if (provider) {
24+
replaceProviderWithNewApi({
25+
sourceFile: sourceFile,
26+
node: node,
27+
provider: provider,
28+
changeTracker,
29+
});
30+
return;
31+
}
32+
33+
ts.forEachChild(node, visitNode);
34+
};
35+
36+
ts.forEachChild(sourceFile, visitNode);
37+
38+
for (const change of changeTracker.recordChanges().get(sourceFile)?.values() ?? []) {
39+
rewriteFn(change.start, change.removeLength ?? 0, change.text);
40+
}
41+
}
42+
43+
function replaceProviderWithNewApi({
44+
sourceFile,
45+
node,
46+
provider,
47+
changeTracker,
48+
}: {
49+
sourceFile: ts.SourceFile;
50+
node: ts.Node;
51+
provider: ProviderInfo;
52+
changeTracker: ChangeTracker;
53+
}) {
54+
const {initializerCode, importInject, provideInitializerFunctionName, initializerToken} =
55+
provider;
56+
57+
const initializerTokenSpecifier = getImportSpecifier(
58+
sourceFile,
59+
angularCoreModule,
60+
initializerToken,
61+
);
62+
63+
// The token doesn't come from `@angular/core`.
64+
if (!initializerTokenSpecifier) {
65+
return;
66+
}
67+
68+
// Replace the provider with the new provide function.
69+
changeTracker.replaceText(
70+
sourceFile,
71+
node.getStart(),
72+
node.getWidth(),
73+
`${provideInitializerFunctionName}(${initializerCode})`,
74+
);
75+
76+
// Import declaration and named imports are necessarily there.
77+
const namedImports = closestNode(initializerTokenSpecifier, ts.isNamedImports)!;
78+
79+
// `provide*Initializer` function is already imported.
80+
const hasProvideInitializeFunction = namedImports.elements.some(
81+
(element) => element.name.getText() === provideInitializerFunctionName,
82+
);
83+
84+
const newNamedImports = ts.factory.updateNamedImports(namedImports, [
85+
// Remove the `*_INITIALIZER` token from imports.
86+
...namedImports.elements.filter((element) => element !== initializerTokenSpecifier),
87+
// Add the `inject` function to imports if needed.
88+
...(importInject ? [createImportSpecifier('inject')] : []),
89+
// Add the `provide*Initializer` function to imports.
90+
...(!hasProvideInitializeFunction
91+
? [createImportSpecifier(provideInitializerFunctionName)]
92+
: []),
93+
]);
94+
changeTracker.replaceNode(namedImports, newNamedImports);
95+
}
96+
97+
function createImportSpecifier(name: string): ts.ImportSpecifier {
98+
return ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name));
99+
}
100+
101+
function tryParseProviderExpression(node: ts.Node): ProviderInfo | undefined {
102+
if (!ts.isObjectLiteralExpression(node)) {
103+
return;
104+
}
105+
106+
let deps: string[] = [];
107+
let initializerToken: string | undefined;
108+
let useExisting: ts.Expression | undefined;
109+
let useFactory: ts.Expression | undefined;
110+
let useValue: ts.Expression | undefined;
111+
let multi = false;
112+
113+
for (const property of node.properties) {
114+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
115+
continue;
116+
}
117+
118+
switch (property.name.text) {
119+
case 'deps':
120+
if (ts.isArrayLiteralExpression(property.initializer)) {
121+
deps = property.initializer.elements.map((el) => el.getText());
122+
}
123+
break;
124+
case 'provide':
125+
initializerToken = property.initializer.getText();
126+
break;
127+
case 'useExisting':
128+
useExisting = property.initializer;
129+
break;
130+
case 'useFactory':
131+
useFactory = property.initializer;
132+
break;
133+
case 'useValue':
134+
useValue = property.initializer;
135+
break;
136+
case 'multi':
137+
multi = property.initializer.kind === ts.SyntaxKind.TrueKeyword;
138+
break;
139+
}
140+
}
141+
142+
if (!initializerToken || !multi) {
143+
return;
144+
}
145+
146+
const provideInitializerFunctionName = initializerTokenToFunctionMap.get(initializerToken);
147+
if (!provideInitializerFunctionName) {
148+
return;
149+
}
150+
151+
const info = {
152+
initializerToken,
153+
provideInitializerFunctionName,
154+
importInject: false,
155+
} satisfies Partial<ProviderInfo>;
156+
157+
if (useExisting) {
158+
return {
159+
...info,
160+
importInject: true,
161+
initializerCode: `() => inject(${useExisting.getText()})()`,
162+
};
163+
}
164+
165+
if (useFactory) {
166+
const args = deps.map((dep) => `inject(${dep})`);
167+
return {
168+
...info,
169+
importInject: deps.length > 0,
170+
initializerCode: `() => { return (${useFactory.getText()})(${args.join(', ')}); }`,
171+
};
172+
}
173+
174+
if (useValue) {
175+
return {...info, initializerCode: useValue.getText()};
176+
}
177+
178+
return;
179+
}
180+
181+
const angularCoreModule = '@angular/core';
182+
183+
const initializerTokenToFunctionMap = new Map<string, string>([
184+
['APP_INITIALIZER', 'provideAppInitializer'],
185+
['ENVIRONMENT_INITIALIZER', 'provideEnvironmentInitializer'],
186+
['PLATFORM_INITIALIZER', 'providePlatformInitializer'],
187+
]);
188+
189+
interface ProviderInfo {
190+
initializerToken: string;
191+
provideInitializerFunctionName: string;
192+
initializerCode: string;
193+
importInject: boolean;
194+
}

0 commit comments

Comments
 (0)