Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a1f2298

Browse files
atscottpkozlowski-opensource
authored andcommitted
feat(core): migrate ExperimentalPendingTasks to PendingTasks (#57533)
This commit promotes the `ExperimentalPendingTasks` service from experimental to developer preview and includes a migration schematic for the rename. BREAKING CHANGE: `ExperimentalPendingTasks` has been renamed to `PendingTasks`. PR Close #57533
1 parent a448f5a commit a1f2298

File tree

33 files changed

+322
-71
lines changed

33 files changed

+322
-71
lines changed

‎adev/shared-docs/components/viewers/docs-viewer/docs-viewer.component.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,16 @@ export class DocViewer implements OnChanges {
8484

8585
// tslint:disable-next-line:no-unused-variable
8686
private animateContent = false;
87-
private readonly pendingRenderTasks = inject(PendingTasks);
87+
private readonly pendingTasks = inject(PendingTasks);
8888

8989
private countOfExamples = 0;
9090

9191
async ngOnChanges(changes: SimpleChanges): Promise<void> {
92-
const taskId = this.pendingRenderTasks.add();
92+
const taskId = this.pendingTasks.add();
9393
if ('docContent' in changes) {
9494
await this.renderContentsAndRunClientSetup(this.docContent!);
9595
}
96-
this.pendingRenderTasks.remove(taskId);
96+
this.pendingTasks.remove(taskId);
9797
}
9898

9999
async renderContentsAndRunClientSetup(content?: string): Promise<void> {
@@ -195,7 +195,7 @@ export class DocViewer implements OnChanges {
195195
const preview = Boolean(placeholder.getAttribute('preview'));
196196
const title = placeholder.getAttribute('header') ?? undefined;
197197
const firstCodeSnippetTitle =
198-
snippets.length > 0 ? snippets[0].title ?? snippets[0].name : undefined;
198+
snippets.length > 0 ? (snippets[0].title ?? snippets[0].name) : undefined;
199199
const exampleRef = this.viewContainer.createComponent(ExampleViewer);
200200

201201
this.countOfExamples++;

‎adev/src/content/guide/zoneless.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ Zoneless applications. In fact, removing these calls can lead to performance reg
8181
are used in applications that still rely on ZoneJS.
8282
</docs-callout>
8383

84-
### `ExperimentalPendingTasks` for Server Side Rendering (SSR)
84+
### `PendingTasks` for Server Side Rendering (SSR)
8585

8686
If you are using SSR with Angular, you may know that it relies on ZoneJS to help determine when the application
8787
is "stable" and can be serialized. If there are asynchronous tasks that should prevent serialization, an application
88-
not using ZoneJS will need to make Angular aware of these with the `ExperimentalPendingTasks` service. Serialization
88+
not using ZoneJS will need to make Angular aware of these with the `PendingTasks` service. Serialization
8989
will wait for the first moment that all pending tasks have been removed.
9090

9191
```typescript
92-
const taskService = inject(ExperimentalPendingTasks);
92+
const taskService = inject(PendingTasks);
9393
const taskCleanup = taskService.add();
9494
await doSomeWorkThatNeedsToBeRendered();
9595
taskCleanup();

‎adev/src/content/reference/errors/NG0506.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,4 @@ class SimpleComponent {
7878

7979
In zoneless scenarios, stability might be delayed by an application code inside of an `effect` running in an infinite loop (potentially because signals used in effect functions keep changing) or a pending HTTP request.
8080

81-
Developers may also explicitly contribute to indicating the application's stability by using the experimental [`PendingTasks`](/api/core/ExperimentalPendingTasks) service. If you use the mentioned APIs in your application, make sure you invoke a function to mark the task as completed.
81+
Developers may also explicitly contribute to indicating the application's stability by using the [`PendingTasks`](/api/core/PendingTasks) service. If you use the mentioned APIs in your application, make sure you invoke a function to mark the task as completed.

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -710,14 +710,6 @@ export interface ExistingSansProvider {
710710
useExisting: any;
711711
}
712712

713-
// @public
714-
export class ExperimentalPendingTasks {
715-
add(): () => void;
716-
run<T>(fn: () => Promise<T>): Promise<T>;
717-
// (undocumented)
718-
static ɵprov: unknown;
719-
}
720-
721713
// @public
722714
export interface FactoryProvider extends FactorySansProvider {
723715
multi?: boolean;
@@ -1351,6 +1343,14 @@ export interface OutputRefSubscription {
13511343
// @public @deprecated
13521344
export const PACKAGE_ROOT_URL: InjectionToken<string>;
13531345

1346+
// @public
1347+
export class PendingTasks {
1348+
add(): () => void;
1349+
run<T>(fn: () => Promise<T>): Promise<T>;
1350+
// (undocumented)
1351+
static ɵprov: unknown;
1352+
}
1353+
13541354
// @public
13551355
export interface Pipe {
13561356
name: string;

‎packages/core/schematics/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ rollup_bundle(
3636
"//packages/core/schematics/ng-generate/standalone-migration:index.ts": "standalone-migration",
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",
39+
"//packages/core/schematics/migrations/pending-tasks:index.ts": "pending-tasks",
3940
},
4041
format = "cjs",
4142
link_workspace_root = True,
@@ -46,6 +47,7 @@ rollup_bundle(
4647
],
4748
deps = [
4849
"//packages/core/schematics/migrations/explicit-standalone-flag",
50+
"//packages/core/schematics/migrations/pending-tasks",
4951
"//packages/core/schematics/ng-generate/control-flow-migration",
5052
"//packages/core/schematics/ng-generate/inject-migration",
5153
"//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
@@ -4,6 +4,11 @@
44
"version": "19.0.0",
55
"description": "Updates non-standalone Directives, Component and Pipes to standalone:false",
66
"factory": "./bundles/explicit-standalone-flag#migrate"
7+
},
8+
"pending-tasks": {
9+
"version": "19.0.0",
10+
"description": "Updates ExperimentalPendingTasks to PendingTasks",
11+
"factory": "./bundles/pending-tasks#migrate"
712
}
813
}
914
}
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 = "pending-tasks",
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: 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.io/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 './migration';
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 afterRender phase 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, program.getTypeChecker(), rewriter);
53+
54+
if (update !== null) {
55+
tree.commitUpdate(update);
56+
}
57+
}
58+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
import {ChangeTracker} from '../../utils/change_tracker';
11+
import {
12+
getImportOfIdentifier,
13+
getImportSpecifier,
14+
getNamedImports,
15+
} from '../../utils/typescript/imports';
16+
17+
const CORE = '@angular/core';
18+
const EXPERIMENTAL_PENDING_TASKS = 'ExperimentalPendingTasks';
19+
20+
type RewriteFn = (startPos: number, width: number, text: string) => void;
21+
22+
export function migrateFile(
23+
sourceFile: ts.SourceFile,
24+
typeChecker: ts.TypeChecker,
25+
rewriteFn: RewriteFn,
26+
) {
27+
const changeTracker = new ChangeTracker(ts.createPrinter());
28+
// Check if there are any imports of the `AfterRenderPhase` enum.
29+
const coreImports = getNamedImports(sourceFile, CORE);
30+
if (!coreImports) {
31+
return;
32+
}
33+
const importSpecifier = getImportSpecifier(sourceFile, CORE, EXPERIMENTAL_PENDING_TASKS);
34+
if (!importSpecifier) {
35+
return;
36+
}
37+
const nodeToReplace = importSpecifier.propertyName ?? importSpecifier.name;
38+
if (!ts.isIdentifier(nodeToReplace)) {
39+
return;
40+
}
41+
42+
changeTracker.replaceNode(nodeToReplace, ts.factory.createIdentifier('PendingTasks'));
43+
44+
ts.forEachChild(sourceFile, function visit(node: ts.Node) {
45+
// import handled above
46+
if (ts.isImportDeclaration(node)) {
47+
return;
48+
}
49+
50+
if (
51+
ts.isIdentifier(node) &&
52+
node.text === EXPERIMENTAL_PENDING_TASKS &&
53+
getImportOfIdentifier(typeChecker, node)?.name === EXPERIMENTAL_PENDING_TASKS
54+
) {
55+
changeTracker.replaceNode(node, ts.factory.createIdentifier('PendingTasks'));
56+
}
57+
58+
ts.forEachChild(node, visit);
59+
});
60+
61+
// Write the changes.
62+
for (const changesInFile of changeTracker.recordChanges().values()) {
63+
for (const change of changesInFile) {
64+
rewriteFn(change.start, change.removeLength ?? 0, change.text);
65+
}
66+
}
67+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core';
10+
import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
11+
import {HostTree} from '@angular-devkit/schematics';
12+
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
13+
import {runfiles} from '@bazel/runfiles';
14+
import shx from 'shelljs';
15+
16+
describe('experimental pending tasks migration', () => {
17+
let runner: SchematicTestRunner;
18+
let host: TempScopedNodeJsSyncHost;
19+
let tree: UnitTestTree;
20+
let tmpDirPath: string;
21+
22+
function writeFile(filePath: string, contents: string) {
23+
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
24+
}
25+
26+
function runMigration() {
27+
return runner.runSchematic('pending-tasks', {}, tree);
28+
}
29+
30+
beforeEach(() => {
31+
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../migrations.json'));
32+
host = new TempScopedNodeJsSyncHost();
33+
tree = new UnitTestTree(new HostTree(host));
34+
35+
writeFile(
36+
'/tsconfig.json',
37+
JSON.stringify({
38+
compilerOptions: {
39+
lib: ['es2015'],
40+
strictNullChecks: true,
41+
},
42+
}),
43+
);
44+
45+
writeFile(
46+
'/angular.json',
47+
JSON.stringify({
48+
version: 1,
49+
projects: {t: {root: '', architect: {build: {options: {tsConfig: './tsconfig.json'}}}}},
50+
}),
51+
);
52+
53+
tmpDirPath = getSystemPath(host.root);
54+
55+
// Switch into the temporary directory path. This allows us to run
56+
// the schematic against our custom unit test tree.
57+
shx.cd(tmpDirPath);
58+
});
59+
60+
it('should update ExperimentalPendingTasks', async () => {
61+
writeFile(
62+
'/index.ts',
63+
`
64+
import {ExperimentalPendingTasks, Directive} from '@angular/core';
65+
66+
@Directive({
67+
selector: '[someDirective]'
68+
})
69+
export class SomeDirective {
70+
x = inject(ExperimentalPendingTasks);
71+
}`,
72+
);
73+
74+
await runMigration();
75+
76+
const content = tree.readContent('/index.ts').replace(/\s+/g, ' ');
77+
expect(content).toContain("import {PendingTasks, Directive} from '@angular/core';");
78+
expect(content).toContain('x = inject(PendingTasks);');
79+
});
80+
81+
it('should update import alias', async () => {
82+
writeFile(
83+
'/index.ts',
84+
`
85+
import {ExperimentalPendingTasks as Y, Directive} from '@angular/core';
86+
87+
@Directive({
88+
selector: '[someDirective]'
89+
})
90+
export class SomeDirective {
91+
x = inject(Y);
92+
}`,
93+
);
94+
95+
await runMigration();
96+
97+
const content = tree.readContent('/index.ts').replace(/\s+/g, ' ');
98+
expect(content).toContain("import {PendingTasks as Y, Directive} from '@angular/core';");
99+
expect(content).toContain('x = inject(Y);');
100+
});
101+
});

‎packages/core/src/application/application_ref.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {ComponentFactory, ComponentRef} from '../linker/component_factory';
2929
import {ComponentFactoryResolver} from '../linker/component_factory_resolver';
3030
import {NgModuleRef} from '../linker/ng_module_factory';
3131
import {ViewRef} from '../linker/view_ref';
32-
import {PendingTasks} from '../pending_tasks';
32+
import {PendingTasksInternal} from '../pending_tasks';
3333
import {RendererFactory2} from '../render/api';
3434
import {AfterRenderManager} from '../render3/after_render/manager';
3535
import {ComponentFactory as R3ComponentFactory} from '../render3/component_ref';
@@ -361,7 +361,7 @@ export class ApplicationRef {
361361
/**
362362
* Returns an Observable that indicates when the application is stable or unstable.
363363
*/
364-
public readonly isStable: Observable<boolean> = inject(PendingTasks).hasPendingTasks.pipe(
364+
public readonly isStable: Observable<boolean> = inject(PendingTasksInternal).hasPendingTasks.pipe(
365365
map((pending) => !pending),
366366
);
367367

‎packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
StaticProvider,
2020
} from '../../di';
2121
import {RuntimeError, RuntimeErrorCode} from '../../errors';
22-
import {PendingTasks} from '../../pending_tasks';
22+
import {PendingTasksInternal} from '../../pending_tasks';
2323
import {performanceMarkFeature} from '../../util/performance';
2424
import {NgZone} from '../../zone';
2525
import {InternalNgZoneOptions} from '../../zone/ng_zone';
@@ -256,7 +256,7 @@ export class ZoneStablePendingTask {
256256
private readonly subscription = new Subscription();
257257
private initialized = false;
258258
private readonly zone = inject(NgZone);
259-
private readonly pendingTasks = inject(PendingTasks);
259+
private readonly pendingTasks = inject(PendingTasksInternal);
260260

261261
initialize() {
262262
if (this.initialized) {

0 commit comments

Comments
 (0)