Skip to content

Commit ef443bf

Browse files
committed
feat(@angular/build): directly support ng-packagr in unit-test builder
This change enables the `unit-test` builder to correctly extract base build options from `@angular/build:ng-packagr` targets. It parses the `ng-package.json` file to retrieve configuration for `assets`, `inlineStyleLanguage`, and `stylePreprocessorOptions`, ensuring that libraries can be unit-tested effectively.
1 parent 164e7db commit ef443bf

File tree

2 files changed

+113
-12
lines changed

2 files changed

+113
-12
lines changed

packages/angular/build/src/builders/unit-test/builder.ts

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
targetStringFromTarget,
1313
} from '@angular-devkit/architect';
1414
import assert from 'node:assert';
15-
import { rm } from 'node:fs/promises';
15+
import { readFile, rm } from 'node:fs/promises';
1616
import path from 'node:path';
1717
import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin';
1818
import { assertIsError } from '../../utils/error';
@@ -244,22 +244,34 @@ export async function* execute(
244244
let buildTargetOptions: ApplicationBuilderInternalOptions;
245245
try {
246246
const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget);
247-
if (
248-
builderName !== '@angular/build:application' &&
249-
// TODO: Add comprehensive support for ng-packagr.
250-
builderName !== '@angular/build:ng-packagr'
251-
) {
247+
if (builderName === '@angular/build:application') {
248+
buildTargetOptions = (await context.validateOptions(
249+
await context.getTargetOptions(normalizedOptions.buildTarget),
250+
builderName,
251+
)) as unknown as ApplicationBuilderInternalOptions;
252+
} else if (builderName === '@angular/build:ng-packagr') {
253+
const ngPackagrOptions = await context.validateOptions(
254+
await context.getTargetOptions(normalizedOptions.buildTarget),
255+
builderName,
256+
);
257+
258+
buildTargetOptions = await transformNgPackagrOptions(
259+
context,
260+
ngPackagrOptions,
261+
normalizedOptions.projectRoot,
262+
);
263+
} else {
252264
context.logger.warn(
253265
`The 'buildTarget' is configured to use '${builderName}', which is not supported. ` +
254-
`The 'unit-test' builder is designed to work with '@angular/build:application'. ` +
266+
`The 'unit-test' builder is designed to work with '@angular/build:application' or '@angular/build:ng-packagr'. ` +
255267
'Unexpected behavior or build failures may occur.',
256268
);
257-
}
258269

259-
buildTargetOptions = (await context.validateOptions(
260-
await context.getTargetOptions(normalizedOptions.buildTarget),
261-
builderName,
262-
)) as unknown as ApplicationBuilderInternalOptions;
270+
buildTargetOptions = (await context.validateOptions(
271+
await context.getTargetOptions(normalizedOptions.buildTarget),
272+
builderName,
273+
)) as unknown as ApplicationBuilderInternalOptions;
274+
}
263275
} catch (e) {
264276
assertIsError(e);
265277
context.logger.error(
@@ -335,3 +347,42 @@ export async function* execute(
335347
yield { success: false };
336348
}
337349
}
350+
351+
async function transformNgPackagrOptions(
352+
context: BuilderContext,
353+
options: Record<string, unknown>,
354+
projectRoot: string,
355+
): Promise<ApplicationBuilderInternalOptions> {
356+
const projectPath = options['project'];
357+
358+
let ngPackagePath: string;
359+
if (projectPath) {
360+
if (typeof projectPath !== 'string') {
361+
throw new Error('ng-packagr builder options "project" property must be a string.');
362+
}
363+
ngPackagePath = path.join(context.workspaceRoot, projectPath);
364+
} else {
365+
ngPackagePath = path.join(projectRoot, 'ng-package.json');
366+
}
367+
368+
let ngPackageJson;
369+
try {
370+
ngPackageJson = JSON.parse(await readFile(ngPackagePath, 'utf-8'));
371+
} catch (e) {
372+
assertIsError(e);
373+
throw new Error(`Could not read ng-package.json at ${ngPackagePath}: ${e.message}`);
374+
}
375+
376+
const lib = ngPackageJson['lib'] || {};
377+
const styleIncludePaths = lib['styleIncludePaths'] || [];
378+
const assets = ngPackageJson['assets'] || [];
379+
const inlineStyleLanguage = ngPackageJson['inlineStyleLanguage'];
380+
381+
return {
382+
stylePreprocessorOptions: styleIncludePaths.length
383+
? { includePaths: styleIncludePaths }
384+
: undefined,
385+
assets: assets.length ? assets : undefined,
386+
inlineStyleLanguage: typeof inlineStyleLanguage === 'string' ? inlineStyleLanguage : undefined,
387+
} as ApplicationBuilderInternalOptions;
388+
}

tests/e2e/tests/vitest/library.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import assert from 'node:assert/strict';
2+
import { updateJsonFile } from '../../utils/project';
3+
import { ng, silentNpm } from '../../utils/process';
4+
import { createDir, writeFile } from '../../utils/fs';
5+
6+
export default async function (): Promise<void> {
7+
// Install Vitest deps
8+
await silentNpm('install', 'vitest@^4.0.8', 'jsdom@^27.1.0', '--save-dev');
9+
10+
// Generate a library
11+
await ng('generate', 'library', 'my-lib', '--test-runner', 'vitest');
12+
13+
// Setup Style Include Paths test
14+
// 1. Create a shared SCSS file
15+
await createDir('projects/my-lib/src/styles');
16+
await writeFile('projects/my-lib/src/styles/_vars.scss', '$primary-color: red;');
17+
18+
// 2. Update ng-package.json to include the styles directory
19+
await updateJsonFile('projects/my-lib/ng-package.json', (json) => {
20+
json['lib'] = {
21+
...json['lib'],
22+
styleIncludePaths: ['./src/styles'],
23+
};
24+
});
25+
26+
// 3. Update the component to use SCSS and import the shared file
27+
// Rename CSS to SCSS
28+
await ng(
29+
'generate',
30+
'component',
31+
'styled-comp',
32+
'--project=my-lib',
33+
'--style=scss',
34+
'--skip-import',
35+
);
36+
37+
await writeFile(
38+
'projects/my-lib/src/lib/styled-comp/styled-comp.component.scss',
39+
`
40+
@use 'vars';
41+
p { color: vars.$primary-color; }
42+
`,
43+
);
44+
45+
// Run the library tests
46+
const { stdout } = await ng('test', 'my-lib');
47+
48+
// Expect tests to pass
49+
assert.match(stdout, /passed/, 'Expected library tests to pass.');
50+
}

0 commit comments

Comments
 (0)