diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 122039595bd9..6f2edd0281d7 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -12,7 +12,7 @@ import { targetStringFromTarget, } from '@angular-devkit/architect'; import assert from 'node:assert'; -import { rm } from 'node:fs/promises'; +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'; @@ -244,22 +244,34 @@ export async function* execute( let buildTargetOptions: ApplicationBuilderInternalOptions; try { const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget); - if ( - builderName !== '@angular/build:application' && - // TODO: Add comprehensive support for ng-packagr. - builderName !== '@angular/build:ng-packagr' - ) { + 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'. ` + + `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; + buildTargetOptions = (await context.validateOptions( + await context.getTargetOptions(normalizedOptions.buildTarget), + builderName, + )) as unknown as ApplicationBuilderInternalOptions; + } } catch (e) { assertIsError(e); context.logger.error( @@ -335,3 +347,42 @@ export async function* execute( yield { success: false }; } } + +async function transformNgPackagrOptions( + context: BuilderContext, + options: Record, + projectRoot: string, +): Promise { + 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}: ${e.message}`); + } + + const lib = ngPackageJson['lib'] || {}; + const styleIncludePaths = lib['styleIncludePaths'] || []; + const assets = ngPackageJson['assets'] || []; + const inlineStyleLanguage = ngPackageJson['inlineStyleLanguage']; + + return { + stylePreprocessorOptions: styleIncludePaths.length + ? { includePaths: styleIncludePaths } + : undefined, + assets: assets.length ? assets : undefined, + inlineStyleLanguage: typeof inlineStyleLanguage === 'string' ? inlineStyleLanguage : undefined, + } as ApplicationBuilderInternalOptions; +} diff --git a/tests/e2e/tests/vitest/library.ts b/tests/e2e/tests/vitest/library.ts new file mode 100644 index 000000000000..ba1e31dc38f8 --- /dev/null +++ b/tests/e2e/tests/vitest/library.ts @@ -0,0 +1,50 @@ +import assert from 'node:assert/strict'; +import { updateJsonFile } from '../../utils/project'; +import { ng, silentNpm } from '../../utils/process'; +import { createDir, writeFile } from '../../utils/fs'; + +export default async function (): Promise { + // Install Vitest deps + await silentNpm('install', 'vitest@^4.0.8', 'jsdom@^27.1.0', '--save-dev'); + + // Generate a library + await ng('generate', 'library', 'my-lib', '--test-runner', 'vitest'); + + // Setup Style Include Paths test + // 1. Create a shared SCSS file + await createDir('projects/my-lib/src/styles'); + await writeFile('projects/my-lib/src/styles/_vars.scss', '$primary-color: red;'); + + // 2. Update ng-package.json to include the styles directory + await updateJsonFile('projects/my-lib/ng-package.json', (json) => { + json['lib'] = { + ...json['lib'], + styleIncludePaths: ['./src/styles'], + }; + }); + + // 3. Update the component to use SCSS and import the shared file + // Rename CSS to SCSS + await ng( + 'generate', + 'component', + 'styled-comp', + '--project=my-lib', + '--style=scss', + '--skip-import', + ); + + await writeFile( + 'projects/my-lib/src/lib/styled-comp/styled-comp.component.scss', + ` + @use 'vars'; + p { color: vars.$primary-color; } + `, + ); + + // Run the library tests + const { stdout } = await ng('test', 'my-lib'); + + // Expect tests to pass + assert.match(stdout, /passed/, 'Expected library tests to pass.'); +}