From 526bc0ae32e87b1cb47d8b7b4ed2f67019074a26 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 09:04:37 +0000 Subject: [PATCH 1/2] fix(@angular/build): preserve entry-point displayName casing and flatten bundle names - Preserve original casing in entry point `displayName` so case-sensitive TypeScript `paths` mappings resolve camelCase secondary entry points while keeping `bundleName` lowercased and flattened. - Skip static `.d.ts` and `.d.mts` subpath exports in `normalizeEntryPoints` so they are preserved as non-compiled static exports in `package.json#exports`. - Validate that resolved entry point file paths stay within `projectRoot` using `isSubDirectory`. --- .../builders/library/pipeline/entry-points.ts | 14 +++- .../library/pipeline/entry-points_spec.ts | 83 ++++++++++++++++++- 2 files changed, 92 insertions(+), 5 deletions(-) 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 f50824c5d9f3..f5de90d7cb34 100644 --- a/packages/angular/build/src/builders/library/pipeline/entry-points.ts +++ b/packages/angular/build/src/builders/library/pipeline/entry-points.ts @@ -7,7 +7,7 @@ */ import path from 'node:path'; -import { toPosixPath } from '../../../utils/path'; +import { isSubDirectory, toPosixPath } from '../../../utils/path'; import type { BuilderState } from '../builder-state'; import type { NormalizedEntryPoint, NormalizedLibraryOptions, PackageJsonData } from '../types'; import type { BundleEntryPointInput } from './bundler'; @@ -55,10 +55,16 @@ function normalizeEntryPoint( } const subpath = isPrimary ? '.' : `./${name}`; - const displayName = (isPrimary ? packageName : `${packageName}/${name}`).toLowerCase(); + const displayName = isPrimary ? packageName : `${packageName}/${name}`; const bundleName = getEntryPointBundleName(packageName, name); const entryFilePath = toPosixPath(path.resolve(projectRoot, targetPath)); + if (!isSubDirectory(projectRoot, entryFilePath)) { + throw new Error( + `Entry point '${key}' file path '${entryFilePath}' is outside the project root '${projectRoot}'.`, + ); + } + if (!/(? { }); it('should replace nested slashes with dashes in secondary entry point name', () => { + expect(getEntryPointBundleName('my-lib', 'testing/http')).toBe('my-lib-testing-http'); expect(getEntryPointBundleName('@my-scope/my-lib', 'testing/sub/deep')).toBe( 'my-scope-my-lib-testing-sub-deep', ); @@ -240,12 +241,67 @@ describe('entry-points pipeline', () => { ); }); - it('should ignore non-TypeScript exports for secondary subpaths', () => { + it('should preserve casing in displayName while lowercasing bundleName', () => { + const rawExports = { + '.': './src/public-api.ts', + './myFeature': './my-feature/src/public-api.ts', + }; + + const entryPoints = normalizeEntryPoints( + rawExports, + projectRoot, + packageJsonPath, + '@My-Scope/My-Lib', + ); + + expect(entryPoints.get('.')).toEqual({ + subpath: '.', + name: '.', + displayName: '@My-Scope/My-Lib', + bundleName: 'my-scope-my-lib', + entryFilePath: join(projectRoot, 'src/public-api.ts'), + isPrimary: true, + }); + + expect(entryPoints.get('myFeature')).toEqual({ + subpath: './myFeature', + name: 'myFeature', + displayName: '@My-Scope/My-Lib/myFeature', + bundleName: 'my-scope-my-lib-myfeature', + entryFilePath: join(projectRoot, 'my-feature/src/public-api.ts'), + isPrimary: false, + }); + }); + + it('should flatten nested secondary entry-point bundleName', () => { + const entryPoints = normalizeEntryPoints( + { + '.': './src/public-api.ts', + './testing/http': './testing/http/src/public-api.ts', + }, + projectRoot, + packageJsonPath, + 'my-lib', + ); + + expect(entryPoints.get('testing/http')).toEqual({ + subpath: './testing/http', + name: 'testing/http', + displayName: 'my-lib/testing/http', + bundleName: 'my-lib-testing-http', + entryFilePath: join(projectRoot, 'testing/http/src/public-api.ts'), + isPrimary: false, + }); + }); + + it('should ignore non-TypeScript exports and static declaration files for secondary subpaths', () => { const rawExports = { '.': './src/public-api.ts', './package.json': './package.json', './styles.css': './styles.css', './theme': { sass: './theme.scss' }, + './custom-types': './src/custom-types.d.ts', + './custom-esm-types': { default: './src/custom-types.d.mts' }, }; const entryPoints = normalizeEntryPoints( @@ -260,6 +316,31 @@ describe('entry-points pipeline', () => { expect(entryPoints.has('package.json')).toBeFalse(); expect(entryPoints.has('styles.css')).toBeFalse(); expect(entryPoints.has('theme')).toBeFalse(); + expect(entryPoints.has('custom-types')).toBeFalse(); + expect(entryPoints.has('custom-esm-types')).toBeFalse(); + }); + + it('should throw when entry point file path escapes projectRoot', () => { + expect(() => + normalizeEntryPoints( + { '.': '../other-lib/src/public-api.ts' }, + projectRoot, + packageJsonPath, + packageName, + ), + ).toThrowError(/is outside the project root/); + + expect(() => + normalizeEntryPoints( + { + '.': './src/public-api.ts', + './testing': '../../outside/public-api.ts', + }, + projectRoot, + packageJsonPath, + packageName, + ), + ).toThrowError(/is outside the project root/); }); it('should throw when exports is missing or not an object/string', () => { From 639bc49727ca595e16277038698a14fc103cb903 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:05:13 +0000 Subject: [PATCH 2/2] fixup! fix(@angular/build): preserve entry-point displayName casing and flatten bundle names --- .../builders/library/pipeline/entry-points.ts | 2 +- .../library/pipeline/entry-points_spec.ts | 22 ------------------- 2 files changed, 1 insertion(+), 23 deletions(-) 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 f5de90d7cb34..f932fb992fc6 100644 --- a/packages/angular/build/src/builders/library/pipeline/entry-points.ts +++ b/packages/angular/build/src/builders/library/pipeline/entry-points.ts @@ -65,7 +65,7 @@ function normalizeEntryPoint( ); } - if (!/(? { }); it('should replace nested slashes with dashes in secondary entry point name', () => { - expect(getEntryPointBundleName('my-lib', 'testing/http')).toBe('my-lib-testing-http'); expect(getEntryPointBundleName('@my-scope/my-lib', 'testing/sub/deep')).toBe( 'my-scope-my-lib-testing-sub-deep', ); @@ -273,27 +272,6 @@ describe('entry-points pipeline', () => { }); }); - it('should flatten nested secondary entry-point bundleName', () => { - const entryPoints = normalizeEntryPoints( - { - '.': './src/public-api.ts', - './testing/http': './testing/http/src/public-api.ts', - }, - projectRoot, - packageJsonPath, - 'my-lib', - ); - - expect(entryPoints.get('testing/http')).toEqual({ - subpath: './testing/http', - name: 'testing/http', - displayName: 'my-lib/testing/http', - bundleName: 'my-lib-testing-http', - entryFilePath: join(projectRoot, 'testing/http/src/public-api.ts'), - isPrimary: false, - }); - }); - it('should ignore non-TypeScript exports and static declaration files for secondary subpaths', () => { const rawExports = { '.': './src/public-api.ts',