Skip to content

Commit fb8884e

Browse files
authored
fix(vitest): handle zoneless Angular apps in vitest configuration generator (#34700)
## Current Behavior When running `@nx/vitest:configuration` on an Angular project, the generator always generates `import '@analogjs/vitest-angular/setup-zone'` regardless of whether the app is zoneless. Additionally, the setup file generation logic for Angular 21+ (which uses `setupTestBed()`) only exists in `packages/angular/src/generators/utils/add-vitest.ts` (`createAnalogSetupFile`), making the vitest generator not self-contained. ## Expected Behavior The vitest configuration generator should: - Auto-detect whether an Angular project is zoneless (by checking polyfills for apps, or zone.js dependency for libraries) - Generate `setup-snapshots` instead of `setup-zone` for zoneless projects - Handle Angular 21+ `setupTestBed()` setup directly, without requiring a separate function in the angular package - Accept an explicit `zoneless` option to override auto-detection ## Related Issue(s) Fixes #33983
1 parent 42f623e commit fb8884e

File tree

6 files changed

+74
-76
lines changed

6 files changed

+74
-76
lines changed

packages/angular/src/generators/utils/add-vitest.ts

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,8 @@ export async function addVitestAnalog(
123123
addPlugin: options.addPlugin ?? false,
124124
skipFormat: options.skipFormat,
125125
skipPackageJson: options.skipPackageJson,
126+
zoneless: options.zoneless,
126127
});
127-
128-
createAnalogSetupFile(tree, options, angularMajorVersion);
129128
}
130129

131130
function validateVitestVersion(tree: Tree): void {
@@ -226,52 +225,3 @@ function addVitestScreenshotsToGitIgnore(tree: Tree): void {
226225
logger.warn(`Couldn't find .gitignore file to update`);
227226
}
228227
}
229-
230-
function createAnalogSetupFile(
231-
tree: Tree,
232-
options: AddVitestAnalogOptions,
233-
angularMajorVersion: number
234-
): void {
235-
let setupFile: string;
236-
237-
if (angularMajorVersion >= 21) {
238-
setupFile = `import '@angular/compiler';
239-
import '@analogjs/vitest-angular/setup-snapshots';
240-
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
241-
242-
setupTestBed(${options.zoneless ? '' : '{ zoneless: false }'});
243-
`;
244-
} else if (angularMajorVersion === 20) {
245-
setupFile = `import '@angular/compiler';
246-
import '@analogjs/vitest-angular/setup-zone';
247-
import {
248-
BrowserTestingModule,
249-
platformBrowserTesting,
250-
} from '@angular/platform-browser/testing';
251-
import { getTestBed } from '@angular/core/testing';
252-
253-
getTestBed().initTestEnvironment(
254-
BrowserTestingModule,
255-
platformBrowserTesting(),
256-
);
257-
`;
258-
} else {
259-
setupFile = `import '@analogjs/vitest-angular/setup-zone';
260-
import {
261-
BrowserDynamicTestingModule,
262-
platformBrowserDynamicTesting,
263-
} from '@angular/platform-browser-dynamic/testing';
264-
import { getTestBed } from '@angular/core/testing';
265-
266-
getTestBed().initTestEnvironment(
267-
BrowserDynamicTestingModule,
268-
platformBrowserDynamicTesting(),
269-
);
270-
`;
271-
}
272-
273-
tree.write(
274-
joinPathFragments(options.projectRoot, 'src/test-setup.ts'),
275-
setupFile
276-
);
277-
}

packages/vite/src/generators/vitest/__snapshots__/vitest.spec.ts.snap

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,10 @@
22

33
exports[`vitest generator angular should generate src/test-setup.ts 1`] = `
44
"import '@angular/compiler';
5-
import '@analogjs/vitest-angular/setup-zone';
5+
import '@analogjs/vitest-angular/setup-snapshots';
6+
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
67
7-
import {
8-
BrowserTestingModule,
9-
platformBrowserTesting,
10-
} from '@angular/platform-browser/testing';
11-
import { getTestBed } from '@angular/core/testing';
12-
13-
getTestBed().initTestEnvironment(
14-
BrowserTestingModule,
15-
platformBrowserTesting(),
16-
);
8+
setupTestBed({ zoneless: false });
179
"
1810
`;
1911

