Skip to content

Commit e6e5d29

Browse files
feat(core): initial version of the output migration (#57604)
Initial version of the migration that changes decorator-based outputs to the equivalent form using new authoring functions. PR Close #57604
1 parent 2679dcf commit e6e5d29

5 files changed

Lines changed: 671 additions & 0 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
2+
3+
ts_library(
4+
name = "migration",
5+
srcs = glob(
6+
["**/*.ts"],
7+
exclude = ["*.spec.ts"],
8+
),
9+
deps = [
10+
"//packages/compiler",
11+
"//packages/compiler-cli",
12+
"//packages/compiler-cli/private",
13+
"//packages/compiler-cli/src/ngtsc/annotations",
14+
"//packages/compiler-cli/src/ngtsc/annotations/directive",
15+
"//packages/compiler-cli/src/ngtsc/imports",
16+
"//packages/compiler-cli/src/ngtsc/metadata",
17+
"//packages/compiler-cli/src/ngtsc/reflection",
18+
"//packages/core/schematics/utils/tsurge",
19+
"@npm//@types/node",
20+
"@npm//typescript",
21+
],
22+
)
23+
24+
ts_library(
25+
name = "test_lib",
26+
testonly = True,
27+
srcs = glob(
28+
["**/*.spec.ts"],
29+
),
30+
deps = [
31+
":migration",
32+
"//packages/compiler-cli",
33+
"//packages/compiler-cli/src/ngtsc/file_system/testing",
34+
"//packages/core/schematics/utils/tsurge",
35+
],
36+
)
37+
38+
jasmine_node_test(
39+
name = "test",
40+
srcs = [":test_lib"],
41+
env = {"FORCE_COLOR": "3"},
42+
)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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 {initMockFileSystem} from '../../../../compiler-cli/src/ngtsc/file_system/testing';
10+
import {runTsurgeMigration} from '../../utils/tsurge/testing';
11+
import {absoluteFrom} from '@angular/compiler-cli';
12+
import {OutputMigration} from './output-migration';
13+
14+
describe('outputs', () => {
15+
beforeEach(() => {
16+
initMockFileSystem('Native');
17+
});
18+
19+
describe('outputs migration', () => {
20+
describe('EventEmitter declarations without problematic access patterns', () => {
21+
it('should migrate declaration with a primitive type hint', () => {
22+
verifyDeclaration({
23+
before: '@Output() readonly someChange = new EventEmitter<string>();',
24+
after: 'readonly someChange = output<string>();',
25+
});
26+
});
27+
28+
it('should migrate declaration with complex type hint', () => {
29+
verifyDeclaration({
30+
before: '@Output() readonly someChange = new EventEmitter<string | number>();',
31+
after: 'readonly someChange = output<string | number>();',
32+
});
33+
});
34+
35+
it('should migrate declaration without type hint', () => {
36+
verifyDeclaration({
37+
before: '@Output() readonly someChange = new EventEmitter();',
38+
after: 'readonly someChange = output();',
39+
});
40+
});
41+
42+
it('should take alias into account', () => {
43+
verifyDeclaration({
44+
before: `@Output({alias: 'otherChange'}) readonly someChange = new EventEmitter();`,
45+
after: `readonly someChange = output({ alias: 'otherChange' });`,
46+
});
47+
});
48+
49+
it('should support alias as statically analyzable reference', () => {
50+
verify({
51+
before: `
52+
import {Directive, Output, EventEmitter} from '@angular/core';
53+
54+
const aliasParam = { alias: 'otherChange' } as const;
55+
56+
@Directive()
57+
export class TestDir {
58+
@Output(aliasParam) someChange = new EventEmitter();
59+
}
60+
`,
61+
after: `
62+
import { Directive, output } from '@angular/core';
63+
64+
const aliasParam = { alias: 'otherChange' } as const;
65+
66+
@Directive()
67+
export class TestDir {
68+
readonly someChange = output(aliasParam);
69+
}
70+
`,
71+
});
72+
});
73+
74+
it('should add readonly modifier', () => {
75+
verifyDeclaration({
76+
before: '@Output() someChange = new EventEmitter();',
77+
after: 'readonly someChange = output();',
78+
});
79+
});
80+
81+
it('should respect visibility modifiers', () => {
82+
verifyDeclaration({
83+
before: `@Output() protected someChange = new EventEmitter();`,
84+
after: `protected readonly someChange = output();`,
85+
});
86+
});
87+
88+
it('should migrate multiple outputs', () => {
89+
// TODO: whitespace are messing up test verification
90+
verifyDeclaration({
91+
before: `@Output() someChange1 = new EventEmitter();
92+
@Output() someChange2 = new EventEmitter();`,
93+
after: `readonly someChange1 = output();
94+
readonly someChange2 = output();`,
95+
});
96+
});
97+
98+
it('should migrate only EventEmitter outputs when multiple outputs exist', () => {
99+
// TODO: whitespace are messing up test verification
100+
verifyDeclaration({
101+
before: `@Output() someChange1 = new EventEmitter();
102+
@Output() someChange2 = new Subject();`,
103+
after: `readonly someChange1 = output();
104+
@Output() someChange2 = new Subject();`,
105+
});
106+
});
107+
});
108+
109+
describe('declarations _with_ problematic access patterns', () => {
110+
it('should _not_ migrate outputs that are used with .pipe', () => {
111+
verifyNoChange(`
112+
import {Directive, Output, EventEmitter} from '@angular/core';
113+
114+
@Directive()
115+
export class TestDir {
116+
@Output() someChange = new EventEmitter();
117+
118+
someMethod() {
119+
this.someChange.pipe();
120+
}
121+
}
122+
`);
123+
});
124+
125+
it('should _not_ migrate outputs that are used with .next', () => {
126+
verifyNoChange(`
127+
import {Directive, Output, EventEmitter} from '@angular/core';
128+
129+
@Directive()
130+
export class TestDir {
131+
@Output() someChange = new EventEmitter<string>();
132+
133+
someMethod() {
134+
this.someChange.next('payload');
135+
}
136+
}
137+
`);
138+
});
139+
140+
it('should _not_ migrate outputs that are used with .complete', () => {
141+
verifyNoChange(`
142+
import {Directive, Output, EventEmitter, OnDestroy} from '@angular/core';
143+
144+
@Directive()
145+
export class TestDir implements OnDestroy {
146+
@Output() someChange = new EventEmitter<string>();
147+
148+
ngOnDestroy() {
149+
this.someChange.complete();
150+
}
151+
}
152+
`);
153+
});
154+
});
155+
});
156+
157+
describe('declarations other than EventEmitter', () => {
158+
it('should _not_ migrate outputs that are initialized with sth else than EventEmitter', () => {
159+
verify({
160+
before: populateDeclarationTestCase('@Output() protected someChange = new Subject();'),
161+
after: populateDeclarationTestCase('@Output() protected someChange = new Subject();'),
162+
});
163+
});
164+
});
165+
});
166+
167+
async function verifyDeclaration(testCase: {before: string; after: string}) {
168+
verify({
169+
before: populateDeclarationTestCase(testCase.before),
170+
after: populateExpectedResult(testCase.after),
171+
});
172+
}
173+
174+
async function verifyNoChange(beforeAndAfter: string) {
175+
verify({before: beforeAndAfter, after: beforeAndAfter});
176+
}
177+
178+
async function verify(testCase: {before: string; after: string}) {
179+
const fs = await runTsurgeMigration(new OutputMigration(), [
180+
{
181+
name: absoluteFrom('/app.component.ts'),
182+
isProgramRootFile: true,
183+
contents: testCase.before,
184+
},
185+
]);
186+
187+
let actual = fs.readFile(absoluteFrom('/app.component.ts'));
188+
189+
expect(actual).toBe(testCase.after);
190+
}
191+
192+
function populateDeclarationTestCase(declaration: string): string {
193+
return `
194+
import {
195+
Directive,
196+
Output,
197+
EventEmitter,
198+
Subject
199+
} from '@angular/core';
200+
201+
@Directive()
202+
export class TestDir {
203+
${declaration}
204+
}
205+
`;
206+
}
207+
208+
function populateExpectedResult(declaration: string): string {
209+
return `
210+
import { Directive, Subject, output } from '@angular/core';
211+
212+
@Directive()
213+
export class TestDir {
214+
${declaration}
215+
}
216+
`;
217+
}

0 commit comments

Comments
 (0)