Skip to content

Commit

Permalink
fixup! fix(ngcc): cope with packages following APF v14+
Browse files Browse the repository at this point in the history
  • Loading branch information
gkalpak committed May 5, 2022
1 parent 1e4527e commit 3686a06
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class ModuleResolver {
*
* This is achieved by checking for the existence of `${modulePath}/package.json`.
* If there is no `package.json`, we check whether this is an APF v14+ secondary entry-point,
* which does not have each own `package.json` but has an `exports` entry in the package's primary
* which does not have its own `package.json` but has an `exports` entry in the package's primary
* `package.json`.
*/
private isEntryPoint(modulePath: AbsoluteFsPath): boolean {
Expand Down
38 changes: 31 additions & 7 deletions packages/compiler-cli/ngcc/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,42 @@ export function loadJson<T extends JsonObject = JsonObject>(
export function loadSecondaryEntryPointInfoForApfV14(
fs: ReadonlyFileSystem, primaryPackageJson: JsonObject|null, packagePath: AbsoluteFsPath,
entryPointPath: AbsoluteFsPath): JsonObject|null {
// Check if primary `package.json` has been loaded and has an `exports` property.
if (primaryPackageJson?.exports === undefined) {
// Check if primary `package.json` has been loaded and has an `exports` property that is an
// object.
const exportMap = primaryPackageJson?.exports;
if (!isExportObject(exportMap)) {
return null;
}

// Find the `exports` key for the secondary entry-point.
const relativeEntryPointPath = fs.relative(packagePath, entryPointPath);
const exportsKey = `./${relativeEntryPointPath}`;
const entryPointExportKey = `./${relativeEntryPointPath}`;

// Read the data (if it exists).
const exportsData =
(primaryPackageJson.exports as JsonObject)[exportsKey] as JsonObject | undefined;
// Read the info for the entry-point.
const entryPointInfo = exportMap[entryPointExportKey];

return exportsData ?? null;
// Check whether the entry-point info exists and is an export map.
return isExportObject(entryPointInfo) ? entryPointInfo : null;
}

/**
* Check whether a value read from a JSON file is a Node.js export map (either the top-level one or
* one for a subpath).
*
* In `package.json` files, the `exports` field can be of type `Object | string | string[]`, but APF
* v14+ uses an object with subpath exports for each entry-point, which in turn are conditional
* exports (see references below). This function verifies that a value read from the top-level
* `exports` field or a subpath is of type `Object` (and not `string` or `string[]`).
*
* References:
* - https://nodejs.org/api/packages.html#exports
* - https://nodejs.org/api/packages.html#subpath-exports
* - https://nodejs.org/api/packages.html#conditional-exports
* - https://v14.angular.io/guide/angular-package-format#exports
*
* @param thing The value read from the JSON file
* @returns True if the value is an `Object` (and not an `Array`).
*/
function isExportObject(thing: JsonValue): thing is JsonObject {
return (typeof thing === 'object') && (thing !== null) && !Array.isArray(thing);
}
66 changes: 61 additions & 5 deletions packages/compiler-cli/ngcc/test/utils_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ runInEachFileSystem(() => {
beforeEach(() => fs = getFileSystem());

describe('loadSecondaryEntryPointInfoForApfV14()', () => {
it('should return `null` of the primary `package.json` failed to be loaded', () => {
it('should return `null` if the primary `package.json` failed to be loaded', () => {
expect(loadSecondaryEntryPointInfoForApfV14(fs, null, _abs('/foo'), _abs('/foo/bar')))
.toBe(null);
});
Expand All @@ -300,12 +300,39 @@ runInEachFileSystem(() => {
.toBe(null);
});

it('should return `null` if the primary `package.json`\'s `exports` property is a string',
() => {
const primaryPackageJson = {
name: 'some-package',
exports: './index.js',
};

expect(loadSecondaryEntryPointInfoForApfV14(
fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar')))
.toBe(null);
});

it('should return `null` if the primary `package.json`\'s `exports` property is a string array',
() => {
const primaryPackageJson = {
name: 'some-package',
exports: [
'./foo.js',
'./bar.js',
],
};

expect(loadSecondaryEntryPointInfoForApfV14(
fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar')))
.toBe(null);
});

it('should return `null` if there is no info for the specified entry-point', () => {
const primaryPackageJson = {
name: 'some-package',
exports: {
'./baz': {
isBar: false,
main: './baz/index.js',
},
},
};
Expand All @@ -315,19 +342,48 @@ runInEachFileSystem(() => {
.toBe(null);
});

it('should return the entry-point info if it exists', () => {
it('should return `null` if the entry-point info is a string', () => {
const primaryPackageJson = {
name: 'some-package',
exports: {
'./bar': './bar/index.js',
},
};

expect(loadSecondaryEntryPointInfoForApfV14(
fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar')))
.toBe(null);
});

it('should return `null` if the entry-point info is a string array', () => {
const primaryPackageJson = {
name: 'some-package',
exports: {
'./bar': [
'./bar/a.js',
'./bar/b.js',
],
},
};

expect(loadSecondaryEntryPointInfoForApfV14(
fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar')))
.toBe(null);
});

it('should return the entry-point info if it exists and is an object', () => {
const primaryPackageJson = {
name: 'some-package',
exports: {
'./bar': {
isBar: true,
main: './bar/index.js',
},
},
};

expect(loadSecondaryEntryPointInfoForApfV14(
fs, primaryPackageJson, _abs('/foo'), _abs('/foo/bar')))
.toEqual({isBar: true});
.toEqual({main: './bar/index.js'});
});
});
});

0 comments on commit 3686a06

Please sign in to comment.