From 132f546b66561391dd566add4706845bb573d730 Mon Sep 17 00:00:00 2001 From: rk Date: Wed, 30 Sep 2026 23:38:59 +0500 Subject: [PATCH 1/2] fix(@angular/build): declare tslib in the built library package A library compiles with importHelpers, so a bundle can carry a live 'import ... from tslib' while the library's own manifest declares nothing and the built package gets no dependencies field at all. On a hoisted node_modules it resolves anyway; on a strict layout the consumer gets ERR_MODULE_NOT_FOUND at runtime. The range comes from @angular/compiler, resolved from the workspace, which is what ng-packagr reads. A tslib the library already declares, in either dependencies or peerDependencies, is left alone. Fixes #34131 --- .../build/src/builders/library/options.ts | 27 +++++++++++++ .../library/pipeline/package-manifests.ts | 15 +++++++- .../pipeline/package-manifests_spec.ts | 38 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/library/options.ts b/packages/angular/build/src/builders/library/options.ts index 7f74954de60b..9b78fdf20262 100644 --- a/packages/angular/build/src/builders/library/options.ts +++ b/packages/angular/build/src/builders/library/options.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ +import { createRequire } from 'node:module'; import type { BuilderContext } from '@angular-devkit/architect'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -49,6 +50,8 @@ export interface PackageJsonData { export interface NormalizedLibraryOptions { workspaceRoot: string; projectRoot: string; + /** The tslib range `@angular/compiler` declares, when it can be resolved from the workspace. */ + tslibVersion?: string; packageName: string; packageJson: PackageJsonData; outputPath: string; @@ -193,6 +196,7 @@ export async function normalizeLibraryOptions( projectRoot, packageName, packageJson, + tslibVersion: resolveAngularTslibRange(workspaceRoot), outputPath: resolvedOutputPath, deleteOutputPath, packageJsonPath, @@ -361,3 +365,26 @@ function normalizeEntryPoints( return entryPoints; } + +/** + * The tslib range `@angular/compiler` declares, or `undefined` when it cannot be resolved. + * + * Resolved from the workspace so a library gets the range of the Angular it is built against, + * which is what ng-packagr reads too. A missing or unreadable manifest is not an error: the + * output then keeps exactly the dependencies the library declared itself. + */ +function resolveAngularTslibRange(workspaceRoot: string): string | undefined { + try { + const workspaceRequire = createRequire(path.join(workspaceRoot, 'index.js')); + const { dependencies, peerDependencies } = workspaceRequire( + '@angular/compiler/package.json', + ) as { + dependencies?: Record; + peerDependencies?: Record; + }; + + return peerDependencies?.['tslib'] ?? dependencies?.['tslib']; + } catch { + return undefined; + } +} diff --git a/packages/angular/build/src/builders/library/pipeline/package-manifests.ts b/packages/angular/build/src/builders/library/pipeline/package-manifests.ts index 8b5c7708bcec..8588f3289aa2 100644 --- a/packages/angular/build/src/builders/library/pipeline/package-manifests.ts +++ b/packages/angular/build/src/builders/library/pipeline/package-manifests.ts @@ -26,7 +26,12 @@ export function generatePackageManifests( options: NormalizedLibraryOptions, isWatchMode: boolean, ): MemoryOutputFile[] { - const { packageJson: rawPackageJson, keepLifecycleScripts, compilationMode } = options; + const { + packageJson: rawPackageJson, + keepLifecycleScripts, + compilationMode, + tslibVersion, + } = options; const { devDependencies: _devDependencies, @@ -73,6 +78,14 @@ export function generatePackageManifests( version: isWatchMode ? `0.0.0-watch+${Date.now()}` : version, }; + // A library compiles with `importHelpers`, so a bundle can carry a live + // `import ... from 'tslib'` while the library's own manifest declares nothing. + // On a hoisted node_modules it resolves anyway; on a strict layout the consumer + // gets ERR_MODULE_NOT_FOUND at runtime. Declare it, as ng-packagr does. + if (tslibVersion && !rawPackageJson.dependencies?.tslib && !rawPackageJson.peerDependencies?.tslib) { + distPackageJson.dependencies = { ...distPackageJson.dependencies, tslib: tslibVersion }; + } + // Retain scripts if keepLifecycleScripts is set if (keepLifecycleScripts && scripts) { distPackageJson.scripts = scripts; diff --git a/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts b/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts index 112234ca89d3..ef70c9a74f3b 100644 --- a/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts +++ b/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts @@ -307,4 +307,42 @@ describe('generatePackageManifests', () => { ); expect(getRootPackageJson(files2).sideEffects).toEqual(['*.css']); }); + + it('declares tslib when the library does not', () => { + // A library compiles with importHelpers, so a bundle can import tslib while the + // library's own manifest declares nothing. On a strict node_modules layout the + // consumer then gets ERR_MODULE_NOT_FOUND. ng-packagr adds it; so does this. + const options = createOptions({ tslibVersion: '^2.3.0' }); + const result = getRootPackageJson(generatePackageManifests(options, false)); + + expect(result.dependencies).toEqual({ tslib: '^2.3.0' }); + }); + + it('leaves a declared tslib alone', () => { + const options = createOptions({ + tslibVersion: '^2.3.0', + packageJson: { name: 'my-lib', dependencies: { tslib: '^2.0.0' } }, + }); + const result = getRootPackageJson(generatePackageManifests(options, false)); + + expect(result.dependencies).toEqual({ tslib: '^2.0.0' }); + }); + + it('does not move a tslib that is declared as a peer', () => { + const options = createOptions({ + tslibVersion: '^2.3.0', + packageJson: { name: 'my-lib', peerDependencies: { tslib: '^2.0.0' } }, + }); + const result = getRootPackageJson(generatePackageManifests(options, false)); + + expect(result.dependencies).toBeUndefined(); + expect(result.peerDependencies).toEqual({ tslib: '^2.0.0' }); + }); + + it('adds nothing when the range cannot be resolved', () => { + const options = createOptions(); + const result = getRootPackageJson(generatePackageManifests(options, false)); + + expect(result.dependencies).toBeUndefined(); + }); }); From 09f34f8f33dd2a0a5701e191d6d09824d6f63b2f Mon Sep 17 00:00:00 2001 From: rk Date: Wed, 30 Sep 2026 23:45:18 +0500 Subject: [PATCH 2/2] fixup: leave a tslib declared in optionalDependencies alone --- .../builders/library/pipeline/package-manifests.ts | 7 ++++++- .../library/pipeline/package-manifests_spec.ts | 11 +++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/angular/build/src/builders/library/pipeline/package-manifests.ts b/packages/angular/build/src/builders/library/pipeline/package-manifests.ts index 8588f3289aa2..8766dbdec713 100644 --- a/packages/angular/build/src/builders/library/pipeline/package-manifests.ts +++ b/packages/angular/build/src/builders/library/pipeline/package-manifests.ts @@ -82,7 +82,12 @@ export function generatePackageManifests( // `import ... from 'tslib'` while the library's own manifest declares nothing. // On a hoisted node_modules it resolves anyway; on a strict layout the consumer // gets ERR_MODULE_NOT_FOUND at runtime. Declare it, as ng-packagr does. - if (tslibVersion && !rawPackageJson.dependencies?.tslib && !rawPackageJson.peerDependencies?.tslib) { + if ( + tslibVersion && + !rawPackageJson.dependencies?.tslib && + !rawPackageJson.peerDependencies?.tslib && + !rawPackageJson.optionalDependencies?.tslib + ) { distPackageJson.dependencies = { ...distPackageJson.dependencies, tslib: tslibVersion }; } diff --git a/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts b/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts index ef70c9a74f3b..5fb50dca49a5 100644 --- a/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts +++ b/packages/angular/build/src/builders/library/pipeline/package-manifests_spec.ts @@ -339,6 +339,17 @@ describe('generatePackageManifests', () => { expect(result.peerDependencies).toEqual({ tslib: '^2.0.0' }); }); + it('does not add a tslib that is declared as optional', () => { + const options = createOptions({ + tslibVersion: '^2.3.0', + packageJson: { name: 'my-lib', optionalDependencies: { tslib: '^2.0.0' } }, + }); + const result = getRootPackageJson(generatePackageManifests(options, false)); + + expect(result.dependencies).toBeUndefined(); + expect(result.optionalDependencies).toEqual({ tslib: '^2.0.0' }); + }); + it('adds nothing when the range cannot be resolved', () => { const options = createOptions(); const result = getRootPackageJson(generatePackageManifests(options, false));