packages/vite/src/generators/vitest/vitest.spec.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ describe('vitest generator', () => {
235235
expect(tree.read('apps/my-test-angular-app/src/test-setup.ts', 'utf-8'))
236236
.toMatchInlineSnapshot(`
237237
"import '@analogjs/vitest-angular/setup-zone';
238-
239238
import {
240239
BrowserDynamicTestingModule,
241240
platformBrowserDynamicTesting,

packages/vitest/src/generators/configuration/configuration.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,25 @@ export async function configurationGeneratorInternal(
126126

127127
const setupFile = joinPathFragments(root, relativeTestSetupPath);
128128
if (!tree.exists(setupFile)) {
129-
if (isAngularV20(tree)) {
129+
const angularMajorVersion = getAngularMajorVersion(tree);
130+
const zoneless =
131+
schema.zoneless ?? isZonelessProject(tree, schema.project);
132+
133+
if (angularMajorVersion >= 21) {
130134
tree.write(
131135
setupFile,
132136
`import '@angular/compiler';
133-
import '@analogjs/vitest-angular/setup-zone';
137+
import '@analogjs/vitest-angular/setup-snapshots';
138+
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
134139
140+
setupTestBed(${zoneless ? '' : '{ zoneless: false }'});
141+
`
142+
);
143+
} else if (angularMajorVersion === 20) {
144+
tree.write(
145+
setupFile,
146+
`import '@angular/compiler';
147+
import '@analogjs/vitest-angular/${zoneless ? 'setup-snapshots' : 'setup-zone'}';
135148
import {
136149
BrowserTestingModule,
137150
platformBrowserTesting,
@@ -140,15 +153,14 @@ import { getTestBed } from '@angular/core/testing';
140153
141154
getTestBed().initTestEnvironment(
142155
BrowserTestingModule,
143-
platformBrowserTesting()
156+
platformBrowserTesting(),
144157
);
145158
`
146159
);
147160
} else {
148161
tree.write(
149162
setupFile,
150-
`import '@analogjs/vitest-angular/setup-zone';
151-
163+
`import '@analogjs/vitest-angular/${zoneless ? 'setup-snapshots' : 'setup-zone'}';
152164
import {
153165
BrowserDynamicTestingModule,
154166
platformBrowserDynamicTesting,
@@ -157,7 +169,7 @@ import { getTestBed } from '@angular/core/testing';
157169
158170
getTestBed().initTestEnvironment(
159171
BrowserDynamicTestingModule,
160-
platformBrowserDynamicTesting()
172+
platformBrowserDynamicTesting(),
161173
);
162174
`
163175
);
@@ -450,26 +462,65 @@ function tryFindSetupFile(tree: Tree, projectRoot: string) {
450462
}
451463
}
452464

453-
function isAngularV20(tree: Tree) {
465+
function getAngularMajorVersion(tree: Tree): number {
454466
const angularVersion = getDependencyVersionFromPackageJson(
455467
tree,
456468
'@angular/core'
457469
);
458470

459471
if (!angularVersion) {
460-
// assume the latest version will be installed, which will be 20 or later
461-
return true;
472+
// assume the latest version will be installed
473+
return 21;
462474
}
463475

464476
const cleanedAngularVersion =
465-
clean(angularVersion) ?? coerce(angularVersion).version;
477+
clean(angularVersion) ?? coerce(angularVersion)?.version;
466478

467479
if (typeof cleanedAngularVersion !== 'string') {
468-
// assume the latest version will be installed,
469-
return true;
480+
// assume the latest version will be installed
481+
return 21;
482+
}
483+
484+
return major(cleanedAngularVersion);
485+
}
486+
487+
function isZonelessProject(tree: Tree, projectName: string): boolean {
488+
const project = readProjectConfiguration(tree, projectName);
489+
490+
if (project.projectType === 'application') {
491+
const buildTarget = findBuildTarget(project);
492+
if (!buildTarget?.options?.polyfills) {
493+
return true;
494+
}
495+
const polyfills = buildTarget.options.polyfills as string[] | string;
496+
const polyfillsList = Array.isArray(polyfills) ? polyfills : [polyfills];
497+
return !polyfillsList.includes('zone.js');
498+
}
499+
500+
// For libraries, check if zone.js is installed in the workspace
501+
return getDependencyVersionFromPackageJson(tree, 'zone.js') === null;
502+
}
503+
504+
function findBuildTarget(project: {
505+
targets?: Record<string, { executor?: string; options?: any }>;
506+
}): { executor?: string; options?: any } | null {
507+
for (const target of Object.values(project.targets ?? {})) {
508+
if (
509+
[
510+
'@angular-devkit/build-angular:browser',
511+
'@angular-devkit/build-angular:browser-esbuild',
512+
'@angular-devkit/build-angular:application',
513+
'@angular/build:application',
514+
'@nx/angular:application',
515+
'@nx/angular:browser-esbuild',
516+
'@nx/angular:webpack-browser',
517+
].includes(target.executor)
518+
) {
519+
return target;
520+
}
470521
}
471522

472-
return major(cleanedAngularVersion) >= 20;
523+
return project.targets?.build ?? null;
473524
}
474525

475526
export default configurationGenerator;

packages/vitest/src/generators/configuration/schema.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ export interface VitestGeneratorSchema {
1414
// internal options
1515
projectType?: 'application' | 'library';
1616
viteVersion?: 5 | 6 | 7;
17+
zoneless?: boolean;
1718
}

packages/vitest/src/generators/configuration/schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
"default": false,
6666
"description": "Do not add dependencies to `package.json`.",
6767
"x-priority": "internal"
68+
},
69+
"zoneless": {
70+
"type": "boolean",
71+
"description": "Whether the Angular project is zoneless. When not provided, it is auto-detected from the project configuration.",
72+
"x-priority": "internal"
6873
}
6974
},
7075
"required": ["project"]

0 commit comments

Comments
 (0)