Skip to content

Commit 54c4eae

Browse files
committed
fix(@schematics/angular): transform Jasmine type annotations in jasmine-to-vitest schematic
This commit enhances the jasmine-to-vitest refactoring schematic by adding support for transforming Jasmine's type annotations. Previously, the schematic only handled function calls, leaving type usages like `jasmine.SpyObj<T>` untouched and causing compilation errors in the transformed code. A new transformer now identifies and converts the following Jasmine types to their Vitest/TypeScript equivalents, preserving generics where appropriate: - `jasmine.Spy` -> `Mock` - `jasmine.SpyObj<T>` -> `MockedObject<T>` - `jasmine.ObjectContaining<T>` -> `Partial<T>` - `jasmine.ObjectContaining` -> `object` - `jasmine.Any` -> `any` - `jasmine.DoneFn` -> `() => void`
1 parent 594c910 commit 54c4eae

File tree

5 files changed

+232
-2
lines changed

5 files changed

+232
-2
lines changed

packages/schematics/angular/refactor/jasmine-vitest/test-file-transformer.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import {
3838
transformSpyCallInspection,
3939
transformSpyReset,
4040
} from './transformers/jasmine-spy';
41+
import { transformJasmineTypes } from './transformers/jasmine-type';
42+
import { getVitestAutoImports } from './utils/ast-helpers';
4143
import { RefactorContext } from './utils/refactor-context';
4244
import { RefactorReporter } from './utils/refactor-reporter';
4345

@@ -78,11 +80,13 @@ export function transformJasmineToVitest(
7880
ts.ScriptKind.TS,
7981
);
8082

83+
const pendingVitestImports = new Set<string>();
8184
const transformer: ts.TransformerFactory<ts.SourceFile> = (context) => {
8285
const refactorCtx: RefactorContext = {
8386
sourceFile,
8487
reporter,
8588
tsContext: context,
89+
pendingVitestImports,
8690
};
8791

8892
const visitor: ts.Visitor = (node) => {
@@ -149,6 +153,8 @@ export function transformJasmineToVitest(
149153
break;
150154
}
151155
}
156+
} else if (ts.isQualifiedName(transformedNode) || ts.isTypeReferenceNode(transformedNode)) {
157+
transformedNode = transformJasmineTypes(transformedNode, refactorCtx);
152158
}
153159

154160
// Visit the children of the node to ensure they are transformed
@@ -163,12 +169,22 @@ export function transformJasmineToVitest(
163169
};
164170

