1
1
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2
2
// See LICENSE in the project root for license information.
3
3
4
- import * as path from 'path' ;
4
+ import path from 'path' ;
5
+ import semver from 'semver' ;
5
6
6
7
import {
7
8
type PackageJsonLookup ,
8
9
FileSystem ,
9
10
JsonFile ,
10
11
type NewlineKind ,
11
12
type INodePackageJson ,
12
- type JsonObject
13
+ type JsonObject ,
14
+ type IPackageJsonExports
13
15
} from '@rushstack/node-core-library' ;
14
16
import { Extractor } from '../api/Extractor' ;
15
17
import type { MessageRouter } from '../collector/MessageRouter' ;
@@ -42,6 +44,125 @@ export class PackageMetadata {
42
44
}
43
45
}
44
46
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
+
45
166
/**
46
167
* This class maintains a cache of analyzed information obtained from package.json
47
168
* files. It is built on top of the PackageJsonLookup class.
@@ -56,7 +177,7 @@ export class PackageMetadata {
56
177
* Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager.
57
178
*/
58
179
export class PackageMetadataManager {
59
- public static tsdocMetadataFilename : string = 'tsdoc-metadata.json' ;
180
+ public static tsdocMetadataFilename : string = TSDOC_METADATA_FILENAME ;
60
181
61
182
private readonly _packageJsonLookup : PackageJsonLookup ;
62
183
private readonly _messageRouter : MessageRouter ;
@@ -70,48 +191,31 @@ export class PackageMetadataManager {
70
191
this . _messageRouter = messageRouter ;
71
192
}
72
193
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 (
76
201
packageFolder : string ,
77
202
packageJson : INodePackageJson
78
203
) : 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
+ }
111
210
}
112
211
113
212
// 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
+ ) ;
115
219
return tsdocMetadataPath ;
116
220
}
117
221
@@ -128,6 +232,7 @@ export class PackageMetadataManager {
128
232
if ( tsdocMetadataPath ) {
129
233
return path . resolve ( packageFolder , tsdocMetadataPath ) ;
130
234
}
235
+
131
236
return PackageMetadataManager . _resolveTsdocMetadataPathFromPackageJson ( packageFolder , packageJson ) ;
132
237
}
133
238
0 commit comments