diff --git a/packages/compiler-cli/ngcc/src/packages/entry_point.ts b/packages/compiler-cli/ngcc/src/packages/entry_point.ts index 31d59cc97d..5fab055868 100644 --- a/packages/compiler-cli/ngcc/src/packages/entry_point.ts +++ b/packages/compiler-cli/ngcc/src/packages/entry_point.ts @@ -28,6 +28,11 @@ export interface EntryPoint extends JsonObject { name: string; /** The path to this entry point. */ path: AbsoluteFsPath; + /** + * The name of the package that contains this entry-point (e.g. `@angular/core` or + * `@angular/common`). + */ + packageName: string; /** The path to the package that contains this entry-point. */ packagePath: AbsoluteFsPath; /** The parsed package.json file for this entry-point. */ @@ -131,7 +136,8 @@ export function getEntryPointInfo( const loadedEntryPointPackageJson = (packagePackageJsonPath === entryPointPackageJsonPath) ? loadedPackagePackageJson : loadPackageJson(fs, entryPointPackageJsonPath); - const packageVersion = getPackageVersion(loadedPackagePackageJson); + const {packageName, packageVersion} = getPackageNameAndVersion( + fs, packagePath, loadedPackagePackageJson, loadedEntryPointPackageJson); const entryPointConfig = config.getPackageConfig(packagePath, packageVersion).entryPoints[entryPointPath]; let entryPointPackageJson: EntryPointPackageJson; @@ -173,6 +179,7 @@ export function getEntryPointInfo( const entryPointInfo: EntryPoint = { name: entryPointPackageJson.name, path: entryPointPath, + packageName, packagePath, packageJson: entryPointPackageJson, typings: resolve(entryPointPath, typings), @@ -304,15 +311,44 @@ function guessTypingsFromPackageJson( } /** - * Find the version of the package at `packageJsonPath`. + * Find or infer the name and version of a package. * - * The version is read off of the `version` property of the package's `package.json` file (if - * available). + * - The name is computed based on the `name` property of the package's or the entry-point's + * `package.json` file (if available) or inferred from the package's path. + * - The version is read off of the `version` property of the package's `package.json` file (if + * available). * - * @param packageJson the parsed `package.json` of the package (if available). - * @returns the version string or `null` if the `pckage.json` file is missing or doesn't contain a - * version. + * @param fs The `FileSystem` instance to use for parsing `packagePath` (if needed). + * @param packagePath the absolute path to the package. + * @param packagePackageJson the parsed `package.json` of the package (if available). + * @param entryPointPackageJson the parsed `package.json` of an entry-point (if available). + * @returns the computed name and version of the package. */ -function getPackageVersion(packageJson: EntryPointPackageJson|null): string|null { - return packageJson?.version ?? null; +function getPackageNameAndVersion( + fs: FileSystem, packagePath: AbsoluteFsPath, packagePackageJson: EntryPointPackageJson|null, + entryPointPackageJson: EntryPointPackageJson| + null): {packageName: string, packageVersion: string|null} { + let packageName: string; + + if (packagePackageJson !== null) { + // We have a valid `package.json` for the package: Get the package name from that. + packageName = packagePackageJson.name; + } else if (entryPointPackageJson !== null) { + // We have a valid `package.json` for the entry-point: Get the package name from that. + // This might be a secondary entry-point, so make sure we only keep the main package's name + // (e.g. only keep `@angular/common` from `@angular/common/http`). + packageName = /^(?:@[^/]+\/)?[^/]*/.exec(entryPointPackageJson.name)![0]; + } else { + // We don't have a valid `package.json`: Infer the package name from the package's path. + const lastSegment = fs.basename(packagePath); + const secondLastSegment = fs.basename(fs.dirname(packagePath)); + + packageName = + secondLastSegment.startsWith('@') ? `${secondLastSegment}/${lastSegment}` : lastSegment; + } + + return { + packageName, + packageVersion: packagePackageJson?.version ?? null, + }; } diff --git a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts index a8e9bad12e..77e204803c 100644 --- a/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts +++ b/packages/compiler-cli/ngcc/test/dependencies/dependency_resolver_spec.ts @@ -60,6 +60,7 @@ runInEachFileSystem(() => { first = { name: 'first', path: _('/first'), + packageName: 'first', packagePath: _('/first'), packageJson: {esm5: './index.js'}, typings: _('/first/index.d.ts'), @@ -67,7 +68,9 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, } as EntryPoint; second = { + name: 'second', path: _('/second'), + packageName: 'second', packagePath: _('/second'), packageJson: {esm2015: './sub/index.js'}, typings: _('/second/sub/index.d.ts'), @@ -75,7 +78,9 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, } as EntryPoint; third = { + name: 'third', path: _('/third'), + packageName: 'third', packagePath: _('/third'), packageJson: {fesm5: './index.js'}, typings: _('/third/index.d.ts'), @@ -83,7 +88,9 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, } as EntryPoint; fourth = { + name: 'fourth', path: _('/fourth'), + packageName: 'fourth', packagePath: _('/fourth'), packageJson: {fesm2015: './sub2/index.js'}, typings: _('/fourth/sub2/index.d.ts'), @@ -91,7 +98,9 @@ runInEachFileSystem(() => { ignoreMissingDependencies: false, } as EntryPoint; fifth = { + name: 'fifth', path: _('/fifth'), + packageName: 'fifth', packagePath: _('/fifth'), packageJson: {module: './index.js'}, typings: _('/fifth/index.d.ts'), @@ -100,7 +109,9 @@ runInEachFileSystem(() => { } as EntryPoint; sixthIgnoreMissing = { + name: 'sixth', path: _('/sixth'), + packageName: 'sixth', packagePath: _('/sixth'), packageJson: {module: './index.js'}, typings: _('/sixth/index.d.ts'), @@ -291,6 +302,7 @@ runInEachFileSystem(() => { const testEntryPoint = { name: 'test-package', path: _('/project/node_modules/test-package'), + packageName: 'test-package', packagePath: _('/project/node_modules/test-package'), packageJson: {esm5: './index.js'}, typings: _('/project/node_modules/test-package/index.d.ts'), diff --git a/packages/compiler-cli/ngcc/test/helpers/utils.ts b/packages/compiler-cli/ngcc/test/helpers/utils.ts index baa4d2ec28..d0a7940671 100644 --- a/packages/compiler-cli/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/ngcc/test/helpers/utils.ts @@ -22,6 +22,7 @@ export function makeTestEntryPoint( return { name: entryPointName, path: absoluteFrom(`/node_modules/${entryPointName}`), + packageName, packagePath: absoluteFrom(`/node_modules/${packageName}`), packageJson: {name: entryPointName}, typings: absoluteFrom(`/node_modules/${entryPointName}/index.d.ts`), diff --git a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts index 922474c522..bbc9b89cf0 100644 --- a/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts +++ b/packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts @@ -1572,7 +1572,7 @@ runInEachFileSystem(() => { }, { name: _('/node_modules/deep_import/package.json'), - contents: '{"name": "deep-import", "es2015": "./index.js", "typings": "./index.d.ts"}', + contents: '{"name": "deep_import", "es2015": "./index.js", "typings": "./index.d.ts"}', }, { name: _('/node_modules/deep_import/entry_point.js'), diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts index 81711a1140..546370ff53 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_bundle_spec.ts @@ -168,6 +168,7 @@ runInEachFileSystem(() => { const entryPoint: EntryPoint = { name: 'test', path: absoluteFrom('/node_modules/test'), + packageName: 'test', packagePath: absoluteFrom('/node_modules/test'), packageJson: {name: 'test'}, typings: absoluteFrom('/node_modules/test/index.d.ts'), @@ -218,6 +219,7 @@ runInEachFileSystem(() => { const entryPoint: EntryPoint = { name: 'test', path: absoluteFrom('/node_modules/test'), + packageName: 'test', packagePath: absoluteFrom('/node_modules/test'), packageJson: {name: 'test'}, typings: absoluteFrom('/node_modules/test/index.d.ts'), @@ -240,6 +242,7 @@ runInEachFileSystem(() => { const entryPoint: EntryPoint = { name: 'internal', path: absoluteFrom('/node_modules/internal'), + packageName: 'internal', packagePath: absoluteFrom('/node_modules/internal'), packageJson: {name: 'internal'}, typings: absoluteFrom('/node_modules/internal/index.d.ts'), @@ -262,6 +265,7 @@ runInEachFileSystem(() => { const entryPoint: EntryPoint = { name: 'test', path: absoluteFrom('/node_modules/test'), + packageName: 'test', packagePath: absoluteFrom('/node_modules/test'), packageJson: {name: 'test'}, typings: absoluteFrom('/node_modules/test/index.d.ts'), @@ -285,6 +289,7 @@ runInEachFileSystem(() => { const entryPoint: EntryPoint = { name: 'secondary', path: absoluteFrom('/node_modules/primary/secondary'), + packageName: 'primary', packagePath: absoluteFrom('/node_modules/primary'), packageJson: {name: 'secondary'}, typings: absoluteFrom('/node_modules/primary/secondary/index.d.ts'), diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_manifest_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_manifest_spec.ts index 4c3cb4b399..d8359827e2 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_manifest_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_manifest_spec.ts @@ -149,6 +149,7 @@ runInEachFileSystem(() => { entryPoint: { name: 'some_package/valid_entry_point', path: _Abs('/project/node_modules/some_package/valid_entry_point'), + packageName: 'some_package', packagePath: _Abs('/project/node_modules/some_package'), packageJson: jasmine.any(Object), typings: diff --git a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts index 7c53a437a5..063adcba39 100644 --- a/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts +++ b/packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, join} from '../../../src/ngtsc/file_system'; +import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, join, relative} from '../../../src/ngtsc/file_system'; import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing'; import {loadTestFiles} from '../../../test/helpers'; import {NgccConfiguration} from '../../src/packages/configuration'; @@ -45,6 +45,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/valid_entry_point', path: _('/project/node_modules/some_package/valid_entry_point'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/valid_entry_point'), typings: @@ -194,6 +195,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/valid_entry_point', path: _('/project/node_modules/some_package/valid_entry_point'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: overriddenPackageJson, typings: _('/project/node_modules/some_package/valid_entry_point/some_other.d.ts'), @@ -242,6 +244,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/missing_package_json', path: _('/project/node_modules/some_package/missing_package_json'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: {name: 'some_package/missing_package_json', ...override}, typings: _( @@ -252,6 +255,154 @@ runInEachFileSystem(() => { }); }); + [false, true].forEach(isScoped => { + const nameWithScope = (baseName: string) => `${isScoped ? '@some-scope/' : ''}${baseName}`; + const getPackageName = (packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath) => { + const config = new NgccConfiguration(fs, _('/project')); + const logger = new MockLogger(); + const entryPoint = getEntryPointInfo(fs, config, logger, packagePath, entryPointPath); + + if (!isEntryPoint(entryPoint)) { + return fail(`Expected an entry point but got ${entryPoint}`); + } + + return entryPoint.packageName; + }; + const setUpPackageWithEntryPointPackageJson = + (entryPointName: string, entryPointPath: AbsoluteFsPath) => { + // Ensure a `package.json` exists for the entry-point (containing `entryPointName`). + loadTestFiles([ + { + name: join(entryPointPath, 'package.json'), + contents: JSON.stringify({name: entryPointName, typings: './index.d.ts'}), + }, + ]); + }; + const setUpPackageWithoutEntryPointPackageJson = + (packagePath: AbsoluteFsPath, entryPointPath: AbsoluteFsPath) => { + // Ensure there is an ngcc config for the entry-point providing a `typings` field to + // avoid returning `INCOMPATIBLE_ENTRY_POINT` (since there is no `package.json`). + loadTestFiles([ + { + name: join(packagePath, 'ngcc.config.js'), + contents: ` + module.exports = { + entryPoints: { + '${relative(packagePath, entryPointPath)}': { + override: {typings: './index.d.ts'}, + }, + }, + }; + `, + }, + ]); + }; + + describe(`should compute the containing ${isScoped ? 'scoped ' : ''}package's name`, () => { + it('for a primary entry-point with a `package.json`', () => { + const packagePath = _(`/project/node_modules/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = packagePath; + const expectedPackageName = nameWithScope('package-json-package-name'); + + setUpPackageWithEntryPointPackageJson(expectedPackageName, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a primary entry-point without a `package.json`', () => { + const packagePath = _(`/project/node_modules/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = packagePath; + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a secondary entry-point with a `package.json`', () => { + const packagePath = _(`/project/node_modules/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = join(packagePath, 'some-entry-point'); + const expectedPackageName = nameWithScope('package-json-package-name'); + + setUpPackageWithEntryPointPackageJson( + `${expectedPackageName}/some-entry-point`, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a secondary entry-point without a `package.json`', () => { + const packagePath = _(`/project/node_modules/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = join(packagePath, 'some-entry-point'); + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a primary entry-point without a `package.json` in nested `node_modules/`', () => { + const packagePath = _(`/project/node_modules/other-package/node_modules/${ + nameWithScope('on-disk-package-name')}`); + const entryPointPath = packagePath; + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a secondary entry-point without a `package.json` in nested `node_modules/`', () => { + const packagePath = _(`/project/node_modules/other-package/node_modules/${ + nameWithScope('on-disk-package-name')}`); + const entryPointPath = join(packagePath, 'some-entry-point'); + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a primary entry-point with a `package.json` outside `node_modules/`', () => { + const packagePath = _(`/project/libs/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = packagePath; + const expectedPackageName = nameWithScope('package-json-package-name'); + + setUpPackageWithEntryPointPackageJson(expectedPackageName, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a primary entry-point without a `package.json` outside `node_modules/`', () => { + const packagePath = _(`/project/libs/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = packagePath; + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a secondary entry-point with a `package.json` outside `node_modules/`', () => { + const packagePath = _(`/project/libs/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = join(packagePath, 'some-entry-point'); + const expectedPackageName = nameWithScope('package-json-package-name'); + + setUpPackageWithEntryPointPackageJson(expectedPackageName, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + + it('for a secondary entry-point without a `package.json` outside `node_modules/`', () => { + const packagePath = _(`/project/libs/${nameWithScope('on-disk-package-name')}`); + const entryPointPath = join(packagePath, 'some-entry-point'); + const expectedPackageName = nameWithScope('on-disk-package-name'); + + setUpPackageWithoutEntryPointPackageJson(packagePath, entryPointPath); + + expect(getPackageName(packagePath, entryPointPath)).toBe(expectedPackageName); + }); + }); + }); it('should return `INCOMPATIBLE_ENTRY_POINT` if there is no typings or types field in the package.json', () => { @@ -323,6 +474,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/missing_typings', path: _('/project/node_modules/some_package/missing_typings'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/missing_typings'), typings: _(`/project/node_modules/some_package/missing_typings/${typingsPath}.d.ts`), @@ -348,6 +500,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/missing_metadata', path: _('/project/node_modules/some_package/missing_metadata'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/missing_metadata'), typings: _(`/project/node_modules/some_package/missing_metadata/missing_metadata.d.ts`), @@ -377,6 +530,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/missing_metadata', path: _('/project/node_modules/some_package/missing_metadata'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/missing_metadata'), typings: _('/project/node_modules/some_package/missing_metadata/missing_metadata.d.ts'), @@ -405,6 +559,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/types_rather_than_typings', path: _('/project/node_modules/some_package/types_rather_than_typings'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/types_rather_than_typings'), @@ -440,6 +595,7 @@ runInEachFileSystem(() => { expect(entryPoint).toEqual({ name: 'some_package/material_style', path: _('/project/node_modules/some_package/material_style'), + packageName: 'some_package', packagePath: SOME_PACKAGE, packageJson: loadPackageJson(fs, '/project/node_modules/some_package/material_style'), typings: _(`/project/node_modules/some_package/material_style/material_style.d.ts`),