@schematics/angular
Version:
Schematics specific to Angular
293 lines • 14.6 kB
JavaScript
;
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = default_1;
const schematics_1 = require("@angular-devkit/schematics");
const posix_1 = require("node:path/posix");
const util_1 = require("util");
const dependency_1 = require("../../utility/dependency");
const json_file_1 = require("../../utility/json-file");
const latest_versions_1 = require("../../utility/latest-versions");
const workspace_1 = require("../../utility/workspace");
const workspace_models_1 = require("../../utility/workspace-models");
const constants_1 = require("./constants");
const karma_processor_1 = require("./karma-processor");
async function processTestTargetOptions(testTarget, projectName, context, tree, removableKarmaConfigs, customBuildOptions, needDevkitPlugin, manualMigrationFiles) {
let needsCoverage = false;
let needsIstanbul = false;
for (const [configName, options] of (0, workspace_1.allTargetOptions)(testTarget, false)) {
const configKey = configName || '';
if (!customBuildOptions[configKey]) {
// Match Karma behavior where AOT was disabled by default
customBuildOptions[configKey] = {
aot: false,
optimization: false,
extractLicenses: false,
};
}
// Collect custom build options
for (const key of constants_1.BUILD_OPTIONS_KEYS) {
if (options[key] !== undefined) {
customBuildOptions[configKey][key] = options[key];
delete options[key];
}
}
// Map Karma options to Unit-Test options
if (options['codeCoverage'] !== undefined) {
options['coverage'] = options['codeCoverage'];
delete options['codeCoverage'];
}
if (options['codeCoverageExclude'] !== undefined) {
options['coverageExclude'] = options['codeCoverageExclude'];
delete options['codeCoverageExclude'];
}
if (options['coverage'] === true || options['coverageExclude'] !== undefined) {
needsCoverage = true;
}
if (options['sourceMap'] !== undefined) {
context.logger.info(`Project "${projectName}" has "sourceMap" set for tests. ` +
`In unit-test builder with Vitest, source maps are always enabled. The option has been removed.`);
delete options['sourceMap'];
}
// Convert browser list to array format if it is a comma-separated string
const browsers = options['browsers'];
if (typeof browsers === 'string') {
options['browsers'] = browsers.split(',').map((b) => b.trim());
}
else if (browsers === false) {
options['browsers'] = [];
}
const updatedBrowsers = options['browsers'];
if (Array.isArray(updatedBrowsers) && updatedBrowsers.length > 0) {
const hasNonChromium = updatedBrowsers.some((b) => {
if (typeof b !== 'string') {
return false;
}
const normalized = b.toLowerCase();
return !['chrome', 'chromium', 'edge'].some((name) => normalized.includes(name));
});
if (hasNonChromium) {
needsIstanbul = true;
}
context.logger.info(`Project "${projectName}" has browsers configured for tests. ` +
`To run tests in a browser with Vitest, you will need to install either ` +
`"@vitest/browser-playwright" or "@vitest/browser-webdriverio" depending on your preference.`);
}
// Check if the karma configuration file can be safely removed and extract settings
const karmaConfig = options['karmaConfig'];
if (typeof karmaConfig === 'string') {
await (0, karma_processor_1.processKarmaConfig)(karmaConfig, options, projectName, context, tree, removableKarmaConfigs, needDevkitPlugin, manualMigrationFiles);
}
// Map the main entry file to the setupFiles of the unit-test builder
const mainFile = options['main'];
if (typeof mainFile === 'string') {
options['setupFiles'] = [mainFile];
context.logger.info(`Project "${projectName}" uses a "main" entry file for tests: "${mainFile}". ` +
`This has been mapped to the unit-test builder "setupFiles" array. ` +
`Please ensure you remove any TestBed.initTestEnvironment calls from this file ` +
`as the builder now handles test environment initialization automatically.`);
}
delete options['main'];
}
return { needsCoverage, needsIstanbul };
}
function updateTsConfigTypes(tree, tsConfigsToUpdate, context) {
for (const tsConfigPath of tsConfigsToUpdate) {
if (tree.exists(tsConfigPath)) {
try {
const json = new json_file_1.JSONFile(tree, tsConfigPath);
const typesPath = ['compilerOptions', 'types'];
const existingTypes = json.get(typesPath) ?? [];
const newTypes = existingTypes.filter((t) => t !== 'jasmine');
if (!newTypes.includes('vitest/globals')) {
newTypes.push('vitest/globals');
}
if (newTypes.length !== existingTypes.length ||
newTypes.some((t, i) => t !== existingTypes[i])) {
json.modify(typesPath, newTypes);
}
}
catch (err) {
context.logger.warn(`Failed to automatically update types in "${tsConfigPath}". ` +
`Please manually remove "jasmine" and add "vitest/globals" to compilerOptions.types.`);
}
}
}
}
function logSummary(context, migratedProjects, skippedNonApplications, skippedMissingAppBuilder, manualMigrationFiles) {
context.logger.info('\n--- Karma to Vitest Migration Summary ---');
context.logger.info(`Projects migrated: ${migratedProjects.length}`);
if (migratedProjects.length > 0) {
context.logger.info(` - ${migratedProjects.join(', ')}`);
}
context.logger.info(`Projects skipped (non-applications): ${skippedNonApplications.length}`);
if (skippedNonApplications.length > 0) {
context.logger.info(` - ${skippedNonApplications.join(', ')}`);
}
context.logger.info(`Projects skipped (missing application builder): ${skippedMissingAppBuilder.length}`);
if (skippedMissingAppBuilder.length > 0) {
context.logger.info(` - ${skippedMissingAppBuilder.join(', ')}`);
}
const uniqueManualFiles = [...new Set(manualMigrationFiles)];
if (uniqueManualFiles.length > 0) {
context.logger.warn(`\nThe following Karma configuration files require manual migration:`);
for (const file of uniqueManualFiles) {
context.logger.warn(` - ${file}`);
}
}
if (migratedProjects.length > 0) {
context.logger.info(`\nNote: To refactor your test files from Jasmine to Vitest, consider running the following command:` +
`\n ng g @schematics/angular:refactor-jasmine-vitest <project_name>`);
}
context.logger.info('-----------------------------------------\n');
}
function updateProjects(tree, context) {
return (0, workspace_1.updateWorkspace)(async (workspace) => {
let needsCoverage = false;
let needsIstanbul = false;
const removableKarmaConfigs = new Map();
const migratedProjects = [];
const tsConfigsToUpdate = new Set();
const skippedNonApplications = [];
const skippedMissingAppBuilder = [];
const manualMigrationFiles = [];
for (const [projectName, project] of workspace.projects) {
// Restrict to application types for now
if (project.extensions.projectType !== 'application') {
skippedNonApplications.push(projectName);
continue;
}
// Check if build target uses the new application builder
const buildTarget = project.targets.get('build');
if (!buildTarget || buildTarget.builder !== '@angular/build:application') {
context.logger.info(`Project "${projectName}" cannot be migrated to Vitest yet. ` +
`The project must first be migrated to use the "@angular/build:application" builder.`);
skippedMissingAppBuilder.push(projectName);
continue;
}
// Find the test target to migrate
const testTarget = project.targets.get('test');
if (!testTarget) {
continue;
}
let isKarma = false;
let needDevkitPlugin = false;
// Check if target uses Karma builders
switch (testTarget.builder) {
case workspace_models_1.Builders.Karma:
isKarma = true;
needDevkitPlugin = true;
break;
case workspace_models_1.Builders.BuildKarma:
isKarma = true;
break;
}
if (!isKarma) {
continue;
}
// Collect tsConfig paths to perform globals updates
const baseTsConfig = testTarget.options?.['tsConfig'];
if (typeof baseTsConfig === 'string') {
tsConfigsToUpdate.add(baseTsConfig);
}
if (testTarget.configurations) {
for (const config of Object.values(testTarget.configurations)) {
if (typeof config?.['tsConfig'] === 'string') {
tsConfigsToUpdate.add(config['tsConfig']);
}
}
}
// Always include fallback to the default tsconfig.spec.json path
tsConfigsToUpdate.add((0, posix_1.join)(project.root, 'tsconfig.spec.json'));
// Store custom build options to move to a new build configuration if needed
const customBuildOptions = Object.create(null);
const projectCoverageInfo = await processTestTargetOptions(testTarget, projectName, context, tree, removableKarmaConfigs, customBuildOptions, needDevkitPlugin, manualMigrationFiles);
if (projectCoverageInfo.needsCoverage) {
needsCoverage = true;
if (projectCoverageInfo.needsIstanbul) {
needsIstanbul = true;
}
}
// If we have custom build options, create testing configurations
const baseOptions = buildTarget.options || {};
for (const [configKey, configOptions] of Object.entries(customBuildOptions)) {
const finalConfig = {};
// Omit options that already have the same value in the base build options.
// Using isDeepStrictEqual for a deep comparison of arrays and objects.
for (const [key, value] of Object.entries(configOptions)) {
if (!(0, util_1.isDeepStrictEqual)(value, baseOptions[key])) {
finalConfig[key] = value;
}
}
if (Object.keys(finalConfig).length > 0) {
buildTarget.configurations ??= {};
const configurations = buildTarget.configurations;
let configName = configKey ? `testing-${configKey}` : 'testing';
if (configurations[configName]) {
let counter = 1;
while (configurations[`${configName}-${counter}`]) {
counter++;
}
configName = `${configName}-${counter}`;
}
configurations[configName] = finalConfig;
if (configKey === '') {
testTarget.options ??= {};
testTarget.options['buildTarget'] = `:build:${configName}`;
}
else {
testTarget.configurations ??= {};
testTarget.configurations[configKey] ??= {};
testTarget.configurations[configKey]['buildTarget'] = `:build:${configName}`;
}
}
}
// Update builder
testTarget.builder = '@angular/build:unit-test';
testTarget.options ??= {};
testTarget.options['runner'] = 'vitest';
migratedProjects.push(projectName);
}
// Perform cleanup of removable karma config files
for (const [configPath, result] of removableKarmaConfigs) {
if (result.isRemovable && tree.exists(configPath)) {
tree.delete(configPath);
}
}
// Update TSConfig files to use Vitest types instead of Jasmine
updateTsConfigTypes(tree, tsConfigsToUpdate, context);
// Log summary
logSummary(context, migratedProjects, skippedNonApplications, skippedMissingAppBuilder, manualMigrationFiles);
if (migratedProjects.length > 0) {
const rules = [
(0, dependency_1.addDependency)('vitest', latest_versions_1.latestVersions['vitest'], {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Skip,
}),
];
if (needsCoverage) {
rules.push((0, dependency_1.addDependency)('@vitest/coverage-v8', latest_versions_1.latestVersions['@vitest/coverage-v8'], {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Skip,
}));
if (needsIstanbul) {
rules.push((0, dependency_1.addDependency)('@vitest/coverage-istanbul', latest_versions_1.latestVersions['@vitest/coverage-istanbul'], {
type: dependency_1.DependencyType.Dev,
existing: dependency_1.ExistingBehavior.Skip,
}));
}
}
return (0, schematics_1.chain)(rules);
}
});
}
function default_1() {
return updateProjects;
}
//# sourceMappingURL=migration.js.map