Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 93bca77

Browse files
committed
Update the algorithm that looks for 'tsdoc-metadata.json' to more correctly reflect the NodeJS resolution algorithm.
1 parent 7eba571 commit 93bca77

File tree

6 files changed

+465
-148
lines changed

6 files changed

+465
-148
lines changed

apps/api-extractor/src/analyzer/PackageMetadataManager.ts

Lines changed: 144 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import * as path from 'path';
4+
import path from 'path';
5+
import semver from 'semver';
56

67
import {
78
type PackageJsonLookup,
89
FileSystem,
910
JsonFile,
1011
type NewlineKind,
1112
type INodePackageJson,
12-
type JsonObject
13+
type JsonObject,
14+
type IPackageJsonExports
1315
} from '@rushstack/node-core-library';
1416
import { Extractor } from '../api/Extractor';
1517
import type { MessageRouter } from '../collector/MessageRouter';
@@ -42,6 +44,125 @@ export class PackageMetadata {
4244
}
4345
}
4446

47+
const TSDOC_METADATA_FILENAME: 'tsdoc-metadata.json' = 'tsdoc-metadata.json';
48+
49+
const TSDOC_METADATA_RESOLUTION_FUNCTIONS: [
50+
...((packageJson: INodePackageJson) => string | undefined)[],
51+
(packageJson: INodePackageJson) => string
52+
] = [
53+
/**
54+
* 1. If package.json a `"tsdocMetadata": "./path1/path2/tsdoc-metadata.json"` field
55+
* then that takes precedence. This convention will be rarely needed, since the other rules below generally
56+
* produce a good result.
57+
*/
58+
({ tsdocMetadata }) => tsdocMetadata,
59+
/**
60+
* 2. If package.json contains a `"exports": { ".": { "types": "./path1/path2/index.d.ts" } }` field,
61+
* then we look for the file under "./path1/path2/tsdoc-metadata.json"
62+
*
63+
* This always looks for a "." and then a "*" entry in the exports field, and then evaluates for
64+
* a "types" field in that entry.
65+
*/
66+
({ exports }) => {
67+
switch (typeof exports) {
68+
case 'string': {
69+
return `${path.dirname(exports)}/${TSDOC_METADATA_FILENAME}`;
70+
}
71+
72+
case 'object': {
73+
if (Array.isArray(exports)) {
74+
const [firstExport] = exports;
75+
// Take the first entry in the array
76+
if (firstExport) {
77+
return `${path.dirname(exports[0])}/${TSDOC_METADATA_FILENAME}`;
78+
}
79+
} else {
80+
const rootExport: IPackageJsonExports | string | null | undefined = exports['.'] ?? exports['*'];
81+
switch (typeof rootExport) {
82+
case 'string': {
83+
return `${path.dirname(rootExport)}/${TSDOC_METADATA_FILENAME}`;
84+
}
85+
86+
case 'object': {
87+
let typesExport: IPackageJsonExports | string | undefined = rootExport?.types;
88+
while (typesExport) {
89+
switch (typeof typesExport) {
90+
case 'string': {
91+
return `${path.dirname(typesExport)}/${TSDOC_METADATA_FILENAME}`;
92+
}
93+
94+
case 'object': {
95+
typesExport = typesExport?.types;
96+
break;
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
103+
break;
104+
}
105+
}
106+
},
107+
/**
108+
* 3. If package.json contains a `typesVersions` field, look for the version
109+
* matching the highest minimum version that either includes a "." or "*" entry.
110+
*/
111+
({ typesVersions }) => {
112+
if (typesVersions) {
113+
let highestMinimumMatchingSemver: semver.SemVer | undefined;
114+
let latestMatchingPath: string | undefined;
115+
for (const [version, paths] of Object.entries(typesVersions)) {
116+
let range: semver.Range;
117+
try {
118+
range = new semver.Range(version);
119+
} catch {
120+
continue;
121+
}
122+
123+
const minimumMatchingSemver: semver.SemVer | null = semver.minVersion(range);
124+
if (
125+
minimumMatchingSemver &&
126+
(!highestMinimumMatchingSemver || semver.gt(minimumMatchingSemver, highestMinimumMatchingSemver))
127+
) {
128+
const pathEntry: string[] | undefined = paths['.'] ?? paths['*'];
129+
const firstPath: string | undefined = pathEntry?.[0];
130+
if (firstPath) {
131+
highestMinimumMatchingSemver = minimumMatchingSemver;
132+
latestMatchingPath = firstPath;
133+
}
134+
}
135+
}
136+
137+
if (latestMatchingPath) {
138+
return `${path.dirname(latestMatchingPath)}/${TSDOC_METADATA_FILENAME}`;
139+
}
140+
}
141+
},
142+
/**
143+
* 4. If package.json contains a `"types": "./path1/path2/index.d.ts"` or a `"typings": "./path1/path2/index.d.ts"`
144+
* field, then we look for the file under "./path1/path2/tsdoc-metadata.json".
145+
*
146+
* @remarks
147+
* `types` takes precedence over `typings`.
148+
*/
149+
({ typings, types }) => {
150+
const typesField: string | undefined = types ?? typings;
151+
if (typesField) {
152+
return `${path.dirname(typesField)}/${TSDOC_METADATA_FILENAME}`;
153+
}
154+
},
155+
({ main }) => {
156+
if (main) {
157+
return `${path.dirname(main)}/${TSDOC_METADATA_FILENAME}`;
158+
}
159+
},
160+
/**
161+
* As a final fallback, place the file in the root of the package.
162+
*/
163+
() => TSDOC_METADATA_FILENAME
164+
];
165+
45166
/**
46167
* This class maintains a cache of analyzed information obtained from package.json
47168
* files. It is built on top of the PackageJsonLookup class.
@@ -56,7 +177,7 @@ export class PackageMetadata {
56177
* Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager.
57178
*/
58179
export class PackageMetadataManager {
59-
public static tsdocMetadataFilename: string = 'tsdoc-metadata.json';
180+
public static tsdocMetadataFilename: string = TSDOC_METADATA_FILENAME;
60181

61182
private readonly _packageJsonLookup: PackageJsonLookup;
62183
private readonly _messageRouter: MessageRouter;
@@ -70,48 +191,31 @@ export class PackageMetadataManager {
70191
this._messageRouter = messageRouter;
71192
}
72193

73-
// This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
74-
// In the future we will use the @microsoft/tsdoc library to read this file.
75-
private static _resolveTsdocMetadataPathFromPackageJson(
194+
/**
195+
* This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
196+
* In the future we will use the @microsoft/tsdoc library to read this file.
197+
*
198+
* @internal
199+
*/
200+
public static _resolveTsdocMetadataPathFromPackageJson(
76201
packageFolder: string,
77202
packageJson: INodePackageJson
78203
): string {
79-
const tsdocMetadataFilename: string = PackageMetadataManager.tsdocMetadataFilename;
80-
81-
let tsdocMetadataRelativePath: string;
82-
83-
if (packageJson.tsdocMetadata) {
84-
// 1. If package.json contains a field such as "tsdocMetadata": "./path1/path2/tsdoc-metadata.json",
85-
// then that takes precedence. This convention will be rarely needed, since the other rules below generally
86-
// produce a good result.
87-
tsdocMetadataRelativePath = packageJson.tsdocMetadata;
88-
} else if (packageJson.typings) {
89-
// 2. If package.json contains a field such as "typings": "./path1/path2/index.d.ts", then we look
90-
// for the file under "./path1/path2/tsdoc-metadata.json"
91-
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.typings), tsdocMetadataFilename);
92-
} else if (packageJson.main) {
93-
// 3. If package.json contains a field such as "main": "./path1/path2/index.js", then we look for
94-
// the file under "./path1/path2/tsdoc-metadata.json"
95-
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.main), tsdocMetadataFilename);
96-
} else if (
97-
typeof packageJson.exports === 'object' &&
98-
!Array.isArray(packageJson.exports) &&
99-
packageJson.exports?.['.']?.types
100-
) {
101-
// 4. If package.json contains a field such as "exports": { ".": { "types": "./path1/path2/index.d.ts" } },
102-
// then we look for the file under "./path1/path2/tsdoc-metadata.json"
103-
tsdocMetadataRelativePath = path.join(
104-
path.dirname(packageJson.exports['.'].types),
105-
tsdocMetadataFilename
106-
);
107-
} else {
108-
// 5. If none of the above rules apply, then by default we look for the file under "./tsdoc-metadata.json"
109-
// since the default entry point is "./index.js"
110-
tsdocMetadataRelativePath = tsdocMetadataFilename;
204+
let tsdocMetadataRelativePath: string | undefined;
205+
for (const tsdocMetadataResolutionFunction of TSDOC_METADATA_RESOLUTION_FUNCTIONS) {
206+
tsdocMetadataRelativePath = tsdocMetadataResolutionFunction(packageJson);
207+
if (tsdocMetadataRelativePath) {
208+
break;
209+
}
111210
}
112211

113212
// Always resolve relative to the package folder.
114-
const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath);
213+
const tsdocMetadataPath: string = path.resolve(
214+
packageFolder,
215+
// This non-null assertion is safe because the last entry in TSDOC_METADATA_RESOLUTION_FUNCTIONS
216+
// returns a non-undefined value.
217+
tsdocMetadataRelativePath!
218+
);
115219
return tsdocMetadataPath;
116220
}
117221

@@ -128,6 +232,7 @@ export class PackageMetadataManager {
128232
if (tsdocMetadataPath) {
129233
return path.resolve(packageFolder, tsdocMetadataPath);
130234
}
235+
131236
return PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(packageFolder, packageJson);
132237
}
133238

0 commit comments

Comments
 (0)