From c22b3bf9a5cfd20a9dfca92ac6d77cd8d011bfa8 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:31:42 +0000 Subject: [PATCH] refactor(@angular/build): collect and log build errors in library builder Instead of throwing errors inside `buildAction` and `compileLibrary` during progress spinner execution, collect formatted diagnostic errors, warnings, and informational logs into `BuildActionResult` and log them via `context.logger` after `withProgress` completes, matching the behavior of the `application` builder. --- .../build/src/builders/library/builder.ts | 52 +++- .../builders/library/pipeline/build-action.ts | 294 ++++++++++-------- .../builders/library/pipeline/compilation.ts | 54 ++-- .../builders/library/pipeline/entry-points.ts | 65 ++++ .../library/tests/behavior/build_spec.ts | 6 + .../allowed-non-peer-dependencies_spec.ts | 10 +- 6 files changed, 317 insertions(+), 164 deletions(-) diff --git a/packages/angular/build/src/builders/library/builder.ts b/packages/angular/build/src/builders/library/builder.ts index 948f53ae568b..84b933e7ceb6 100644 --- a/packages/angular/build/src/builders/library/builder.ts +++ b/packages/angular/build/src/builders/library/builder.ts @@ -24,6 +24,7 @@ import { getSupportedBrowsers } from '../../utils/supported-browsers'; import { assertCompatibleAngularVersion } from '../../utils/version'; import type { BuildWatcher } from '../../utils/watcher'; import { normalizeLibraryOptions } from './options'; +import type { BuildActionResult } from './pipeline/build-action'; import { updateWatchedEntryPoints } from './pipeline/entry-points'; import type { createComponentStylesheetBundlerForLibrary } from './pipeline/stylesheet-bundler'; import { loadPackageJson } from './pipeline/utils'; @@ -185,6 +186,17 @@ export async function* executeLibraryBuilder( } } +/** + * Executes a single build iteration with progress reporting, logs any resulting + * messages, and registers newly discovered files with the watcher. + * + * @param message Progress spinner message to display while building. + * @param actionContext The build action context and state. + * @param withProgress Progress wrapper function (`withSpinner` or `withNoProgress`). + * @param watcher Optional file watcher for watch mode. + * @param buildAction The library build action function to execute. + * @returns A promise resolving to the Architect `BuilderOutput`. + */ async function executeBuild( message: string, actionContext: import('./pipeline/build-action').BuildActionContext, @@ -196,13 +208,16 @@ async function executeBuild( const { context, watchedCompilationFiles, isWatchMode, options } = actionContext; try { - await withProgress(message, () => buildAction(actionContext)); - logBuildResult(context.logger, startTime, true, options.outputPath); - if (isWatchMode) { + const result = await withProgress(message, () => buildAction(actionContext)); + logMessages(context.logger, result); + + const success = result.errors.length === 0; + logBuildResult(context.logger, startTime, success, options.outputPath); + if (success && isWatchMode) { logCumulativeDurations(); } - return { success: true }; + return { success }; } catch (error) { assertIsError(error); logBuildResult(context.logger, startTime, false); @@ -300,6 +315,7 @@ async function* runWatchLoop( assertIsError(error); await buildState.singleProgramCache?.compilationInstance.update?.(changedFiles); buildState.hasEmittedManifests = false; + yield { success: false, error: `Failed to reload 'package.json': ${error.message}`, @@ -343,8 +359,36 @@ async function* runWatchLoop( } } +/** + * Logs informational, warning, and error messages produced during a build iteration. + * + * @param logger The Architect logger API. + * @param result The build action result containing logs, warnings, and errors. + */ +function logMessages( + logger: logging.LoggerApi, + { logs, warnings, errors }: BuildActionResult, +): void { + if (logs.length) { + logger.info(logs.join('\n')); + } + + if (warnings.length) { + logger.warn(warnings.join('\n')); + } + + if (errors.length) { + logger.error(errors.join('\n')); + } +} + /** * Logs the build completion time and status. + * + * @param logger The Architect logger API. + * @param startTime High-resolution timestamp recorded when the build iteration started. + * @param success Whether the build iteration succeeded without errors. + * @param outputPath Optional output directory path to log on successful builds. */ function logBuildResult( logger: logging.LoggerApi, diff --git a/packages/angular/build/src/builders/library/pipeline/build-action.ts b/packages/angular/build/src/builders/library/pipeline/build-action.ts index a430312ac925..6d40f5fe4383 100644 --- a/packages/angular/build/src/builders/library/pipeline/build-action.ts +++ b/packages/angular/build/src/builders/library/pipeline/build-action.ts @@ -10,14 +10,30 @@ import type { BuilderContext } from '@angular-devkit/architect'; import path from 'node:path'; import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets'; import { emitFilesToDisk } from '../../../utils/emit-files-to-disk'; +import { assertIsError } from '../../../utils/error'; import { toPosixPath } from '../../../utils/path'; import type { NormalizedLibraryOptions, SingleBuildState } from '../types'; import { collectAssetsToEmit } from './assets'; -import { type BundleEntryPointInput, bundleEntryPoints } from './bundler'; -import { compileLibrary } from './compilation'; +import { bundleEntryPoints } from './bundler'; +import { type LibraryCompilationDiagnostics, compileLibrary } from './compilation'; +import { collectEntryPointsToBundle } from './entry-points'; import { generatePackageManifests } from './package-manifests'; import type { DiskOutputFile, OutputFile } from './utils'; +/** + * Result of executing a single library build iteration. + */ +export interface BuildActionResult { + /** Formatted error messages produced during the build. */ + errors: string[]; + + /** Formatted warning messages produced during the build. */ + warnings: string[]; + + /** Informational log messages produced during the build. */ + logs: string[]; +} + /** * Creates a fresh {@link SingleBuildState} instance. */ @@ -76,10 +92,9 @@ export interface BuildActionContext { * * @param actionContext The build action state and configuration. */ -export async function buildAction(actionContext: BuildActionContext): Promise { +export async function buildAction(actionContext: BuildActionContext): Promise { const { options, - context, stylesheetBundler, isWatchMode, watchedCompilationFiles, @@ -87,6 +102,10 @@ export async function buildAction(actionContext: BuildActionContext): Promise 0 || pendingChangedDtsFiles.size > 0 || hasModifiedWatchedFile(modifiedFiles, watchedCompilationFiles, posixPackageJsonPath); - const shouldGenerateManifests = !buildState.hasEmittedManifests; - if (shouldGenerateManifests) { - verifyAllowedDependencies(options); - } + try { + if (!buildState.hasEmittedManifests) { + const dependencyErrors = verifyAllowedDependencies(options); + if (dependencyErrors.length > 0) { + errors.push(...dependencyErrors); - const filesToEmit: OutputFile[] = []; - - if (shouldCompileEntryPoints) { - buildState.hasCompilationError = true; - buildState.hasEntryPointsChanges = false; - - const { - esmFiles, - dtsFiles, - changedEsmFiles, - changedDtsFiles, - referencedFiles, - cache, - diagnosePromise, - declarationMap, - compilationMode, - } = await compileLibrary( - options.entryPoints.values(), - options, - stylesheetBundler, - buildState.singleProgramCache, - modifiedFiles, - ); - buildState.singleProgramCache = cache; - if (buildState.compilationMode !== compilationMode) { - buildState.compilationMode = compilationMode; - buildState.hasEmittedManifests = false; + return { errors, warnings, logs }; + } } - for (const file of referencedFiles) { - watchedCompilationFiles.add(file); - } + const filesToEmit: OutputFile[] = []; - for (const file of changedEsmFiles) { - pendingChangedEsmFiles.add(file); - } - for (const file of changedDtsFiles) { - pendingChangedDtsFiles.add(file); - } + if (shouldCompileEntryPoints) { + buildState.hasCompilationError = true; + buildState.hasEntryPointsChanges = false; - const itemsToBundle: BundleEntryPointInput[] = []; + const { + esmFiles, + dtsFiles, + changedEsmFiles, + changedDtsFiles, + referencedFiles, + cache, + hasStylesheetErrors, + diagnosePromise, + declarationMap, + compilationMode, + } = await compileLibrary( + options.entryPoints.values(), + options, + stylesheetBundler, + buildState.singleProgramCache, + modifiedFiles, + ); + buildState.singleProgramCache = cache; + if (buildState.compilationMode !== compilationMode) { + buildState.compilationMode = compilationMode; + buildState.hasEmittedManifests = false; + } - for (const entryPoint of options.entryPoints.values()) { - const previousBundleResult = buildState.previousBundleResults.get(entryPoint.name); - const hasEsmChanges = - !previousBundleResult || - hasEntryPointChanges(previousBundleResult.esmModuleIds, pendingChangedEsmFiles); - const hasDtsChanges = - !previousBundleResult || - hasEntryPointChanges(previousBundleResult.dtsModuleIds, pendingChangedDtsFiles); - - if (hasEsmChanges || hasDtsChanges) { - context.logger.info(`Compiling ${entryPoint.displayName}...`); - itemsToBundle.push({ - entryPoint, - hasEsmChanges, - hasDtsChanges, - previousBundleResult, - }); + for (const file of referencedFiles) { + watchedCompilationFiles.add(file); + } + for (const file of changedEsmFiles) { + pendingChangedEsmFiles.add(file); + } + for (const file of changedDtsFiles) { + pendingChangedDtsFiles.add(file); } - } - let bundleOutput: Awaited>; - let warnings: string[]; - try { - [bundleOutput, warnings] = await Promise.all([ - bundleEntryPoints(itemsToBundle, esmFiles, dtsFiles, { - ...options, - declarationMap, - compilationMode, - }), - diagnosePromise, - ]); - } catch (error) { - // Prioritize TypeScript/Angular diagnostic errors over secondary bundler failures. - await diagnosePromise; - throw error; - } + const itemsToBundle = collectEntryPointsToBundle(options, buildState, logs); + + let bundleOutput: Awaited> | undefined; + let diagnostics: LibraryCompilationDiagnostics; + if (hasStylesheetErrors) { + diagnostics = await diagnosePromise; + } else { + try { + [bundleOutput, diagnostics] = await Promise.all([ + bundleEntryPoints(itemsToBundle, esmFiles, dtsFiles, { + ...options, + declarationMap, + compilationMode, + }), + diagnosePromise, + ]); + } catch (error) { + assertIsError(error); + // Prioritize TypeScript/Angular diagnostic errors over secondary bundler failures. + diagnostics = await diagnosePromise; + if (diagnostics.errors.length === 0) { + errors.push(error.message); + } + } + } + + warnings.push(...diagnostics.warnings, ...(bundleOutput?.warnings ?? [])); + if (diagnostics.errors.length > 0 || errors.length > 0 || !bundleOutput) { + errors.push(...diagnostics.errors); + + return { errors, warnings, logs }; + } - buildState.hasCompilationError = false; - pendingChangedEsmFiles.clear(); - pendingChangedDtsFiles.clear(); + buildState.hasCompilationError = false; + pendingChangedEsmFiles.clear(); + pendingChangedDtsFiles.clear(); - for (const warning of [...warnings, ...bundleOutput.warnings]) { - context.logger.warn(warning); + filesToEmit.push(...bundleOutput.filesToEmit); + for (const [name, bundleResult] of bundleOutput.bundleResults) { + buildState.previousBundleResults.set(name, bundleResult); + } } - filesToEmit.push(...bundleOutput.filesToEmit); - for (const [name, bundleResult] of bundleOutput.bundleResults) { - buildState.previousBundleResults.set(name, bundleResult); + let hasTslibImport = false; + for (const entryPoint of options.entryPoints.values()) { + if (buildState.previousBundleResults.get(entryPoint.name)?.hasTslibImport) { + hasTslibImport = true; + break; + } } - } - let hasTslibImport = false; - for (const entryPoint of options.entryPoints.values()) { - if (buildState.previousBundleResults.get(entryPoint.name)?.hasTslibImport) { - hasTslibImport = true; - break; + if (!buildState.hasEmittedManifests || buildState.hasEmittedTslib !== hasTslibImport) { + filesToEmit.push( + ...(await generatePackageManifests( + { + ...options, + compilationMode: buildState.compilationMode ?? options.compilationMode ?? 'partial', + }, + isWatchMode, + hasTslibImport, + )), + ); } - } - if (!buildState.hasEmittedManifests || buildState.hasEmittedTslib !== hasTslibImport) { - filesToEmit.push( - ...(await generatePackageManifests( - { - ...options, - compilationMode: buildState.compilationMode ?? options.compilationMode ?? 'partial', - }, - isWatchMode, - hasTslibImport, - )), - ); + const resolvedAssetsToEmit = (actionContext.assetsToEmit ??= await collectAssetsToEmit( + options.assets, + options.workspaceRoot, + buildState.hasEmittedAssets ? modifiedFiles : undefined, + )); + filesToEmit.push(...resolvedAssetsToEmit); + + await emitFilesToDisk(filesToEmit, (file) => path.join(options.outputPath, file.path)); + + buildState.hasEmittedManifests = true; + buildState.hasEmittedTslib = hasTslibImport; + buildState.hasEmittedAssets = true; + } catch (error) { + assertIsError(error); + errors.push(error.message); } - const resolvedAssetsToEmit = (actionContext.assetsToEmit ??= await collectAssetsToEmit( - options.assets, - options.workspaceRoot, - buildState.hasEmittedAssets ? modifiedFiles : undefined, - )); - filesToEmit.push(...resolvedAssetsToEmit); - - await emitFilesToDisk(filesToEmit, (file) => path.join(options.outputPath, file.path)); - - buildState.hasEmittedManifests = true; - buildState.hasEmittedTslib = hasTslibImport; - buildState.hasEmittedAssets = true; + return { errors, warnings, logs }; } +/** + * Checks whether any modified file (excluding `package.json`) is part of the + * tracked compilation file set. + * + * @param modifiedFiles Set of POSIX file paths modified since the last build. + * @param watchedCompilationFiles Set of POSIX file paths tracked for compilation. + * @param posixPackageJsonPath The normalized POSIX path to the library's `package.json`. + * @returns `true` if a watched compilation file was modified; otherwise `false`. + */ export function hasModifiedWatchedFile( modifiedFiles: ReadonlySet, watchedCompilationFiles: ReadonlySet, @@ -250,36 +279,29 @@ export function hasModifiedWatchedFile( return false; } -function hasEntryPointChanges( - moduleIds: ReadonlySet, - changedFiles: ReadonlySet, -): boolean { - if (changedFiles.size === 0) { - return false; - } - - for (const file of changedFiles) { - if (moduleIds.has(file)) { - return true; - } - } - - return false; -} - -function verifyAllowedDependencies(options: NormalizedLibraryOptions): void { +/** + * Validates that all direct and optional dependencies in `package.json` are + * permitted by `allowedNonPeerDependencies`. + * + * @param options The normalized library builder options. + * @returns An array of error messages for any disallowed dependencies. + */ +function verifyAllowedDependencies(options: NormalizedLibraryOptions): string[] { const { packageJson, allowedNonPeerDependencies } = options; const dependencies = { ...(packageJson.dependencies ?? {}), ...(packageJson.optionalDependencies ?? {}), }; + const errors: string[] = []; for (const dep of Object.keys(dependencies)) { if (!allowedNonPeerDependencies.some((regex) => regex.test(dep))) { - throw new Error( + errors.push( `Dependency '${dep}' must be explicitly allowed using the 'allowedNonPeerDependencies' option, ` + `or moved to 'peerDependencies' in 'package.json'.`, ); } } + + return errors; } diff --git a/packages/angular/build/src/builders/library/pipeline/compilation.ts b/packages/angular/build/src/builders/library/pipeline/compilation.ts index 51a8fb0d231e..afc5c319854e 100644 --- a/packages/angular/build/src/builders/library/pipeline/compilation.ts +++ b/packages/angular/build/src/builders/library/pipeline/compilation.ts @@ -20,6 +20,17 @@ import { isDeclarationFile, isDeclarationSourceMapFile } from './utils'; const EMITTED_EXTENSIONS = ['.js', '.mjs', '.cjs', '.d.ts', '.d.mts', '.d.cts']; +/** + * Formatted diagnostic messages produced during library compilation. + */ +export interface LibraryCompilationDiagnostics { + /** Formatted compilation error messages. */ + readonly errors: string[]; + + /** Formatted compilation warning messages. */ + readonly warnings: string[]; +} + /** * Output of the unified library compilation step. */ @@ -42,8 +53,11 @@ export interface LibraryCompilationOutput { /** Cached compilation state for subsequent incremental builds. */ readonly cache: SingleProgramCache; - /** Promise that resolves to formatted compilation warnings or rejects on diagnostic errors. */ - readonly diagnosePromise: Promise; + /** Whether any component stylesheet bundling errors occurred. */ + readonly hasStylesheetErrors: boolean; + + /** Promise that resolves to formatted compilation diagnostic errors and warnings. */ + readonly diagnosePromise: Promise; /** Effective declarationMap setting resolved from builder options and tsconfig. */ readonly declarationMap: boolean; @@ -145,8 +159,8 @@ export async function compileLibrary( 'library', ); - const emittedFiles = - stylesheetErrors.length === 0 ? await compilationInstance.emitAffectedFiles() : []; + const hasStylesheetErrors = stylesheetErrors.length > 0; + const emittedFiles = hasStylesheetErrors ? [] : await compilationInstance.emitAffectedFiles(); const diagnosePromise = runDiagnosticsAndFormat( compilationInstance, stylesheetErrors, @@ -154,9 +168,6 @@ export async function compileLibrary( colors, ); - // Prevent unhandled promise rejection if an error occurs before diagnosePromise is awaited. - diagnosePromise.catch(() => {}); - const esmFiles = cached?.esmFiles ?? new Map(); const dtsFiles = cached?.dtsFiles ?? new Map(); const changedEsmFiles = new Set(); @@ -225,6 +236,7 @@ export async function compileLibrary( dtsFiles, failedFiles: failedFiles.size > 0 ? failedFiles : undefined, }, + hasStylesheetErrors, diagnosePromise, declarationMap: !!compilerOptions.declarationMap, compilationMode: compilerOptions.compilationMode === 'full' ? 'full' : 'partial', @@ -238,34 +250,31 @@ export async function compileLibrary( } /** - * Validates stylesheet bundling results and runs TypeScript/Angular diagnostics. + * Formats stylesheet bundling diagnostics and TypeScript/Angular diagnostics. * * @param compilationInstance The active Angular compilation instance. * @param stylesheetErrors List of stylesheet bundling errors. * @param stylesheetWarnings List of stylesheet bundling warnings. * @param colors Whether diagnostic messages should be formatted with ANSI colors. - * @returns A promise resolving to formatted warning messages. - * @throws If stylesheet bundling errors or compilation errors occur. + * @returns A promise resolving to the formatted diagnostic errors and warnings. */ async function runDiagnosticsAndFormat( compilationInstance: AngularCompilation, stylesheetErrors: PartialMessage[], stylesheetWarnings: PartialMessage[], colors: boolean, -): Promise { - if (stylesheetErrors.length > 0) { - const formatted = await formatMessages(stylesheetErrors, { kind: 'error', color: colors }); - - throw new Error(`Failed to bundle stylesheet:\n${formatted.join('\n')}`); - } - +): Promise { + const errorsOut: string[] = []; const warningsOut: string[] = []; - if (useTypeChecking) { + if (stylesheetErrors.length > 0) { + const formatted = await formatMessages(stylesheetErrors, { kind: 'error', color: colors }); + errorsOut.push(...formatted); + } else if (useTypeChecking) { const { errors, warnings } = await compilationInstance.diagnoseFiles(); if (errors?.length) { - const errorMessages = await formatMessages(errors, { kind: 'error', color: colors }); - throw new Error(`Compilation failed with errors:\n${errorMessages.join('\n')}`); + const formatted = await formatMessages(errors, { kind: 'error', color: colors }); + errorsOut.push(...formatted); } if (warnings?.length) { @@ -282,5 +291,8 @@ async function runDiagnosticsAndFormat( warningsOut.push(...formattedStyleWarnings); } - return warningsOut; + return { + errors: errorsOut, + warnings: warningsOut, + }; } diff --git a/packages/angular/build/src/builders/library/pipeline/entry-points.ts b/packages/angular/build/src/builders/library/pipeline/entry-points.ts index 93a96307d9e4..7797f069fade 100644 --- a/packages/angular/build/src/builders/library/pipeline/entry-points.ts +++ b/packages/angular/build/src/builders/library/pipeline/entry-points.ts @@ -14,6 +14,7 @@ import type { PackageJsonData, SingleBuildState, } from '../types'; +import type { BundleEntryPointInput } from './bundler'; /** * Computes the base bundle file name for an entry point. @@ -270,3 +271,67 @@ export function updateWatchedEntryPoints( buildState.hasEntryPointsChanges = true; } } + +/** + * Determines which entry points have modified JavaScript or declaration files + * and collects them for bundling. + * + * @param options The normalized library builder options. + * @param buildState The state preserved across incremental builds. + * @param logs Array of informational log messages to append to when an entry point is queued. + * @returns The list of entry points that require bundling in the current build iteration. + */ +export function collectEntryPointsToBundle( + options: NormalizedLibraryOptions, + buildState: SingleBuildState, + logs: string[], +): BundleEntryPointInput[] { + const { pendingChangedEsmFiles, pendingChangedDtsFiles, previousBundleResults } = buildState; + const itemsToBundle: BundleEntryPointInput[] = []; + + for (const entryPoint of options.entryPoints.values()) { + const previousBundleResult = previousBundleResults.get(entryPoint.name); + const hasEsmChanges = + !previousBundleResult || + hasEntryPointChanges(previousBundleResult.esmModuleIds, pendingChangedEsmFiles); + const hasDtsChanges = + !previousBundleResult || + hasEntryPointChanges(previousBundleResult.dtsModuleIds, pendingChangedDtsFiles); + + if (hasEsmChanges || hasDtsChanges) { + logs.push(`Compiling ${entryPoint.displayName}...`); + itemsToBundle.push({ + entryPoint, + hasEsmChanges, + hasDtsChanges, + previousBundleResult, + }); + } + } + + return itemsToBundle; +} + +/** + * Checks whether any file in `changedFiles` belongs to the given entry point's `moduleIds`. + * + * @param moduleIds Set of module IDs referenced by the entry point's previous bundle. + * @param changedFiles Set of file paths that changed during compilation. + * @returns `true` if at least one changed file is part of `moduleIds`; otherwise `false`. + */ +function hasEntryPointChanges( + moduleIds: ReadonlySet, + changedFiles: ReadonlySet, +): boolean { + if (changedFiles.size === 0) { + return false; + } + + for (const file of changedFiles) { + if (moduleIds.has(file)) { + return true; + } + } + + return false; +} diff --git a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts index e55a414c1ada..4f5c6736c1ff 100644 --- a/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts +++ b/packages/angular/build/src/builders/library/tests/behavior/build_spec.ts @@ -216,6 +216,12 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeFalse(); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringContaining('TS2322'), + }), + ); expect(logs).toContain( jasmine.objectContaining({ level: 'error', diff --git a/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts b/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts index caca7c594f26..c75699d068a3 100644 --- a/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts +++ b/packages/angular/build/src/builders/library/tests/options/allowed-non-peer-dependencies_spec.ts @@ -24,10 +24,14 @@ describeLibraryBuilder(executeLibraryBuilder, LIBRARY_BUILDER_INFO, (harness) => ...BASE_OPTIONS, }); - const { result } = await harness.executeOnce(); + const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false }); expect(result?.success).toBeFalse(); - expect(result?.error).toContain('allowedNonPeerDependencies'); - expect(result?.error).toContain('lodash-es'); + expect(logs).toContain( + jasmine.objectContaining({ + level: 'error', + message: jasmine.stringMatching(/lodash-es.*allowedNonPeerDependencies/), + }), + ); }); it('should succeed build when dependency matches allowedNonPeerDependencies pattern', async () => {