/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/angular/build/src/builders/unit-test/builder.ts
391 строка
12 KB
Charles Lyding
fix(@angular/build): canonicalize drive letter casing for workspace root on Windows
20 июл 2026, 18:37
20 июл 2026, 18:37
4187e5e
Код
Авторство
О чём код?
/** * @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 */ import { type BuilderContext, type BuilderOutput, targetStringFromTarget, } from '@angular-devkit/architect'; import assert from 'node:assert'; import { readFile, rm } from 'node:fs/promises'; import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; import { assertIsError } from '../../utils/error'; import { writeTestFiles } from '../../utils/test-files'; import { buildApplicationInternal } from '../application'; import type { ApplicationBuilderExtensions, ApplicationBuilderInternalOptions, } from '../application/options'; import { ResultKind } from '../application/results'; import { normalizeOptions } from './options'; import type { TestRunner } from './runners/api'; import { MissingDependenciesError } from './runners/dependency-checker'; import type { Schema as UnitTestBuilderOptions } from './schema'; import { findTests } from './test-discovery'; export type { UnitTestBuilderOptions }; async function loadTestRunner(runnerName: string): Promise<TestRunner> { // Harden against directory traversal if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) { throw new Error( `Invalid runner name "${runnerName}". Runner names can only contain alphanumeric characters and hyphens.`, ); } let runnerModule; try { runnerModule = await import(`./runners/${runnerName}/index`); } catch (e) { assertIsError(e); if (e.code === 'ERR_MODULE_NOT_FOUND') { throw new Error(`Unknown test runner "${runnerName}".`, { cause: e }); } throw new Error( `Failed to load the '${runnerName}' test runner. The package may be corrupted or improperly installed.`, { cause: e }, ); } const runner = runnerModule.default; if ( !runner || typeof runner.getBuildOptions !== 'function' || typeof runner.createExecutor !== 'function' ) { throw new Error( `The loaded test runner '${runnerName}' does not appear to be a valid TestRunner implementation.`, ); } return runner; } function prepareBuildExtensions( virtualFiles: Record<string, string> | undefined, projectSourceRoot: string, extensions?: ApplicationBuilderExtensions, ): ApplicationBuilderExtensions | undefined { if (!virtualFiles) { return extensions; } extensions ??= {}; extensions.codePlugins ??= []; for (const [namespace, contents] of Object.entries(virtualFiles)) { extensions.codePlugins.push( createVirtualModulePlugin({ namespace, loadContent: () => { return { contents, loader: 'js', resolveDir: projectSourceRoot, }; }, }), ); } return extensions; } async function* runBuildAndTest( executor: import('./runners/api').TestExecutor, applicationBuildOptions: ApplicationBuilderInternalOptions, context: BuilderContext, dumpDirectory: string | undefined, extensions: ApplicationBuilderExtensions | undefined, ): AsyncIterable<BuilderOutput> { let consecutiveErrorCount = 0; for await (const buildResult of buildApplicationInternal( applicationBuildOptions, context, extensions, )) { if (buildResult.kind === ResultKind.Failure) { yield { success: false }; continue; } else if ( buildResult.kind !== ResultKind.Full && buildResult.kind !== ResultKind.Incremental ) { assert.fail( 'A full and/or incremental build result is required from the application builder.', ); } assert(buildResult.files, 'Builder did not provide result files.'); if (dumpDirectory) { if (buildResult.kind === ResultKind.Full) { // Full build, so clean the directory await rm(dumpDirectory, { recursive: true, force: true }); } else { // Incremental build, so delete removed files for (const file of buildResult.removed) { await rm(path.join(dumpDirectory, file.path), { force: true }); } } await writeTestFiles(buildResult.files, dumpDirectory); context.logger.info(`Build output files successfully dumped to '${dumpDirectory}'.`); } // Pass the build artifacts to the executor try { yield* executor.execute(buildResult); // Successful execution resets the failure counter consecutiveErrorCount = 0; } catch (e) { assertIsError(e); context.logger.error(`An exception occurred during test execution:\n${e.stack ?? e.message}`); if (e instanceof AggregateError) { e.errors.forEach((inner) => { assertIsError(inner); context.logger.error(inner.stack ?? inner.message); }); } yield { success: false }; consecutiveErrorCount++; } if (consecutiveErrorCount >= 3) { context.logger.error( 'Test runner process has failed multiple times in a row. Please fix the configuration and restart the process.', ); return; } } } /** * @experimental Direct usage of this function is considered experimental. */ export async function* execute( options: UnitTestBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions, ): AsyncIterable<BuilderOutput> { // Determine project name from builder context target const projectName = context.target?.project; if (!projectName) { context.logger.error(`The builder requires a target to be specified.`); return; } // Initialize the test runner and normalize options let runner; let normalizedOptions; try { normalizedOptions = await normalizeOptions(context, projectName, options); runner = await loadTestRunner(normalizedOptions.runnerName); await runner.validateDependencies?.(normalizedOptions); } catch (e) { assertIsError(e); if (e instanceof MissingDependenciesError) { context.logger.error(e.message); } else { context.logger.error( `An exception occurred during initialization of the test runner:\n${e.stack ?? e.message}`, ); } yield { success: false }; return; } if (normalizedOptions.listTests) { const testFiles = await findTests( normalizedOptions.include, normalizedOptions.exclude ?? [], normalizedOptions.workspaceRoot, normalizedOptions.projectSourceRoot, ); context.logger.info('Discovered test files:'); for (const file of testFiles) { context.logger.info(` ${path.relative(normalizedOptions.workspaceRoot, file)}`); } yield { success: true }; return; } if (runner.isStandalone) { try { await using executor = await runner.createExecutor(context, normalizedOptions, undefined); yield* executor.execute({ kind: ResultKind.Full, files: {}, }); } catch (e) { assertIsError(e); context.logger.error( `An exception occurred during standalone test execution:\n${e.stack ?? e.message}`, ); yield { success: false }; } return; } // Get base build options from the buildTarget let buildTargetOptions: ApplicationBuilderInternalOptions; try { const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); if (builderName === '@angular/build:application') { buildTargetOptions = (await context.validateOptions( await context.getTargetOptions(normalizedOptions.buildTarget), builderName, )) as unknown as ApplicationBuilderInternalOptions; } else if (builderName === '@angular/build:ng-packagr') { const ngPackagrOptions = await context.validateOptions( await context.getTargetOptions(normalizedOptions.buildTarget), builderName, ); buildTargetOptions = await transformNgPackagrOptions( context, ngPackagrOptions, normalizedOptions.projectRoot, ); } else { context.logger.warn( `The 'buildTarget' is configured to use '${builderName}', which is not supported. ` + `The 'unit-test' builder is designed to work with '@angular/build:application' or '@angular/build:ng-packagr'. ` + 'Unexpected behavior or build failures may occur.', ); buildTargetOptions = (await context.validateOptions( await context.getTargetOptions(normalizedOptions.buildTarget), builderName, )) as unknown as ApplicationBuilderInternalOptions; } } catch (e) { assertIsError(e); context.logger.error( `Could not load build target options for "${targetStringFromTarget( normalizedOptions.buildTarget, )}".\n` + `Please check your 'angular.json' configuration.\n` + `Error: ${e.message}`, ); yield { success: false }; return; } // Get runner-specific build options let runnerBuildOptions; let virtualFiles; let testEntryPointMappings; try { ({ buildOptions: runnerBuildOptions, virtualFiles, testEntryPointMappings, } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions)); } catch (e) { assertIsError(e); context.logger.error( `An exception occurred while getting runner-specific build options:\n${e.stack ?? e.message}`, ); yield { success: false }; return; } try { await using executor = await runner.createExecutor( context, normalizedOptions, testEntryPointMappings, ); const finalExtensions = prepareBuildExtensions( virtualFiles, normalizedOptions.projectSourceRoot, extensions, ); // Prepare and run the application build const applicationBuildOptions = { ...buildTargetOptions, ...runnerBuildOptions, watch: normalizedOptions.watch, progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress, quiet: normalizedOptions.quiet, ...(normalizedOptions.tsConfig ? { tsConfig: normalizedOptions.tsConfig } : {}), preserveSymlinks: normalizedOptions.preserveSymlinks, } satisfies ApplicationBuilderInternalOptions; const dumpDirectory = normalizedOptions.dumpVirtualFiles ? path.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files') : undefined; yield* runBuildAndTest( executor, applicationBuildOptions, context, dumpDirectory, finalExtensions, ); } catch (e) { assertIsError(e); context.logger.error( `An exception occurred while creating the test executor:\n${e.stack ?? e.message}`, ); yield { success: false }; } } async function transformNgPackagrOptions( context: BuilderContext, options: Record<string, unknown>, projectRoot: string, ): Promise<ApplicationBuilderInternalOptions> { const projectPath = options['project']; let ngPackagePath: string; if (projectPath) { if (typeof projectPath !== 'string') { throw new Error('ng-packagr builder options "project" property must be a string.'); } ngPackagePath = path.join(context.workspaceRoot, projectPath); } else { ngPackagePath = path.join(projectRoot, 'ng-package.json'); } let ngPackageJson; try { ngPackageJson = JSON.parse(await readFile(ngPackagePath, 'utf-8')); } catch (e) { assertIsError(e); throw new Error(`Could not read ng-package.json at ${ngPackagePath}`, { cause: e, }); } const { lib: { styleIncludePaths = [] } = {}, assets = [], inlineStyleLanguage } = ngPackageJson; const includePaths = styleIncludePaths.map((includePath: string) => path.resolve(path.dirname(ngPackagePath), includePath), ); return { stylePreprocessorOptions: includePaths.length ? { includePaths } : undefined, assets: assets.length ? assets : undefined, inlineStyleLanguage, } as ApplicationBuilderInternalOptions; }