165171
const result = ts.transform(sourceFile, [transformer]);
166-
if (result.transformed[0] === sourceFile && !reporter.hasTodos) {
172+
let transformedSourceFile = result.transformed[0];
173+
174+
if (transformedSourceFile === sourceFile && !reporter.hasTodos && !pendingVitestImports.size) {
167175
return content;
168176
}
169177

178+
const vitestImport = getVitestAutoImports(pendingVitestImports);
179+
if (vitestImport) {
180+
transformedSourceFile = ts.factory.updateSourceFile(transformedSourceFile, [
181+
vitestImport,
182+
...transformedSourceFile.statements,
183+
]);
184+
}
185+
170186
const printer = ts.createPrinter();
171-
const transformedContentWithPlaceholders = printer.printFile(result.transformed[0]);
187+
const transformedContentWithPlaceholders = printer.printFile(transformedSourceFile);
172188

173189
return restoreBlankLines(transformedContentWithPlaceholders);
174190
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
/**
10+
* @fileoverview This file contains a transformer that migrates Jasmine type definitions to
11+
* their Vitest equivalents. It handles the conversion of types like `jasmine.Spy` and
12+
* `jasmine.SpyObj` to Vitest's `Mock` and `MockedObject` types, and ensures that the
13+
* necessary `vitest` imports are added to the file.
14+
*/
15+
16+
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
17+
import { addVitestAutoImport } from '../utils/ast-helpers';
18+
import { RefactorContext } from '../utils/refactor-context';
19+
20+
export function transformJasmineTypes(
21+
node: ts.Node,
22+
{ sourceFile, reporter, pendingVitestImports }: RefactorContext,
23+
): ts.Node {
24+
const typeNameNode = ts.isTypeReferenceNode(node) ? node.typeName : node;
25+
if (
26+
!ts.isQualifiedName(typeNameNode) ||
27+
!ts.isIdentifier(typeNameNode.left) ||
28+
typeNameNode.left.text !== 'jasmine'
29+
) {
30+
return node;
31+
}
32+
33+
const jasmineTypeName = typeNameNode.right.text;
34+
35+
switch (jasmineTypeName) {
36+
case 'Spy': {
37+
const vitestTypeName = 'Mock';
38+
reporter.reportTransformation(
39+
sourceFile,
40+
node,
41+
`Transformed type \`jasmine.Spy\` to \`${vitestTypeName}\`.`,
42+
);
43+
addVitestAutoImport(pendingVitestImports, vitestTypeName);
44+
45+
return ts.factory.createIdentifier(vitestTypeName);
46+
}
47+
case 'SpyObj': {
48+
const vitestTypeName = 'MockedObject';
49+
reporter.reportTransformation(
50+
sourceFile,
51+
node,
52+
`Transformed type \`jasmine.SpyObj\` to \`${vitestTypeName}\`.`,
53+
);
54+
addVitestAutoImport(pendingVitestImports, vitestTypeName);
55+
56+
if (ts.isTypeReferenceNode(node)) {
57+
return ts.factory.updateTypeReferenceNode(
58+
node,
59+
ts.factory.createIdentifier(vitestTypeName),
60+
node.typeArguments,
61+
);
62+
}
63+
64+
return ts.factory.createIdentifier(vitestTypeName);
65+
}
66+
case 'Any':
67+
reporter.reportTransformation(
68+
sourceFile,
69+
node,
70+
`Transformed type \`jasmine.Any\` to \`any\`.`,
71+
);
72+
73+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
74+
case 'ObjectContaining': {
75+
const typeArguments = ts.isTypeReferenceNode(node) ? node.typeArguments : undefined;
76+
if (typeArguments && typeArguments.length > 0) {
77+
reporter.reportTransformation(
78+
sourceFile,
79+
node,
80+
`Transformed type \`jasmine.ObjectContaining\` to \`Partial\`.`,
81+
);
82+
83+
return ts.factory.createTypeReferenceNode('Partial', typeArguments);
84+
}
85+
86+
reporter.reportTransformation(
87+
sourceFile,
88+
node,
89+
`Transformed type \`jasmine.ObjectContaining\` to \`object\`.`,
90+
);
91+
92+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
93+
}
94+
case 'DoneFn':
95+
reporter.reportTransformation(
96+
sourceFile,
97+
node,
98+
'Transformed type `jasmine.DoneFn` to `() => void`.',
99+
);
100+
101+
return ts.factory.createFunctionTypeNode(
102+
undefined,
103+
[],
104+
ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword),
105+
);
106+
}
107+
108+
return node;
109+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 { expectTransformation } from '../test-helpers';
10+
11+
describe('Jasmine to Vitest Transformer', () => {
12+
describe('transformJasmineTypes', () => {
13+
const testCases = [
14+
{
15+
description: 'should transform a variable with a jasmine.Spy type',
16+
input: `let mySpy: jasmine.Spy;`,
17+
expected: `
18+
import type { Mock } from 'vitest';
19+
let mySpy: Mock;
20+
`,
21+
},
22+
{
23+
description: 'should transform a variable with a jasmine.SpyObj type',
24+
input: `let mySpy: jasmine.SpyObj<MyService>;`,
25+
expected: `
26+
import type { MockedObject } from 'vitest';
27+
let mySpy: MockedObject<MyService>;
28+
`,
29+
},
30+
{
31+
description: 'should handle multiple jasmine types and create a single import',
32+
input: `
33+
let mySpy: jasmine.Spy;
34+
let mySpyObj: jasmine.SpyObj<MyService>;
35+
`,
36+
expected: `
37+
import type { Mock, MockedObject } from 'vitest';
38+
39+
let mySpy: Mock;
40+
let mySpyObj: MockedObject<MyService>;
41+
`,
42+
},
43+
{
44+
description: 'should not add an import if no jasmine types are used',
45+
input: `let mySpy: any;`,
46+
expected: `let mySpy: any;`,
47+
},
48+
{
49+
description: 'should transform jasmine.Any to any',
50+
input: `let myMatcher: jasmine.Any;`,
51+
expected: `let myMatcher: any;`,
52+
},
53+
{
54+
description: 'should transform jasmine.ObjectContaining<T> to Partial<T>',
55+
input: `let myMatcher: jasmine.ObjectContaining<MyService>;`,
56+
expected: `let myMatcher: Partial<MyService>;`,
57+
},
58+
{
59+
description: 'should transform jasmine.ObjectContaining to object',
60+
input: `let myMatcher: jasmine.ObjectContaining;`,
61+
expected: `let myMatcher: object;`,
62+
},
63+
{
64+
description: 'should transform jasmine.DoneFn to () => void',
65+
input: `let myDoneFn: jasmine.DoneFn;`,
66+
expected: `let myDoneFn: () => void;`,
67+
},
68+
];
69+
70+
testCases.forEach(({ description, input, expected }) => {
71+
it(description, async () => {
72+
await expectTransformation(input, expected);
73+
});
74+
});
75+
});
76+
});

packages/schematics/angular/refactor/jasmine-vitest/utils/ast-helpers.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@
88

99
import ts from '../../../third_party/github.com/Microsoft/TypeScript/lib/typescript';
1010

11+
export function addVitestAutoImport(imports: Set<string>, importName: string): void {
12+
imports.add(importName);
13+
}
14+
15+
export function getVitestAutoImports(imports: Set<string>): ts.ImportDeclaration | undefined {
16+
if (!imports?.size) {
17+
return undefined;
18+
}
19+
20+
const importNames = [...imports];
21+
importNames.sort();
22+
const importSpecifiers = importNames.map((i) =>
23+
ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(i)),
24+
);
25+
26+
return ts.factory.createImportDeclaration(
27+
undefined,
28+
ts.factory.createImportClause(
29+
ts.SyntaxKind.TypeKeyword,
30+
undefined,
31+
ts.factory.createNamedImports(importSpecifiers),
32+
),
33+
ts.factory.createStringLiteral('vitest'),
34+
);
35+
}
36+
1137
export function createViCallExpression(
1238
methodName: string,
1339
args: readonly ts.Expression[] = [],

packages/schematics/angular/refactor/jasmine-vitest/utils/refactor-context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface RefactorContext {
2222

2323
/** The official context from the TypeScript Transformer API. */
2424
readonly tsContext: ts.TransformationContext;
25+
26+
/** A set of Vitest type imports to be added to the file. */
27+
readonly pendingVitestImports: Set<string>;
2528
}
2629

2730
/**

0 commit comments

Comments
 (0)