Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions goldens/public-api/core/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const APP_BOOTSTRAP_LISTENER: InjectionToken<readonly ((compRef: Componen
// @public
export const APP_ID: InjectionToken<string>;

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

// @public
Expand Down Expand Up @@ -653,7 +653,7 @@ export abstract class EmbeddedViewRef<C> extends ViewRef {
// @public
export function enableProdMode(): void;

// @public
// @public @deprecated
export const ENVIRONMENT_INITIALIZER: InjectionToken<readonly (() => void)[]>;

// @public
Expand Down Expand Up @@ -1376,7 +1376,7 @@ export interface PipeTransform {
// @public
export const PLATFORM_ID: InjectionToken<Object>;

// @public
// @public @deprecated
export const PLATFORM_INITIALIZER: InjectionToken<readonly (() => void)[]>;

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

// @public
export function provideAppInitializer(initializerFn: () => Observable<unknown> | Promise<unknown> | void): EnvironmentProviders;

// @public
export function provideEnvironmentInitializer(initializerFn: () => void): EnvironmentProviders;

// @public
export function provideExperimentalCheckNoChangesForDebug(options: {
interval?: number;
Expand All @@ -1410,6 +1416,9 @@ export function provideExperimentalCheckNoChangesForDebug(options: {
// @public
export function provideExperimentalZonelessChangeDetection(): EnvironmentProviders;

// @public
export function providePlatformInitializer(initializerFn: () => void): EnvironmentProviders;

// @public
export type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[];

Expand Down
2 changes: 2 additions & 0 deletions packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ rollup_bundle(
"//packages/core/schematics/ng-generate/signal-input-migration:index.ts": "signal-input-migration",
"//packages/core/schematics/migrations/explicit-standalone-flag:index.ts": "explicit-standalone-flag",
"//packages/core/schematics/migrations/pending-tasks:index.ts": "pending-tasks",
"//packages/core/schematics/migrations/provide-initializer:index.ts": "provide-initializer",
},
format = "cjs",
link_workspace_root = True,
Expand All @@ -48,6 +49,7 @@ rollup_bundle(
deps = [
"//packages/core/schematics/migrations/explicit-standalone-flag",
"//packages/core/schematics/migrations/pending-tasks",
"//packages/core/schematics/migrations/provide-initializer",
"//packages/core/schematics/ng-generate/control-flow-migration",
"//packages/core/schematics/ng-generate/inject-migration",
"//packages/core/schematics/ng-generate/route-lazy-loading",
Expand Down
6 changes: 6 additions & 0 deletions packages/core/schematics/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
"version": "19.0.0",
"description": "Updates ExperimentalPendingTasks to PendingTasks",
"factory": "./bundles/pending-tasks#migrate"
},
"provide-initializer": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this migration is safe to run automatically. The provider functions in this PR return EnvironmentProviders (as they should) but existing usages may not be typed in a way to handle that type.

We can exclude it and use it as an optional migration for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: to make it optional you can just add “optional”: true

@yjaaidi yjaaidi Oct 21, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!
What would be the solution to make it automatic for v20?
We could detect where it is used and transform when it is safe. When it's not safe what would be the fallback? A type cast? A compact function? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thx @JeanMeche for handling this while I'm AFK 😊

"version": "19.0.0",
"description": "Replaces `APP_INITIALIZER`, 'ENVIRONMENT_INITIALIZER' & 'PLATFORM_INITIALIZER' respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatormInitializer`.",
"factory": "./bundles/provide-initializer#migrate",
"optional": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
load("//tools:defaults.bzl", "ts_library")

package(
default_visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
)

ts_library(
name = "provide-initializer",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)
25 changes: 25 additions & 0 deletions packages/core/schematics/migrations/provide-initializer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Replace `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER`, and `PLATFORM_INITIALIZER` with provider functions

Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER`, and `PLATFORM_INITIALIZER` with their respective provider functions: `provideAppInitializer`, `provideEnvironmentInitializer`, and `providePlatformInitializer`.

#### Before

```ts
import {APP_INITIALIZER} from '@angular/core';

const providers = [
{
provide: APP_INITIALIZER,
useValue: () => { console.log('hello'); },
multi: true,
}
];
```

#### After

```ts
import {provideAppInitializer} from '@angular/core';

const providers = [provideAppInitializer(() => { console.log('hello'); })];
```
58 changes: 58 additions & 0 deletions packages/core/schematics/migrations/provide-initializer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {Rule, SchematicsException, Tree, UpdateRecorder} from '@angular-devkit/schematics';
import {relative} from 'path';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host';
import {migrateFile} from './utils';

export function migrate(): Rule {
return async (tree: Tree) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];

if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run the provide initializer migration.',
);
}

for (const tsconfigPath of allPaths) {
runMigration(tree, tsconfigPath, basePath);
}
};
}

function runMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const program = createMigrationProgram(tree, tsconfigPath, basePath);
const sourceFiles = program
.getSourceFiles()
.filter((sourceFile) => canMigrateFile(basePath, sourceFile, program));

for (const sourceFile of sourceFiles) {
let update: UpdateRecorder | null = null;

const rewriter = (startPos: number, width: number, text: string | null) => {
if (update === null) {
// Lazily initialize update, because most files will not require migration.
update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
}
update.remove(startPos, width);
if (text !== null) {
update.insertLeft(startPos, text);
}
};
migrateFile(sourceFile, rewriter);

if (update !== null) {
tree.commitUpdate(update);
}
}
}
194 changes: 194 additions & 0 deletions packages/core/schematics/migrations/provide-initializer/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import ts from 'typescript';

import {ChangeTracker} from '../../utils/change_tracker';
import {getImportSpecifier} from '../../utils/typescript/imports';
import {closestNode} from '../../utils/typescript/nodes';

export type RewriteFn = (startPos: number, width: number, text: string) => void;

export function migrateFile(sourceFile: ts.SourceFile, rewriteFn: RewriteFn) {
const changeTracker = new ChangeTracker(ts.createPrinter());

const visitNode = (node: ts.Node) => {
const provider = tryParseProviderExpression(node);

if (provider) {
replaceProviderWithNewApi({
sourceFile: sourceFile,
node: node,
provider: provider,
changeTracker,
});
return;
}

ts.forEachChild(node, visitNode);
};

ts.forEachChild(sourceFile, visitNode);

for (const change of changeTracker.recordChanges().get(sourceFile)?.values() ?? []) {
rewriteFn(change.start, change.removeLength ?? 0, change.text);
}
}

function replaceProviderWithNewApi({
sourceFile,
node,
provider,
changeTracker,
}: {
sourceFile: ts.SourceFile;
node: ts.Node;
provider: ProviderInfo;
changeTracker: ChangeTracker;
}) {
const {initializerCode, importInject, provideInitializerFunctionName, initializerToken} =
provider;

const initializerTokenSpecifier = getImportSpecifier(
sourceFile,
angularCoreModule,
initializerToken,
);

// The token doesn't come from `@angular/core`.
if (!initializerTokenSpecifier) {
return;
}

// Replace the provider with the new provide function.
changeTracker.replaceText(
sourceFile,
node.getStart(),
node.getWidth(),
`${provideInitializerFunctionName}(${initializerCode})`,
);

// Import declaration and named imports are necessarily there.
const namedImports = closestNode(initializerTokenSpecifier, ts.isNamedImports)!;

// `provide*Initializer` function is already imported.
const hasProvideInitializeFunction = namedImports.elements.some(
(element) => element.name.getText() === provideInitializerFunctionName,
);

const newNamedImports = ts.factory.updateNamedImports(namedImports, [
// Remove the `*_INITIALIZER` token from imports.
...namedImports.elements.filter((element) => element !== initializerTokenSpecifier),
// Add the `inject` function to imports if needed.
...(importInject ? [createImportSpecifier('inject')] : []),
// Add the `provide*Initializer` function to imports.
...(!hasProvideInitializeFunction
? [createImportSpecifier(provideInitializerFunctionName)]
: []),
]);
changeTracker.replaceNode(namedImports, newNamedImports);
}

function createImportSpecifier(name: string): ts.ImportSpecifier {
return ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(name));
}

function tryParseProviderExpression(node: ts.Node): ProviderInfo | undefined {
if (!ts.isObjectLiteralExpression(node)) {
return;
}

let deps: string[] = [];
let initializerToken: string | undefined;
let useExisting: ts.Expression | undefined;
let useFactory: ts.Expression | undefined;
let useValue: ts.Expression | undefined;
let multi = false;

for (const property of node.properties) {
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
continue;
}

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;
}
}

if (!initializerToken || !multi) {
return;
}

const provideInitializerFunctionName = initializerTokenToFunctionMap.get(initializerToken);
if (!provideInitializerFunctionName) {
return;
}

const info = {
initializerToken,
provideInitializerFunctionName,
importInject: false,
} satisfies Partial<ProviderInfo>;

if (useExisting) {
return {
...info,
importInject: true,
initializerCode: `() => inject(${useExisting.getText()})()`,
};
}

if (useFactory) {
const args = deps.map((dep) => `inject(${dep})`);
return {
...info,
importInject: deps.length > 0,
initializerCode: `() => { return (${useFactory.getText()})(${args.join(', ')}); }`,
};
}

if (useValue) {
return {...info, initializerCode: useValue.getText()};
}

return;
}

const angularCoreModule = '@angular/core';

const initializerTokenToFunctionMap = new Map<string, string>([
['APP_INITIALIZER', 'provideAppInitializer'],
['ENVIRONMENT_INITIALIZER', 'provideEnvironmentInitializer'],
['PLATFORM_INITIALIZER', 'providePlatformInitializer'],
]);

interface ProviderInfo {
initializerToken: string;
provideInitializerFunctionName: string;
initializerCode: string;
importInject: boolean;
}
Loading