build: package api-extractor and -model (#9920)

* fix(ExceptText): don't display import("d..-types/v10"). in return type

* Squashed 'packages/api-extractor-model/' content from commit 39ecb196c

git-subtree-dir: packages/api-extractor-model
git-subtree-split: 39ecb196ca210bdf84ba6c9cadb1bb93571849d7

* Squashed 'packages/api-extractor/' content from commit 341ad6c51

git-subtree-dir: packages/api-extractor
git-subtree-split: 341ad6c51b01656d4f73b74ad4bdb3095f9262c4

* feat(api-extractor): add api-extractor and -model

* fix: package.json docs script

* fix(SourcLink): use <> instead of function syntax

* fix: make packages private

* fix: rest params showing in docs, added labels

* fix: missed two files

* fix: cpy-cli & pnpm-lock

* fix: increase icon size

* fix: icon size again
This commit is contained in:
Qjuh
2023-11-07 21:53:36 +01:00
committed by GitHub
parent 95c0b1a59f
commit 5c0fad3b2d
251 changed files with 36017 additions and 296 deletions

23
packages/api-extractor-model/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Packages
node_modules
# Log files
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Env
.env
# Dist
dist
# Miscellaneous
.turbo
.tmp
coverage

View File

@@ -0,0 +1 @@
module.exports = require('../../.lintstagedrc.json');

View File

@@ -0,0 +1,30 @@
# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO.
# Ignore all files by default, to avoid accidentally publishing unintended files.
*
# Use negative patterns to bring back the specific things we want to publish.
!/bin/**
!/lib/**
!/lib-*/**
!/dist/**
!ThirdPartyNotice.txt
# Ignore certain patterns that should not get published.
/dist/*.stats.*
/lib/**/test/
/lib-*/**/test/
*.test.js
# NOTE: These don't need to be specified, because NPM includes them automatically.
#
# package.json
# README (and its variants)
# CHANGELOG (and its variants)
# LICENSE / LICENCE
#--------------------------------------------
# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE
#--------------------------------------------
# (Add your project-specific overrides here)

View File

@@ -0,0 +1,5 @@
.turbo
coverage
dist
CHANGELOG.md
tsup.config.bundled*

View File

@@ -0,0 +1 @@
module.exports = require('../../.prettierrc.json');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,965 @@
# Change Log - @microsoft/api-extractor-model
This log was last generated on Thu, 28 Sep 2023 20:53:16 GMT and should not be manually modified.
## 7.28.2
Thu, 28 Sep 2023 20:53:16 GMT
_Version update only_
## 7.28.1
Tue, 26 Sep 2023 09:30:33 GMT
### Patches
- Update type-only imports to include the type modifier.
## 7.28.0
Fri, 15 Sep 2023 00:36:58 GMT
### Minor changes
- Update @types/node from 14 to 18
## 7.27.6
Tue, 08 Aug 2023 07:10:40 GMT
_Version update only_
## 7.27.5
Wed, 19 Jul 2023 00:20:32 GMT
_Version update only_
## 7.27.4
Thu, 06 Jul 2023 00:16:20 GMT
_Version update only_
## 7.27.3
Thu, 15 Jun 2023 00:21:01 GMT
_Version update only_
## 7.27.2
Wed, 07 Jun 2023 22:45:16 GMT
_Version update only_
## 7.27.1
Mon, 29 May 2023 15:21:15 GMT
_Version update only_
## 7.27.0
Mon, 22 May 2023 06:34:32 GMT
### Minor changes
- Upgrade the TypeScript dependency to ~5.0.4
## 7.26.9
Fri, 12 May 2023 00:23:05 GMT
_Version update only_
## 7.26.8
Thu, 04 May 2023 00:20:28 GMT
### Patches
- Fix a mistake in the documentation for ApiParameterListMixin.overloadIndex
## 7.26.7
Mon, 01 May 2023 15:23:20 GMT
_Version update only_
## 7.26.6
Sat, 29 Apr 2023 00:23:03 GMT
_Version update only_
## 7.26.5
Thu, 27 Apr 2023 17:18:43 GMT
_Version update only_
## 7.26.4
Fri, 10 Feb 2023 01:18:51 GMT
_Version update only_
## 7.26.3
Sun, 05 Feb 2023 03:02:02 GMT
_Version update only_
## 7.26.2
Wed, 01 Feb 2023 02:16:34 GMT
_Version update only_
## 7.26.1
Mon, 30 Jan 2023 16:22:30 GMT
_Version update only_
## 7.26.0
Wed, 25 Jan 2023 07:26:55 GMT
### Minor changes
- Add new .api.json field `isAbstract` to track `abstract` modifier in ApiClass, ApiMethod, and ApiProperty via ApiAbstractMixin (GitHub #3661)
## 7.25.3
Fri, 09 Dec 2022 16:18:28 GMT
_Version update only_
## 7.25.2
Wed, 26 Oct 2022 00:16:16 GMT
### Patches
- Update the @microsoft/tsdoc dependency version to 0.14.2.
## 7.25.1
Thu, 13 Oct 2022 00:20:15 GMT
_Version update only_
## 7.25.0
Tue, 11 Oct 2022 23:49:12 GMT
### Minor changes
- Add a new fileUrlPath property to relevant API items and serialize this to the .api.json. Additionally, add a SourceFile helper class for constructing file URLs from these paths and the projectFolderUrl.
## 7.24.4
Mon, 10 Oct 2022 15:23:44 GMT
_Version update only_
## 7.24.3
Thu, 29 Sep 2022 07:13:06 GMT
_Version update only_
## 7.24.2
Wed, 21 Sep 2022 20:21:10 GMT
_Version update only_
## 7.24.1
Thu, 15 Sep 2022 00:18:52 GMT
_Version update only_
## 7.24.0
Fri, 02 Sep 2022 17:48:42 GMT
### Minor changes
- Add new ApiExportedMixin mixin class for determining whether an API item is exported or not
## 7.23.3
Wed, 24 Aug 2022 03:01:22 GMT
_Version update only_
## 7.23.2
Wed, 24 Aug 2022 00:14:38 GMT
### Patches
- Remove use of LegacyAdapters.sortStable
## 7.23.1
Fri, 19 Aug 2022 00:17:19 GMT
_Version update only_
## 7.23.0
Wed, 03 Aug 2022 18:40:35 GMT
### Minor changes
- Upgrade TypeScript dependency to 4.7
## 7.22.2
Mon, 01 Aug 2022 02:45:32 GMT
_Version update only_
## 7.22.1
Thu, 21 Jul 2022 23:30:27 GMT
### Patches
- Improve IFindApiItemMessage and fix two small bugs with ApiItemContainerMixin.findMembersWithInheritance()
## 7.22.0
Thu, 21 Jul 2022 00:16:14 GMT
### Minor changes
- Add a new ApiItemContainerMixin.findMembersWithInheritance() method for finding an item's inherited members
## 7.21.0
Thu, 30 Jun 2022 04:48:53 GMT
### Minor changes
- Update model to reflect that index signatures can also be readonly
## 7.20.3
Tue, 28 Jun 2022 22:47:13 GMT
_Version update only_
## 7.20.2
Tue, 28 Jun 2022 00:23:32 GMT
_Version update only_
## 7.20.1
Mon, 27 Jun 2022 18:43:09 GMT
_Version update only_
## 7.20.0
Sat, 25 Jun 2022 21:00:40 GMT
### Minor changes
- Add a new initializerTokenRange field to ApiProperty and ApiVariable items.
## 7.19.1
Sat, 25 Jun 2022 01:54:29 GMT
_Version update only_
## 7.19.0
Fri, 24 Jun 2022 07:16:47 GMT
### Minor changes
- Added new configuration for ItemContainerMixin member ordering
## 7.18.2
Fri, 17 Jun 2022 09:17:54 GMT
_Version update only_
## 7.18.1
Fri, 17 Jun 2022 00:16:18 GMT
_Version update only_
## 7.18.0
Tue, 07 Jun 2022 09:37:04 GMT
### Minor changes
- Add an "isReadonly" field to ApiProperty, ApiPropertySignature, and ApiVariable
- Add an "isProtected" field to ApiConstructor, ApiMethod, and ApiProperty
## 7.17.3
Tue, 10 May 2022 01:20:43 GMT
_Version update only_
## 7.17.2
Sat, 23 Apr 2022 02:13:07 GMT
_Version update only_
## 7.17.1
Fri, 15 Apr 2022 00:12:36 GMT
_Version update only_
## 7.17.0
Wed, 13 Apr 2022 15:12:40 GMT
### Minor changes
- Add a new isOptional property to TypeParameters deserialized from the .api.json file with api-extractor-model.
## 7.16.2
Tue, 12 Apr 2022 02:58:32 GMT
### Patches
- Update TSDoc dependencies.
## 7.16.1
Sat, 09 Apr 2022 02:24:26 GMT
### Patches
- Rename the "master" branch to "main".
- Update a path in the README.
## 7.16.0
Thu, 31 Mar 2022 02:06:05 GMT
### Minor changes
- Updated api-extractor-model to store whether a parameter is optional.
## 7.15.4
Tue, 15 Mar 2022 19:15:53 GMT
_Version update only_
## 7.15.3
Wed, 05 Jan 2022 16:07:47 GMT
_Version update only_
## 7.15.2
Mon, 27 Dec 2021 16:10:40 GMT
_Version update only_
## 7.15.1
Thu, 09 Dec 2021 20:34:41 GMT
_Version update only_
## 7.15.0
Thu, 09 Dec 2021 00:21:54 GMT
### Minor changes
- Replace const enums with conventional enums to allow for compatibility with JavaScript consumers.
## 7.14.0
Wed, 08 Dec 2021 16:14:05 GMT
### Minor changes
- Update to TypeScript 4.5
## 7.13.18
Mon, 06 Dec 2021 16:08:33 GMT
_Version update only_
## 7.13.17
Fri, 03 Dec 2021 03:05:22 GMT
_Version update only_
## 7.13.16
Sat, 06 Nov 2021 00:09:13 GMT
_Version update only_
## 7.13.15
Fri, 05 Nov 2021 15:09:18 GMT
_Version update only_
## 7.13.14
Wed, 27 Oct 2021 00:08:15 GMT
### Patches
- Update the package.json repository field to include the directory property.
## 7.13.13
Wed, 13 Oct 2021 15:09:54 GMT
_Version update only_
## 7.13.12
Fri, 08 Oct 2021 08:08:34 GMT
_Version update only_
## 7.13.11
Thu, 07 Oct 2021 07:13:35 GMT
_Version update only_
## 7.13.10
Tue, 05 Oct 2021 15:08:38 GMT
_Version update only_
## 7.13.9
Fri, 24 Sep 2021 00:09:29 GMT
_Version update only_
## 7.13.8
Thu, 23 Sep 2021 00:10:40 GMT
### Patches
- Upgrade the `@types/node` dependency to version to version 12.
## 7.13.7
Tue, 14 Sep 2021 01:17:04 GMT
_Version update only_
## 7.13.6
Mon, 13 Sep 2021 15:07:06 GMT
_Version update only_
## 7.13.5
Wed, 11 Aug 2021 00:07:21 GMT
_Version update only_
## 7.13.4
Mon, 12 Jul 2021 23:08:26 GMT
_Version update only_
## 7.13.3
Fri, 04 Jun 2021 19:59:53 GMT
_Version update only_
## 7.13.2
Wed, 19 May 2021 00:11:39 GMT
_Version update only_
## 7.13.1
Mon, 03 May 2021 15:10:29 GMT
_Version update only_
## 7.13.0
Tue, 20 Apr 2021 04:59:51 GMT
### Minor changes
- The .api.json file format now stores the TSDoc configuration used for parsing doc comments
## 7.12.5
Mon, 12 Apr 2021 15:10:28 GMT
_Version update only_
## 7.12.4
Thu, 08 Apr 2021 06:05:31 GMT
### Patches
- Fix minor typo in README.md
## 7.12.3
Tue, 06 Apr 2021 15:14:22 GMT
_Version update only_
## 7.12.2
Fri, 05 Feb 2021 16:10:42 GMT
_Version update only_
## 7.12.1
Thu, 10 Dec 2020 23:25:49 GMT
### Patches
- Enable support for @decorator
## 7.12.0
Wed, 18 Nov 2020 08:19:54 GMT
### Minor changes
- Introduce an ApiOptionalMixin base class for representing optional properties and methods
## 7.11.0
Wed, 18 Nov 2020 06:21:57 GMT
### Minor changes
- Update .api.json file format to store a new field "isOptional" for documenting optional properties
## 7.10.10
Wed, 11 Nov 2020 01:08:59 GMT
_Version update only_
## 7.10.9
Tue, 10 Nov 2020 23:13:12 GMT
_Version update only_
## 7.10.8
Fri, 30 Oct 2020 06:38:39 GMT
_Version update only_
## 7.10.7
Fri, 30 Oct 2020 00:10:14 GMT
_Version update only_
## 7.10.6
Thu, 29 Oct 2020 06:14:19 GMT
### Patches
- Fix .d.ts error when the library is imported by a project using TypeScript 4.0
## 7.10.5
Wed, 28 Oct 2020 01:18:03 GMT
_Version update only_
## 7.10.4
Tue, 27 Oct 2020 15:10:14 GMT
_Version update only_
## 7.10.3
Tue, 06 Oct 2020 00:24:06 GMT
_Version update only_
## 7.10.2
Mon, 05 Oct 2020 22:36:57 GMT
_Version update only_
## 7.10.1
Wed, 30 Sep 2020 18:39:17 GMT
### Patches
- Update to build with @rushstack/heft-node-rig
## 7.10.0
Wed, 30 Sep 2020 06:53:53 GMT
### Minor changes
- Upgrade compiler; the API now requires TypeScript 3.9 or newer
### Patches
- Update README.md
## 7.9.7
Tue, 22 Sep 2020 05:45:57 GMT
_Version update only_
## 7.9.6
Tue, 22 Sep 2020 01:45:31 GMT
_Version update only_
## 7.9.5
Tue, 22 Sep 2020 00:08:53 GMT
_Version update only_
## 7.9.4
Sat, 19 Sep 2020 04:37:27 GMT
_Version update only_
## 7.9.3
Sat, 19 Sep 2020 03:33:07 GMT
_Version update only_
## 7.9.2
Fri, 18 Sep 2020 22:57:24 GMT
_Version update only_
## 7.9.1
Fri, 18 Sep 2020 21:49:54 GMT
_Version update only_
## 7.9.0
Sun, 13 Sep 2020 01:53:20 GMT
### Minor changes
- Add support for system selectors in declaration references
## 7.8.22
Fri, 11 Sep 2020 02:13:35 GMT
_Version update only_
## 7.8.21
Mon, 07 Sep 2020 07:37:37 GMT
_Version update only_
## 7.8.20
Sat, 05 Sep 2020 18:56:34 GMT
### Patches
- Fix "Converting circular structure to JSON" error (GitHub #2152)
## 7.8.19
Thu, 27 Aug 2020 11:27:06 GMT
_Version update only_
## 7.8.18
Mon, 24 Aug 2020 07:35:20 GMT
_Version update only_
## 7.8.17
Sat, 22 Aug 2020 05:55:43 GMT
_Version update only_
## 7.8.16
Tue, 18 Aug 2020 23:59:42 GMT
_Version update only_
## 7.8.15
Mon, 17 Aug 2020 04:53:23 GMT
_Version update only_
## 7.8.14
Wed, 12 Aug 2020 00:10:05 GMT
### Patches
- Updated project to build with Heft
## 7.8.13
Wed, 05 Aug 2020 18:27:33 GMT
_Version update only_
## 7.8.12
Fri, 03 Jul 2020 15:09:04 GMT
_Version update only_
## 7.8.11
Thu, 25 Jun 2020 06:43:35 GMT
_Version update only_
## 7.8.10
Wed, 24 Jun 2020 09:50:48 GMT
_Version update only_
## 7.8.9
Wed, 24 Jun 2020 09:04:28 GMT
_Version update only_
## 7.8.8
Wed, 10 Jun 2020 20:48:30 GMT
_Version update only_
## 7.8.7
Sat, 30 May 2020 02:59:54 GMT
_Version update only_
## 7.8.6
Thu, 28 May 2020 05:59:02 GMT
_Version update only_
## 7.8.5
Wed, 27 May 2020 05:15:11 GMT
_Version update only_
## 7.8.4
Tue, 26 May 2020 23:00:25 GMT
_Version update only_
## 7.8.3
Fri, 22 May 2020 15:08:43 GMT
_Version update only_
## 7.8.2
Thu, 21 May 2020 23:09:44 GMT
_Version update only_
## 7.8.1
Thu, 21 May 2020 15:42:00 GMT
_Version update only_
## 7.8.0
Wed, 06 May 2020 08:23:45 GMT
### Minor changes
- Enable canonicalReference to ApiItem lookup
## 7.7.11
Wed, 08 Apr 2020 04:07:33 GMT
_Version update only_
## 7.7.10
Sat, 28 Mar 2020 00:37:16 GMT
### Patches
- Upgrade to TSdoc 0.12.19
## 7.7.9
Wed, 18 Mar 2020 15:07:47 GMT
### Patches
- Upgrade cyclic dependencies
## 7.7.8
Tue, 17 Mar 2020 23:55:58 GMT
### Patches
- Replace dependencies whose NPM scope was renamed from `@microsoft` to `@rushstack`
## 7.7.7
Tue, 28 Jan 2020 02:23:44 GMT
_Version update only_
## 7.7.6
Thu, 23 Jan 2020 01:07:56 GMT
_Version update only_
## 7.7.5
Tue, 21 Jan 2020 21:56:14 GMT
_Version update only_
## 7.7.4
Sun, 19 Jan 2020 02:26:52 GMT
### Patches
- Upgrade Node typings to Node 10
## 7.7.3
Fri, 17 Jan 2020 01:08:23 GMT
_Version update only_
## 7.7.2
Thu, 09 Jan 2020 06:44:13 GMT
_Version update only_
## 7.7.1
Wed, 08 Jan 2020 00:11:31 GMT
_Version update only_
## 7.7.0
Tue, 03 Dec 2019 03:17:43 GMT
### Minor changes
- Improve declaration reference syntax to allow linking to overloaded functions/methods
## 7.6.0
Sun, 24 Nov 2019 00:54:04 GMT
### Minor changes
- Added support for `@throws`
## 7.5.6
Fri, 15 Nov 2019 04:50:50 GMT
_Version update only_
## 7.5.5
Mon, 11 Nov 2019 16:07:56 GMT
_Version update only_
## 7.5.4
Tue, 05 Nov 2019 06:49:28 GMT
### Patches
- Fix an issue where API reports sometimes were ordered differently depending on the version of NodeJS (GitHub #1552)
## 7.5.3
Tue, 05 Nov 2019 01:08:39 GMT
### Patches
- Clarified an error message
## 7.5.2
Tue, 22 Oct 2019 06:24:44 GMT
### Patches
- Refactor some code as part of migration from TSLint to ESLint
## 7.5.1
Sun, 29 Sep 2019 23:56:29 GMT
### Patches
- Update repository URL
## 7.5.0
Wed, 25 Sep 2019 15:15:31 GMT
### Minor changes
- Add ApiItem.getMergedSiblings() API
## 7.4.2
Mon, 23 Sep 2019 15:14:55 GMT
### Patches
- Remove unnecessary dependency on @types/node
## 7.4.1
Tue, 10 Sep 2019 22:32:23 GMT
### Patches
- Update documentation
## 7.4.0
Tue, 10 Sep 2019 20:38:33 GMT
### Minor changes
- Add 'canonicalReference' to ExcerptToken
## 7.3.4
Wed, 04 Sep 2019 18:28:06 GMT
_Version update only_
## 7.3.3
Wed, 04 Sep 2019 15:15:37 GMT
### Patches
- Update TSDoc dependency to 0.12.14
## 7.3.2
Thu, 08 Aug 2019 15:14:17 GMT
_Version update only_
## 7.3.1
Thu, 08 Aug 2019 00:49:05 GMT
### Patches
- (Experimental) Add ApiExtractor.canonicalReference which is a beta implementation of the revised TSDoc declaration reference notation
## 7.3.0
Mon, 22 Jul 2019 19:13:10 GMT
### Minor changes
- Rename `ApiItem.canonicalReference` to `.containerKey`; rename `ApiItemContainerMixin.tryGetMember()` to `.tryGetMemberByKey()`; rename `Api___.getCanonicalReference()` to `.getContainerKey()`
## 7.2.0
Tue, 11 Jun 2019 00:48:06 GMT
### Minor changes
- Add API support for type parameters and type alias types
### Patches
- Improve the .api.json deserializer to validate the schema version and support backwards compatibility
## 7.1.3
Wed, 05 Jun 2019 19:12:34 GMT
### Patches
- Fix an issue where TSDoc index selectors (ApiParameterListMixin.overloadIndex) started from 0, whereas TSDoc requires a nonzero number
## 7.1.2
Tue, 04 Jun 2019 05:51:53 GMT
### Patches
- Fix an issue where ApiConstructor inherited from ApiStaticMixin, but TypeScript constructors cannot be static
## 7.1.1
Mon, 27 May 2019 04:13:44 GMT
### Patches
- Make the strings returned by ApiItem.displayName less verbose
- Improve formatting of the strings returned by ApiItem.getScopedNameWithinPackage()
## 7.1.0
Tue, 16 Apr 2019 11:01:37 GMT
### Minor changes
- Initial stable release of API Extractor 7
## 7.0.28
Wed, 20 Mar 2019 19:14:49 GMT
_Version update only_
## 7.0.27
Mon, 18 Mar 2019 04:28:43 GMT
### Patches
- Add helper functions for ReleaseTag
- Export IApiItemConstructor to eliminate the ae-forgotten-export warning
## 7.0.26
Wed, 13 Mar 2019 19:13:14 GMT
### Patches
- Refactor code to move the IndentedWriter API from api-extractor-model to api-documenter
## 7.0.25
Wed, 13 Mar 2019 01:14:05 GMT
### Patches
- Upgrade TSDoc
## 7.0.24
Mon, 11 Mar 2019 16:13:36 GMT
### Patches
- Initial setup of new package @microsoft/api-extractor-model

View File

@@ -0,0 +1,24 @@
@microsoft/api-extractor
Copyright (c) Microsoft Corporation. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,67 @@
# @microsoft/api-extractor-model
Use this library to read and write \*.api.json files as defined by the [API Extractor](https://api-extractor.com/) tool.
These files are used to generate a documentation website for your TypeScript package. The files store the
API signatures and doc comments that were extracted from your package.
API documentation for this package: https://rushstack.io/pages/api/api-extractor-model/
## Example Usage
The following code sample shows how to load `example.api.json`, which would be generated by API Extractor
when it analyzes a hypothetical NPM package called `example`:
```ts
import { ApiModel, ApiPackage } from '@discordjs/api-extractor-model';
const apiModel: ApiModel = new ApiModel();
const apiPackage: ApiPackage = apiModel.loadPackage('example.api.json');
for (const member of apiPackage.members) {
console.log(member.displayName);
}
```
The `ApiModel` is acts as a container for various packages that are loaded and operated on as a group.
For example, a documentation tool may need to resolve `@link` references across different packages.
In this case we would load the various packages into the `ApiModel`, and then use
the `ApiModel.resolveDeclarationReference()` to resolve the `@link` targets.
The data structure forms a tree of various classes that start with the `Api` prefix. The nesting hierarchy
might look like this:
```
- ApiModel
- ApiPackage
- ApiEntryPoint
- ApiClass
- ApiMethod
- ApiProperty
- ApiEnum
- ApiEnumMember
- ApiInterface
- ApiMethodSignature
- ApiPropertySignature
- ApiNamespace
- (ApiClass, ApiEnum, ApiInterface, ...)
```
You can use the `ApiItem.members` property to traverse this tree.
Note that the non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
the function that generates a subclass, an interface that describes the members of the subclass, and
a namespace containing static members of the class.
> For a complete project that uses these APIs to generate an API reference web site,
> see the [@microsoft/api-documenter](https://www.npmjs.com/package/@microsoft/api-documenter) source code.
## Links
- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/libraries/api-extractor-model/CHANGELOG.md) - Find
out what's new in the latest version
- [API Reference](https://rushstack.io/pages/api/api-extractor-model/)
API Extractor is part of the [Rush Stack](https://rushstack.io/) family of projects.

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
"apiReport": {
"enabled": true,
"reportFolder": "../../../common/reviews/api"
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "../../../common/temp/api/<unscopedPackageName>.api.json"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/rollup.d.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json",
// Enable code coverage for Jest
"collectCoverage": true,
"coverageDirectory": "<rootDir>/coverage",
"coverageReporters": ["cobertura", "html"],
// Use v8 coverage provider to avoid Babel
"coverageProvider": "v8"
}

View File

@@ -0,0 +1,49 @@
{
"name": "@discordjs/api-extractor-model",
"version": "7.28.2",
"description": "A helper library for loading and saving the .api.json files created by API Extractor",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/discordjs/discord.js.git",
"directory": "packages/api-extractor-model"
},
"homepage": "https://discord.js.org",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc --noEmit && tsup",
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src"
},
"exports": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@rushstack/node-core-library": "3.61.0"
},
"devDependencies": {
"@types/node": "^18.18.8",
"@types/jest": "^29.5.7",
"cross-env": "^7.0.3",
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"tsup": "^7.2.0",
"turbo": "^1.10.16"
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { TSDocConfiguration, TSDocTagDefinition, TSDocTagSyntaxKind, StandardTags } from '@microsoft/tsdoc';
/**
* @internal
* @deprecated - tsdoc configuration is now constructed from tsdoc.json files associated with each package.
*/
export class AedocDefinitions {
public static readonly betaDocumentation: TSDocTagDefinition = new TSDocTagDefinition({
tagName: '@betaDocumentation',
syntaxKind: TSDocTagSyntaxKind.ModifierTag,
});
public static readonly internalRemarks: TSDocTagDefinition = new TSDocTagDefinition({
tagName: '@internalRemarks',
syntaxKind: TSDocTagSyntaxKind.BlockTag,
});
public static readonly preapprovedTag: TSDocTagDefinition = new TSDocTagDefinition({
tagName: '@preapproved',
syntaxKind: TSDocTagSyntaxKind.ModifierTag,
});
public static get tsdocConfiguration(): TSDocConfiguration {
if (!AedocDefinitions._tsdocConfiguration) {
const configuration: TSDocConfiguration = new TSDocConfiguration();
configuration.addTagDefinitions(
[AedocDefinitions.betaDocumentation, AedocDefinitions.internalRemarks, AedocDefinitions.preapprovedTag],
true,
);
configuration.setSupportForTags(
[
StandardTags.alpha,
StandardTags.beta,
StandardTags.decorator,
StandardTags.defaultValue,
StandardTags.deprecated,
StandardTags.eventProperty,
StandardTags.example,
StandardTags.inheritDoc,
StandardTags.internal,
StandardTags.link,
StandardTags.override,
StandardTags.packageDocumentation,
StandardTags.param,
StandardTags.privateRemarks,
StandardTags.public,
StandardTags.readonly,
StandardTags.remarks,
StandardTags.returns,
StandardTags.sealed,
StandardTags.throws,
StandardTags.virtual,
],
true,
);
AedocDefinitions._tsdocConfiguration = configuration;
}
return AedocDefinitions._tsdocConfiguration;
}
private static _tsdocConfiguration: TSDocConfiguration | undefined;
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/**
* A "release tag" is a custom TSDoc tag that is applied to an API to communicate the level of support
* provided for third-party developers.
*
* @remarks
*
* The four release tags are: `@internal`, `@alpha`, `@beta`, and `@public`. They are applied to API items such
* as classes, member functions, enums, etc. The release tag applies recursively to members of a container
* (e.g. class or interface). For example, if a class is marked as `@beta`, then all of its members automatically
* have this status; you DON'T need add the `@beta` tag to each member function. However, you could add
* `@internal` to a member function to give it a different release status.
* @public
*/
export enum ReleaseTag {
/**
* No release tag was specified in the AEDoc summary.
*/
None = 0,
/**
* Indicates that an API item is meant only for usage by other NPM packages from the same
* maintainer. Third parties should never use "internal" APIs. (To emphasize this, their
* names are prefixed by underscores.)
*/
Internal = 1,
/**
* Indicates that an API item is eventually intended to be public, but currently is in an
* early stage of development. Third parties should not use "alpha" APIs.
*/
Alpha = 2,
/**
* Indicates that an API item has been released in an experimental state. Third parties are
* encouraged to try it and provide feedback. However, a "beta" API should NOT be used
* in production.
*/
Beta = 3,
/**
* Indicates that an API item has been officially released. It is part of the supported
* contract (e.g. SemVer) for a package.
*/
Public = 4,
}
/**
* Helper functions for working with the `ReleaseTag` enum.
*
* @public
*/
// export namespace ReleaseTag {
/**
* Returns the TSDoc tag name for a `ReleaseTag` value.
*
* @remarks
* For example, `getTagName(ReleaseTag.Internal)` would return the string `@internal`.
*/
export function getTagName(releaseTag: ReleaseTag): string {
switch (releaseTag) {
case ReleaseTag.None:
return '(none)';
case ReleaseTag.Internal:
return '@internal';
case ReleaseTag.Alpha:
return '@alpha';
case ReleaseTag.Beta:
return '@beta';
case ReleaseTag.Public:
return '@public';
default:
throw new Error('Unsupported release tag');
}
}
/**
* Compares two `ReleaseTag` values. Their values must not be `ReleaseTag.None`.
*
* @returns 0 if `a` and `b` are equal, a positive number if `a` is more public than `b`,
* and a negative number if `a` is less public than `b`.
* @remarks
* For example, `compareReleaseTag(ReleaseTag.Beta, ReleaseTag.Alpha)` will return a positive
* number because beta is more public than alpha.
*/
export function compare(a: ReleaseTag, b: ReleaseTag): number {
return a - b;
}
// }

View File

@@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/**
* Use this library to read and write *.api.json files as defined by the
* {@link https://api-extractor.com/ | API Extractor} tool. These files are used to generate a documentation
* website for your TypeScript package. The files store the API signatures and doc comments that were extracted
* from your package.
*
* @packageDocumentation
*/
export { AedocDefinitions } from './aedoc/AedocDefinitions.js';
export { ReleaseTag, compare as releaseTagCompare, getTagName as releaseTagGetTagName } from './aedoc/ReleaseTag.js';
// items
export { type IApiDeclaredItemOptions, ApiDeclaredItem } from './items/ApiDeclaredItem.js';
export { type IApiDocumentedItemOptions, ApiDocumentedItem } from './items/ApiDocumentedItem.js';
export { ApiItemKind, type IApiItemOptions, ApiItem, type IApiItemConstructor } from './items/ApiItem.js';
export { type IApiPropertyItemOptions, ApiPropertyItem } from './items/ApiPropertyItem.js';
// mixins
export {
type IApiParameterListMixinOptions,
type IApiParameterOptions,
ApiParameterListMixin,
} from './mixins/ApiParameterListMixin.js';
export {
type IApiTypeParameterOptions,
type IApiTypeParameterListMixinOptions,
ApiTypeParameterListMixin,
} from './mixins/ApiTypeParameterListMixin.js';
export { type IApiAbstractMixinOptions, ApiAbstractMixin } from './mixins/ApiAbstractMixin.js';
export { type IApiItemContainerMixinOptions, ApiItemContainerMixin } from './mixins/ApiItemContainerMixin.js';
export { type IApiProtectedMixinOptions, ApiProtectedMixin } from './mixins/ApiProtectedMixin.js';
export { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from './mixins/ApiReleaseTagMixin.js';
export { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from './mixins/ApiReturnTypeMixin.js';
export { type IApiStaticMixinOptions, ApiStaticMixin } from './mixins/ApiStaticMixin.js';
export { type IApiNameMixinOptions, ApiNameMixin } from './mixins/ApiNameMixin.js';
export { type IApiOptionalMixinOptions, ApiOptionalMixin } from './mixins/ApiOptionalMixin.js';
export { type IApiReadonlyMixinOptions, ApiReadonlyMixin } from './mixins/ApiReadonlyMixin.js';
export { type IApiInitializerMixinOptions, ApiInitializerMixin } from './mixins/ApiInitializerMixin.js';
export { type IApiExportedMixinOptions, ApiExportedMixin } from './mixins/ApiExportedMixin.js';
export {
type IFindApiItemsResult,
type IFindApiItemsMessage,
FindApiItemsMessageId,
} from './mixins/IFindApiItemsResult.js';
export {
ExcerptTokenKind,
type IExcerptTokenRange,
type IExcerptToken,
ExcerptToken,
Excerpt,
} from './mixins/Excerpt.js';
export type { Constructor, PropertiesOf } from './mixins/Mixin.js';
// model
export { type IApiCallSignatureOptions, ApiCallSignature } from './model/ApiCallSignature.js';
export { type IApiClassOptions, ApiClass } from './model/ApiClass.js';
export { type IApiConstructorOptions, ApiConstructor } from './model/ApiConstructor.js';
export { type IApiConstructSignatureOptions, ApiConstructSignature } from './model/ApiConstructSignature.js';
export { type IApiEntryPointOptions, ApiEntryPoint } from './model/ApiEntryPoint.js';
export { type IApiEnumOptions, ApiEnum } from './model/ApiEnum.js';
export { type IApiEnumMemberOptions, ApiEnumMember, EnumMemberOrder } from './model/ApiEnumMember.js';
export { type IApiFunctionOptions, ApiFunction } from './model/ApiFunction.js';
export { type IApiIndexSignatureOptions, ApiIndexSignature } from './model/ApiIndexSignature.js';
export { type IApiInterfaceOptions, ApiInterface } from './model/ApiInterface.js';
export { type IApiMethodOptions, ApiMethod } from './model/ApiMethod.js';
export { type IApiMethodSignatureOptions, ApiMethodSignature } from './model/ApiMethodSignature.js';
export { ApiModel } from './model/ApiModel.js';
export { type IApiNamespaceOptions, ApiNamespace } from './model/ApiNamespace.js';
export { type IApiPackageOptions, ApiPackage, type IApiPackageSaveOptions } from './model/ApiPackage.js';
export { type IParameterOptions, Parameter } from './model/Parameter.js';
export { type IApiPropertyOptions, ApiProperty } from './model/ApiProperty.js';
export { type IApiPropertySignatureOptions, ApiPropertySignature } from './model/ApiPropertySignature.js';
export { type IApiTypeAliasOptions, ApiTypeAlias } from './model/ApiTypeAlias.js';
export { type ITypeParameterOptions, TypeParameter } from './model/TypeParameter.js';
export { type IApiVariableOptions, ApiVariable } from './model/ApiVariable.js';
export type { IResolveDeclarationReferenceResult } from './model/ModelReferenceResolver.js';
export { HeritageType } from './model/HeritageType.js';
export { type ISourceLocationOptions, SourceLocation } from './model/SourceLocation.js';
export { Navigation, Meaning } from './items/ApiItem.js';

View File

@@ -0,0 +1,225 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { Excerpt, ExcerptToken, type IExcerptTokenRange, type IExcerptToken } from '../mixins/Excerpt.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import { SourceLocation } from '../model/SourceLocation.js';
import { ApiDocumentedItem, type IApiDocumentedItemJson, type IApiDocumentedItemOptions } from './ApiDocumentedItem.js';
import type { ApiItem } from './ApiItem.js';
/**
* Constructor options for {@link ApiDeclaredItem}.
*
* @public
*/
export interface IApiDeclaredItemOptions extends IApiDocumentedItemOptions {
excerptTokens: IExcerptToken[];
fileColumn?: number | undefined;
fileLine?: number | undefined;
fileUrlPath?: string | undefined;
}
export interface IApiDeclaredItemJson extends IApiDocumentedItemJson {
excerptTokens: IExcerptToken[];
fileColumn?: number;
fileLine?: number;
fileUrlPath?: string | undefined;
}
/**
* The base class for API items that have an associated source code excerpt containing a TypeScript declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* Most `ApiItem` subclasses have declarations and thus extend `ApiDeclaredItem`. Counterexamples include
* `ApiModel` and `ApiPackage`, which do not have any corresponding TypeScript source code.
* @public
*/
export class ApiDeclaredItem extends ApiDocumentedItem {
private readonly _excerptTokens: ExcerptToken[];
private readonly _excerpt: Excerpt;
private readonly _fileUrlPath?: string | undefined;
private readonly _fileLine?: number | undefined;
private readonly _fileColumn?: number | undefined;
private _sourceLocation?: SourceLocation;
public constructor(options: IApiDeclaredItemOptions) {
super(options);
this._excerptTokens = options.excerptTokens.map((token) => {
const canonicalReference: DeclarationReference | undefined =
token.canonicalReference === undefined ? undefined : DeclarationReference.parse(token.canonicalReference);
return new ExcerptToken(token.kind, token.text, canonicalReference);
});
this._excerpt = new Excerpt(this.excerptTokens, { startIndex: 0, endIndex: this.excerptTokens.length });
this._fileUrlPath = options.fileUrlPath;
this._fileLine = options.fileLine;
this._fileColumn = options.fileColumn;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiDeclaredItemOptions>,
context: DeserializerContext,
jsonObject: IApiDeclaredItemJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.excerptTokens = jsonObject.excerptTokens;
options.fileUrlPath = jsonObject.fileUrlPath;
options.fileLine = jsonObject.fileLine;
options.fileColumn = jsonObject.fileColumn;
}
/**
* The source code excerpt where the API item is declared.
*/
public get excerpt(): Excerpt {
return this._excerpt;
}
/**
* The individual source code tokens that comprise the main excerpt.
*/
public get excerptTokens(): readonly ExcerptToken[] {
return this._excerptTokens;
}
/**
* The file URL path relative to the `projectFolder` and `projectFolderURL` fields
* as defined in the `api-extractor.json` config. Is `undefined` if the path is
* the same as the parent API item's.
*/
public get fileUrlPath(): string | undefined {
return this._fileUrlPath;
}
/**
* The line in the `fileUrlPath` where the API item is declared.
*/
public get fileLine(): number | undefined {
return this._fileLine;
}
/**
* The column in the `fileUrlPath` where the API item is declared.
*/
public get fileColumn(): number | undefined {
return this._fileColumn;
}
/**
* Returns the source location where the API item is declared.
*/
public get sourceLocation(): SourceLocation {
if (!this._sourceLocation) {
this._sourceLocation = this._buildSourceLocation();
}
return this._sourceLocation;
}
/**
* If the API item has certain important modifier tags such as `@sealed`, `@virtual`, or `@override`,
* this prepends them as a doc comment above the excerpt.
*/
public getExcerptWithModifiers(): string {
const excerpt: string = this.excerpt.text;
const modifierTags: string[] = [];
if (excerpt.length > 0 && this instanceof ApiDocumentedItem) {
if (this.tsdocComment) {
if (this.tsdocComment.modifierTagSet.isSealed()) {
modifierTags.push('@sealed');
}
if (this.tsdocComment.modifierTagSet.isVirtual()) {
modifierTags.push('@virtual');
}
if (this.tsdocComment.modifierTagSet.isOverride()) {
modifierTags.push('@override');
}
}
if (modifierTags.length > 0) {
return '/** ' + modifierTags.join(' ') + ' */\n' + excerpt;
}
}
return excerpt;
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiDeclaredItemJson>): void {
super.serializeInto(jsonObject);
jsonObject.excerptTokens = this.excerptTokens.map((x) => {
const excerptToken: IExcerptToken = { kind: x.kind, text: x.text };
if (x.canonicalReference !== undefined) {
excerptToken.canonicalReference = x.canonicalReference.toString();
}
return excerptToken;
});
// Only serialize this API item's file URL path if it exists and it's different from its parent's
// (a little optimization to keep the doc model succinct).
if (
this.fileUrlPath &&
(!(this.parent instanceof ApiDeclaredItem) || this.fileUrlPath !== this.parent.fileUrlPath)
) {
jsonObject.fileUrlPath = this.fileUrlPath;
}
if (this.fileLine) {
jsonObject.fileLine = this.fileLine;
}
if (this.fileColumn) {
jsonObject.fileColumn = this.fileColumn;
}
}
/**
* Constructs a new {@link Excerpt} corresponding to the provided token range.
*/
public buildExcerpt(tokenRange: IExcerptTokenRange): Excerpt {
return new Excerpt(this.excerptTokens, tokenRange);
}
/**
* Builds the cached object used by the `sourceLocation` property.
*/
private _buildSourceLocation(): SourceLocation {
const projectFolderUrl: string | undefined = this.getAssociatedPackage()?.projectFolderUrl;
let fileUrlPath: string | undefined;
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (current instanceof ApiDeclaredItem && current.fileUrlPath) {
fileUrlPath = current.fileUrlPath;
break;
}
}
return new SourceLocation({
projectFolderUrl,
fileUrlPath,
sourceFileColumn: this.fileColumn,
sourceFileLine: this.fileLine,
});
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as tsdoc from '@microsoft/tsdoc';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import { ApiItem, type IApiItemOptions, type IApiItemJson } from './ApiItem.js';
/**
* Constructor options for {@link ApiDocumentedItem}.
*
* @public
*/
export interface IApiDocumentedItemOptions extends IApiItemOptions {
docComment: tsdoc.DocComment | undefined;
}
export interface IApiDocumentedItemJson extends IApiItemJson {
docComment: string;
}
/**
* An abstract base class for API declarations that can have an associated TSDoc comment.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
* @public
*/
export class ApiDocumentedItem extends ApiItem {
private readonly _tsdocComment: tsdoc.DocComment | undefined;
public constructor(options: IApiDocumentedItemOptions) {
super(options);
this._tsdocComment = options.docComment;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiDocumentedItemOptions>,
context: DeserializerContext,
jsonObject: IApiItemJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
const documentedJson: IApiDocumentedItemJson = jsonObject as IApiDocumentedItemJson;
if (documentedJson.docComment) {
const tsdocParser: tsdoc.TSDocParser = new tsdoc.TSDocParser(context.tsdocConfiguration);
// NOTE: For now, we ignore TSDoc errors found in a serialized .api.json file.
// Normally these errors would have already been reported by API Extractor during analysis.
// However, they could also arise if the JSON file was edited manually, or if the file was saved
// using a different release of the software that used an incompatible syntax.
const parserContext: tsdoc.ParserContext = tsdocParser.parseString(documentedJson.docComment);
options.docComment = parserContext.docComment;
}
}
public get tsdocComment(): tsdoc.DocComment | undefined {
return this._tsdocComment;
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiDocumentedItemJson>): void {
super.serializeInto(jsonObject);
if (this.tsdocComment === undefined) {
jsonObject.docComment = '';
} else {
jsonObject.docComment = this.tsdocComment.emitAsTsdoc();
}
}
}

View File

@@ -0,0 +1,367 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { InternalError } from '@rushstack/node-core-library';
import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js';
import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import type { Constructor, PropertiesOf } from '../mixins/Mixin.js';
import type { ApiModel } from '../model/ApiModel.js';
import type { ApiPackage } from '../model/ApiPackage.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* The type returned by the {@link ApiItem.kind} property, which can be used to easily distinguish subclasses of
* {@link ApiItem}.
*
* @public
*/
export enum ApiItemKind {
CallSignature = 'CallSignature',
Class = 'Class',
ConstructSignature = 'ConstructSignature',
Constructor = 'Constructor',
EntryPoint = 'EntryPoint',
Enum = 'Enum',
EnumMember = 'EnumMember',
Function = 'Function',
IndexSignature = 'IndexSignature',
Interface = 'Interface',
Method = 'Method',
MethodSignature = 'MethodSignature',
Model = 'Model',
Namespace = 'Namespace',
None = 'None',
Package = 'Package',
Property = 'Property',
PropertySignature = 'PropertySignature',
TypeAlias = 'TypeAlias',
Variable = 'Variable',
}
/**
* Indicates the symbol table from which to resolve the next symbol component.
*
* @beta
*/
export enum Navigation {
Exports = '.',
Locals = '~',
Members = '#',
}
/**
* @beta
*/
export enum Meaning {
CallSignature = 'call',
Class = 'class',
ComplexType = 'complex',
ConstructSignature = 'new',
Constructor = 'constructor',
Enum = 'enum',
Event = 'event',
Function = 'function',
IndexSignature = 'index',
Interface = 'interface',
Member = 'member',
Namespace = 'namespace',
TypeAlias = 'type',
Variable = 'var',
}
/**
* Constructor options for {@link ApiItem}.
*
* @public
*/
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface IApiItemOptions {}
export interface IApiItemJson {
canonicalReference: string;
kind: ApiItemKind;
}
// PRIVATE - Allows ApiItemContainerMixin to assign the parent.
//
export const apiItem_onParentChanged: unique symbol = Symbol('ApiItem._onAddToContainer');
/**
* The abstract base class for all members of an `ApiModel` object.
*
* @remarks
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
* @public
*/
export class ApiItem {
private _canonicalReference: DeclarationReference | undefined;
private _parent: ApiItem | undefined;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
public constructor(_options: IApiItemOptions) {
// ("options" is not used here, but part of the inheritance pattern)
}
public static deserialize(jsonObject: IApiItemJson, context: DeserializerContext): ApiItem {
// The Deserializer class is coupled with a ton of other classes, so we delay loading it
// to avoid ES5 circular imports.
// eslint-disable-next-line @typescript-eslint/consistent-type-imports, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
const deserializerModule: typeof import('../model/Deserializer') = require('../model/Deserializer');
return deserializerModule.Deserializer.deserialize(context, jsonObject);
}
/**
* @virtual
*/
public static onDeserializeInto(
_options: Partial<IApiItemOptions>,
_context: DeserializerContext,
_jsonObject: IApiItemJson,
): void {
// (implemented by subclasses)
}
/**
* @virtual
*/
public serializeInto(jsonObject: Partial<IApiItemJson>): void {
jsonObject.kind = this.kind;
jsonObject.canonicalReference = this.canonicalReference.toString();
}
/**
* Identifies the subclass of the `ApiItem` base class.
*
* @virtual
*/
public get kind(): ApiItemKind {
throw new Error('ApiItem.kind was not implemented by the child class');
}
/**
* Warning: This API is used internally by API extractor but is not yet ready for general usage.
*
* @remarks
*
* Returns a `DeclarationReference` object using the experimental new declaration reference notation.
* @beta
*/
public get canonicalReference(): DeclarationReference {
if (!this._canonicalReference) {
try {
this._canonicalReference = this.buildCanonicalReference();
} catch (error) {
const name: string = this.getScopedNameWithinPackage() || this.displayName;
throw new InternalError(`Error building canonical reference for ${name}:\n` + (error as Error).message);
}
}
return this._canonicalReference;
}
/**
* Returns a string key that can be used to efficiently retrieve an `ApiItem` from an `ApiItemContainerMixin`.
* The key is unique within the container. Its format is undocumented and may change at any time.
*
* @remarks
* Use the `getContainerKey()` static member to construct the key. Each subclass has a different implementation
* of this function, according to the aspects that are important for identifying it.
* @virtual
*/
public get containerKey(): string {
throw new InternalError('ApiItem.containerKey was not implemented by the child class');
}
/**
* Returns a name for this object that can be used in diagnostic messages, for example.
*
* @remarks
* For an object that inherits ApiNameMixin, this will return the declared name (e.g. the name of a TypeScript
* function). Otherwise, it will return a string such as "(call signature)" or "(model)".
* @virtual
*/
public get displayName(): string {
switch (this.kind) {
case ApiItemKind.CallSignature:
return '(call)';
case ApiItemKind.Constructor:
return '(constructor)';
case ApiItemKind.ConstructSignature:
return '(new)';
case ApiItemKind.IndexSignature:
return '(indexer)';
case ApiItemKind.Model:
return '(model)';
default:
return '(???)'; // All other types should inherit ApiNameMixin which will override this property
}
}
/**
* If this item was added to a ApiItemContainerMixin item, then this returns the container item.
* If this is an Parameter that was added to a method or function, then this returns the function item.
* Otherwise, it returns undefined.
*
* @virtual
*/
public get parent(): ApiItem | undefined {
return this._parent;
}
/**
* This property supports a visitor pattern for walking the tree.
* For items with ApiItemContainerMixin, it returns the contained items, sorted alphabetically.
* Otherwise it returns an empty array.
*
* @virtual
*/
public get members(): readonly ApiItem[] {
return [];
}
/**
* If this item has a name (i.e. extends `ApiNameMixin`), then return all items that have the same parent
* and the same name. Otherwise, return all items that have the same parent and the same `ApiItemKind`.
*
* @remarks
* Examples: For a function, this would return all overloads for the function. For a constructor, this would
* return all overloads for the constructor. For a merged declaration (e.g. a `namespace` and `enum` with the
* same name), this would return both declarations. If this item does not have a parent, or if it is the only
* item of its name/kind, then the result is an array containing only this item.
*/
public getMergedSiblings(): readonly ApiItem[] {
const parent: ApiItem | undefined = this._parent;
if (parent && ApiItemContainerMixin.isBaseClassOf(parent)) {
return parent._getMergedSiblingsForMember(this);
}
return [];
}
/**
* Returns the chain of ancestors, starting from the root of the tree, and ending with the this item.
*/
public getHierarchy(): readonly ApiItem[] {
const hierarchy: ApiItem[] = [];
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
hierarchy.push(current);
}
hierarchy.reverse();
return hierarchy;
}
/**
* This returns a scoped name such as `"Namespace1.Namespace2.MyClass.myMember()"`. It does not include the
* package name or entry point.
*
* @remarks
* If called on an ApiEntrypoint, ApiPackage, or ApiModel item, the result is an empty string.
*/
public getScopedNameWithinPackage(): string {
const reversedParts: string[] = [];
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (
current.kind === ApiItemKind.Model ||
current.kind === ApiItemKind.Package ||
current.kind === ApiItemKind.EntryPoint
) {
break;
}
if (reversedParts.length === 0) {
switch (current.kind) {
case ApiItemKind.CallSignature:
case ApiItemKind.ConstructSignature:
case ApiItemKind.Constructor:
case ApiItemKind.IndexSignature:
// These functional forms don't have a proper name, so we don't append the "()" suffix
break;
default:
if (ApiParameterListMixin.isBaseClassOf(current)) {
reversedParts.push('()');
}
}
} else {
reversedParts.push('.');
}
reversedParts.push(current.displayName);
}
return reversedParts.reverse().join('');
}
/**
* If this item is an ApiPackage or has an ApiPackage as one of its parents, then that object is returned.
* Otherwise undefined is returned.
*/
public getAssociatedPackage(): ApiPackage | undefined {
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (current.kind === ApiItemKind.Package) {
return current as ApiPackage;
}
}
return undefined;
}
/**
* If this item is an ApiModel or has an ApiModel as one of its parents, then that object is returned.
* Otherwise undefined is returned.
*/
public getAssociatedModel(): ApiModel | undefined {
for (let current: ApiItem | undefined = this; current !== undefined; current = current.parent) {
if (current.kind === ApiItemKind.Model) {
return current as ApiModel;
}
}
return undefined;
}
/**
* A text string whose value determines the sort order that is automatically applied by the
* {@link (ApiItemContainerMixin:interface)} class.
*
* @remarks
* The value of this string is undocumented and may change at any time.
* If {@link (ApiItemContainerMixin:interface).preserveMemberOrder} is enabled for the `ApiItem`'s parent,
* then no sorting is performed, and this key is not used.
* @virtual
*/
public getSortKey(): string {
return this.containerKey;
}
/**
* PRIVATE
*
* @privateRemarks
* Allows ApiItemContainerMixin to assign the parent when the item is added to a container.
* @internal
*/
public [apiItem_onParentChanged](parent: ApiItem | undefined): void {
this._parent = parent;
this._canonicalReference = undefined;
}
/**
* Builds the cached object used by the `canonicalReference` property.
*
* @virtual
*/
protected buildCanonicalReference(): DeclarationReference {
throw new InternalError('ApiItem.canonicalReference was not implemented by the child class');
}
}
/**
* This abstraction is used by the mixin pattern.
* It describes a class type that inherits from {@link ApiItem}.
*
* @public
*/
export interface IApiItemConstructor extends Constructor<ApiItem>, PropertiesOf<typeof ApiItem> {}

View File

@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js';
import { ApiReadonlyMixin, type IApiReadonlyMixinOptions } from '../mixins/ApiReadonlyMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import type { Excerpt, IExcerptTokenRange } from '../mixins/Excerpt.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem, type IApiDeclaredItemJson } from './ApiDeclaredItem.js';
/**
* Constructor options for {@link ApiPropertyItem}.
*
* @public
*/
export interface IApiPropertyItemOptions
extends IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiOptionalMixinOptions,
IApiReadonlyMixinOptions,
IApiDeclaredItemOptions {
propertyTypeTokenRange: IExcerptTokenRange;
}
export interface IApiPropertyItemJson extends IApiDeclaredItemJson {
propertyTypeTokenRange: IExcerptTokenRange;
}
/**
* The abstract base class for {@link ApiProperty} and {@link ApiPropertySignature}.
*
* @public
*/
export class ApiPropertyItem extends ApiNameMixin(
ApiReleaseTagMixin(ApiOptionalMixin(ApiReadonlyMixin(ApiDeclaredItem))),
) {
/**
* An {@link Excerpt} that describes the type of the property.
*/
public readonly propertyTypeExcerpt: Excerpt;
public constructor(options: IApiPropertyItemOptions) {
super(options);
this.propertyTypeExcerpt = this.buildExcerpt(options.propertyTypeTokenRange);
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiPropertyItemOptions>,
context: DeserializerContext,
jsonObject: IApiPropertyItemJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.propertyTypeTokenRange = jsonObject.propertyTypeTokenRange;
}
/**
* Returns true if this property should be documented as an event.
*
* @remarks
* The `@eventProperty` TSDoc modifier can be added to readonly properties to indicate that they return an
* event object that event handlers can be attached to. The event-handling API is implementation-defined, but
* typically the return type would be a class with members such as `addHandler()` and `removeHandler()`.
* The documentation should display such properties under an "Events" heading instead of the
* usual "Properties" heading.
*/
public get isEventProperty(): boolean {
if (this.tsdocComment) {
return this.tsdocComment.modifierTagSet.isEventProperty();
}
return false;
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiPropertyItemJson>): void {
super.serializeInto(jsonObject);
jsonObject.propertyTypeTokenRange = this.propertyTypeExcerpt.tokenRange;
}
}

View File

@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (ApiAbstractMixin:interface)}.
*
* @public
*/
export interface IApiAbstractMixinOptions extends IApiItemOptions {
isAbstract: boolean;
}
export interface IApiAbstractMixinJson extends IApiItemJson {
isAbstract: boolean;
}
const _isAbstract: unique symbol = Symbol('ApiAbstractMixin._isAbstract');
/**
* The mixin base class for API items that have an abstract modifier.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiAbstractMixin extends ApiItem {
/**
* Indicates that the API item's value has an 'abstract' modifier.
*/
readonly isAbstract: boolean;
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiAbstractMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiAbstractMixin:interface)}
* functionality.
* @public
*/
export function ApiAbstractMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiAbstractMixin) {
class MixedClass extends baseClass implements ApiAbstractMixin {
public [_isAbstract]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiAbstractMixinOptions = args[0];
this[_isAbstract] = options.isAbstract;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiAbstractMixinOptions>,
context: DeserializerContext,
jsonObject: IApiAbstractMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.isAbstract = jsonObject.isAbstract || false;
}
public get isAbstract(): boolean {
return this[_isAbstract];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiAbstractMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.isAbstract = this.isAbstract;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiAbstractMixin:interface)}.
*
* @public
*/
export namespace ApiAbstractMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiAbstractMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiAbstractMixin {
return apiItem.hasOwnProperty(_isAbstract);
}
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import { Navigation } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (IApiExportedMixinOptions:interface)}.
*
* @public
*/
export interface IApiExportedMixinOptions extends IApiItemOptions {
isExported: boolean;
}
export interface IApiExportedMixinJson extends IApiItemJson {
isExported: boolean;
}
const _isExported: unique symbol = Symbol('ApiExportedMixin._isExported');
/**
* The mixin base class for API items that can be exported.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiExportedMixin extends ApiItem {
/**
* Whether the declaration is exported from its parent item container (i.e. either an `ApiEntryPoint` or an
* `ApiNamespace`).
*
* @remarks
* Suppose `index.ts` is your entry point:
*
* ```ts
* // index.ts
*
* export class A {}
* class B {}
*
* namespace n {
* export class C {}
* class D {}
* }
*
* // file.ts
* export class E {}
* ```
*
* Classes `A` and `C` are both exported, while classes `B`, `D`, and `E` are not. `E` is exported from its
* local file, but not from its parent item container (i.e. the entry point).
*/
readonly isExported: boolean;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiExportedMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiExportedMixin:interface)} functionality.
* @public
*/
export function ApiExportedMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiExportedMixin) {
class MixedClass extends baseClass implements ApiExportedMixin {
public [_isExported]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiExportedMixinOptions = args[0];
this[_isExported] = options.isExported;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiExportedMixinOptions>,
context: DeserializerContext,
jsonObject: IApiExportedMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
const declarationReference: DeclarationReference = DeclarationReference.parse(jsonObject.canonicalReference);
options.isExported = declarationReference.navigation === (Navigation.Exports as any); // ambient const enums suck...
}
public get isExported(): boolean {
return this[_isExported];
}
/**
* The `isExported` property is intentionally not serialized because the information is already present
* in the item's `canonicalReference`.
*
* @override
*/
public override serializeInto(jsonObject: Partial<IApiExportedMixinJson>): void {
super.serializeInto(jsonObject);
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiExportedMixin:interface)}.
*
* @public
*/
export namespace ApiExportedMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiExportedMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiExportedMixin {
return apiItem.hasOwnProperty(_isExported);
}
}

View File

@@ -0,0 +1,130 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import type { IExcerptTokenRange, Excerpt } from './Excerpt.js';
/**
* Constructor options for {@link (IApiInitializerMixinOptions:interface)}.
*
* @public
*/
export interface IApiInitializerMixinOptions extends IApiItemOptions {
initializerTokenRange?: IExcerptTokenRange | undefined;
}
export interface IApiInitializerMixinJson extends IApiItemJson {
initializerTokenRange?: IExcerptTokenRange;
}
const _initializerExcerpt: unique symbol = Symbol('ApiInitializerMixin._initializerExcerpt');
/**
* The mixin base class for API items that can have an initializer.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiInitializerMixin extends ApiItem {
/**
* An {@link Excerpt} that describes the item's initializer.
*/
readonly initializerExcerpt?: Excerpt | undefined;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiInitializerMixinJson>): void;
}
/**
* Mixin function for {@link (ApiInitializerMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiInitializerMixin:interface)} functionality.
* @public
*/
export function ApiInitializerMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiInitializerMixin) {
class MixedClass extends baseClass implements ApiInitializerMixin {
public [_initializerExcerpt]?: Excerpt;
public constructor(...args: any[]) {
super(...args);
const options: IApiInitializerMixinOptions = args[0];
if (this instanceof ApiDeclaredItem) {
if (options.initializerTokenRange) {
this[_initializerExcerpt] = this.buildExcerpt(options.initializerTokenRange);
}
} else {
throw new InternalError('ApiInitializerMixin expects a base class that inherits from ApiDeclaredItem');
}
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiInitializerMixinOptions>,
context: DeserializerContext,
jsonObject: IApiInitializerMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.initializerTokenRange = jsonObject.initializerTokenRange;
}
public get initializerExcerpt(): Excerpt | undefined {
return this[_initializerExcerpt];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiInitializerMixinJson>): void {
super.serializeInto(jsonObject);
// Note that JSON does not support the "undefined" value, so we simply omit the field entirely if it is undefined
if (this.initializerExcerpt) {
jsonObject.initializerTokenRange = this.initializerExcerpt.tokenRange;
}
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiInitializerMixin:interface)}.
*
* @public
*/
export namespace ApiInitializerMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiInitializerMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiInitializerMixin {
return apiItem.hasOwnProperty(_initializerExcerpt);
}
}

View File

@@ -0,0 +1,562 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { InternalError } from '@rushstack/node-core-library';
import {
ApiItem,
apiItem_onParentChanged,
type IApiItemJson,
type IApiItemOptions,
type IApiItemConstructor,
ApiItemKind,
} from '../items/ApiItem.js';
import type { ApiClass } from '../model/ApiClass.js';
import type { ApiInterface } from '../model/ApiInterface.js';
import type { ApiModel } from '../model/ApiModel.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import type { HeritageType } from '../model/HeritageType.js';
import type { IResolveDeclarationReferenceResult } from '../model/ModelReferenceResolver.js';
import { ApiNameMixin } from './ApiNameMixin.js';
import { type ExcerptToken, ExcerptTokenKind } from './Excerpt.js';
import { type IFindApiItemsResult, type IFindApiItemsMessage, FindApiItemsMessageId } from './IFindApiItemsResult.js';
/**
* Constructor options for {@link (ApiItemContainerMixin:interface)}.
*
* @public
*/
export interface IApiItemContainerMixinOptions extends IApiItemOptions {
members?: ApiItem[] | undefined;
preserveMemberOrder?: boolean | undefined;
}
export interface IApiItemContainerJson extends IApiItemJson {
members: IApiItemJson[];
preserveMemberOrder?: boolean;
}
const _members: unique symbol = Symbol('ApiItemContainerMixin._members');
const _membersSorted: unique symbol = Symbol('ApiItemContainerMixin._membersSorted');
const _membersByContainerKey: unique symbol = Symbol('ApiItemContainerMixin._membersByContainerKey');
const _membersByName: unique symbol = Symbol('ApiItemContainerMixin._membersByName');
const _membersByKind: unique symbol = Symbol('ApiItemContainerMixin._membersByKind');
const _preserveMemberOrder: unique symbol = Symbol('ApiItemContainerMixin._preserveMemberOrder');
/**
* The mixin base class for API items that act as containers for other child items.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
*
* Examples of `ApiItemContainerMixin` child classes include `ApiModel`, `ApiPackage`, `ApiEntryPoint`,
* and `ApiEnum`. But note that `Parameter` is not considered a "member" of an `ApiMethod`; this relationship
* is modeled using {@link (ApiParameterListMixin:interface).parameters} instead
* of {@link ApiItem.members}.
* @public
*/
export interface ApiItemContainerMixin extends ApiItem {
/**
* For a given member of this container, return its `ApiItem.getMergedSiblings()` list.
*
* @internal
*/
_getMergedSiblingsForMember(memberApiItem: ApiItem): readonly ApiItem[];
/**
* Adds a new member to the container.
*
* @remarks
* An ApiItem cannot be added to more than one container.
*/
addMember(member: ApiItem): void;
/**
* Returns a list of members with the specified name.
*/
findMembersByName(name: string): readonly ApiItem[];
/**
* Finds all of the ApiItem's immediate and inherited members by walking up the inheritance tree.
*
* @remarks
*
* Given the following class heritage:
*
* ```
* export class A {
* public a: number|boolean;
* }
*
* export class B extends A {
* public a: number;
* public b: string;
* }
*
* export class C extends B {
* public c: boolean;
* }
* ```
*
* Calling `findMembersWithInheritance` on `C` will return `B.a`, `B.b`, and `C.c`. Calling the
* method on `B` will return `B.a` and `B.b`. And calling the method on `A` will return just
* `A.a`.
*
* The inherited members returned by this method may be incomplete. If so, there will be a flag
* on the result object indicating this as well as messages explaining the errors in more detail.
* Some scenarios include:
*
* - Interface extending from a type alias.
*
* - Class extending from a variable.
*
* - Extending from a declaration not present in the model (e.g. external package).
*
* - Extending from an unexported declaration (e.g. ae-forgotten-export). Common in mixin
* patterns.
*
* - Unexpected runtime errors...
*
* Lastly, be aware that the types of inherited members are returned with respect to their
* defining class as opposed to with respect to the inheriting class. For example, consider
* the following:
*
* ```
* export class A<T> {
* public a: T;
* }
*
* export class B extends A<number> {}
* ```
*
* When called on `B`, this method will return `B.a` with type `T` as opposed to type
* `number`, although the latter is more accurate.
*/
findMembersWithInheritance(): IFindApiItemsResult;
/**
* Disables automatic sorting of {@link ApiItem.members}.
*
* @remarks
* By default `ApiItemContainerMixin` will automatically sort its members according to their
* {@link ApiItem.getSortKey} string, which provides a standardized mostly alphabetical ordering
* that is appropriate for most API items. When loading older .api.json files the automatic sorting
* is reapplied and may update the ordering.
*
* Set `preserveMemberOrder` to true to disable automatic sorting for this container; instead, the
* members will retain whatever ordering appeared in the {@link IApiItemContainerMixinOptions.members} array.
* The `preserveMemberOrder` option is saved in the .api.json file.
*/
readonly preserveMemberOrder: boolean;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
/**
* Attempts to retrieve a member using its containerKey, or returns `undefined` if no matching member was found.
*
* @remarks
* Use the `getContainerKey()` static member to construct the key. Each subclass has a different implementation
* of this function, according to the aspects that are important for identifying it.
*
* See {@link ApiItem.containerKey} for more information.
*/
tryGetMemberByKey(containerKey: string): ApiItem | undefined;
}
/**
* Mixin function for {@link ApiDeclaredItem}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiItemContainerMixin:interface)} functionality.
* @public
*/
export function ApiItemContainerMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiItemContainerMixin) {
class MixedClass extends baseClass implements ApiItemContainerMixin {
public readonly [_members]: ApiItem[];
public [_membersSorted]: boolean;
public [_membersByContainerKey]: Map<string, ApiItem>;
public [_preserveMemberOrder]: boolean;
// For members of this container that extend ApiNameMixin, this stores the list of members with a given name.
// Examples include merged declarations, overloaded functions, etc.
public [_membersByName]: Map<string, ApiItem[]> | undefined;
// For members of this container that do NOT extend ApiNameMixin, this stores the list of members
// that share a common ApiItemKind. Examples include overloaded constructors or index signatures.
public [_membersByKind]: Map<string, ApiItem[]> | undefined; // key is ApiItemKind
public constructor(...args: any[]) {
super(...args);
const options: IApiItemContainerMixinOptions = args[0] as IApiItemContainerMixinOptions;
this[_members] = [];
this[_membersSorted] = false;
this[_membersByContainerKey] = new Map<string, ApiItem>();
this[_preserveMemberOrder] = options.preserveMemberOrder ?? false;
if (options.members) {
for (const member of options.members) {
this.addMember(member);
}
}
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiItemContainerMixinOptions>,
context: DeserializerContext,
jsonObject: IApiItemContainerJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.preserveMemberOrder = jsonObject.preserveMemberOrder;
options.members = [];
for (const memberObject of jsonObject.members) {
options.members.push(ApiItem.deserialize(memberObject, context));
}
}
/**
* @override
*/
public override get members(): readonly ApiItem[] {
if (!this[_membersSorted] && !this[_preserveMemberOrder]) {
this[_members].sort((x, y) => x.getSortKey().localeCompare(y.getSortKey()));
this[_membersSorted] = true;
}
return this[_members];
}
public get preserveMemberOrder(): boolean {
return this[_preserveMemberOrder];
}
public addMember(member: ApiItem): void {
if (this[_membersByContainerKey].has(member.containerKey)) {
throw new Error(
`Another member has already been added with the same name (${member.displayName})` +
` and containerKey (${member.containerKey})`,
);
}
const existingParent: ApiItem | undefined = member.parent;
if (existingParent !== undefined) {
throw new Error(`This item has already been added to another container: "${existingParent.displayName}"`);
}
this[_members].push(member);
this[_membersByName] = undefined; // invalidate the lookup
this[_membersByKind] = undefined; // invalidate the lookup
this[_membersSorted] = false;
this[_membersByContainerKey].set(member.containerKey, member);
member[apiItem_onParentChanged](this);
}
public tryGetMemberByKey(containerKey: string): ApiItem | undefined {
return this[_membersByContainerKey].get(containerKey);
}
public findMembersByName(name: string): readonly ApiItem[] {
this._ensureMemberMaps();
return this[_membersByName]!.get(name) ?? [];
}
public findMembersWithInheritance(): IFindApiItemsResult {
const messages: IFindApiItemsMessage[] = [];
let maybeIncompleteResult = false;
// For API items that don't support inheritance, this method just returns the item's
// immediate members.
switch (this.kind) {
case ApiItemKind.Class:
case ApiItemKind.Interface:
break;
default: {
return {
items: this.members.concat(),
messages,
maybeIncompleteResult,
};
}
}
const membersByName: Map<string, ApiItem[]> = new Map();
const membersByKind: Map<ApiItemKind, ApiItem[]> = new Map();
const toVisit: ApiItem[] = [];
let next: ApiItem | undefined = this;
while (next) {
const membersToAdd: ApiItem[] = [];
// For each member, check to see if we've already seen a member with the same name
// previously in the inheritance tree. If so, we know we won't inherit it, and thus
// do not add it to our `membersToAdd` array.
for (const member of next.members) {
// We add the to-be-added members to an intermediate array instead of immediately
// to the maps themselves to support method overloads with the same name.
if (ApiNameMixin.isBaseClassOf(member)) {
if (!membersByName.has(member.name)) {
membersToAdd.push(member);
}
} else if (!membersByKind.has(member.kind)) {
membersToAdd.push(member);
}
}
for (const member of membersToAdd) {
if (ApiNameMixin.isBaseClassOf(member)) {
const members: ApiItem[] = membersByName.get(member.name) ?? [];
members.push(member);
membersByName.set(member.name, members);
} else {
const members: ApiItem[] = membersByKind.get(member.kind) ?? [];
members.push(member);
membersByKind.set(member.kind, members);
}
}
// Interfaces can extend multiple interfaces, so iterate through all of them.
const extendedItems: ApiItem[] = [];
let extendsTypes: readonly HeritageType[] | undefined;
switch (next.kind) {
case ApiItemKind.Class: {
const apiClass: ApiClass = next as ApiClass;
extendsTypes = apiClass.extendsType ? [apiClass.extendsType] : [];
break;
}
case ApiItemKind.Interface: {
const apiInterface: ApiInterface = next as ApiInterface;
extendsTypes = apiInterface.extendsTypes;
break;
}
default:
break;
}
if (extendsTypes === undefined) {
messages.push({
messageId: FindApiItemsMessageId.UnsupportedKind,
text: `Unable to analyze references of API item ${next.displayName} because it is of unsupported kind ${next.kind}`,
});
maybeIncompleteResult = true;
next = toVisit.shift();
continue;
}
for (const extendsType of extendsTypes) {
// We want to find the reference token associated with the actual inherited declaration.
// In every case we support, this is the first reference token. For example:
//
// ```
// export class A extends B {}
// ^
// export class A extends B<C> {}
// ^
// export class A extends B.C {}
// ^^^
// ```
const firstReferenceToken: ExcerptToken | undefined = extendsType.excerpt.spannedTokens.find(
(token: ExcerptToken) => {
return token.kind === ExcerptTokenKind.Reference && token.canonicalReference;
},
);
if (!firstReferenceToken) {
messages.push({
messageId: FindApiItemsMessageId.ExtendsClauseMissingReference,
text: `Unable to analyze extends clause ${extendsType.excerpt.text} of API item ${next.displayName} because no canonical reference was found`,
});
maybeIncompleteResult = true;
continue;
}
const apiModel: ApiModel | undefined = this.getAssociatedModel();
if (!apiModel) {
messages.push({
messageId: FindApiItemsMessageId.NoAssociatedApiModel,
text: `Unable to analyze references of API item ${next.displayName} because it is not associated with an ApiModel`,
});
maybeIncompleteResult = true;
continue;
}
const canonicalReference: DeclarationReference = firstReferenceToken.canonicalReference!;
const apiItemResult: IResolveDeclarationReferenceResult = apiModel.resolveDeclarationReference(
canonicalReference,
undefined,
);
const apiItem: ApiItem | undefined = apiItemResult.resolvedApiItem;
if (!apiItem) {
messages.push({
messageId: FindApiItemsMessageId.DeclarationResolutionFailed,
text: `Unable to resolve declaration reference within API item ${next.displayName}: ${apiItemResult.errorMessage}`,
});
maybeIncompleteResult = true;
continue;
}
extendedItems.push(apiItem);
}
// For classes, this array will only have one item. For interfaces, there may be multiple items. Sort the array
// into alphabetical order before adding to our list of API items to visit. This ensures that in the case
// of multiple interface inheritance, a member inherited from multiple interfaces is attributed to the interface
// earlier in alphabetical order (as opposed to source order).
//
// For example, in the code block below, `Bar.x` is reported as the inherited item, not `Foo.x`.
//
// ```
// interface Foo {
// public x: string;
// }
//
// interface Bar {
// public x: string;
// }
//
// interface FooBar extends Foo, Bar {}
// ```
extendedItems.sort((x: ApiItem, y: ApiItem) => x.getSortKey().localeCompare(y.getSortKey()));
toVisit.push(...extendedItems);
next = toVisit.shift();
}
const items: ApiItem[] = [];
for (const members of membersByName.values()) {
items.push(...members);
}
for (const members of membersByKind.values()) {
items.push(...members);
}
items.sort((x: ApiItem, y: ApiItem) => x.getSortKey().localeCompare(y.getSortKey()));
return {
items,
messages,
maybeIncompleteResult,
};
}
/**
* @internal
*/
public _getMergedSiblingsForMember(memberApiItem: ApiItem): readonly ApiItem[] {
this._ensureMemberMaps();
let result: ApiItem[] | undefined;
if (ApiNameMixin.isBaseClassOf(memberApiItem)) {
result = this[_membersByName]!.get(memberApiItem.name);
} else {
result = this[_membersByKind]!.get(memberApiItem.kind);
}
if (!result) {
throw new InternalError('Item was not found in the _membersByName/_membersByKind lookup');
}
return result;
}
/**
* @internal
*/
public _ensureMemberMaps(): void {
// Build the _membersByName and _membersByKind tables if they don't already exist
if (this[_membersByName] === undefined) {
const membersByName: Map<string, ApiItem[]> = new Map<string, ApiItem[]>();
const membersByKind: Map<string, ApiItem[]> = new Map<string, ApiItem[]>();
for (const member of this[_members]) {
let map: Map<ApiItemKind, ApiItem[]> | Map<string, ApiItem[]>;
let key: ApiItemKind | string;
if (ApiNameMixin.isBaseClassOf(member)) {
map = membersByName;
key = member.name;
} else {
map = membersByKind;
key = member.kind;
}
let list: ApiItem[] | undefined = map.get(key);
if (list === undefined) {
list = [];
map.set(key, list);
}
list.push(member);
}
this[_membersByName] = membersByName;
this[_membersByKind] = membersByKind;
}
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiItemContainerJson>): void {
super.serializeInto(jsonObject);
const memberObjects: IApiItemJson[] = [];
for (const member of this.members) {
const memberJsonObject: Partial<IApiItemJson> = {};
member.serializeInto(memberJsonObject);
memberObjects.push(memberJsonObject as IApiItemJson);
}
jsonObject.preserveMemberOrder = this.preserveMemberOrder;
jsonObject.members = memberObjects;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiItemContainerMixin:interface)}.
*
* @public
*/
export namespace ApiItemContainerMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiItemContainerMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiItemContainerMixin {
return apiItem.hasOwnProperty(_members);
}
}

View File

@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (IApiNameMixinOptions:interface)}.
*
* @public
*/
export interface IApiNameMixinOptions extends IApiItemOptions {
name: string;
}
export interface IApiNameMixinJson extends IApiItemJson {
name: string;
}
const _name: unique symbol = Symbol('ApiNameMixin._name');
/**
* The mixin base class for API items that have a name. For example, a class has a name, but a class constructor
* does not.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiNameMixin extends ApiItem {
/**
* The exported name of this API item.
*
* @remarks
* Note that due tue type aliasing, the exported name may be different from the locally declared name.
*/
readonly name: string;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiNameMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiNameMixin:interface)} functionality.
* @public
*/
export function ApiNameMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiNameMixin) {
class MixedClass extends baseClass implements ApiNameMixin {
public readonly [_name]: string;
public constructor(...args: any[]) {
super(...args);
const options: IApiNameMixinOptions = args[0];
this[_name] = options.name;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiNameMixinOptions>,
context: DeserializerContext,
jsonObject: IApiNameMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.name = jsonObject.name;
}
public get name(): string {
return this[_name];
}
/**
* @override
*/
public override get displayName(): string {
return this[_name];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiNameMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.name = this.name;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiNameMixin:interface)}.
*
* @public
*/
export namespace ApiNameMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiNameMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiNameMixin {
return apiItem.hasOwnProperty(_name);
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (IApiOptionalMixinOptions:interface)}.
*
* @public
*/
export interface IApiOptionalMixinOptions extends IApiItemOptions {
isOptional: boolean;
}
export interface IApiOptionalMixinJson extends IApiItemJson {
isOptional: boolean;
}
const _isOptional: unique symbol = Symbol('ApiOptionalMixin._isOptional');
/**
* The mixin base class for API items that can be marked as optional by appending a `?` to them.
* For example, a property of an interface can be optional.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiOptionalMixin extends ApiItem {
/**
* True if this is an optional property.
*
* @remarks
* For example:
* ```ts
* interface X {
* y: string; // not optional
* z?: string; // optional
* }
* ```
*/
readonly isOptional: boolean;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiOptionalMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiOptionalMixin:interface)} functionality.
* @public
*/
export function ApiOptionalMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiOptionalMixin) {
class MixedClass extends baseClass implements ApiOptionalMixin {
public [_isOptional]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiOptionalMixinOptions = args[0];
this[_isOptional] = Boolean(options.isOptional);
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiOptionalMixinOptions>,
context: DeserializerContext,
jsonObject: IApiOptionalMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.isOptional = Boolean(jsonObject.isOptional);
}
public get isOptional(): boolean {
return this[_isOptional];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiOptionalMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.isOptional = this.isOptional;
}
}
return MixedClass;
}
/**
* Optional members for {@link (ApiOptionalMixin:interface)}.
*
* @public
*/
export namespace ApiOptionalMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiOptionalMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiOptionalMixin {
return apiItem.hasOwnProperty(_isOptional);
}
}

View File

@@ -0,0 +1,202 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import { Parameter } from '../model/Parameter.js';
import type { IExcerptTokenRange } from './Excerpt.js';
/**
* Represents parameter information that is part of {@link IApiParameterListMixinOptions}
*
* @public
*/
export interface IApiParameterOptions {
isOptional: boolean;
isRest: boolean;
parameterName: string;
parameterTypeTokenRange: IExcerptTokenRange;
}
/**
* Constructor options for {@link (ApiParameterListMixin:interface)}.
*
* @public
*/
export interface IApiParameterListMixinOptions extends IApiItemOptions {
overloadIndex: number;
parameters: IApiParameterOptions[];
}
export interface IApiParameterListJson extends IApiItemJson {
overloadIndex: number;
parameters: IApiParameterOptions[];
}
const _overloadIndex: unique symbol = Symbol('ApiParameterListMixin._overloadIndex');
const _parameters: unique symbol = Symbol('ApiParameterListMixin._parameters');
/**
* The mixin base class for API items that can have function parameters (but not necessarily a return value).
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiParameterListMixin extends ApiItem {
/**
* When a function has multiple overloaded declarations, this one-based integer index can be used to uniquely
* identify them.
*
* @remarks
*
* Consider this overloaded declaration:
*
* ```ts
* export namespace Versioning {
* // TSDoc: Versioning.(addVersions:1)
* export function addVersions(x: number, y: number): number;
*
* // TSDoc: Versioning.(addVersions:2)
* export function addVersions(x: string, y: string): string;
*
* // (implementation)
* export function addVersions(x: number|string, y: number|string): number|string {
* // . . .
* }
* }
* ```
*
* In the above example, there are two overloaded declarations. The overload using numbers will have
* `overloadIndex = 1`. The overload using strings will have `overloadIndex = 2`. The third declaration that
* accepts all possible inputs is considered part of the implementation, and is not processed by API Extractor.
*/
readonly overloadIndex: number;
/**
* The function parameters.
*/
readonly parameters: readonly Parameter[];
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiParameterListMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiParameterListMixin:interface)} functionality.
* @public
*/
export function ApiParameterListMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiParameterListMixin) {
class MixedClass extends baseClass implements ApiParameterListMixin {
public readonly [_overloadIndex]: number;
public readonly [_parameters]: Parameter[];
public constructor(...args: any[]) {
super(...args);
const options: IApiParameterListMixinOptions = args[0];
this[_overloadIndex] = options.overloadIndex;
this[_parameters] = [];
if (this instanceof ApiDeclaredItem) {
if (options.parameters) {
for (const parameterOptions of options.parameters) {
const parameter: Parameter = new Parameter({
name: parameterOptions.parameterName,
parameterTypeExcerpt: this.buildExcerpt(parameterOptions.parameterTypeTokenRange),
// Prior to ApiJsonSchemaVersion.V_1005 this input will be undefined
isOptional: Boolean(parameterOptions.isOptional),
isRest: Boolean(parameterOptions.isRest),
parent: this,
});
this[_parameters].push(parameter);
}
}
} else {
throw new InternalError('ApiReturnTypeMixin expects a base class that inherits from ApiDeclaredItem');
}
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiParameterListMixinOptions>,
context: DeserializerContext,
jsonObject: IApiParameterListJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.overloadIndex = jsonObject.overloadIndex;
options.parameters = jsonObject.parameters || [];
}
public get overloadIndex(): number {
return this[_overloadIndex];
}
public get parameters(): readonly Parameter[] {
return this[_parameters];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiParameterListJson>): void {
super.serializeInto(jsonObject);
jsonObject.overloadIndex = this.overloadIndex;
const parameterObjects: IApiParameterOptions[] = [];
for (const parameter of this.parameters) {
parameterObjects.push({
parameterName: parameter.name,
parameterTypeTokenRange: parameter.parameterTypeExcerpt.tokenRange,
isOptional: parameter.isOptional,
isRest: parameter.isRest,
});
}
jsonObject.parameters = parameterObjects;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiParameterListMixin:interface)}.
*
* @public
*/
export namespace ApiParameterListMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiParameterListMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiParameterListMixin {
return apiItem.hasOwnProperty(_parameters);
}
}

View File

@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (IApiProtectedMixinOptions:interface)}.
*
* @public
*/
export interface IApiProtectedMixinOptions extends IApiItemOptions {
isProtected: boolean;
}
export interface IApiProtectedMixinJson extends IApiItemJson {
isProtected: boolean;
}
const _isProtected: unique symbol = Symbol('ApiProtectedMixin._isProtected');
/**
* The mixin base class for API items that can have the TypeScript `protected` keyword applied to them.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiProtectedMixin extends ApiItem {
/**
* Whether the declaration has the TypeScript `protected` keyword.
*/
readonly isProtected: boolean;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiProtectedMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiProtectedMixin:interface)} functionality.
* @public
*/
export function ApiProtectedMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiProtectedMixin) {
class MixedClass extends baseClass implements ApiProtectedMixin {
public [_isProtected]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiProtectedMixinOptions = args[0];
this[_isProtected] = options.isProtected;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiProtectedMixinOptions>,
context: DeserializerContext,
jsonObject: IApiProtectedMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.isProtected = jsonObject.isProtected;
}
public get isProtected(): boolean {
return this[_isProtected];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiProtectedMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.isProtected = this.isProtected;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiProtectedMixin:interface)}.
*
* @public
*/
export namespace ApiProtectedMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiProtectedMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiProtectedMixin {
return apiItem.hasOwnProperty(_isProtected);
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (ApiReadonlyMixin:interface)}.
*
* @public
*/
export interface IApiReadonlyMixinOptions extends IApiItemOptions {
isReadonly: boolean;
}
export interface IApiReadonlyMixinJson extends IApiItemJson {
isReadonly: boolean;
}
const _isReadonly: unique symbol = Symbol('ApiReadonlyMixin._isReadonly');
/**
* The mixin base class for API items that cannot be modified after instantiation.
* Examples such as the readonly modifier and only having a getter but no setter.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiReadonlyMixin extends ApiItem {
/**
* Indicates that the API item's value cannot be assigned by an external consumer.
*
* @remarks
* Examples of API items that would be considered "read only" by API Extractor:
*
* - A class or interface's property that has the `readonly` modifier.
*
* - A variable that has the `const` modifier.
*
* - A property or variable whose TSDoc comment includes the `@readonly` tag.
*
* - A property declaration with a getter but no setter.
*
* Note that if the `readonly` keyword appears in a type annotation, this does not
* guarantee that that the API item will be considered readonly. For example:
*
* ```ts
* declare class C {
* // isReadonly=false in this case, because C.x is assignable
* public x: readonly string[];
* }
* ```
*/
readonly isReadonly: boolean;
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiReadonlyMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiReadonlyMixin:interface)}
* functionality.
* @public
*/
export function ApiReadonlyMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiReadonlyMixin) {
class MixedClass extends baseClass implements ApiReadonlyMixin {
public [_isReadonly]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiReadonlyMixinOptions = args[0];
this[_isReadonly] = options.isReadonly;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiReadonlyMixinOptions>,
context: DeserializerContext,
jsonObject: IApiReadonlyMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.isReadonly = jsonObject.isReadonly || false;
}
public get isReadonly(): boolean {
return this[_isReadonly];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiReadonlyMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.isReadonly = this.isReadonly;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiReadonlyMixin:interface)}.
*
* @public
*/
export namespace ApiReadonlyMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReadonlyMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReadonlyMixin {
return apiItem.hasOwnProperty(_isReadonly);
}
}

View File

@@ -0,0 +1,132 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { Enum } from '@rushstack/node-core-library';
import { ReleaseTag } from '../aedoc/ReleaseTag.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (ApiReleaseTagMixin:interface)}.
*
* @public
*/
export interface IApiReleaseTagMixinOptions extends IApiItemOptions {
releaseTag: ReleaseTag;
}
export interface IApiReleaseTagMixinJson extends IApiItemJson {
releaseTag: string;
}
const _releaseTag: unique symbol = Symbol('ApiReleaseTagMixin._releaseTag');
/**
* The mixin base class for API items that can be attributed with a TSDoc tag such as `@internal`,
* `@alpha`, `@beta`, or `@public`. These "release tags" indicate the support level for an API.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiReleaseTagMixin extends ApiItem {
/**
* The effective release tag for this declaration. If it is not explicitly specified, the value may be
* inherited from a containing declaration.
*
* @remarks
* For example, an `ApiEnumMember` may inherit its release tag from the containing `ApiEnum`.
*/
readonly releaseTag: ReleaseTag;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiReleaseTagMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiReleaseTagMixin:interface)} functionality.
* @public
*/
export function ApiReleaseTagMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiReleaseTagMixin) {
class MixedClass extends baseClass implements ApiReleaseTagMixin {
public [_releaseTag]: ReleaseTag;
public constructor(...args: any[]) {
super(...args);
const options: IApiReleaseTagMixinOptions = args[0];
this[_releaseTag] = options.releaseTag;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiReleaseTagMixinOptions>,
context: DeserializerContext,
jsonObject: IApiReleaseTagMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
const deserializedReleaseTag: ReleaseTag | undefined = Enum.tryGetValueByKey<ReleaseTag>(
ReleaseTag as any,
jsonObject.releaseTag,
);
if (deserializedReleaseTag === undefined) {
throw new Error(`Failed to deserialize release tag ${JSON.stringify(jsonObject.releaseTag)}`);
}
options.releaseTag = deserializedReleaseTag;
}
public get releaseTag(): ReleaseTag {
return this[_releaseTag];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiReleaseTagMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.releaseTag = ReleaseTag[this.releaseTag];
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiReleaseTagMixin:interface)}.
*
* @public
*/
export namespace ApiReleaseTagMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReleaseTagMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReleaseTagMixin {
return apiItem.hasOwnProperty(_releaseTag);
}
}

View File

@@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import type { IExcerptTokenRange, Excerpt } from './Excerpt.js';
/**
* Constructor options for {@link (ApiReturnTypeMixin:interface)}.
*
* @public
*/
export interface IApiReturnTypeMixinOptions extends IApiItemOptions {
returnTypeTokenRange: IExcerptTokenRange;
}
export interface IApiReturnTypeMixinJson extends IApiItemJson {
returnTypeTokenRange: IExcerptTokenRange;
}
const _returnTypeExcerpt: unique symbol = Symbol('ApiReturnTypeMixin._returnTypeExcerpt');
/**
* The mixin base class for API items that are functions that return a value.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiReturnTypeMixin extends ApiItem {
/**
* An {@link Excerpt} that describes the type of the function's return value.
*/
readonly returnTypeExcerpt: Excerpt;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiReturnTypeMixinJson>): void;
}
/**
* Mixin function for {@link (ApiReturnTypeMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiReturnTypeMixin:interface)} functionality.
* @public
*/
export function ApiReturnTypeMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiReturnTypeMixin) {
class MixedClass extends baseClass implements ApiReturnTypeMixin {
public [_returnTypeExcerpt]: Excerpt;
public constructor(...args: any[]) {
super(...args);
const options: IApiReturnTypeMixinOptions = args[0];
if (this instanceof ApiDeclaredItem) {
this[_returnTypeExcerpt] = this.buildExcerpt(options.returnTypeTokenRange);
} else {
throw new InternalError('ApiReturnTypeMixin expects a base class that inherits from ApiDeclaredItem');
}
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiReturnTypeMixinOptions>,
context: DeserializerContext,
jsonObject: IApiReturnTypeMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.returnTypeTokenRange = jsonObject.returnTypeTokenRange;
}
public get returnTypeExcerpt(): Excerpt {
return this[_returnTypeExcerpt];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiReturnTypeMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.returnTypeTokenRange = this.returnTypeExcerpt.tokenRange;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiReturnTypeMixin:interface)}.
*
* @public
*/
export namespace ApiReturnTypeMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiReturnTypeMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiReturnTypeMixin {
return apiItem.hasOwnProperty(_returnTypeExcerpt);
}
}

View File

@@ -0,0 +1,117 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
/**
* Constructor options for {@link (IApiStaticMixinOptions:interface)}.
*
* @public
*/
export interface IApiStaticMixinOptions extends IApiItemOptions {
isStatic: boolean;
}
export interface IApiStaticMixinJson extends IApiItemJson {
isStatic: boolean;
}
const _isStatic: unique symbol = Symbol('ApiStaticMixin._isStatic');
/**
* The mixin base class for API items that can have the TypeScript `static` keyword applied to them.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiStaticMixin extends ApiItem {
/**
* Whether the declaration has the TypeScript `static` keyword.
*/
readonly isStatic: boolean;
/**
* @override
*/
serializeInto(jsonObject: Partial<IApiItemJson>): void;
}
/**
* Mixin function for {@link (ApiStaticMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiStaticMixin:interface)} functionality.
* @public
*/
export function ApiStaticMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiStaticMixin) {
class MixedClass extends baseClass implements ApiStaticMixin {
public [_isStatic]: boolean;
public constructor(...args: any[]) {
super(...args);
const options: IApiStaticMixinOptions = args[0];
this[_isStatic] = options.isStatic;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiStaticMixinOptions>,
context: DeserializerContext,
jsonObject: IApiStaticMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.isStatic = jsonObject.isStatic;
}
public get isStatic(): boolean {
return this[_isStatic];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiStaticMixinJson>): void {
super.serializeInto(jsonObject);
jsonObject.isStatic = this.isStatic;
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiStaticMixin:interface)}.
*
* @public
*/
export namespace ApiStaticMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiStaticMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiStaticMixin {
return apiItem.hasOwnProperty(_isStatic);
}
}

View File

@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import { ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import type { ApiItem, IApiItemJson, IApiItemConstructor, IApiItemOptions } from '../items/ApiItem.js';
import type { DeserializerContext } from '../model/DeserializerContext.js';
import { TypeParameter } from '../model/TypeParameter.js';
import type { Excerpt, IExcerptTokenRange } from './Excerpt.js';
/**
* Represents parameter information that is part of {@link IApiTypeParameterListMixinOptions}
*
* @public
*/
export interface IApiTypeParameterOptions {
constraintTokenRange: IExcerptTokenRange;
defaultTypeTokenRange: IExcerptTokenRange;
typeParameterName: string;
}
/**
* Constructor options for {@link (ApiTypeParameterListMixin:interface)}.
*
* @public
*/
export interface IApiTypeParameterListMixinOptions extends IApiItemOptions {
typeParameters: IApiTypeParameterOptions[];
}
export interface IApiTypeParameterListMixinJson extends IApiItemJson {
typeParameters: IApiTypeParameterOptions[];
}
const _typeParameters: unique symbol = Symbol('ApiTypeParameterListMixin._typeParameters');
/**
* The mixin base class for API items that can have type parameters.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations. The non-abstract classes (e.g. `ApiClass`, `ApiEnum`, `ApiInterface`, etc.) use
* TypeScript "mixin" functions (e.g. `ApiDeclaredItem`, `ApiItemContainerMixin`, etc.) to add various
* features that cannot be represented as a normal inheritance chain (since TypeScript does not allow a child class
* to extend more than one base class). The "mixin" is a TypeScript merged declaration with three components:
* the function that generates a subclass, an interface that describes the members of the subclass, and
* a namespace containing static members of the class.
* @public
*/
export interface ApiTypeParameterListMixin extends ApiItem {
serializeInto(jsonObject: Partial<IApiItemJson>): void;
/**
* The type parameters.
*/
readonly typeParameters: readonly TypeParameter[];
}
/**
* Mixin function for {@link (ApiTypeParameterListMixin:interface)}.
*
* @param baseClass - The base class to be extended
* @returns A child class that extends baseClass, adding the {@link (ApiTypeParameterListMixin:interface)}
* functionality.
* @public
*/
export function ApiTypeParameterListMixin<TBaseClass extends IApiItemConstructor>(
baseClass: TBaseClass,
): TBaseClass & (new (...args: any[]) => ApiTypeParameterListMixin) {
class MixedClass extends baseClass implements ApiTypeParameterListMixin {
public readonly [_typeParameters]: TypeParameter[];
public constructor(...args: any[]) {
super(...args);
const options: IApiTypeParameterListMixinOptions = args[0];
this[_typeParameters] = [];
if (this instanceof ApiDeclaredItem) {
if (options.typeParameters) {
for (const typeParameterOptions of options.typeParameters) {
const defaultTypeExcerpt: Excerpt = this.buildExcerpt(typeParameterOptions.defaultTypeTokenRange);
const typeParameter: TypeParameter = new TypeParameter({
name: typeParameterOptions.typeParameterName,
constraintExcerpt: this.buildExcerpt(typeParameterOptions.constraintTokenRange),
defaultTypeExcerpt,
isOptional: !defaultTypeExcerpt.isEmpty,
parent: this,
});
this[_typeParameters].push(typeParameter);
}
}
} else {
throw new InternalError('ApiTypeParameterListMixin expects a base class that inherits from ApiDeclaredItem');
}
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiTypeParameterListMixinOptions>,
context: DeserializerContext,
jsonObject: IApiTypeParameterListMixinJson,
): void {
baseClass.onDeserializeInto(options, context, jsonObject);
options.typeParameters = jsonObject.typeParameters || [];
}
public get typeParameters(): readonly TypeParameter[] {
return this[_typeParameters];
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiTypeParameterListMixinJson>): void {
super.serializeInto(jsonObject);
const typeParameterObjects: IApiTypeParameterOptions[] = [];
for (const typeParameter of this.typeParameters) {
typeParameterObjects.push({
typeParameterName: typeParameter.name,
constraintTokenRange: typeParameter.constraintExcerpt.tokenRange,
defaultTypeTokenRange: typeParameter.defaultTypeExcerpt.tokenRange,
});
}
if (typeParameterObjects.length > 0) {
jsonObject.typeParameters = typeParameterObjects;
}
}
}
return MixedClass;
}
/**
* Static members for {@link (ApiTypeParameterListMixin:interface)}.
*
* @public
*/
export namespace ApiTypeParameterListMixin {
/**
* A type guard that tests whether the specified `ApiItem` subclass extends the `ApiParameterListMixin` mixin.
*
* @remarks
*
* The JavaScript `instanceof` operator cannot be used to test for mixin inheritance, because each invocation of
* the mixin function produces a different subclass. (This could be mitigated by `Symbol.hasInstance`, however
* the TypeScript type system cannot invoke a runtime test.)
*/
export function isBaseClassOf(apiItem: ApiItem): apiItem is ApiTypeParameterListMixin {
return apiItem.hasOwnProperty(_typeParameters);
}
}

View File

@@ -0,0 +1,173 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference';
import { Text } from '@rushstack/node-core-library';
/**
* @public
*/
export enum ExcerptTokenKind {
/**
* Generic text without any special properties
*/
Content = 'Content',
/**
* A reference to an API declaration
*/
Reference = 'Reference',
}
/**
* Used by {@link Excerpt} to indicate a range of indexes within an array of `ExcerptToken` objects.
*
* @public
*/
export interface IExcerptTokenRange {
/**
* The index of the last member of the span, plus one.
*
* @remarks
*
* If `startIndex` and `endIndex` are the same number, then the span is empty.
*/
endIndex: number;
/**
* The starting index of the span.
*/
startIndex: number;
}
/**
* @public
*/
export interface IExcerptToken {
canonicalReference?: string | undefined;
readonly kind: ExcerptTokenKind;
text: string;
}
/**
* Represents a fragment of text belonging to an {@link Excerpt} object.
*
* @public
*/
export class ExcerptToken {
private readonly _kind: ExcerptTokenKind;
private readonly _text: string;
private readonly _canonicalReference: DeclarationReference | undefined;
public constructor(kind: ExcerptTokenKind, text: string, canonicalReference?: DeclarationReference) {
this._kind = kind;
// Standardize the newlines across operating systems. Even though this may deviate from the actual
// input source file that was parsed, it's useful because the newline gets serialized inside
// a string literal in .api.json, which cannot be automatically normalized by Git.
this._text = Text.convertToLf(text);
this._canonicalReference = canonicalReference;
}
/**
* Indicates the kind of token.
*/
public get kind(): ExcerptTokenKind {
return this._kind;
}
/**
* The text fragment.
*/
public get text(): string {
return this._text;
}
/**
* The hyperlink target for a token whose type is `ExcerptTokenKind.Reference`. For other token types,
* this property will be `undefined`.
*/
public get canonicalReference(): DeclarationReference | undefined {
return this._canonicalReference;
}
}
/**
* The `Excerpt` class is used by {@link ApiDeclaredItem} to represent a TypeScript code fragment that may be
* annotated with hyperlinks to declared types (and in the future, source code locations).
*
* @remarks
* API Extractor's .api.json file format stores excerpts compactly as a start/end indexes into an array of tokens.
* Every `ApiDeclaredItem` has a "main excerpt" corresponding to the full list of tokens. The declaration may
* also have have "captured" excerpts that correspond to subranges of tokens.
*
* For example, if the main excerpt is:
*
* ```
* function parse(s: string): Vector | undefined;
* ```
*
* ...then this entire signature is the "main excerpt", whereas the function's return type `Vector | undefined` is a
* captured excerpt. The `Vector` token might be a hyperlink to that API item.
*
* An excerpt may be empty (i.e. a token range containing zero tokens). For example, if a function's return value
* is not explicitly declared, then the returnTypeExcerpt will be empty. By contrast, a class constructor cannot
* have a return value, so ApiConstructor has no returnTypeExcerpt property at all.
* @public
*/
export class Excerpt {
/**
* The complete list of tokens for the source code fragment that this excerpt is based upon.
* If this object is the main excerpt, then it will span all of the tokens; otherwise, it will correspond to
* a range within the array.
*/
public readonly tokens: readonly ExcerptToken[];
/**
* Specifies the excerpt's range within the `tokens` array.
*/
public readonly tokenRange: Readonly<IExcerptTokenRange>;
/**
* The tokens spanned by this excerpt. It is the range of the `tokens` array as specified by the `tokenRange`
* property.
*/
public readonly spannedTokens: readonly ExcerptToken[];
private _text: string | undefined;
public constructor(tokens: readonly ExcerptToken[], tokenRange: IExcerptTokenRange) {
this.tokens = tokens;
this.tokenRange = tokenRange;
if (
this.tokenRange.startIndex < 0 ||
this.tokenRange.endIndex > this.tokens.length ||
this.tokenRange.startIndex > this.tokenRange.endIndex
) {
throw new Error('Invalid token range');
}
this.spannedTokens = this.tokens.slice(this.tokenRange.startIndex, this.tokenRange.endIndex);
}
/**
* The excerpted text, formed by concatenating the text of the `spannedTokens` strings.
*/
public get text(): string {
if (this._text === undefined) {
this._text = this.spannedTokens.map((x) => x.text).join('');
}
return this._text;
}
/**
* Returns true if the excerpt is an empty range.
*/
public get isEmpty(): boolean {
return this.tokenRange.startIndex === this.tokenRange.endIndex;
}
}

View File

@@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { ApiItem } from '../items/ApiItem.js';
/**
* Generic result object for finding API items used by different kinds of find operations.
*
* @public
*/
export interface IFindApiItemsResult {
/**
* The API items that were found. Not guaranteed to be complete, see `maybeIncompleteResult`.
*/
items: ApiItem[];
/**
* Indicates whether the result is potentially incomplete due to errors during the find operation.
* If true, the `messages` explain the errors in more detail.
*/
maybeIncompleteResult: boolean;
/**
* Diagnostic messages regarding the find operation.
*/
messages: IFindApiItemsMessage[];
}
/**
* This object is used for messages returned as part of `IFindApiItemsResult`.
*
* @public
*/
export interface IFindApiItemsMessage {
/**
* Unique identifier for the message.
*
* @beta
*/
messageId: FindApiItemsMessageId;
/**
* Text description of the message.
*/
text: string;
}
/**
* Unique identifiers for messages returned as part of `IFindApiItemsResult`.
*
* @public
*/
export enum FindApiItemsMessageId {
/**
* "Unable to resolve declaration reference within API item ___: ___"
*/
DeclarationResolutionFailed = 'declaration-resolution-failed',
/**
* "Unable to analyze extends clause ___ of API item ___ because no canonical reference was found."
*/
ExtendsClauseMissingReference = 'extends-clause-missing-reference',
/**
* "Unable to analyze references of API item ___ because it is not associated with an ApiModel"
*/
NoAssociatedApiModel = 'no-associated-api-model',
/**
* "Unable to analyze references of API item ___ because it is of unsupported kind ___"
*/
UnsupportedKind = 'unsupported-kind',
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/**
* This abstraction is used by the mixin pattern.
* It describes a class constructor.
*
* @public
*/
export type Constructor<T = {}> = new (...args: any[]) => T;
/**
* This abstraction is used by the mixin pattern.
* It describes the "static side" of a class.
*
* @public
*/
export type PropertiesOf<T> = { [K in keyof T]: T[K] };

View File

@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js';
import {
type IApiTypeParameterListMixinOptions,
ApiTypeParameterListMixin,
} from '../mixins/ApiTypeParameterListMixin.js';
/**
* Constructor options for {@link ApiCallSignature}.
*
* @public
*/
export interface IApiCallSignatureOptions
extends IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript function call signature.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiCallSignature` represents a TypeScript declaration such as `(x: number, y: number): number`
* in this example:
*
* ```ts
* export interface IChooser {
* // A call signature:
* (x: number, y: number): number;
*
* // Another overload for this call signature:
* (x: string, y: string): string;
* }
*
* function chooseFirst<T>(x: T, y: T): T {
* return x;
* }
*
* let chooser: IChooser = chooseFirst;
* ```
* @public
*/
export class ApiCallSignature extends ApiTypeParameterListMixin(
ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiDeclaredItem))),
) {
public constructor(options: IApiCallSignatureOptions) {
super(options);
}
public static getContainerKey(overloadIndex: number): string {
return `|${ApiItemKind.CallSignature}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.CallSignature;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiCallSignature.getContainerKey(this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const parent: DeclarationReference = this.parent
? this.parent.canonicalReference
: // .withMeaning() requires some kind of component
DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)');
return parent.withMeaning(Meaning.CallSignature as any).withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import {
ApiAbstractMixin,
type IApiAbstractMixinJson,
type IApiAbstractMixinOptions,
} from '../mixins/ApiAbstractMixin.js';
import {
type IApiExportedMixinJson,
type IApiExportedMixinOptions,
ApiExportedMixin,
} from '../mixins/ApiExportedMixin.js';
import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import {
ApiTypeParameterListMixin,
type IApiTypeParameterListMixinOptions,
type IApiTypeParameterListMixinJson,
} from '../mixins/ApiTypeParameterListMixin.js';
import type { IExcerptTokenRange } from '../mixins/Excerpt.js';
import type { DeserializerContext } from './DeserializerContext.js';
import { HeritageType } from './HeritageType.js';
/**
* Constructor options for {@link ApiClass}.
*
* @public
*/
export interface IApiClassOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiAbstractMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiTypeParameterListMixinOptions,
IApiExportedMixinOptions {
extendsTokenRange: IExcerptTokenRange | undefined;
implementsTokenRanges: IExcerptTokenRange[];
}
export interface IApiClassJson
extends IApiDeclaredItemJson,
IApiAbstractMixinJson,
IApiTypeParameterListMixinJson,
IApiExportedMixinJson {
extendsTokenRange?: IExcerptTokenRange;
implementsTokenRanges: IExcerptTokenRange[];
}
/**
* Represents a TypeScript class declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiClass` represents a TypeScript declaration such as this:
*
* ```ts
* export class X { }
* ```
* @public
*/
export class ApiClass extends ApiItemContainerMixin(
ApiNameMixin(ApiAbstractMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))))),
) {
/**
* The base class that this class inherits from (using the `extends` keyword), or undefined if there is no base class.
*/
public readonly extendsType: HeritageType | undefined;
private readonly _implementsTypes: HeritageType[] = [];
public constructor(options: IApiClassOptions) {
super(options);
if (options.extendsTokenRange) {
this.extendsType = new HeritageType(this.buildExcerpt(options.extendsTokenRange));
} else {
this.extendsType = undefined;
}
for (const implementsTokenRange of options.implementsTokenRanges) {
this._implementsTypes.push(new HeritageType(this.buildExcerpt(implementsTokenRange)));
}
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.Class}`;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiClassOptions>,
context: DeserializerContext,
jsonObject: IApiClassJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.extendsTokenRange = jsonObject.extendsTokenRange;
options.implementsTokenRanges = jsonObject.implementsTokenRanges;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Class;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiClass.getContainerKey(this.name);
}
/**
* The list of interfaces that this class implements using the `implements` keyword.
*/
public get implementsTypes(): readonly HeritageType[] {
return this._implementsTypes;
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiClassJson>): void {
super.serializeInto(jsonObject);
// Note that JSON does not support the "undefined" value, so we simply omit the field entirely if it is undefined
if (this.extendsType) {
jsonObject.extendsTokenRange = this.extendsType.excerpt.tokenRange;
}
jsonObject.implementsTokenRanges = this.implementsTypes.map((x) => x.excerpt.tokenRange);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Class as any);
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js';
import {
ApiTypeParameterListMixin,
type IApiTypeParameterListMixinOptions,
} from '../mixins/ApiTypeParameterListMixin.js';
/**
* Constructor options for {@link ApiConstructor}.
*
* @public
*/
export interface IApiConstructSignatureOptions
extends IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript construct signature that belongs to an `ApiInterface`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiConstructSignature` represents a construct signature using the `new` keyword such as in this example:
*
* ```ts
* export interface IVector {
* x: number;
* y: number;
* }
*
* export interface IVectorConstructor {
* // A construct signature:
* new(x: number, y: number): IVector;
* }
*
* export function createVector(vectorConstructor: IVectorConstructor,
* x: number, y: number): IVector {
* return new vectorConstructor(x, y);
* }
*
* class Vector implements IVector {
* public x: number;
* public y: number;
* public constructor(x: number, y: number) {
* this.x = x;
* this.y = y;
* }
* }
*
* let vector: Vector = createVector(Vector, 1, 2);
* ```
*
* Compare with {@link ApiConstructor}, which describes the class constructor itself.
* @public
*/
export class ApiConstructSignature extends ApiTypeParameterListMixin(
ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiDeclaredItem))),
) {
public constructor(options: IApiConstructSignatureOptions) {
super(options);
}
public static getContainerKey(overloadIndex: number): string {
return `|${ApiItemKind.ConstructSignature}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.ConstructSignature;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiConstructSignature.getContainerKey(this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const parent: DeclarationReference = this.parent
? this.parent.canonicalReference
: // .withMeaning() requires some kind of component
DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)');
return parent.withMeaning(Meaning.ConstructSignature as any).withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
/**
* Constructor options for {@link ApiConstructor}.
*
* @public
*/
export interface IApiConstructorOptions
extends IApiParameterListMixinOptions,
IApiProtectedMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript class constructor declaration that belongs to an `ApiClass`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiConstructor` represents a declaration using the `constructor` keyword such as in this example:
*
* ```ts
* export class Vector {
* public x: number;
* public y: number;
*
* // A class constructor:
* public constructor(x: number, y: number) {
* this.x = x;
* this.y = y;
* }
* }
* ```
*
* Compare with {@link ApiConstructSignature}, which describes the construct signature for a class constructor.
* @public
*/
export class ApiConstructor extends ApiParameterListMixin(ApiProtectedMixin(ApiReleaseTagMixin(ApiDeclaredItem))) {
public constructor(options: IApiConstructorOptions) {
super(options);
}
public static getContainerKey(overloadIndex: number): string {
return `|${ApiItemKind.Constructor}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Constructor;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiConstructor.getContainerKey(this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const parent: DeclarationReference = this.parent
? this.parent.canonicalReference
: // .withMeaning() requires some kind of component
DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)');
return parent.withMeaning(Meaning.Constructor as any).withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiItem, ApiItemKind } from '../items/ApiItem.js';
import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiPackage } from './ApiPackage.js';
/**
* Constructor options for {@link ApiEntryPoint}.
*
* @public
*/
export interface IApiEntryPointOptions extends IApiItemContainerMixinOptions, IApiNameMixinOptions {}
/**
* Represents the entry point for an NPM package.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiEntryPoint` represents the entry point to an NPM package. API Extractor does not currently support
* analysis of multiple entry points, but the `ApiEntryPoint` object is included to support a future feature.
* In the current implementation, `ApiEntryPoint.importPath` is always the empty string.
*
* For example, suppose the package.json file looks like this:
*
* ```json
* {
* "name": "example-library",
* "version": "1.0.0",
* "main": "./lib/index.js",
* "typings": "./lib/index.d.ts"
* }
* ```
*
* In this example, the `ApiEntryPoint` would represent the TypeScript module for `./lib/index.js`.
* @public
*/
export class ApiEntryPoint extends ApiItemContainerMixin(ApiNameMixin(ApiItem)) {
public constructor(options: IApiEntryPointOptions) {
super(options);
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.EntryPoint;
}
/**
* @override
*/
public override get containerKey(): string {
// No prefix needed, because ApiEntryPoint is the only possible member of an ApiPackage
return this.name;
}
/**
* The module path for this entry point, relative to the parent `ApiPackage`. In the current implementation,
* this is always the empty string, indicating the default entry point.
*
* @remarks
*
* API Extractor does not currently support analysis of multiple entry points. If that feature is implemented
* in the future, then the `ApiEntryPoint.importPath` will be used to distinguish different entry points,
* for example: `controls/Button` in `import { Button } from "example-package/controls/Button";`.
*
* The `ApiEntryPoint.name` property stores the same value as `ApiEntryPoint.importPath`.
*/
public get importPath(): string {
return this.name;
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
if (this.parent instanceof ApiPackage) {
return DeclarationReference.package(this.parent.name, this.importPath);
}
return DeclarationReference.empty();
}
}

View File

@@ -0,0 +1,97 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js';
import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import type { ApiEnumMember } from './ApiEnumMember.js';
/**
* Constructor options for {@link ApiEnum}.
*
* @public
*/
export interface IApiEnumOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiExportedMixinOptions {}
/**
* Represents a TypeScript enum declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiEnum` represents an enum declaration such as `FontSizes` in the example below:
*
* ```ts
* export enum FontSizes {
* Small = 100,
* Medium = 200,
* Large = 300
* }
* ```
* @public
*/
export class ApiEnum extends ApiItemContainerMixin(
ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))),
) {
public constructor(options: IApiEnumOptions) {
super(options);
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.Enum}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Enum;
}
/**
* @override
*/
public override get members(): readonly ApiEnumMember[] {
return super.members as readonly ApiEnumMember[];
}
/**
* @override
*/
public override get containerKey(): string {
return ApiEnum.getContainerKey(this.name);
}
/**
* @override
*/
public override addMember(member: ApiEnumMember): void {
if (member.kind !== ApiItemKind.EnumMember) {
throw new Error('Only ApiEnumMember objects can be added to an ApiEnum');
}
super.addMember(member);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Enum as any);
}
}

View File

@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
/**
* Constructor options for {@link ApiEnumMember}.
*
* @public
*/
export interface IApiEnumMemberOptions
extends IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiInitializerMixinOptions {}
/**
* Options for customizing the sort order of {@link ApiEnum} members.
*
* @privateRemarks
* This enum is currently only used by the `@microsoft/api-extractor` package; it is declared here
* because we anticipate that if more options are added in the future, their sorting will be implemented
* by the `@microsoft/api-extractor-model` package.
*
* See https://github.com/microsoft/rushstack/issues/918 for details.
* @public
*/
export enum EnumMemberOrder {
/**
* `ApiEnumMember` items are sorted according to their {@link ApiItem.getSortKey}. The order is
* basically alphabetical by identifier name, but otherwise unspecified to allow for cosmetic improvements.
*
* This is the default behavior.
*/
ByName = 'by-name',
/**
* `ApiEnumMember` items preserve the original order of the declarations in the source file.
* (This disables the automatic sorting that is normally applied based on {@link ApiItem.getSortKey}.)
*/
Preserve = 'preserve',
}
/**
* Represents a member of a TypeScript enum declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiEnumMember` represents an enum member such as `Small = 100` in the example below:
*
* ```ts
* export enum FontSizes {
* Small = 100,
* Medium = 200,
* Large = 300
* }
* ```
* @public
*/
export class ApiEnumMember extends ApiNameMixin(ApiReleaseTagMixin(ApiInitializerMixin(ApiDeclaredItem))) {
public constructor(options: IApiEnumMemberOptions) {
super(options);
}
public static getContainerKey(name: string): string {
// No prefix needed, because ApiEnumMember is the only possible member of an ApiEnum
return name;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.EnumMember;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiEnumMember.getContainerKey(this.name);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(Navigation.Exports as any, nameComponent)
.withMeaning(Meaning.Member as any);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js';
import {
type IApiTypeParameterListMixinOptions,
ApiTypeParameterListMixin,
} from '../mixins/ApiTypeParameterListMixin.js';
/**
* Constructor options for {@link ApiFunction}.
*
* @public
*/
export interface IApiFunctionOptions
extends IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiDeclaredItemOptions,
IApiExportedMixinOptions {}
/**
* Represents a TypeScript function declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiFunction` represents a TypeScript declaration such as this example:
*
* ```ts
* export function getAverage(x: number, y: number): number {
* return (x + y) / 2.0;
* }
* ```
*
* Functions are exported by an entry point module or by a namespace. Compare with {@link ApiMethod}, which
* represents a function that is a member of a class.
* @public
*/
export class ApiFunction extends ApiNameMixin(
ApiTypeParameterListMixin(
ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiExportedMixin(ApiDeclaredItem)))),
),
) {
public constructor(options: IApiFunctionOptions) {
super(options);
}
public static getContainerKey(name: string, overloadIndex: number): string {
return `${name}|${ApiItemKind.Function}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Function;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiFunction.getContainerKey(this.name, this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Function as any)
.withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { type IApiReadonlyMixinOptions, ApiReadonlyMixin } from '../mixins/ApiReadonlyMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js';
/**
* Constructor options for {@link ApiIndexSignature}.
*
* @public
*/
export interface IApiIndexSignatureOptions
extends IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiReadonlyMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript index signature.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiIndexSignature` represents a TypeScript declaration such as `[x: number]: number` in this example:
*
* ```ts
* export interface INumberTable {
* // An index signature
* [value: number]: number;
*
* // An overloaded index signature
* [name: string]: number;
* }
* ```
* @public
*/
export class ApiIndexSignature extends ApiParameterListMixin(
ApiReleaseTagMixin(ApiReturnTypeMixin(ApiReadonlyMixin(ApiDeclaredItem))),
) {
public constructor(options: IApiIndexSignatureOptions) {
super(options);
}
public static getContainerKey(overloadIndex: number): string {
return `|${ApiItemKind.IndexSignature}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.IndexSignature;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiIndexSignature.getContainerKey(this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const parent: DeclarationReference = this.parent
? this.parent.canonicalReference
: // .withMeaning() requires some kind of component
DeclarationReference.empty().addNavigationStep(Navigation.Members as any, '(parent)');
return parent.withMeaning(Meaning.IndexSignature as any).withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,143 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import {
type IApiExportedMixinJson,
type IApiExportedMixinOptions,
ApiExportedMixin,
} from '../mixins/ApiExportedMixin.js';
import {
ApiItemContainerMixin,
type IApiItemContainerMixinOptions,
type IApiItemContainerJson,
} from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin, type IApiNameMixinJson } from '../mixins/ApiNameMixin.js';
import {
type IApiReleaseTagMixinOptions,
ApiReleaseTagMixin,
type IApiReleaseTagMixinJson,
} from '../mixins/ApiReleaseTagMixin.js';
import {
type IApiTypeParameterListMixinOptions,
type IApiTypeParameterListMixinJson,
ApiTypeParameterListMixin,
} from '../mixins/ApiTypeParameterListMixin.js';
import type { IExcerptTokenRange } from '../mixins/Excerpt.js';
import type { DeserializerContext } from './DeserializerContext.js';
import { HeritageType } from './HeritageType.js';
/**
* Constructor options for {@link ApiInterface}.
*
* @public
*/
export interface IApiInterfaceOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiExportedMixinOptions {
extendsTokenRanges: IExcerptTokenRange[];
}
export interface IApiInterfaceJson
extends IApiItemContainerJson,
IApiNameMixinJson,
IApiTypeParameterListMixinJson,
IApiReleaseTagMixinJson,
IApiDeclaredItemJson,
IApiExportedMixinJson {
extendsTokenRanges: IExcerptTokenRange[];
}
/**
* Represents a TypeScript class declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiInterface` represents a TypeScript declaration such as this:
*
* ```ts
* export interface X extends Y {
* }
* ```
* @public
*/
export class ApiInterface extends ApiItemContainerMixin(
ApiNameMixin(ApiTypeParameterListMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem)))),
) {
private readonly _extendsTypes: HeritageType[] = [];
public constructor(options: IApiInterfaceOptions) {
super(options);
for (const extendsTokenRange of options.extendsTokenRanges) {
this._extendsTypes.push(new HeritageType(this.buildExcerpt(extendsTokenRange)));
}
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.Interface}`;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiInterfaceOptions>,
context: DeserializerContext,
jsonObject: IApiInterfaceJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.extendsTokenRanges = jsonObject.extendsTokenRanges;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Interface;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiInterface.getContainerKey(this.name);
}
/**
* The list of base interfaces that this interface inherits from using the `extends` keyword.
*/
public get extendsTypes(): readonly HeritageType[] {
return this._extendsTypes;
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiInterfaceJson>): void {
super.serializeInto(jsonObject);
jsonObject.extendsTokenRanges = this.extendsTypes.map((x) => x.excerpt.tokenRange);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Interface as any);
}
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiAbstractMixinOptions, ApiAbstractMixin } from '../mixins/ApiAbstractMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js';
import { type IApiParameterListMixinOptions, ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js';
import { type IApiReleaseTagMixinOptions, ApiReleaseTagMixin } from '../mixins/ApiReleaseTagMixin.js';
import { ApiReturnTypeMixin, type IApiReturnTypeMixinOptions } from '../mixins/ApiReturnTypeMixin.js';
import { ApiStaticMixin, type IApiStaticMixinOptions } from '../mixins/ApiStaticMixin.js';
import {
ApiTypeParameterListMixin,
type IApiTypeParameterListMixinOptions,
} from '../mixins/ApiTypeParameterListMixin.js';
/**
* Constructor options for {@link ApiMethod}.
*
* @public
*/
export interface IApiMethodOptions
extends IApiNameMixinOptions,
IApiAbstractMixinOptions,
IApiOptionalMixinOptions,
IApiParameterListMixinOptions,
IApiProtectedMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiStaticMixinOptions,
IApiTypeParameterListMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript member function declaration that belongs to an `ApiClass`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiMethod` represents a TypeScript declaration such as the `render` member function in this example:
*
* ```ts
* export class Widget {
* public render(): void { }
* }
* ```
*
* Compare with {@link ApiMethodSignature}, which represents a method belonging to an interface.
* For example, a class method can be `static` but an interface method cannot.
* @public
*/
export class ApiMethod extends ApiNameMixin(
ApiAbstractMixin(
ApiOptionalMixin(
ApiParameterListMixin(
ApiProtectedMixin(
ApiReleaseTagMixin(ApiReturnTypeMixin(ApiStaticMixin(ApiTypeParameterListMixin(ApiDeclaredItem)))),
),
),
),
),
) {
public constructor(options: IApiMethodOptions) {
super(options);
}
public static getContainerKey(name: string, isStatic: boolean, overloadIndex: number): string {
if (isStatic) {
return `${name}|${ApiItemKind.Method}|static|${overloadIndex}`;
} else {
return `${name}|${ApiItemKind.Method}|instance|${overloadIndex}`;
}
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Method;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiMethod.getContainerKey(this.name, this.isStatic, this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep((this.isStatic ? Navigation.Exports : Navigation.Members) as any, nameComponent)
.withMeaning(Meaning.Member as any)
.withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiOptionalMixin, type IApiOptionalMixinOptions } from '../mixins/ApiOptionalMixin.js';
import { ApiParameterListMixin, type IApiParameterListMixinOptions } from '../mixins/ApiParameterListMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import { type IApiReturnTypeMixinOptions, ApiReturnTypeMixin } from '../mixins/ApiReturnTypeMixin.js';
import {
type IApiTypeParameterListMixinOptions,
ApiTypeParameterListMixin,
} from '../mixins/ApiTypeParameterListMixin.js';
/**
* @public
*/
export interface IApiMethodSignatureOptions
extends IApiNameMixinOptions,
IApiTypeParameterListMixinOptions,
IApiParameterListMixinOptions,
IApiReleaseTagMixinOptions,
IApiReturnTypeMixinOptions,
IApiOptionalMixinOptions,
IApiDeclaredItemOptions {}
/**
* Represents a TypeScript member function declaration that belongs to an `ApiInterface`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiMethodSignature` represents a TypeScript declaration such as the `render` member function in this example:
*
* ```ts
* export interface IWidget {
* render(): void;
* }
* ```
*
* Compare with {@link ApiMethod}, which represents a method belonging to a class.
* For example, a class method can be `static` but an interface method cannot.
* @public
*/
export class ApiMethodSignature extends ApiNameMixin(
ApiTypeParameterListMixin(
ApiParameterListMixin(ApiReleaseTagMixin(ApiReturnTypeMixin(ApiOptionalMixin(ApiDeclaredItem)))),
),
) {
public constructor(options: IApiMethodSignatureOptions) {
super(options);
}
public static getContainerKey(name: string, overloadIndex: number): string {
return `${name}|${ApiItemKind.MethodSignature}|${overloadIndex}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.MethodSignature;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiMethodSignature.getContainerKey(this.name, this.overloadIndex);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(Navigation.Members as any, nameComponent)
.withMeaning(Meaning.Member as any)
.withOverloadIndex(this.overloadIndex);
}
}

View File

@@ -0,0 +1,207 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DocDeclarationReference } from '@microsoft/tsdoc';
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { PackageName } from '@rushstack/node-core-library';
import { ApiItem, ApiItemKind } from '../items/ApiItem.js';
import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js';
import { ApiPackage } from './ApiPackage.js';
import { ModelReferenceResolver, type IResolveDeclarationReferenceResult } from './ModelReferenceResolver.js';
/**
* A serializable representation of a collection of API declarations.
*
* @remarks
*
* An `ApiModel` represents a collection of API declarations that can be serialized to disk. It captures all the
* important information needed to generate documentation, without any reliance on the TypeScript compiler engine.
*
* An `ApiModel` acts as the root of a tree of objects that all inherit from the `ApiItem` base class.
* The tree children are determined by the {@link (ApiItemContainerMixin:interface)} mixin base class. The model
* contains packages. Packages have an entry point (today, only one). And the entry point can contain various types
* of API declarations. The container relationships might look like this:
*
* ```
* Things that can contain other things:
*
* - ApiModel
* - ApiPackage
* - ApiEntryPoint
* - ApiClass
* - ApiMethod
* - ApiProperty
* - ApiEnum
* - ApiEnumMember
* - ApiInterface
* - ApiMethodSignature
* - ApiPropertySignature
* - ApiNamespace
* - (ApiClass, ApiEnum, ApiInterace, ...)
*
* ```
*
* Normally, API Extractor writes an .api.json file to disk for each project that it builds. Then, a tool like
* API Documenter can load the various `ApiPackage` objects into a single `ApiModel` and process them as a group.
* This is useful because compilation generally occurs separately (e.g. because projects may reside in different
* Git repos, or because they build with different TypeScript compiler configurations that may be incompatible),
* whereas API Documenter cannot detect broken hyperlinks without seeing the entire documentation set.
* @public
*/
export class ApiModel extends ApiItemContainerMixin(ApiItem) {
private readonly _resolver: ModelReferenceResolver;
private _packagesByName: Map<string, ApiPackage> | undefined = undefined;
private _apiItemsByCanonicalReference: Map<string, ApiItem> | undefined = undefined;
public constructor() {
super({});
this._resolver = new ModelReferenceResolver(this);
}
public loadPackage(apiJsonFilename: string): ApiPackage {
const apiPackage: ApiPackage = ApiPackage.loadFromJsonFile(apiJsonFilename);
this.addMember(apiPackage);
return apiPackage;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Model;
}
/**
* @override
*/
// eslint-disable-next-line @typescript-eslint/class-literal-property-style
public override get containerKey(): string {
return '';
}
public get packages(): readonly ApiPackage[] {
return this.members as readonly ApiPackage[];
}
/**
* @override
*/
public override addMember(member: ApiPackage): void {
if (member.kind !== ApiItemKind.Package) {
throw new Error('Only items of type ApiPackage may be added to an ApiModel');
}
super.addMember(member);
this._packagesByName = undefined; // invalidate the cache
this._apiItemsByCanonicalReference = undefined; // invalidate the cache
}
/**
* Efficiently finds a package by the NPM package name.
*
* @remarks
*
* If the NPM scope is omitted in the package name, it will still be found provided that it is an unambiguous match.
* For example, it's often convenient to write `{@link node-core-library#JsonFile}` instead of
* `{@link @rushstack/node-core-library#JsonFile}`.
*/
public tryGetPackageByName(packageName: string): ApiPackage | undefined {
// Build the lookup on demand
if (this._packagesByName === undefined) {
this._packagesByName = new Map<string, ApiPackage>();
const unscopedMap: Map<string, ApiPackage | undefined> = new Map<string, ApiPackage | undefined>();
for (const apiPackage of this.packages) {
if (this._packagesByName.get(apiPackage.name)) {
// This should not happen
throw new Error(`The model contains multiple packages with the name ${apiPackage.name}`);
}
this._packagesByName.set(apiPackage.name, apiPackage);
const unscopedName: string = PackageName.parse(apiPackage.name).unscopedName;
if (unscopedMap.has(unscopedName)) {
// If another package has the same unscoped name, then we won't register it
unscopedMap.set(unscopedName, undefined);
} else {
unscopedMap.set(unscopedName, apiPackage);
}
}
for (const [unscopedName, apiPackage] of unscopedMap) {
if (apiPackage && !this._packagesByName.has(unscopedName)) {
// If the unscoped name is unambiguous, then we can also use it as a lookup
this._packagesByName.set(unscopedName, apiPackage);
}
}
}
return this._packagesByName.get(packageName);
}
public resolveDeclarationReference(
declarationReference: DeclarationReference | DocDeclarationReference,
contextApiItem: ApiItem | undefined,
): IResolveDeclarationReferenceResult {
if (declarationReference instanceof DocDeclarationReference) {
return this._resolver.resolve(declarationReference, contextApiItem);
} else if (declarationReference instanceof DeclarationReference) {
// use this._apiItemsByCanonicalReference to look up ApiItem
// Build the lookup on demand
if (!this._apiItemsByCanonicalReference) {
this._apiItemsByCanonicalReference = new Map<string, ApiItem>();
for (const apiPackage of this.packages) {
this._initApiItemsRecursive(apiPackage, this._apiItemsByCanonicalReference);
}
}
const result: IResolveDeclarationReferenceResult = {
resolvedApiItem: undefined,
errorMessage: undefined,
};
const apiItem: ApiItem | undefined = this._apiItemsByCanonicalReference.get(declarationReference.toString());
if (apiItem) {
result.resolvedApiItem = apiItem;
} else {
result.errorMessage = `${declarationReference.toString()} can not be located`;
}
return result;
} else {
// NOTE: The "instanceof DeclarationReference" test assumes a specific version of the @microsoft/tsdoc package.
throw new TypeError(
'The "declarationReference" parameter must be an instance of' +
' DocDeclarationReference or DeclarationReference',
);
}
}
private _initApiItemsRecursive(apiItem: ApiItem, apiItemsByCanonicalReference: Map<string, ApiItem>): void {
if (apiItem.canonicalReference && !apiItem.canonicalReference.isEmpty) {
apiItemsByCanonicalReference.set(apiItem.canonicalReference.toString(), apiItem);
}
// Recurse container members
if (ApiItemContainerMixin.isBaseClassOf(apiItem)) {
for (const apiMember of apiItem.members) {
this._initApiItemsRecursive(apiMember, apiItemsByCanonicalReference);
}
}
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
return DeclarationReference.empty();
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { type IApiDeclaredItemOptions, ApiDeclaredItem } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { type IApiExportedMixinOptions, ApiExportedMixin } from '../mixins/ApiExportedMixin.js';
import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
/**
* Constructor options for {@link ApiClass}.
*
* @public
*/
export interface IApiNamespaceOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiExportedMixinOptions {}
/**
* Represents a TypeScript namespace declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiNamespace` represents a TypeScript declaration such `X` or `Y` in this example:
*
* ```ts
* export namespace X {
* export namespace Y {
* export interface IWidget {
* render(): void;
* }
* }
* }
* ```
* @public
*/
export class ApiNamespace extends ApiItemContainerMixin(
ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))),
) {
public constructor(options: IApiNamespaceOptions) {
super(options);
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.Namespace}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Namespace;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiNamespace.getContainerKey(this.name);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Namespace as any);
}
}

View File

@@ -0,0 +1,312 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { TSDocConfiguration } from '@microsoft/tsdoc';
import { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { TSDocConfigFile } from '@microsoft/tsdoc-config';
import {
JsonFile,
type IJsonFileSaveOptions,
PackageJsonLookup,
type IPackageJson,
type JsonObject,
} from '@rushstack/node-core-library';
import { ApiDocumentedItem, type IApiDocumentedItemOptions } from '../items/ApiDocumentedItem.js';
import { ApiItem, ApiItemKind, type IApiItemJson } from '../items/ApiItem.js';
import { ApiItemContainerMixin, type IApiItemContainerMixinOptions } from '../mixins/ApiItemContainerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import type { ApiEntryPoint } from './ApiEntryPoint.js';
import { DeserializerContext, ApiJsonSchemaVersion } from './DeserializerContext.js';
/**
* Constructor options for {@link ApiPackage}.
*
* @public
*/
export interface IApiPackageOptions
extends IApiItemContainerMixinOptions,
IApiNameMixinOptions,
IApiDocumentedItemOptions {
projectFolderUrl?: string | undefined;
tsdocConfiguration: TSDocConfiguration;
}
export interface IApiPackageMetadataJson {
/**
* To support forwards compatibility, the `oldestForwardsCompatibleVersion` field tracks the oldest schema version
* whose corresponding deserializer could safely load this file.
*
* @remarks
* Normally api-extractor-model should refuse to load a schema version that is newer than the latest version
* that its deserializer understands. However, sometimes a schema change may merely introduce some new fields
* without modifying or removing any existing fields. In this case, an older api-extractor-model library can
* safely deserialize the newer version (by ignoring the extra fields that it doesn't recognize). The newer
* serializer can use this field to communicate that.
*
* If present, the `oldestForwardsCompatibleVersion` must be less than or equal to
* `IApiPackageMetadataJson.schemaVersion`.
*/
oldestForwardsCompatibleVersion?: ApiJsonSchemaVersion;
/**
* The schema version for the .api.json file format. Used for determining whether the file format is
* supported, and for backwards compatibility.
*/
schemaVersion: ApiJsonSchemaVersion;
/**
* The NPM package name for the tool that wrote the *.api.json file.
* For informational purposes only.
*/
toolPackage: string;
/**
* The NPM package version for the tool that wrote the *.api.json file.
* For informational purposes only.
*/
toolVersion: string;
/**
* The TSDoc configuration that was used when analyzing the API for this package.
*
* @remarks
*
* The structure of this objet is defined by the `@microsoft/tsdoc-config` library.
* Normally this configuration is loaded from the project's tsdoc.json file. It is stored
* in the .api.json file so that doc comments can be parsed accurately when loading the file.
*/
tsdocConfig: JsonObject;
}
export interface IApiPackageJson extends IApiItemJson {
/**
* A file header that stores metadata about the tool that wrote the *.api.json file.
*/
metadata: IApiPackageMetadataJson;
/**
* The base URL where the project's source code can be viewed on a website such as GitHub or
* Azure DevOps. This URL path corresponds to the `<projectFolder>` path on disk. Provided via the
* `api-extractor.json` config.
*/
projectFolderUrl?: string;
}
/**
* Options for {@link ApiPackage.saveToJsonFile}.
*
* @public
*/
export interface IApiPackageSaveOptions extends IJsonFileSaveOptions {
/**
* Set to true only when invoking API Extractor's test harness.
*
* @remarks
* When `testMode` is true, the `toolVersion` field in the .api.json file is assigned an empty string
* to prevent spurious diffs in output files tracked for tests.
*/
testMode?: boolean;
/**
* Optionally specifies a value for the "toolPackage" field in the output .api.json data file;
* otherwise, the value will be "api-extractor-model".
*/
toolPackage?: string;
/**
* Optionally specifies a value for the "toolVersion" field in the output .api.json data file;
* otherwise, the value will be the current version of the api-extractor-model package.
*/
toolVersion?: string;
}
/**
* Represents an NPM package containing API declarations.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
* @public
*/
export class ApiPackage extends ApiItemContainerMixin(ApiNameMixin(ApiDocumentedItem)) {
private readonly _tsdocConfiguration: TSDocConfiguration;
private readonly _projectFolderUrl?: string | undefined;
public constructor(options: IApiPackageOptions) {
super(options);
this._tsdocConfiguration = options.tsdocConfiguration;
this._projectFolderUrl = options.projectFolderUrl;
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiPackageOptions>,
context: DeserializerContext,
jsonObject: IApiPackageJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.projectFolderUrl = jsonObject.projectFolderUrl;
}
public static loadFromJsonFile(apiJsonFilename: string): ApiPackage {
const jsonObject: IApiPackageJson = JsonFile.load(apiJsonFilename);
if (!jsonObject?.metadata || typeof jsonObject.metadata.schemaVersion !== 'number') {
throw new Error(
`Error loading ${apiJsonFilename}:` +
`\nThe file format is not recognized; the "metadata.schemaVersion" field is missing or invalid`,
);
}
const schemaVersion: number = jsonObject.metadata.schemaVersion;
if (schemaVersion < ApiJsonSchemaVersion.OLDEST_SUPPORTED) {
throw new Error(
`Error loading ${apiJsonFilename}:` +
`\nThe file format is version ${schemaVersion},` +
` whereas ${ApiJsonSchemaVersion.OLDEST_SUPPORTED} is the oldest version supported by this tool`,
);
}
let oldestForwardsCompatibleVersion: number = schemaVersion;
if (jsonObject.metadata.oldestForwardsCompatibleVersion) {
// Sanity check
if (jsonObject.metadata.oldestForwardsCompatibleVersion > schemaVersion) {
throw new Error(
`Error loading ${apiJsonFilename}:` +
`\nInvalid file format; "oldestForwardsCompatibleVersion" cannot be newer than "schemaVersion"`,
);
}
oldestForwardsCompatibleVersion = jsonObject.metadata.oldestForwardsCompatibleVersion;
}
let versionToDeserialize: number = schemaVersion;
if (versionToDeserialize > ApiJsonSchemaVersion.LATEST) {
// If the file format is too new, can we treat it as some earlier compatible version
// as indicated by oldestForwardsCompatibleVersion?
versionToDeserialize = Math.max(oldestForwardsCompatibleVersion, ApiJsonSchemaVersion.LATEST);
if (versionToDeserialize > ApiJsonSchemaVersion.LATEST) {
// Nope, still too new
throw new Error(
`Error loading ${apiJsonFilename}:` +
`\nThe file format version ${schemaVersion} was written by a newer release of` +
` the api-extractor-model library; you may need to upgrade your software`,
);
}
}
const tsdocConfiguration: TSDocConfiguration = new TSDocConfiguration();
if (versionToDeserialize >= ApiJsonSchemaVersion.V_1004) {
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromObject(jsonObject.metadata.tsdocConfig);
if (tsdocConfigFile.hasErrors) {
throw new Error(`Error loading ${apiJsonFilename}:\n` + tsdocConfigFile.getErrorSummary());
}
tsdocConfigFile.configureParser(tsdocConfiguration);
}
const context: DeserializerContext = new DeserializerContext({
apiJsonFilename,
toolPackage: jsonObject.metadata.toolPackage,
toolVersion: jsonObject.metadata.toolVersion,
versionToDeserialize,
tsdocConfiguration,
});
return ApiItem.deserialize(jsonObject, context) as ApiPackage;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Package;
}
/**
* @override
*/
public override get containerKey(): string {
// No prefix needed, because ApiPackage is the only possible member of an ApiModel
return this.name;
}
public get entryPoints(): readonly ApiEntryPoint[] {
return this.members as readonly ApiEntryPoint[];
}
/**
* The TSDoc configuration that was used when analyzing the API for this package.
*
* @remarks
*
* Normally this configuration is loaded from the project's tsdoc.json file. It is stored
* in the .api.json file so that doc comments can be parsed accurately when loading the file.
*/
public get tsdocConfiguration(): TSDocConfiguration {
return this._tsdocConfiguration;
}
public get projectFolderUrl(): string | undefined {
return this._projectFolderUrl;
}
/**
* @override
*/
public override addMember(member: ApiEntryPoint): void {
if (member.kind !== ApiItemKind.EntryPoint) {
throw new Error('Only items of type ApiEntryPoint may be added to an ApiPackage');
}
super.addMember(member);
}
public findEntryPointsByPath(importPath: string): readonly ApiEntryPoint[] {
return this.findMembersByName(importPath) as readonly ApiEntryPoint[];
}
public saveToJsonFile(apiJsonFilename: string, options?: IApiPackageSaveOptions): void {
const ioptions = options ?? {};
const packageJson: IPackageJson = PackageJsonLookup.loadOwnPackageJson(__dirname);
const tsdocConfigFile: TSDocConfigFile = TSDocConfigFile.loadFromParser(this.tsdocConfiguration);
const tsdocConfig: JsonObject = tsdocConfigFile.saveToObject();
const jsonObject: IApiPackageJson = {
metadata: {
toolPackage: ioptions.toolPackage ?? packageJson.name,
// In test mode, we don't write the real version, since that would cause spurious diffs whenever
// the version is bumped. Instead we write a placeholder string.
toolVersion: ioptions.testMode ? '[test mode]' : ioptions.toolVersion ?? packageJson.version,
schemaVersion: ApiJsonSchemaVersion.LATEST,
oldestForwardsCompatibleVersion: ApiJsonSchemaVersion.OLDEST_FORWARDS_COMPATIBLE,
tsdocConfig,
},
} as IApiPackageJson;
if (this.projectFolderUrl) {
jsonObject.projectFolderUrl = this.projectFolderUrl;
}
this.serializeInto(jsonObject);
JsonFile.save(jsonObject, apiJsonFilename, ioptions);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
return DeclarationReference.package(this.name);
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { ApiPropertyItem, type IApiPropertyItemOptions } from '../items/ApiPropertyItem.js';
import { ApiAbstractMixin, type IApiAbstractMixinOptions } from '../mixins/ApiAbstractMixin.js';
import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js';
import { ApiProtectedMixin, type IApiProtectedMixinOptions } from '../mixins/ApiProtectedMixin.js';
import { ApiStaticMixin, type IApiStaticMixinOptions } from '../mixins/ApiStaticMixin.js';
/**
* Constructor options for {@link ApiProperty}.
*
* @public
*/
export interface IApiPropertyOptions
extends IApiPropertyItemOptions,
IApiAbstractMixinOptions,
IApiProtectedMixinOptions,
IApiStaticMixinOptions,
IApiInitializerMixinOptions {}
/**
* Represents a TypeScript property declaration that belongs to an `ApiClass`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiProperty` represents a TypeScript declaration such as the `width` and `height` members in this example:
*
* ```ts
* export class Widget {
* public width: number = 100;
*
* public get height(): number {
* if (this.isSquashed()) {
* return 0;
* } else {
* return this.clientArea.height;
* }
* }
* }
* ```
*
* Note that member variables are also considered to be properties.
*
* If the property has both a getter function and setter function, they will be represented by a single `ApiProperty`
* and must have a single documentation comment.
*
* Compare with {@link ApiPropertySignature}, which represents a property belonging to an interface.
* For example, a class property can be `static` but an interface property cannot.
* @public
*/
export class ApiProperty extends ApiAbstractMixin(
ApiProtectedMixin(ApiStaticMixin(ApiInitializerMixin(ApiPropertyItem))),
) {
public constructor(options: IApiPropertyOptions) {
super(options);
}
public static getContainerKey(name: string, isStatic: boolean): string {
if (isStatic) {
return `${name}|${ApiItemKind.Property}|static`;
} else {
return `${name}|${ApiItemKind.Property}|instance`;
}
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Property;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiProperty.getContainerKey(this.name, this.isStatic);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep((this.isStatic ? Navigation.Exports : Navigation.Members) as any, nameComponent)
.withMeaning(Meaning.Member as any);
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import { ApiPropertyItem, type IApiPropertyItemOptions } from '../items/ApiPropertyItem.js';
/**
* Constructor options for {@link ApiPropertySignature}.
*
* @public
*/
export interface IApiPropertySignatureOptions extends IApiPropertyItemOptions {}
/**
* Represents a TypeScript property declaration that belongs to an `ApiInterface`.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiPropertySignature` represents a TypeScript declaration such as the `width` and `height` members in this example:
*
* ```ts
* export interface IWidget {
* readonly width: number;
* height: number;
* }
* ```
*
* Compare with {@link ApiProperty}, which represents a property belonging to a class.
* For example, a class property can be `static` but an interface property cannot.
* @public
*/
export class ApiPropertySignature extends ApiPropertyItem {
public constructor(options: IApiPropertySignatureOptions) {
super(options);
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.PropertySignature}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.PropertySignature;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiPropertySignature.getContainerKey(this.name);
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(Navigation.Members as any, nameComponent)
.withMeaning(Meaning.Member as any);
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import {
type IApiExportedMixinJson,
type IApiExportedMixinOptions,
ApiExportedMixin,
} from '../mixins/ApiExportedMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import {
ApiTypeParameterListMixin,
type IApiTypeParameterListMixinOptions,
type IApiTypeParameterListMixinJson,
} from '../mixins/ApiTypeParameterListMixin.js';
import type { Excerpt, IExcerptTokenRange } from '../mixins/Excerpt.js';
import type { DeserializerContext } from './DeserializerContext.js';
/**
* Constructor options for {@link ApiTypeAlias}.
*
* @public
*/
export interface IApiTypeAliasOptions
extends IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiDeclaredItemOptions,
IApiTypeParameterListMixinOptions,
IApiExportedMixinOptions {
typeTokenRange: IExcerptTokenRange;
}
export interface IApiTypeAliasJson extends IApiDeclaredItemJson, IApiTypeParameterListMixinJson, IApiExportedMixinJson {
typeTokenRange: IExcerptTokenRange;
}
/**
* Represents a TypeScript type alias declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiTypeAlias` represents a definition such as one of these examples:
*
* ```ts
* // A union type:
* export type Shape = Square | Triangle | Circle;
*
* // A generic type alias:
* export type BoxedValue<T> = { value: T };
*
* export type BoxedArray<T> = { array: T[] };
*
* // A conditional type alias:
* export type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
*
* ```
* @public
*/
export class ApiTypeAlias extends ApiTypeParameterListMixin(
ApiNameMixin(ApiReleaseTagMixin(ApiExportedMixin(ApiDeclaredItem))),
) {
/**
* An {@link Excerpt} that describes the type of the alias.
*
* @remarks
* In the example below, the `typeExcerpt` would correspond to the subexpression
* `T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;`:
*
* ```ts
* export type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
* ```
*/
public readonly typeExcerpt: Excerpt;
public constructor(options: IApiTypeAliasOptions) {
super(options);
this.typeExcerpt = this.buildExcerpt(options.typeTokenRange);
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiTypeAliasOptions>,
context: DeserializerContext,
jsonObject: IApiTypeAliasJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.typeTokenRange = jsonObject.typeTokenRange;
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.TypeAlias}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.TypeAlias;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiTypeAlias.getContainerKey(this.name);
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiTypeAliasJson>): void {
super.serializeInto(jsonObject);
jsonObject.typeTokenRange = this.typeExcerpt.tokenRange;
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.TypeAlias as any);
}
}

View File

@@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { DeclarationReference, type Component } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import { ApiDeclaredItem, type IApiDeclaredItemOptions, type IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { ApiItemKind, Navigation, Meaning } from '../items/ApiItem.js';
import {
type IApiExportedMixinJson,
type IApiExportedMixinOptions,
ApiExportedMixin,
} from '../mixins/ApiExportedMixin.js';
import { ApiInitializerMixin, type IApiInitializerMixinOptions } from '../mixins/ApiInitializerMixin.js';
import { type IApiNameMixinOptions, ApiNameMixin } from '../mixins/ApiNameMixin.js';
import { ApiReadonlyMixin, type IApiReadonlyMixinOptions } from '../mixins/ApiReadonlyMixin.js';
import { ApiReleaseTagMixin, type IApiReleaseTagMixinOptions } from '../mixins/ApiReleaseTagMixin.js';
import type { IExcerptTokenRange, Excerpt } from '../mixins/Excerpt.js';
import type { DeserializerContext } from './DeserializerContext.js';
/**
* Constructor options for {@link ApiVariable}.
*
* @public
*/
export interface IApiVariableOptions
extends IApiNameMixinOptions,
IApiReleaseTagMixinOptions,
IApiReadonlyMixinOptions,
IApiDeclaredItemOptions,
IApiInitializerMixinOptions,
IApiExportedMixinOptions {
variableTypeTokenRange: IExcerptTokenRange;
}
export interface IApiVariableJson extends IApiDeclaredItemJson, IApiExportedMixinJson {
variableTypeTokenRange: IExcerptTokenRange;
}
/**
* Represents a TypeScript variable declaration.
*
* @remarks
*
* This is part of the {@link ApiModel} hierarchy of classes, which are serializable representations of
* API declarations.
*
* `ApiVariable` represents an exported `const` or `let` object such as these examples:
*
* ```ts
* // A variable declaration
* export let verboseLogging: boolean;
*
* // A constant variable declaration with an initializer
* export const canvas: IWidget = createCanvas();
* ```
* @public
*/
export class ApiVariable extends ApiNameMixin(
ApiReleaseTagMixin(ApiReadonlyMixin(ApiInitializerMixin(ApiExportedMixin(ApiDeclaredItem)))),
) {
/**
* An {@link Excerpt} that describes the type of the variable.
*/
public readonly variableTypeExcerpt: Excerpt;
public constructor(options: IApiVariableOptions) {
super(options);
this.variableTypeExcerpt = this.buildExcerpt(options.variableTypeTokenRange);
}
/**
* @override
*/
public static override onDeserializeInto(
options: Partial<IApiVariableOptions>,
context: DeserializerContext,
jsonObject: IApiVariableJson,
): void {
super.onDeserializeInto(options, context, jsonObject);
options.variableTypeTokenRange = jsonObject.variableTypeTokenRange;
}
public static getContainerKey(name: string): string {
return `${name}|${ApiItemKind.Variable}`;
}
/**
* @override
*/
public override get kind(): ApiItemKind {
return ApiItemKind.Variable;
}
/**
* @override
*/
public override get containerKey(): string {
return ApiVariable.getContainerKey(this.name);
}
/**
* @override
*/
public override serializeInto(jsonObject: Partial<IApiVariableJson>): void {
super.serializeInto(jsonObject);
jsonObject.variableTypeTokenRange = this.variableTypeExcerpt.tokenRange;
}
/**
* @beta @override
*/
public override buildCanonicalReference(): DeclarationReference {
const nameComponent: Component = DeclarationReference.parseComponent(this.name);
const navigation: Navigation = this.isExported ? Navigation.Exports : Navigation.Locals;
return (this.parent ? this.parent.canonicalReference : DeclarationReference.empty())
.addNavigationStep(navigation as any, nameComponent)
.withMeaning(Meaning.Variable as any);
}
}

View File

@@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { IApiDeclaredItemJson } from '../items/ApiDeclaredItem.js';
import { type IApiItemJson, type IApiItemOptions, type ApiItem, ApiItemKind } from '../items/ApiItem.js';
import type { IApiPropertyItemJson } from '../items/ApiPropertyItem.js';
import { ApiCallSignature, type IApiCallSignatureOptions } from './ApiCallSignature.js';
import { ApiClass, type IApiClassOptions, type IApiClassJson } from './ApiClass.js';
import { ApiConstructSignature, type IApiConstructSignatureOptions } from './ApiConstructSignature.js';
import { ApiConstructor, type IApiConstructorOptions } from './ApiConstructor.js';
import { ApiEntryPoint, type IApiEntryPointOptions } from './ApiEntryPoint.js';
import { ApiEnum, type IApiEnumOptions } from './ApiEnum.js';
import { ApiEnumMember, type IApiEnumMemberOptions } from './ApiEnumMember.js';
import { ApiFunction, type IApiFunctionOptions } from './ApiFunction.js';
import { ApiIndexSignature, type IApiIndexSignatureOptions } from './ApiIndexSignature.js';
import { ApiInterface, type IApiInterfaceOptions, type IApiInterfaceJson } from './ApiInterface.js';
import { ApiMethod, type IApiMethodOptions } from './ApiMethod.js';
import { ApiMethodSignature, type IApiMethodSignatureOptions } from './ApiMethodSignature.js';
import { ApiModel } from './ApiModel.js';
import { ApiNamespace, type IApiNamespaceOptions } from './ApiNamespace.js';
import { ApiPackage, type IApiPackageOptions, type IApiPackageJson } from './ApiPackage.js';
import { ApiProperty, type IApiPropertyOptions } from './ApiProperty.js';
import { ApiPropertySignature, type IApiPropertySignatureOptions } from './ApiPropertySignature.js';
import { ApiTypeAlias, type IApiTypeAliasOptions, type IApiTypeAliasJson } from './ApiTypeAlias.js';
import { ApiVariable, type IApiVariableOptions, type IApiVariableJson } from './ApiVariable.js';
import type { DeserializerContext } from './DeserializerContext.js';
export class Deserializer {
public static deserialize(context: DeserializerContext, jsonObject: IApiItemJson): ApiItem {
const options: Partial<IApiItemOptions> = {};
switch (jsonObject.kind) {
case ApiItemKind.Class:
ApiClass.onDeserializeInto(options, context, jsonObject as IApiClassJson);
return new ApiClass(options as IApiClassOptions);
case ApiItemKind.CallSignature:
ApiCallSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiCallSignature(options as IApiCallSignatureOptions);
case ApiItemKind.Constructor:
ApiConstructor.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiConstructor(options as IApiConstructorOptions);
case ApiItemKind.ConstructSignature:
ApiConstructSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiConstructSignature(options as IApiConstructSignatureOptions);
case ApiItemKind.EntryPoint:
ApiEntryPoint.onDeserializeInto(options, context, jsonObject);
return new ApiEntryPoint(options as IApiEntryPointOptions);
case ApiItemKind.Enum:
ApiEnum.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiEnum(options as IApiEnumOptions);
case ApiItemKind.EnumMember:
ApiEnumMember.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiEnumMember(options as IApiEnumMemberOptions);
case ApiItemKind.Function:
ApiFunction.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiFunction(options as IApiFunctionOptions);
case ApiItemKind.IndexSignature:
ApiIndexSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiIndexSignature(options as IApiIndexSignatureOptions);
case ApiItemKind.Interface:
ApiInterface.onDeserializeInto(options, context, jsonObject as IApiInterfaceJson);
return new ApiInterface(options as IApiInterfaceOptions);
case ApiItemKind.Method:
ApiMethod.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiMethod(options as IApiMethodOptions);
case ApiItemKind.MethodSignature:
ApiMethodSignature.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiMethodSignature(options as IApiMethodSignatureOptions);
case ApiItemKind.Model:
return new ApiModel();
case ApiItemKind.Namespace:
ApiNamespace.onDeserializeInto(options, context, jsonObject as IApiDeclaredItemJson);
return new ApiNamespace(options as IApiNamespaceOptions);
case ApiItemKind.Package:
ApiPackage.onDeserializeInto(options, context, jsonObject as IApiPackageJson);
return new ApiPackage(options as IApiPackageOptions);
case ApiItemKind.Property:
ApiProperty.onDeserializeInto(options, context, jsonObject as IApiPropertyItemJson);
return new ApiProperty(options as IApiPropertyOptions);
case ApiItemKind.PropertySignature:
ApiPropertySignature.onDeserializeInto(options, context, jsonObject as IApiPropertyItemJson);
return new ApiPropertySignature(options as IApiPropertySignatureOptions);
case ApiItemKind.TypeAlias:
ApiTypeAlias.onDeserializeInto(options, context, jsonObject as IApiTypeAliasJson);
return new ApiTypeAlias(options as IApiTypeAliasOptions);
case ApiItemKind.Variable:
ApiVariable.onDeserializeInto(options, context, jsonObject as IApiVariableJson);
return new ApiVariable(options as IApiVariableOptions);
default:
throw new Error(`Failed to deserialize unsupported API item type ${JSON.stringify(jsonObject.kind)}`);
}
}
}

View File

@@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/prefer-literal-enum-member */
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { TSDocConfiguration } from '@microsoft/tsdoc';
export enum ApiJsonSchemaVersion {
/**
* The initial release.
*/
V_1000 = 1_000,
/**
* Add support for type parameters and type alias types.
*/
V_1001 = 1_001,
/**
* Remove `canonicalReference` field. This field was for diagnostic purposes only and was never deserialized.
*/
V_1002 = 1_002,
/**
* Reintroduce the `canonicalReference` field using the experimental new TSDoc declaration reference notation.
*
* This is not a breaking change because this field is never deserialized; it is provided for informational
* purposes only.
*/
V_1003 = 1_003,
/**
* Add a `tsdocConfig` field that tracks the TSDoc configuration for parsing doc comments.
*
* This is not a breaking change because an older implementation will still work correctly. The
* custom tags will be skipped over by the parser.
*/
V_1004 = 1_004,
/**
* Add an `isOptional` field to `Parameter` and `TypeParameter` to track whether a function parameter is optional.
*
* When loading older JSON files, the value defaults to `false`.
*/
V_1005 = 1_005,
/**
* Add an `isProtected` field to `ApiConstructor`, `ApiMethod`, and `ApiProperty` to
* track whether a class member has the `protected` modifier.
*
* Add an `isReadonly` field to `ApiProperty`, `ApiPropertySignature`, and `ApiVariable` to
* track whether the item is readonly.
*
* When loading older JSON files, the values default to `false`.
*/
V_1006 = 1_006,
/**
* Add `ApiItemContainerMixin.preserveMemberOrder` to support enums that preserve their original sort order.
*
* When loading older JSON files, the value default to `false`.
*/
V_1007 = 1_007,
/**
* Add an `initializerTokenRange` field to `ApiProperty` and `ApiVariable` to track the item's
* initializer.
*
* When loading older JSON files, this range is empty.
*/
V_1008 = 1_008,
/**
* Add an `isReadonly` field to `ApiIndexSignature` to track whether the item is readonly.
*
* When loading older JSON files, the values defaults to `false`.
*/
V_1009 = 1_009,
/**
* Add a `fileUrlPath` field to `ApiDeclaredItem` to track the URL to a declared item's source file.
*
* When loading older JSON files, the value defaults to `undefined`.
*/
V_1010 = 1_010,
/**
* Add an `isAbstract` field to `ApiClass`, `ApiMethod`, and `ApiProperty` to
* track whether the item is abstract.
*
* When loading older JSON files, the value defaults to `false`.
*/
V_1011 = 1_011,
/**
* The current latest .api.json schema version.
*
* IMPORTANT: When incrementing this number, consider whether `OLDEST_SUPPORTED` or `OLDEST_FORWARDS_COMPATIBLE`
* should be updated.
*/
LATEST = V_1011,
/**
* The oldest .api.json schema version that is still supported for backwards compatibility.
*
* This must be updated if you change to the file format and do not implement compatibility logic for
* deserializing the older representation.
*/
OLDEST_SUPPORTED = V_1001,
/**
* Used to assign `IApiPackageMetadataJson.oldestForwardsCompatibleVersion`.
*
* This value must be \<= `ApiJsonSchemaVersion.LATEST`. It must be reset to the `LATEST` value
* if the older library would not be able to deserialize your new file format. Adding a nonessential field
* is generally okay. Removing, modifying, or reinterpreting existing fields is NOT safe.
*/
OLDEST_FORWARDS_COMPATIBLE = V_1001,
}
export class DeserializerContext {
/**
* The path of the file being deserialized, which may be useful for diagnostic purposes.
*/
public readonly apiJsonFilename: string;
/**
* Metadata from `IApiPackageMetadataJson.toolPackage`.
*/
public readonly toolPackage: string;
/**
* Metadata from `IApiPackageMetadataJson.toolVersion`.
*/
public readonly toolVersion: string;
/**
* The version of the schema being deserialized, as obtained from `IApiPackageMetadataJson.schemaVersion`.
*/
public readonly versionToDeserialize: ApiJsonSchemaVersion;
/**
* The TSDoc configuration for the context.
*/
public readonly tsdocConfiguration: TSDocConfiguration;
public constructor(options: DeserializerContext) {
this.apiJsonFilename = options.apiJsonFilename;
this.toolPackage = options.toolPackage;
this.toolVersion = options.toolVersion;
this.versionToDeserialize = options.versionToDeserialize;
this.tsdocConfiguration = options.tsdocConfiguration;
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type { Excerpt } from '../mixins/Excerpt.js';
/**
* Represents a type referenced via an "extends" or "implements" heritage clause for a TypeScript class
* or interface.
*
* @remarks
*
* For example, consider this declaration:
*
* ```ts
* export class Widget extends Controls.WidgetBase implements Controls.IWidget, IDisposable {
* // . . .
* }
* ```
*
* The heritage types are `Controls.WidgetBase`, `Controls.IWidget`, and `IDisposable`.
* @public
*/
export class HeritageType {
/**
* An excerpt corresponding to the referenced type.
*
* @remarks
*
* For example, consider this declaration:
*
* ```ts
* export class Widget extends Controls.WidgetBase implements Controls.IWidget, IDisposable {
* // . . .
* }
* ```
*
* The excerpt might be `Controls.WidgetBase`, `Controls.IWidget`, or `IDisposable`.
*/
public readonly excerpt: Excerpt;
public constructor(excerpt: Excerpt) {
this.excerpt = excerpt;
}
}

View File

@@ -0,0 +1,235 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { type DocDeclarationReference, type DocMemberSelector, SelectorKind } from '@microsoft/tsdoc';
import { type ApiItem, ApiItemKind } from '../items/ApiItem.js';
import { ApiItemContainerMixin } from '../mixins/ApiItemContainerMixin.js';
import { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import type { ApiEntryPoint } from './ApiEntryPoint.js';
import type { ApiModel } from './ApiModel.js';
import type { ApiPackage } from './ApiPackage.js';
/**
* Result object for {@link ApiModel.resolveDeclarationReference}.
*
* @public
*/
export interface IResolveDeclarationReferenceResult {
/**
* If resolvedApiItem is undefined, then this will always contain an error message explaining why the
* resolution failed.
*/
errorMessage: string | undefined;
/**
* The referenced ApiItem, if the declaration reference could be resolved.
*/
resolvedApiItem: ApiItem | undefined;
}
/**
* This resolves a TSDoc declaration reference by walking the `ApiModel` hierarchy.
*
* @remarks
*
* This class is analogous to `AstReferenceResolver` from the `@microsoft/api-extractor` project,
* which resolves declaration references by walking the compiler state.
*/
export class ModelReferenceResolver {
private readonly _apiModel: ApiModel;
public constructor(apiModel: ApiModel) {
this._apiModel = apiModel;
}
public resolve(
declarationReference: DocDeclarationReference,
contextApiItem: ApiItem | undefined,
): IResolveDeclarationReferenceResult {
const result: IResolveDeclarationReferenceResult = {
resolvedApiItem: undefined,
errorMessage: undefined,
};
let apiPackage: ApiPackage | undefined;
// Is this an absolute reference?
if (declarationReference.packageName === undefined) {
// If the package name is omitted, try to infer it from the context
if (contextApiItem !== undefined) {
apiPackage = contextApiItem.getAssociatedPackage();
}
if (apiPackage === undefined) {
result.errorMessage = `The reference does not include a package name, and the package could not be inferred from the context`;
return result;
}
} else {
apiPackage = this._apiModel.tryGetPackageByName(declarationReference.packageName);
if (apiPackage === undefined) {
result.errorMessage = `The package "${declarationReference.packageName}" could not be located`;
return result;
}
}
const importPath: string = declarationReference.importPath ?? '';
const foundEntryPoints: readonly ApiEntryPoint[] = apiPackage.findEntryPointsByPath(importPath);
if (foundEntryPoints.length < 1) {
result.errorMessage = `The import path "${importPath}" could not be resolved`;
return result;
}
let currentItem: ApiItem = foundEntryPoints[0]!;
// Now search for the member reference
for (const memberReference of declarationReference.memberReferences) {
if (memberReference.memberSymbol !== undefined) {
result.errorMessage = `Symbols are not yet supported in declaration references`;
return result;
}
if (memberReference.memberIdentifier === undefined) {
result.errorMessage = `Missing member identifier`;
return result;
}
const identifier: string = memberReference.memberIdentifier.identifier;
if (!ApiItemContainerMixin.isBaseClassOf(currentItem)) {
// For example, {@link MyClass.myMethod.X} is invalid because methods cannot contain members
result.errorMessage = `Unable to resolve ${JSON.stringify(
identifier,
)} because ${currentItem.getScopedNameWithinPackage()} cannot act as a container`;
return result;
}
const foundMembers: readonly ApiItem[] = currentItem.findMembersByName(identifier);
if (foundMembers.length === 0) {
result.errorMessage = `The member reference ${JSON.stringify(identifier)} was not found`;
return result;
}
const memberSelector: DocMemberSelector | undefined = memberReference.selector;
if (memberSelector === undefined) {
if (foundMembers.length > 1) {
result.errorMessage = `The member reference ${JSON.stringify(identifier)} was ambiguous`;
return result;
}
currentItem = foundMembers[0]!;
} else {
let memberSelectorResult: IResolveDeclarationReferenceResult;
switch (memberSelector.selectorKind) {
case SelectorKind.System:
memberSelectorResult = this._selectUsingSystemSelector(foundMembers, memberSelector, identifier);
break;
case SelectorKind.Index:
memberSelectorResult = this._selectUsingIndexSelector(foundMembers, memberSelector, identifier);
break;
default:
result.errorMessage = `The selector "${memberSelector.selector}" is not a supported selector type`;
return result;
}
if (memberSelectorResult.resolvedApiItem === undefined) {
return memberSelectorResult;
}
currentItem = memberSelectorResult.resolvedApiItem;
}
}
result.resolvedApiItem = currentItem;
return result;
}
private _selectUsingSystemSelector(
foundMembers: readonly ApiItem[],
memberSelector: DocMemberSelector,
identifier: string,
): IResolveDeclarationReferenceResult {
const result: IResolveDeclarationReferenceResult = {
resolvedApiItem: undefined,
errorMessage: undefined,
};
const selectorName: string = memberSelector.selector;
let selectorItemKind: ApiItemKind;
switch (selectorName) {
case 'class':
selectorItemKind = ApiItemKind.Class;
break;
case 'enum':
selectorItemKind = ApiItemKind.Enum;
break;
case 'function':
selectorItemKind = ApiItemKind.Function;
break;
case 'interface':
selectorItemKind = ApiItemKind.Interface;
break;
case 'namespace':
selectorItemKind = ApiItemKind.Namespace;
break;
case 'type':
selectorItemKind = ApiItemKind.TypeAlias;
break;
case 'variable':
selectorItemKind = ApiItemKind.Variable;
break;
default:
result.errorMessage = `Unsupported system selector "${selectorName}"`;
return result;
}
const matches: ApiItem[] = foundMembers.filter((x) => x.kind === selectorItemKind);
if (matches.length === 0) {
result.errorMessage = `A declaration for "${identifier}" was not found that matches the TSDoc selector "${selectorName}"`;
return result;
}
if (matches.length > 1) {
result.errorMessage = `More than one declaration "${identifier}" matches the TSDoc selector "${selectorName}"`;
}
result.resolvedApiItem = matches[0];
return result;
}
private _selectUsingIndexSelector(
foundMembers: readonly ApiItem[],
memberSelector: DocMemberSelector,
identifier: string,
): IResolveDeclarationReferenceResult {
const result: IResolveDeclarationReferenceResult = {
resolvedApiItem: undefined,
errorMessage: undefined,
};
const selectedMembers: ApiItem[] = [];
const selectorOverloadIndex: number = Number.parseInt(memberSelector.selector, 10);
for (const foundMember of foundMembers) {
if (ApiParameterListMixin.isBaseClassOf(foundMember) && foundMember.overloadIndex === selectorOverloadIndex) {
selectedMembers.push(foundMember);
}
}
if (selectedMembers.length === 0) {
result.errorMessage =
`An overload for ${JSON.stringify(identifier)} was not found that matches` +
` the TSDoc selector ":${selectorOverloadIndex}"`;
return result;
}
if (selectedMembers.length === 1) {
result.resolvedApiItem = selectedMembers[0];
return result;
}
result.errorMessage = `The member reference ${JSON.stringify(identifier)} was ambiguous`;
return result;
}
}

View File

@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type * as tsdoc from '@microsoft/tsdoc';
import { ApiDocumentedItem } from '../items/ApiDocumentedItem.js';
import type { ApiParameterListMixin } from '../mixins/ApiParameterListMixin.js';
import type { Excerpt } from '../mixins/Excerpt.js';
/**
* Constructor options for {@link Parameter}.
*
* @public
*/
export interface IParameterOptions {
isOptional: boolean;
isRest: boolean;
name: string;
parameterTypeExcerpt: Excerpt;
parent: ApiParameterListMixin;
}
/**
* Represents a named parameter for a function-like declaration.
*
* @remarks
*
* `Parameter` represents a TypeScript declaration such as `x: number` in this example:
*
* ```ts
* export function add(x: number, y: number): number {
* return x + y;
* }
* ```
*
* `Parameter` objects belong to the {@link (ApiParameterListMixin:interface).parameters} collection.
* @public
*/
export class Parameter {
/**
* An {@link Excerpt} that describes the type of the parameter.
*/
public readonly parameterTypeExcerpt: Excerpt;
/**
* The parameter name.
*/
public name: string;
/**
* Whether the parameter is optional.
*/
public isOptional: boolean;
/**
* Whether the parameter is a rest parameter
*/
public isRest: boolean;
private readonly _parent: ApiParameterListMixin;
public constructor(options: IParameterOptions) {
this.name = options.name;
this.parameterTypeExcerpt = options.parameterTypeExcerpt;
this.isOptional = options.isOptional;
this.isRest = options.isRest;
this._parent = options.parent;
}
/**
* Returns the `@param` documentation for this parameter, if present.
*/
public get tsdocParamBlock(): tsdoc.DocParamBlock | undefined {
if (this._parent instanceof ApiDocumentedItem && this._parent.tsdocComment) {
return this._parent.tsdocComment.params.tryGetBlockByName(this.name);
}
return undefined;
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { URL } from 'node:url';
/**
* Constructor options for `SourceLocation`.
*
* @public
*/
export interface ISourceLocationOptions {
/**
* The file URL path relative to the `projectFolder` and `projectFolderURL` fields as
* defined in the `api-extractor.json` config.
*/
fileUrlPath?: string | undefined;
/**
* The project folder URL as defined by the `api-extractor.json` config `projectFolderUrl`
* setting.
*/
projectFolderUrl?: string | undefined;
/**
* The column number in the source file. The first column number is 1.
*/
sourceFileColumn?: number | undefined;
/**
* The line number in the source file. The first line number is 1.
*/
sourceFileLine?: number | undefined;
}
/**
* The source location where a given API item is declared.
*
* @remarks
* The source location points to the `.ts` source file where the API item was originally
* declared. However, in some cases, if source map resolution fails, it falls back to pointing
* to the `.d.ts` file instead.
* @public
*/
export class SourceLocation {
private readonly _projectFolderUrl?: string | undefined;
private readonly _fileUrlPath?: string | undefined;
private readonly _fileLine?: number | undefined;
private readonly _fileColumn?: number | undefined;
public constructor(options: ISourceLocationOptions) {
this._projectFolderUrl = options.projectFolderUrl;
this._fileUrlPath = options.fileUrlPath;
this._fileLine = options.sourceFileLine;
this._fileColumn = options.sourceFileColumn;
}
/**
* Returns the file URL to the given source location. Returns `undefined` if the file URL
* cannot be determined.
*/
public get fileUrl(): string | undefined {
if (this._projectFolderUrl === undefined || this._fileUrlPath === undefined) {
return undefined;
}
let projectFolderUrl: string = this._projectFolderUrl;
if (!projectFolderUrl.endsWith('/')) {
projectFolderUrl += '/';
}
const url: URL = new URL(this._fileUrlPath, projectFolderUrl);
return url.href;
}
/**
* The line in the `fileUrlPath` where the API item is declared.
*/
public get fileLine(): number | undefined {
return this._fileLine;
}
/**
* The column in the `fileUrlPath` where the API item is declared.
*/
public get fileColumn(): number | undefined {
return this._fileColumn;
}
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type * as tsdoc from '@microsoft/tsdoc';
import { ApiDocumentedItem } from '../items/ApiDocumentedItem.js';
import type { ApiTypeParameterListMixin } from '../mixins/ApiTypeParameterListMixin.js';
import type { Excerpt } from '../mixins/Excerpt.js';
/**
* Constructor options for {@link TypeParameter}.
*
* @public
*/
export interface ITypeParameterOptions {
constraintExcerpt: Excerpt;
defaultTypeExcerpt: Excerpt;
isOptional: boolean;
name: string;
parent: ApiTypeParameterListMixin;
}
/**
* Represents a named type parameter for a generic declaration.
*
* @remarks
*
* `TypeParameter` represents a TypeScript declaration such as `T` in this example:
*
* ```ts
* interface IIdentifier {
* getCode(): string;
* }
*
* class BarCode implements IIdentifier {
* private _value: number;
* public getCode(): string { return this._value.toString(); }
* }
*
* class Book<TIdentifier extends IIdentifier = BarCode> {
* public identifier: TIdentifier;
* }
* ```
*
* `TypeParameter` objects belong to the {@link (ApiTypeParameterListMixin:interface).typeParameters} collection.
* @public
*/
export class TypeParameter {
/**
* An {@link Excerpt} that describes the base constraint of the type parameter.
*
* @remarks
* In the example below, the `constraintExcerpt` would correspond to the `IIdentifier` subexpression:
*
* ```ts
* class Book<TIdentifier extends IIdentifier = BarCode> {
* public identifier: TIdentifier;
* }
* ```
*/
public readonly constraintExcerpt: Excerpt;
/**
* An {@link Excerpt} that describes the default type of the type parameter.
*
* @remarks
* In the example below, the `defaultTypeExcerpt` would correspond to the `BarCode` subexpression:
*
* ```ts
* class Book<TIdentifier extends IIdentifier = BarCode> {
* public identifier: TIdentifier;
* }
* ```
*/
public readonly defaultTypeExcerpt: Excerpt;
/**
* The parameter name.
*/
public name: string;
/**
* Whether the type parameter is optional. True IFF there exists a `defaultTypeExcerpt`.
*/
public isOptional: boolean;
private readonly _parent: ApiTypeParameterListMixin;
public constructor(options: ITypeParameterOptions) {
this.name = options.name;
this.constraintExcerpt = options.constraintExcerpt;
this.defaultTypeExcerpt = options.defaultTypeExcerpt;
this.isOptional = options.isOptional;
this._parent = options.parent;
}
/**
* Returns the `@typeParam` documentation for this parameter, if present.
*/
public get tsdocTypeParamBlock(): tsdoc.DocParamBlock | undefined {
if (this._parent instanceof ApiDocumentedItem && this._parent.tsdocComment) {
return this._parent.tsdocComment.typeParams.tryGetBlockByName(this.name);
}
return undefined;
}
}

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true
},
"include": ["*.ts", "*.tsx", "*.js", "*.cjs", "*.mjs", "src"]
}

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": ["jest", "node"],
"isolatedModules": false
}
}

View File

@@ -0,0 +1,3 @@
import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig();

View File

@@ -45,7 +45,7 @@
},
"homepage": "https://discord.js.org",
"dependencies": {
"@microsoft/api-extractor-model": "7.28.2",
"@discordjs/api-extractor-model": "workspace:^",
"@microsoft/tsdoc": "0.14.2"
},
"devDependencies": {

View File

@@ -16,8 +16,8 @@ import type {
ApiConstructor,
ApiItemContainerMixin,
ApiReturnTypeMixin,
} from '@microsoft/api-extractor-model';
import { ApiDeclaredItem, ApiItemKind } from '@microsoft/api-extractor-model';
} from '@discordjs/api-extractor-model';
import { ApiDeclaredItem, ApiItemKind } from '@discordjs/api-extractor-model';
import { generateTypeParamData } from './TypeParameterJSONEncoder.js';
import { type TokenDocumentation, resolveName, genReference, genToken, genParameter, generatePath } from './parse.js';
import type { DocBlockJSON } from './tsdoc/CommentBlock.js';

View File

@@ -1,4 +1,4 @@
import type { TypeParameter, ApiModel, ApiItem } from '@microsoft/api-extractor-model';
import type { TypeParameter, ApiModel, ApiItem } from '@discordjs/api-extractor-model';
import { type TokenDocumentation, genToken } from './parse.js';
import { type DocBlockJSON, block } from './tsdoc/CommentBlock.js';

View File

@@ -11,7 +11,7 @@ import {
type Parameter,
type ApiFunction,
ApiDeclaredItem,
} from '@microsoft/api-extractor-model';
} from '@discordjs/api-extractor-model';
import type { DocNode, DocParagraph, DocPlainText } from '@microsoft/tsdoc';
import { type Meaning, ModuleSource } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference.js';
import type { DocBlockJSON } from './tsdoc/CommentBlock.js';

View File

@@ -1,4 +1,4 @@
import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model';
import type { ApiModel, ApiItem } from '@discordjs/api-extractor-model';
import type { DocBlock } from '@microsoft/tsdoc';
import { blockTag, type DocBlockTagJSON } from './CommentBlockTag.js';
import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js';

View File

@@ -1,4 +1,4 @@
import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model';
import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model';
import type { DocNodeContainer } from '@microsoft/tsdoc';
import { type AnyDocNodeJSON, type DocNodeJSON, node } from './CommentNode.js';
import { createCommentNode } from './index.js';

View File

@@ -1,4 +1,4 @@
import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model';
import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model';
import type { DocDeclarationReference, DocLinkTag } from '@microsoft/tsdoc';
import { resolveName, generatePath } from '../parse.js';
import { type DocNodeJSON, node } from './CommentNode.js';

View File

@@ -1,4 +1,4 @@
import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model';
import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model';
import type { DocParamBlock } from '@microsoft/tsdoc';
import { block, type DocBlockJSON } from './CommentBlock.js';

View File

@@ -1,4 +1,4 @@
import type { ApiItem, ApiModel } from '@microsoft/api-extractor-model';
import type { ApiItem, ApiModel } from '@discordjs/api-extractor-model';
import type { DocComment } from '@microsoft/tsdoc';
import { block, type DocBlockJSON } from './CommentBlock.js';
import { type DocNodeJSON, node } from './CommentNode.js';

View File

@@ -1,4 +1,4 @@
import type { ApiModel, ApiItem } from '@microsoft/api-extractor-model';
import type { ApiModel, ApiItem } from '@discordjs/api-extractor-model';
import {
type DocNode,
type DocPlainText,

23
packages/api-extractor/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Packages
node_modules
# Log files
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Env
.env
# Dist
dist
# Miscellaneous
.turbo
.tmp
coverage

View File

@@ -0,0 +1 @@
module.exports = require('../../.lintstagedrc.json');

View File

@@ -0,0 +1,32 @@
# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO.
# Ignore all files by default, to avoid accidentally publishing unintended files.
*
# Use negative patterns to bring back the specific things we want to publish.
!/bin/**
!/lib/**
!/lib-*/**
!/dist/**
!ThirdPartyNotice.txt
# Ignore certain patterns that should not get published.
/dist/*.stats.*
/lib/**/test/
/lib-*/**/test/
*.test.js
# NOTE: These don't need to be specified, because NPM includes them automatically.
#
# package.json
# README (and its variants)
# CHANGELOG (and its variants)
# LICENSE / LICENCE
#--------------------------------------------
# DO NOT MODIFY THE TEMPLATE ABOVE THIS LINE
#--------------------------------------------
# (Add your project-specific overrides here)
!/extends/*.json

View File

@@ -0,0 +1,5 @@
.turbo
coverage
dist
CHANGELOG.md
tsup.config.bundled*

View File

@@ -0,0 +1 @@
module.exports = require('../../.prettierrc.json');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
@microsoft/api-extractor
Copyright (c) Microsoft Corporation. All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,49 @@
# @microsoft/api-extractor
![API Extractor](https://github.com/microsoft/rushstack/raw/main/common/wiki-images/api-extractor-title.png?raw=true)
<br />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; https://api-extractor.com/
<!-- ------------------------------------------------------------------ -->
<!-- Text below this line should stay in sync with the web site content -->
<!-- ------------------------------------------------------------------ -->
**API Extractor** helps you build better [TypeScript](https://www.typescriptlang.org/) library packages. Suppose for example that your company has published an NPM package called "**awesome-widgets**" that exports many classes and interfaces. As developers start to depend on your library, you may encounter issues such as...
- **Accidental breaks:** People keep reporting that their code won't compile after a supposedly "minor" update. To address this, you boldly propose that every **awesome-widgets** pull request must be approved by an experienced developer from your team. But that proves unrealistic -- nobody has time to look at every single PR! What you really need is a way to detect PRs that change API contracts, and flag them for review. That would focus attention in the right place... but how to do that?
- **Missing exports:** Suppose the **awesome-widgets** package exports an API function `AwesomeButton.draw()` that requires a parameter of type `DrawStyle`, but you forgot to export this enum. Things seem fine at first, but when a developer tries to call that function, they discover that there's no way to specify the `DrawStyle`. How to avoid these oversights?
- **Accidental exports:** You meant for your `DrawHelper` class to be kept internal, but one day you realize it's being exported. When you try to remove it, consumers complain that they're using it. How do we avoid this in the future?
- **Alpha/Beta graduation:** You want to release previews of new APIs that are not ready for prime time yet. But if you did a major SemVer bump every time these definitions evolve, the villagers would be after you with torches and pitchforks! A better approach is to designate certain classes/members as **alpha** quality, then promote them to **beta** and finally to **public** as they mature. But how to indicate this to your consumers? (And how to detect scoping mistakes? A **public** function should never return a **beta** result.)
- **\*.d.ts rollup:** You webpacked your library into a nice **\*.js** bundle file -- so why ship your typings as a messy tree of **lib/\*.d.ts** files full of private definitions? Can't we consolidate them into a tidy **\*.d.ts** rollup file? And if you publish internal/beta/public releases, each release type should get its own **\*.d.ts** file with appropriate trimming. Developers building a production project don't want to see a bunch of **internal** and **beta** members in their VS Code IntelliSense!
- **Online documentation:** You have faithfully annotated each TypeScript member with nice [TSDoc](https://github.com/microsoft/tsdoc) descriptions. Now that your library has shipped, it's time to set up [a nicely formatted](https://docs.microsoft.com/en-us/javascript/api/sp-http) API reference. What tool to use?
**API Extractor** provides an integrated, professional-quality solution for all these problems. It is invoked at build time by your toolchain and leverages the TypeScript compiler engine to:
- Detect a project's exported API surface
- Capture the contracts in a concise report designed to facilitate review
- Warn about common mistakes (e.g. missing exports, inconsistent visibility, etc.)
- Generate \*.d.ts rollups with trimming according to release type
- Output API documentation in a portable format that's easy to integrate with your content pipeline
Best of all, **API Extractor** is free and open source. Join the community and create a pull request!
<!-- ------------------------------------------------------------------ -->
<!-- Text above this line should stay in sync with the web site content -->
<!-- ------------------------------------------------------------------ -->
## Getting Started
For more details and support resources, please visit: https://api-extractor.com/
## Links
- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/apps/api-extractor/CHANGELOG.md) - Find
out what's new in the latest version
- [API Reference](https://rushstack.io/pages/api/api-extractor/)
API Extractor is part of the [Rush Stack](https://rushstack.io/) family of projects.

View File

@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../dist/start.js');

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"mainEntryPointFilePath": "<projectFolder>/lib/index.d.ts",
"apiReport": {
"enabled": true,
"reportFolder": "../../../common/reviews/api"
},
"docModel": {
"enabled": true,
"apiJsonFilePath": "../../../common/temp/api/<unscopedPackageName>.api.json"
},
"dtsRollup": {
"enabled": true,
"untrimmedFilePath": "<projectFolder>/dist/rollup.d.ts"
}
}

View File

@@ -0,0 +1,64 @@
{
// By default, don't hide console output
"silent": false,
// In order for HeftJestReporter to receive console.log() events, we must set verbose=false
"verbose": false,
// If mocks are not cleared between tests, it opens the door to accidental reliance on
// ordering of tests or describe blocks, eventually resulting in intermittent failures.
//
// We suggest this setting for any heft project (in a monorepo or not).
"clearMocks": true,
// "Adding '<rootDir>/dist' here enables dist/__mocks__ to be used for mocking Node.js system modules
"roots": ["<rootDir>/dist"],
"testMatch": ["<rootDir>/dist/**/*.test.{cjs,js}"],
"testPathIgnorePatterns": ["/node_modules/"],
"collectCoverageFrom": [
"dist/**/*.{cjs,js}",
"!dist/**/*.d.ts",
"!dist/**/*.test.{cjs,js}",
"!dist/**/test/**",
"!dist/**/__tests__/**",
"!dist/**/__fixtures__/**",
"!dist/**/__mocks__/**"
],
"coveragePathIgnorePatterns": ["/node_modules/"],
"testEnvironment": "jest-environment-node",
"testEnvironmentOptions": {
"url": "http://localhost/",
"customExportConditions": ["require", "node"]
},
// Retain pre-Jest 29 snapshot behavior
"snapshotFormat": {
"escapeString": true,
"printBasicPrototype": true
},
// Instruct jest not to run the transformer pipeline by default on JS files. The output files from TypeScript
// will already be fully transformed, so this avoids redundant file system operations.
"transformIgnorePatterns": ["\\.c?js$"],
// The modulePathIgnorePatterns below accepts these sorts of paths:
// - <rootDir>/dist
// - <rootDir>/dist/file.js
// ...and ignores anything else under <rootDir>
"modulePathIgnorePatterns": [],
// Prefer .cjs to .js to catch explicit commonjs output. Optimize for local files, which will be .js
"moduleFileExtensions": ["cjs", "js", "json", "node"],
// Enable code coverage for Jest
"collectCoverage": true,
"coverageDirectory": "<rootDir>/coverage",
"coverageReporters": ["cobertura", "html"],
// Use v8 coverage provider to avoid Babel
"coverageProvider": "v8"
}

View File

@@ -0,0 +1,72 @@
/**
* This file defines the TSDoc custom tags for use with API Extractor.
*
* If your project has a custom tsdoc.json file, then it should use the "extends" field to
* inherit the definitions from this file. For example:
*
* ```
* {
* "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
* "extends": [ "@microsoft/api-extractor/extends/tsdoc-config.json" ],
* . . .
* }
* ```
*
* For details about this config file, please see: https://tsdoc.org/pages/packages/tsdoc-config/
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
/**
* The "AEDoc" custom tags:
*/
"tagDefinitions": [
{
"tagName": "@betaDocumentation",
"syntaxKind": "modifier"
},
{
"tagName": "@internalRemarks",
"syntaxKind": "block"
},
{
"tagName": "@preapproved",
"syntaxKind": "modifier"
}
],
/**
* TSDoc tags implemented by API Extractor:
*/
"supportForTags": {
"@alpha": true,
"@beta": true,
"@defaultValue": true,
"@decorator": true,
"@deprecated": true,
"@eventProperty": true,
"@example": true,
"@experimental": true,
"@inheritDoc": true,
"@internal": true,
"@label": true,
"@link": true,
"@override": true,
"@packageDocumentation": true,
"@param": true,
"@privateRemarks": true,
"@public": true,
"@readonly": true,
"@remarks": true,
"@returns": true,
"@sealed": true,
"@see": true,
"@throws": true,
"@typeParam": true,
"@virtual": true,
"@betaDocumentation": true,
"@internalRemarks": true,
"@preapproved": true
}
}

View File

@@ -0,0 +1,81 @@
{
"name": "@discordjs/api-extractor",
"version": "7.38.1",
"description": "Analyze the exported API for a TypeScript library and generate reviews, documentation, and .d.ts rollups",
"private": true,
"keywords": [
"typescript",
"API",
"JSDoc",
"AEDoc",
"TSDoc",
"generate",
"documentation",
"declaration",
"dts",
".d.ts",
"rollup",
"bundle",
"compiler",
"alpha",
"beta"
],
"repository": {
"type": "git",
"url": "https://github.com/discordjs/discord.js.git",
"directory": "packages/api-extractor"
},
"homepage": "https://discord.js.org",
"bin": {
"api-extractor": "./bin/api-extractor"
},
"license": "MIT",
"scripts": {
"build": "tsc --noEmit && tsup && pnpm run copy-files",
"copy-files": "cpy 'src/schemas/*.json' 'dist/schemas' && cpy 'extends/*.json' 'dist/extends'",
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src"
},
"exports": {
".": {
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
}
},
"dependencies": {
"@discordjs/api-extractor-model": "workspace:^",
"@microsoft/tsdoc": "0.14.2",
"@microsoft/tsdoc-config": "0.16.2",
"@rushstack/node-core-library": "3.61.0",
"@rushstack/rig-package": "0.5.1",
"@rushstack/ts-command-line": "4.17.1",
"colors": "~1.2.1",
"lodash": "~4.17.15",
"resolve": "~1.22.1",
"semver": "~7.5.4",
"source-map": "~0.6.1",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/lodash": "^4.14.200",
"@types/node": "^18.18.8",
"@types/resolve": "^1.20.4",
"@types/semver": "^7.5.0",
"@types/jest": "^29.5.7",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.3",
"eslint": "^8.53.0",
"eslint-config-neon": "^0.1.57",
"eslint-formatter-pretty": "^5.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"tsup": "^7.2.0",
"turbo": "^1.10.16"
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as ts from 'typescript';
import { ExtractorMessageId } from '../api/ExtractorMessageId.js';
import type { Collector } from '../collector/Collector.js';
export class PackageDocComment {
/**
* For the given source file, see if it starts with a TSDoc comment containing the `@packageDocumentation` tag.
*/
public static tryFindInSourceFile(sourceFile: ts.SourceFile, collector: Collector): ts.TextRange | undefined {
// The @packageDocumentation comment is special because it is not attached to an AST
// definition. Instead, it is part of the "trivia" tokens that the compiler treats
// as irrelevant white space.
//
// WARNING: If the comment doesn't precede an export statement, the compiler will omit
// it from the *.d.ts file, and API Extractor won't find it. If this happens, you need
// to rearrange your statements to ensure it is passed through.
//
// This implementation assumes that the "@packageDocumentation" will be in the first TSDoc comment
// that appears in the entry point *.d.ts file. We could possibly look in other places,
// but the above warning suggests enforcing a standardized layout. This design choice is open
// to feedback.
let packageCommentRange: ts.TextRange | undefined; // empty string
for (const commentRange of ts.getLeadingCommentRanges(sourceFile.text, sourceFile.getFullStart()) ?? []) {
if (commentRange.kind === ts.SyntaxKind.MultiLineCommentTrivia) {
const commentBody: string = sourceFile.text.slice(commentRange.pos, commentRange.end);
// Choose the first JSDoc-style comment
if (/^\s*\/\*\*/.test(commentBody)) {
// But only if it looks like it's trying to be @packageDocumentation
// (The TSDoc parser will validate this more rigorously)
if (/@packagedocumentation/i.test(commentBody)) {
packageCommentRange = commentRange;
}
break;
}
}
}
if (!packageCommentRange) {
// If we didn't find the @packageDocumentation tag in the expected place, is it in some
// wrong place? This sanity check helps people to figure out why there comment isn't working.
for (const statement of sourceFile.statements) {
const ranges: ts.CommentRange[] = [];
ranges.push(...(ts.getLeadingCommentRanges(sourceFile.text, statement.getFullStart()) ?? []));
ranges.push(...(ts.getTrailingCommentRanges(sourceFile.text, statement.getEnd()) ?? []));
for (const commentRange of ranges) {
const commentBody: string = sourceFile.text.slice(commentRange.pos, commentRange.end);
if (/@packagedocumentation/i.test(commentBody)) {
collector.messageRouter.addAnalyzerIssueForPosition(
ExtractorMessageId.MisplacedPackageTag,
'The @packageDocumentation comment must appear at the top of entry point *.d.ts file',
sourceFile,
commentRange.pos,
);
break;
}
}
}
}
return packageCommentRange;
}
}

View File

@@ -0,0 +1,288 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { AstEntity } from './AstEntity.js';
import type { AstSymbol } from './AstSymbol.js';
import { Span } from './Span.js';
/**
* Constructor options for AstDeclaration
*/
export interface IAstDeclarationOptions {
readonly astSymbol: AstSymbol;
readonly declaration: ts.Declaration;
readonly parent: AstDeclaration | undefined;
}
/**
* The AstDeclaration and AstSymbol classes are API Extractor's equivalent of the compiler's
* ts.Declaration and ts.Symbol objects. They are created by the `AstSymbolTable` class.
*
* @remarks
* The AstDeclaration represents one or more syntax components of a symbol. Usually there is
* only one AstDeclaration per AstSymbol, but certain TypeScript constructs can have multiple
* declarations (e.g. overloaded functions, merged declarations, etc.).
*
* Because of this, the `AstDeclaration` manages the parent/child nesting hierarchy (e.g. with
* declaration merging, each declaration has its own children) and becomes the main focus
* of analyzing AEDoc and emitting *.d.ts files.
*
* The AstDeclarations correspond to items from the compiler's ts.Node hierarchy, but
* omitting/skipping any nodes that don't match the AstDeclaration.isSupportedSyntaxKind()
* criteria. This simplification makes the other API Extractor stages easier to implement.
*/
export class AstDeclaration {
public readonly declaration: ts.Declaration;
public readonly astSymbol: AstSymbol;
/**
* The parent, if this object is nested inside another AstDeclaration.
*/
public readonly parent: AstDeclaration | undefined;
/**
* A bit set of TypeScript modifiers such as "private", "protected", etc.
*/
public readonly modifierFlags: ts.ModifierFlags;
/**
* Additional information that is calculated later by the `Collector`. The actual type is `DeclarationMetadata`,
* but we declare it as `unknown` because consumers must obtain this object by calling
* `Collector.fetchDeclarationMetadata()`.
*/
public declarationMetadata: unknown;
/**
* Additional information that is calculated later by the `Collector`. The actual type is `ApiItemMetadata`,
* but we declare it as `unknown` because consumers must obtain this object by calling
* `Collector.fetchApiItemMetadata()`.
*/
public apiItemMetadata: unknown;
// NOTE: This array becomes immutable after astSymbol.analyze() sets astSymbol.analyzed=true
private readonly _analyzedChildren: AstDeclaration[] = [];
private readonly _analyzedReferencedAstEntitiesSet: Set<AstEntity> = new Set<AstEntity>();
// Reverse lookup used by findChildrenWithName()
private _childrenByName: Map<string, AstDeclaration[]> | undefined = undefined;
public constructor(options: IAstDeclarationOptions) {
this.declaration = options.declaration;
this.astSymbol = options.astSymbol;
this.parent = options.parent;
this.astSymbol._notifyDeclarationAttach(this);
if (this.parent) {
this.parent._notifyChildAttach(this);
}
this.modifierFlags = ts.getCombinedModifierFlags(this.declaration);
// Check for ECMAScript private fields, for example:
//
// class Person { #name: string; }
//
const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(this.declaration);
if (declarationName && ts.isPrivateIdentifier(declarationName)) {
this.modifierFlags |= ts.ModifierFlags.Private;
}
}
/**
* Returns the children for this AstDeclaration.
*
* @remarks
* The collection will be empty until AstSymbol.analyzed is true.
*/
public get children(): readonly AstDeclaration[] {
return this.astSymbol.analyzed ? this._analyzedChildren : [];
}
/**
* Returns the AstEntity objects referenced by this node.
*
* @remarks
* NOTE: The collection will be empty until AstSymbol.analyzed is true.
*
* Since we assume references are always collected by a traversal starting at the
* root of the nesting declarations, this array omits the following items because they
* would be redundant:
* - symbols corresponding to parents of this declaration (e.g. a method that returns its own class)
* - symbols already listed in the referencedAstSymbols property for parents of this declaration
* (e.g. a method that returns its own class's base class)
* - symbols that are referenced only by nested children of this declaration
* (e.g. if a method returns an enum, this doesn't imply that the method's class references that enum)
*/
public get referencedAstEntities(): readonly AstEntity[] {
return this.astSymbol.analyzed ? [...this._analyzedReferencedAstEntitiesSet] : [];
}
/**
* This is an internal callback used when the AstSymbolTable attaches a new
* child AstDeclaration to this object.
*
* @internal
*/
public _notifyChildAttach(child: AstDeclaration): void {
if (child.parent !== this) {
throw new InternalError('Invalid call to notifyChildAttach()');
}
if (this.astSymbol.analyzed) {
throw new InternalError('_notifyChildAttach() called after analysis is already complete');
}
this._analyzedChildren.push(child);
}
/**
* Returns a diagnostic dump of the tree, which reports the hierarchy of
* AstDefinition objects.
*/
public getDump(indent: string = ''): string {
const declarationKind: string = ts.SyntaxKind[this.declaration.kind];
let result: string = indent + `+ ${this.astSymbol.localName} (${declarationKind})`;
if (this.astSymbol.nominalAnalysis) {
result += ' (nominal)';
}
result += '\n';
for (const referencedAstEntity of this._analyzedReferencedAstEntitiesSet.values()) {
result += indent + ` ref: ${referencedAstEntity.localName}\n`;
}
for (const child of this.children) {
result += child.getDump(indent + ' ');
}
return result;
}
/**
* Returns a diagnostic dump using Span.getDump(), which reports the detailed
* compiler structure.
*/
public getSpanDump(indent: string = ''): string {
const span: Span = new Span(this.declaration);
return span.getDump(indent);
}
/**
* This is an internal callback used when AstSymbolTable.analyze() discovers a new
* type reference associated with this declaration.
*
* @internal
*/
public _notifyReferencedAstEntity(referencedAstEntity: AstEntity): void {
if (this.astSymbol.analyzed) {
throw new InternalError('_notifyReferencedAstEntity() called after analysis is already complete');
}
for (let current: AstDeclaration | undefined = this; current; current = current.parent) {
// Don't add references to symbols that are already referenced by a parent
if (current._analyzedReferencedAstEntitiesSet.has(referencedAstEntity)) {
return;
}
// Don't add the symbols of parents either
if (referencedAstEntity === current.astSymbol) {
return;
}
}
this._analyzedReferencedAstEntitiesSet.add(referencedAstEntity);
}
/**
* Visits all the current declaration and all children recursively in a depth-first traversal,
* and performs the specified action for each one.
*/
public forEachDeclarationRecursive(action: (astDeclaration: AstDeclaration) => void): void {
action(this);
for (const child of this.children) {
child.forEachDeclarationRecursive(action);
}
}
/**
* Returns the list of child declarations whose `AstSymbol.localName` matches the provided `name`.
*
* @remarks
* This is an efficient O(1) lookup.
*/
public findChildrenWithName(name: string): readonly AstDeclaration[] {
// The children property returns:
//
// return this.astSymbol.analyzed ? this._analyzedChildren : [];
//
if (!this.astSymbol.analyzed || this._analyzedChildren.length === 0) {
return [];
}
if (this._childrenByName === undefined) {
// Build the lookup table
const childrenByName: Map<string, AstDeclaration[]> = new Map<string, AstDeclaration[]>();
for (const child of this._analyzedChildren) {
const childName: string = child.astSymbol.localName;
let array: AstDeclaration[] | undefined = childrenByName.get(childName);
if (array === undefined) {
array = [];
childrenByName.set(childName, array);
}
array.push(child);
}
this._childrenByName = childrenByName;
}
return this._childrenByName.get(name) ?? [];
}
/**
* This function determines which ts.Node kinds will generate an AstDeclaration.
* These correspond to the definitions that we can add AEDoc to.
*/
public static isSupportedSyntaxKind(kind: ts.SyntaxKind): boolean {
// (alphabetical order)
switch (kind) {
case ts.SyntaxKind.CallSignature:
case ts.SyntaxKind.ClassDeclaration:
case ts.SyntaxKind.ConstructSignature: // Example: "new(x: number): IMyClass"
case ts.SyntaxKind.Constructor: // Example: "constructor(x: number)"
case ts.SyntaxKind.EnumDeclaration:
case ts.SyntaxKind.EnumMember:
case ts.SyntaxKind.FunctionDeclaration: // Example: "(x: number): number"
case ts.SyntaxKind.GetAccessor:
case ts.SyntaxKind.SetAccessor:
case ts.SyntaxKind.IndexSignature: // Example: "[key: string]: string"
case ts.SyntaxKind.InterfaceDeclaration:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
case ts.SyntaxKind.ModuleDeclaration: // Used for both "module" and "namespace" declarations
case ts.SyntaxKind.PropertyDeclaration:
case ts.SyntaxKind.PropertySignature:
case ts.SyntaxKind.TypeAliasDeclaration: // Example: "type Shape = Circle | Square"
case ts.SyntaxKind.VariableDeclaration:
return true;
// NOTE: Prior to TypeScript 3.7, in the emitted .d.ts files, the compiler would merge a GetAccessor/SetAccessor
// pair into a single PropertyDeclaration.
// NOTE: In contexts where a source file is treated as a module, we do create "nominal analysis"
// AstSymbol objects corresponding to a ts.SyntaxKind.SourceFile node. However, a source file
// is NOT considered a nesting structure, and it does NOT act as a root for the declarations
// appearing in the file. This is because the *.d.ts generator is in the business of rolling up
// source files, and thus wants to ignore them in general.
default:
return false;
}
}
}

View File

@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/**
* `AstEntity` is the abstract base class for analyzer objects that can become a `CollectorEntity`.
*
* @remarks
*
* The subclasses are:
* ```
* - AstEntity
* - AstSymbol
* - AstSyntheticEntity
* - AstImport
* - AstNamespaceImport
* ```
*/
export abstract class AstEntity {
/**
* The original name of the symbol, as exported from the module (i.e. source file)
* containing the original TypeScript definition. Constructs such as
* `import { X as Y } from` may introduce other names that differ from the local name.
*
* @remarks
* For the most part, `localName` corresponds to `followedSymbol.name`, but there
* are some edge cases. For example, the ts.Symbol.name for `export default class X { }`
* is actually `"default"`, not `"X"`.
*/
public abstract readonly localName: string;
}
/**
* `AstSyntheticEntity` is the abstract base class for analyzer objects whose emitted declarations
* are not text transformations performed by the `Span` helper.
*
* @remarks
* Most of API Extractor's output is produced by using the using the `Span` utility to regurgitate strings from
* the input .d.ts files. If we need to rename an identifier, the `Span` visitor can pick out an interesting
* node and rewrite its string, but otherwise the transformation operates on dumb text and not compiler concepts.
* (Historically we did this because the compiler's emitter was an internal API, but it still has some advantages,
* for example preserving syntaxes generated by an older compiler to avoid incompatibilities.)
*
* This strategy does not work for cases where the output looks very different from the input. Today these
* cases are always kinds of `import` statements, but that may change in the future.
*/
export abstract class AstSyntheticEntity extends AstEntity {}

View File

@@ -0,0 +1,168 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import { AstSyntheticEntity } from './AstEntity.js';
import type { AstSymbol } from './AstSymbol.js';
/**
* Indicates the import kind for an `AstImport`.
*/
export enum AstImportKind {
/**
* An import statement such as `import X from "y";`.
*/
DefaultImport,
/**
* An import statement such as `import { X } from "y";`.
*/
NamedImport,
/**
* An import statement such as `import * as x from "y";`.
*/
StarImport,
/**
* An import statement such as `import x = require("y");`.
*/
EqualsImport,
/**
* An import statement such as `interface foo { foo: import("bar").a.b.c }`.
*/
ImportType,
}
/**
* Constructor parameters for AstImport
*
* @privateRemarks
* Our naming convention is to use I____Parameters for constructor options and
* I____Options for general function options. However the word "parameters" is
* confusingly similar to the terminology for function parameters modeled by API Extractor,
* so we use I____Options for both cases in this code base.
*/
export interface IAstImportOptions {
readonly exportName: string;
readonly importKind: AstImportKind;
readonly isTypeOnly: boolean;
readonly modulePath: string;
}
/**
* For a symbol that was imported from an external package, this tracks the import
* statement that was used to reach it.
*/
export class AstImport extends AstSyntheticEntity {
public readonly importKind: AstImportKind;
/**
* The name of the external package (and possibly module path) that this definition
* was imported from.
*
* Example: "\@rushstack/node-core-library/lib/FileSystem"
*/
public readonly modulePath: string;
/**
* The name of the symbol being imported.
*
* @remarks
*
* The name depends on the type of import:
*
* ```ts
* // For AstImportKind.DefaultImport style, exportName would be "X" in this example:
* import X from "y";
*
* // For AstImportKind.NamedImport style, exportName would be "X" in this example:
* import { X } from "y";
*
* // For AstImportKind.StarImport style, exportName would be "x" in this example:
* import * as x from "y";
*
* // For AstImportKind.EqualsImport style, exportName would be "x" in this example:
* import x = require("y");
*
* // For AstImportKind.ImportType style, exportName would be "a.b.c" in this example:
* interface foo { foo: import('bar').a.b.c };
* ```
*/
public readonly exportName: string;
/**
* Whether it is a type-only import, for example:
*
* ```ts
* import type { X } from "y";
* ```
*
* This is set to true ONLY if the type-only form is used in *every* reference to this AstImport.
*/
public isTypeOnlyEverywhere: boolean;
/**
* If this import statement refers to an API from an external package that is tracked by API Extractor
* (according to `PackageMetadataManager.isAedocSupportedFor()`), then this property will return the
* corresponding AstSymbol. Otherwise, it is undefined.
*/
public astSymbol: AstSymbol | undefined;
/**
* If modulePath and exportName are defined, then this is a dictionary key
* that combines them with a colon (":").
*
* Example: "\@rushstack/node-core-library/lib/FileSystem:FileSystem"
*/
public readonly key: string;
public constructor(options: IAstImportOptions) {
super();
this.importKind = options.importKind;
this.modulePath = options.modulePath;
this.exportName = options.exportName;
// We start with this assumption, but it may get changed later if non-type-only import is encountered.
this.isTypeOnlyEverywhere = options.isTypeOnly;
this.key = AstImport.getKey(options);
}
/**
* {@inheritdoc}
*/
public get localName(): string {
// abstract
return this.exportName;
}
/**
* Calculates the lookup key used with `AstImport.key`
*/
public static getKey(options: IAstImportOptions): string {
switch (options.importKind) {
case AstImportKind.DefaultImport:
return `${options.modulePath}:${options.exportName}`;
case AstImportKind.NamedImport:
return `${options.modulePath}:${options.exportName}`;
case AstImportKind.StarImport:
return `${options.modulePath}:*`;
case AstImportKind.EqualsImport:
return `${options.modulePath}:=`;
case AstImportKind.ImportType: {
const subKey: string = options.exportName
? options.exportName.includes('.') // Equivalent to a named export
? options.exportName.split('.')[0]!
: options.exportName
: '*';
return `${options.modulePath}:${subKey}`;
}
default:
throw new InternalError('Unknown AstImportKind');
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type * as ts from 'typescript';
import type { AstEntity } from './AstEntity.js';
import type { AstSymbol } from './AstSymbol.js';
/**
* Represents information collected by {@link AstSymbolTable.fetchAstModuleExportInfo}
*/
export class AstModuleExportInfo {
public readonly exportedLocalEntities: Map<string, AstEntity> = new Map<string, AstEntity>();
public readonly starExportedExternalModules: Set<AstModule> = new Set<AstModule>();
}
/**
* Constructor parameters for AstModule
*
* @privateRemarks
* Our naming convention is to use I____Parameters for constructor options and
* I____Options for general function options. However the word "parameters" is
* confusingly similar to the terminology for function parameters modeled by API Extractor,
* so we use I____Options for both cases in this code base.
*/
export interface IAstModuleOptions {
externalModulePath: string | undefined;
moduleSymbol: ts.Symbol;
sourceFile: ts.SourceFile;
}
/**
* An internal data structure that represents a source file that is analyzed by AstSymbolTable.
*/
export class AstModule {
/**
* The source file that declares this TypeScript module. In most cases, the source file's
* top-level exports constitute the module.
*/
public readonly sourceFile: ts.SourceFile;
/**
* The symbol for the module. Typically this corresponds to ts.SourceFile itself, however
* in some cases the ts.SourceFile may contain multiple modules declared using the `module` keyword.
*/
public readonly moduleSymbol: ts.Symbol;
/**
* Example: "\@rushstack/node-core-library/lib/FileSystem"
* but never: "./FileSystem"
*/
public readonly externalModulePath: string | undefined;
/**
* A list of other `AstModule` objects that appear in `export * from "___";` statements.
*/
public readonly starExportedModules: Set<AstModule>;
/**
* A partial map of entities exported by this module. The key is the exported name.
*/
public readonly cachedExportedEntities: Map<string, AstEntity>; // exportName --> entity
/**
* Additional state calculated by `AstSymbolTable.fetchWorkingPackageModule()`.
*/
public astModuleExportInfo: AstModuleExportInfo | undefined;
public constructor(options: IAstModuleOptions) {
this.sourceFile = options.sourceFile;
this.moduleSymbol = options.moduleSymbol;
this.externalModulePath = options.externalModulePath;
this.starExportedModules = new Set<AstModule>();
this.cachedExportedEntities = new Map<string, AstSymbol>();
this.astModuleExportInfo = undefined;
}
/**
* If false, then this source file is part of the working package being processed by the `Collector`.
*/
public get isExternal(): boolean {
return this.externalModulePath !== undefined;
}
}

View File

@@ -0,0 +1,95 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import type * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import { AstSyntheticEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
export interface IAstNamespaceImportOptions {
readonly astModule: AstModule;
readonly declaration: ts.Declaration;
readonly namespaceName: string;
readonly symbol: ts.Symbol;
}
/**
* `AstNamespaceImport` represents a namespace that is created implicitly by a statement
* such as `import * as example from "./file";`
*
* @remarks
*
* A typical input looks like this:
* ```ts
* // Suppose that example.ts exports two functions f1() and f2().
* import * as example from "./file";
* export { example };
* ```
*
* API Extractor's .d.ts rollup will transform it into an explicit namespace, like this:
* ```ts
* declare f1(): void;
* declare f2(): void;
*
* declare namespace example {
* export {
* f1,
* f2
* }
* }
* ```
*
* The current implementation does not attempt to relocate f1()/f2() to be inside the `namespace`
* because other type signatures may reference them directly (without using the namespace qualifier).
* The `declare namespace example` is a synthetic construct represented by `AstNamespaceImport`.
*/
export class AstNamespaceImport extends AstSyntheticEntity {
/**
* Returns true if the AstSymbolTable.analyze() was called for this object.
* See that function for details.
*/
public analyzed: boolean = false;
/**
* For example, if the original statement was `import * as example from "./file";`
* then `astModule` refers to the `./file.d.ts` file.
*/
public readonly astModule: AstModule;
/**
* For example, if the original statement was `import * as example from "./file";`
* then `namespaceName` would be `example`.
*/
public readonly namespaceName: string;
/**
* The original `ts.SyntaxKind.NamespaceImport` which can be used as a location for error messages.
*/
public readonly declaration: ts.Declaration;
/**
* The original `ts.SymbolFlags.Namespace` symbol.
*/
public readonly symbol: ts.Symbol;
public constructor(options: IAstNamespaceImportOptions) {
super();
this.astModule = options.astModule;
this.namespaceName = options.namespaceName;
this.declaration = options.declaration;
this.symbol = options.symbol;
}
/**
* {@inheritdoc}
*/
public get localName(): string {
// abstract
return this.namespaceName;
}
public fetchAstModuleExportInfo(collector: Collector): AstModuleExportInfo {
const astModuleExportInfo: AstModuleExportInfo = collector.astSymbolTable.fetchAstModuleExportInfo(this.astModule);
return astModuleExportInfo;
}
}

View File

@@ -0,0 +1,296 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as tsdoc from '@microsoft/tsdoc';
import * as ts from 'typescript';
import type { Collector } from '../collector/Collector.js';
import type { DeclarationMetadata } from '../collector/DeclarationMetadata.js';
import type { WorkingPackage } from '../collector/WorkingPackage.js';
import type { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule } from './AstModule.js';
import { AstSymbol } from './AstSymbol.js';
import type { AstSymbolTable } from './AstSymbolTable.js';
/**
* Used by `AstReferenceResolver` to report a failed resolution.
*
* @privateRemarks
* This class is similar to an `Error` object, but the intent of `ResolverFailure` is to describe
* why a reference could not be resolved. This information could be used to throw an actual `Error` object,
* but normally it is handed off to the `MessageRouter` instead.
*/
export class ResolverFailure {
/**
* Details about why the failure occurred.
*/
public readonly reason: string;
public constructor(reason: string) {
this.reason = reason;
}
}
/**
* This resolves a TSDoc declaration reference by walking the `AstSymbolTable` compiler state.
*
* @remarks
*
* This class is analogous to `ModelReferenceResolver` from the `@microsoft/api-extractor-model` project,
* which resolves declaration references by walking the hierarchy loaded from an .api.json file.
*/
export class AstReferenceResolver {
private readonly _collector: Collector;
private readonly _astSymbolTable: AstSymbolTable;
private readonly _workingPackage: WorkingPackage;
public constructor(collector: Collector) {
this._collector = collector;
this._astSymbolTable = collector.astSymbolTable;
this._workingPackage = collector.workingPackage;
}
public resolve(declarationReference: tsdoc.DocDeclarationReference): AstDeclaration | ResolverFailure {
// Is it referring to the working package?
if (
declarationReference.packageName !== undefined &&
declarationReference.packageName !== this._workingPackage.name
) {
return new ResolverFailure('External package references are not supported');
}
// Is it a path-based import?
if (declarationReference.importPath) {
return new ResolverFailure('Import paths are not supported');
}
const astModule: AstModule = this._astSymbolTable.fetchAstModuleFromWorkingPackage(
this._workingPackage.entryPointSourceFile,
);
if (declarationReference.memberReferences.length === 0) {
return new ResolverFailure('Package references are not supported');
}
const rootMemberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[0]!;
const exportName: ResolverFailure | string = this._getMemberReferenceIdentifier(rootMemberReference);
if (exportName instanceof ResolverFailure) {
return exportName;
}
const rootAstEntity: AstEntity | undefined = this._astSymbolTable.tryGetExportOfAstModule(exportName, astModule);
if (rootAstEntity === undefined) {
return new ResolverFailure(`The package "${this._workingPackage.name}" does not have an export "${exportName}"`);
}
if (!(rootAstEntity instanceof AstSymbol)) {
return new ResolverFailure('This type of declaration is not supported yet by the resolver');
}
let currentDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
rootAstEntity.astDeclarations,
rootMemberReference,
rootAstEntity.localName,
);
if (currentDeclaration instanceof ResolverFailure) {
return currentDeclaration;
}
for (let index = 1; index < declarationReference.memberReferences.length; ++index) {
const memberReference: tsdoc.DocMemberReference = declarationReference.memberReferences[index]!;
const memberName: ResolverFailure | string = this._getMemberReferenceIdentifier(memberReference);
if (memberName instanceof ResolverFailure) {
return memberName;
}
const matchingChildren: readonly AstDeclaration[] = currentDeclaration.findChildrenWithName(memberName);
if (matchingChildren.length === 0) {
return new ResolverFailure(`No member was found with name "${memberName}"`);
}
const selectedDeclaration: AstDeclaration | ResolverFailure = this._selectDeclaration(
matchingChildren,
memberReference,
memberName,
);
if (selectedDeclaration instanceof ResolverFailure) {
return selectedDeclaration;
}
currentDeclaration = selectedDeclaration;
}
return currentDeclaration;
}
private _getMemberReferenceIdentifier(memberReference: tsdoc.DocMemberReference): ResolverFailure | string {
if (memberReference.memberSymbol !== undefined) {
return new ResolverFailure('ECMAScript symbol selectors are not supported');
}
if (memberReference.memberIdentifier === undefined) {
return new ResolverFailure('The member identifier is missing in the root member reference');
}
return memberReference.memberIdentifier.identifier;
}
private _selectDeclaration(
astDeclarations: readonly AstDeclaration[],
memberReference: tsdoc.DocMemberReference,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const memberSelector: tsdoc.DocMemberSelector | undefined = memberReference.selector;
if (memberSelector === undefined) {
if (astDeclarations.length === 1) {
return astDeclarations[0]!;
} else {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(astDeclarations);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`The reference is ambiguous because "${astSymbolName}"` +
` has more than one declaration; you need to add a TSDoc member reference selector`,
);
}
}
switch (memberSelector.selectorKind) {
case tsdoc.SelectorKind.System:
return this._selectUsingSystemSelector(astDeclarations, memberSelector, astSymbolName);
case tsdoc.SelectorKind.Index:
return this._selectUsingIndexSelector(astDeclarations, memberSelector, astSymbolName);
default:
return new ResolverFailure(`The selector "${memberSelector.selector}" is not a supported selector type`);
}
}
private _selectUsingSystemSelector(
astDeclarations: readonly AstDeclaration[],
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const selectorName: string = memberSelector.selector;
let selectorSyntaxKind: ts.SyntaxKind;
switch (selectorName) {
case 'class':
selectorSyntaxKind = ts.SyntaxKind.ClassDeclaration;
break;
case 'enum':
selectorSyntaxKind = ts.SyntaxKind.EnumDeclaration;
break;
case 'function':
selectorSyntaxKind = ts.SyntaxKind.FunctionDeclaration;
break;
case 'interface':
selectorSyntaxKind = ts.SyntaxKind.InterfaceDeclaration;
break;
case 'namespace':
selectorSyntaxKind = ts.SyntaxKind.ModuleDeclaration;
break;
case 'type':
selectorSyntaxKind = ts.SyntaxKind.TypeAliasDeclaration;
break;
case 'variable':
selectorSyntaxKind = ts.SyntaxKind.VariableDeclaration;
break;
default:
return new ResolverFailure(`Unsupported system selector "${selectorName}"`);
}
const matches: AstDeclaration[] = astDeclarations.filter((x) => x.declaration.kind === selectorSyntaxKind);
if (matches.length === 0) {
return new ResolverFailure(
`A declaration for "${astSymbolName}" was not found that matches the TSDoc selector "${selectorName}"`,
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration "${astSymbolName}" matches the TSDoc selector "${selectorName}"`,
);
}
return matches[0]!;
}
private _selectUsingIndexSelector(
astDeclarations: readonly AstDeclaration[],
memberSelector: tsdoc.DocMemberSelector,
astSymbolName: string,
): AstDeclaration | ResolverFailure {
const selectorOverloadIndex: number = Number.parseInt(memberSelector.selector, 10);
const matches: AstDeclaration[] = [];
for (const astDeclaration of astDeclarations) {
const overloadIndex: number = this._collector.getOverloadIndex(astDeclaration);
if (overloadIndex === selectorOverloadIndex) {
matches.push(astDeclaration);
}
}
if (matches.length === 0) {
return new ResolverFailure(
`An overload for "${astSymbolName}" was not found that matches the` +
` TSDoc selector ":${selectorOverloadIndex}"`,
);
}
if (matches.length > 1) {
// If we found multiple matches, but the extra ones are all ancillary declarations,
// then return the main declaration.
const nonAncillaryMatch: AstDeclaration | undefined = this._tryDisambiguateAncillaryMatches(matches);
if (nonAncillaryMatch) {
return nonAncillaryMatch;
}
return new ResolverFailure(
`More than one declaration for "${astSymbolName}" matches the TSDoc selector ":${selectorOverloadIndex}"`,
);
}
return matches[0]!;
}
/**
* This resolves an ambiguous match in the case where the extra matches are all ancillary declarations,
* except for one match that is the main declaration.
*/
private _tryDisambiguateAncillaryMatches(matches: readonly AstDeclaration[]): AstDeclaration | undefined {
let result: AstDeclaration | undefined;
for (const match of matches) {
const declarationMetadata: DeclarationMetadata = this._collector.fetchDeclarationMetadata(match);
if (!declarationMetadata.isAncillary) {
if (result) {
return undefined; // more than one match
}
result = match;
}
}
return result;
}
}

View File

@@ -0,0 +1,192 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import type * as ts from 'typescript';
import type { AstDeclaration } from './AstDeclaration.js';
import { AstEntity } from './AstEntity.js';
/**
* Constructor options for AstSymbol
*/
export interface IAstSymbolOptions {
readonly followedSymbol: ts.Symbol;
readonly isExternal: boolean;
readonly localName: string;
readonly nominalAnalysis: boolean;
readonly parentAstSymbol: AstSymbol | undefined;
readonly rootAstSymbol: AstSymbol | undefined;
}
/**
* The AstDeclaration and AstSymbol classes are API Extractor's equivalent of the compiler's
* ts.Declaration and ts.Symbol objects. They are created by the `AstSymbolTable` class.
*
* @remarks
* The AstSymbol represents the ts.Symbol information for an AstDeclaration. For example,
* if a method has 3 overloads, each overloaded signature will have its own AstDeclaration,
* but they will all share a common AstSymbol.
*
* For nested definitions, the AstSymbol has a unique parent (i.e. AstSymbol.rootAstSymbol),
* but the parent/children for each AstDeclaration may be different. Consider this example:
*
* ```ts
* export namespace N {
* export function f(): void { }
* }
*
* export interface N {
* g(): void;
* }
* ```
*
* Note how the parent/child relationships are different for the symbol tree versus
* the declaration tree, and the declaration tree has two roots:
*
* ```
* AstSymbol tree: AstDeclaration tree:
* - N - N (namespace)
* - f - f
* - g - N (interface)
* - g
* ```
*/
export class AstSymbol extends AstEntity {
/**
* {@inheritdoc}
*/
public readonly localName: string; // abstract
/**
* If true, then the `followedSymbol` (i.e. original declaration) of this symbol
* is not part of the working package. The working package may still export this symbol,
* but if so it should be emitted as an alias such as `export { X } from "package1";`.
*/
public readonly isExternal: boolean;
/**
* The compiler symbol where this type was defined, after following any aliases.
*
* @remarks
* This is a normal form that can be reached from any symbol alias by calling
* `TypeScriptHelpers.followAliases()`. It can be compared to determine whether two
* symbols refer to the same underlying type.
*/
public readonly followedSymbol: ts.Symbol;
/**
* If true, then this AstSymbol represents a foreign object whose structure will be
* ignored. The AstDeclaration objects will not have any parent or children, and its references
* will not be analyzed.
*
* Nominal symbols are tracked e.g. when they are reexported by the working package.
*/
public readonly nominalAnalysis: boolean;
/**
* Returns the symbol of the parent of this AstSymbol, or undefined if there is no parent.
*
* @remarks
* If a symbol has multiple declarations, we assume (as an axiom) that their parent
* declarations will belong to the same symbol. This means that the "parent" of a
* symbol is a well-defined concept. However, the "children" of a symbol are not very
* meaningful, because different declarations may have different nested members,
* so we usually need to traverse declarations to find children.
*/
public readonly parentAstSymbol: AstSymbol | undefined;
/**
* Returns the symbol of the root of the AstDeclaration hierarchy.
*
* @remarks
* NOTE: If this AstSymbol is the root, then rootAstSymbol will point to itself.
*/
public readonly rootAstSymbol: AstSymbol;
/**
* Additional information that is calculated later by the `Collector`. The actual type is `SymbolMetadata`,
* but we declare it as `unknown` because consumers must obtain this object by calling
* `Collector.fetchSymbolMetadata()`.
*/
public symbolMetadata: unknown;
private readonly _astDeclarations: AstDeclaration[];
// This flag is unused if this is not the root symbol.
// Being "analyzed" is a property of the root symbol.
private _analyzed: boolean = false;
public constructor(options: IAstSymbolOptions) {
super();
this.followedSymbol = options.followedSymbol;
this.localName = options.localName;
this.isExternal = options.isExternal;
this.nominalAnalysis = options.nominalAnalysis;
this.parentAstSymbol = options.parentAstSymbol;
this.rootAstSymbol = options.rootAstSymbol ?? this;
this._astDeclarations = [];
}
/**
* The one or more declarations for this symbol.
*
* @remarks
* For example, if this symbol is a method, then the declarations might be
* various method overloads. If this symbol is a namespace, then the declarations
* might be separate namespace blocks with the same name that get combined via
* declaration merging.
*/
public get astDeclarations(): readonly AstDeclaration[] {
return this._astDeclarations;
}
/**
* Returns true if the AstSymbolTable.analyze() was called for this object.
* See that function for details.
*
* @remarks
* AstSymbolTable.analyze() is always performed on the root AstSymbol. This function
* returns true if-and-only-if the root symbol was analyzed.
*/
public get analyzed(): boolean {
return this.rootAstSymbol._analyzed;
}
/**
* This is an internal callback used when the AstSymbolTable attaches a new
* AstDeclaration to this object.
*
* @internal
*/
public _notifyDeclarationAttach(astDeclaration: AstDeclaration): void {
if (this.analyzed) {
throw new InternalError('_notifyDeclarationAttach() called after analysis is already complete');
}
this._astDeclarations.push(astDeclaration);
}
/**
* This is an internal callback used when the AstSymbolTable.analyze()
* has processed this object.
*
* @internal
*/
public _notifyAnalyzed(): void {
if (this.parentAstSymbol) {
throw new InternalError('_notifyAnalyzed() called for an AstSymbol which is not the root');
}
this._analyzed = true;
}
/**
* Helper that calls AstDeclaration.forEachDeclarationRecursive() for each AstDeclaration.
*/
public forEachDeclarationRecursive(action: (astDeclaration: AstDeclaration) => void): void {
for (const astDeclaration of this.astDeclarations) {
astDeclaration.forEachDeclarationRecursive(action);
}
}
}

View File

@@ -0,0 +1,709 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
// for ts.SymbolFlags
import { type PackageJsonLookup, InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { MessageRouter } from '../collector/MessageRouter';
import { AstDeclaration } from './AstDeclaration.js';
import type { AstEntity } from './AstEntity.js';
import type { AstModule, AstModuleExportInfo } from './AstModule.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import { ExportAnalyzer } from './ExportAnalyzer.js';
import { PackageMetadataManager } from './PackageMetadataManager.js';
import { SourceFileLocationFormatter } from './SourceFileLocationFormatter.js';
import { SyntaxHelpers } from './SyntaxHelpers.js';
import { TypeScriptHelpers } from './TypeScriptHelpers.js';
import { TypeScriptInternals, type IGlobalVariableAnalyzer } from './TypeScriptInternals.js';
/**
* Options for `AstSymbolTable._fetchAstSymbol()`
*/
export interface IFetchAstSymbolOptions {
/**
* True while populating the `AstSymbolTable`; false if we're doing a passive lookup
* without adding anything new to the table
*/
addIfMissing: boolean;
/**
* The symbol after any symbol aliases have been followed using TypeScriptHelpers.followAliases()
*/
followedSymbol: ts.Symbol;
/**
* If true, symbols with AstSymbol.nominalAnalysis=true will be returned.
* Otherwise `undefined` will be returned for such symbols.
*/
includeNominalAnalysis: boolean;
/**
* True if followedSymbol is not part of the working package
*/
isExternal: boolean;
/**
* A hint to help `_fetchAstSymbol()` determine the `AstSymbol.localName`.
*/
localName?: string;
}
/**
* AstSymbolTable is the workhorse that builds AstSymbol and AstDeclaration objects.
* It maintains a cache of already constructed objects. AstSymbolTable constructs
* AstModule objects, but otherwise the state that it maintains is agnostic of
* any particular entry point. (For example, it does not track whether a given AstSymbol
* is "exported" or not.)
*
* Internally, AstSymbolTable relies on ExportAnalyzer to crawl import statements and determine where symbols
* are declared (i.e. the AstImport information needed to import them).
*/
export class AstSymbolTable {
private readonly _program: ts.Program;
private readonly _typeChecker: ts.TypeChecker;
private readonly _messageRouter: MessageRouter;
private readonly _globalVariableAnalyzer: IGlobalVariableAnalyzer;
private readonly _packageMetadataManager: PackageMetadataManager;
private readonly _exportAnalyzer: ExportAnalyzer;
private readonly _alreadyWarnedGlobalNames: Set<string>;
/**
* A mapping from ts.Symbol --\> AstSymbol
* NOTE: The AstSymbol.followedSymbol will always be a lookup key, but additional keys
* are possible.
*
* After following type aliases, we use this map to look up the corresponding AstSymbol.
*/
private readonly _astSymbolsBySymbol: Map<ts.Symbol, AstSymbol> = new Map<ts.Symbol, AstSymbol>();
/**
* A mapping from ts.Declaration --\> AstDeclaration
*/
private readonly _astDeclarationsByDeclaration: Map<ts.Node, AstDeclaration> = new Map<ts.Node, AstDeclaration>();
// Note that this is a mapping from specific AST nodes that we analyzed, based on the underlying symbol
// for that node.
private readonly _entitiesByNode: Map<ts.Identifier | ts.ImportTypeNode, AstEntity | undefined> = new Map<
ts.Identifier,
AstEntity | undefined
>();
public constructor(
program: ts.Program,
typeChecker: ts.TypeChecker,
packageJsonLookup: PackageJsonLookup,
bundledPackageNames: ReadonlySet<string>,
messageRouter: MessageRouter,
) {
this._program = program;
this._typeChecker = typeChecker;
this._messageRouter = messageRouter;
this._globalVariableAnalyzer = TypeScriptInternals.getGlobalVariableAnalyzer(program);
this._packageMetadataManager = new PackageMetadataManager(packageJsonLookup, messageRouter);
this._exportAnalyzer = new ExportAnalyzer(this._program, this._typeChecker, bundledPackageNames, {
analyze: this.analyze.bind(this),
fetchAstSymbol: this._fetchAstSymbol.bind(this),
});
this._alreadyWarnedGlobalNames = new Set<string>();
}
/**
* Used to analyze an entry point that belongs to the working package.
*/
public fetchAstModuleFromWorkingPackage(sourceFile: ts.SourceFile): AstModule {
return this._exportAnalyzer.fetchAstModuleFromSourceFile(sourceFile, undefined, false);
}
/**
* This crawls the specified entry point and collects the full set of exported AstSymbols.
*/
public fetchAstModuleExportInfo(astModule: AstModule): AstModuleExportInfo {
return this._exportAnalyzer.fetchAstModuleExportInfo(astModule);
}
/**
* Attempts to retrieve an export by name from the specified `AstModule`.
* Returns undefined if no match was found.
*/
public tryGetExportOfAstModule(exportName: string, astModule: AstModule): AstEntity | undefined {
return this._exportAnalyzer.tryGetExportOfAstModule(exportName, astModule);
}
/**
* Ensures that AstSymbol.analyzed is true for the provided symbol. The operation
* starts from the root symbol and then fills out all children of all declarations, and
* also calculates AstDeclaration.referencedAstSymbols for all declarations.
* If the symbol is not imported, any non-imported references are also analyzed.
*
* @remarks
* This is an expensive operation, so we only perform it for top-level exports of an
* the AstModule. For example, if some code references a nested class inside
* a namespace from another library, we do not analyze any of that class's siblings
* or members. (We do always construct its parents however, since AstDefinition.parent
* is immutable, and needed e.g. to calculate release tag inheritance.)
*/
public analyze(astEntity: AstEntity): void {
if (astEntity instanceof AstSymbol) {
this._analyzeAstSymbol(astEntity);
return;
}
if (astEntity instanceof AstNamespaceImport) {
this._analyzeAstNamespaceImport(astEntity);
}
}
/**
* For a given astDeclaration, this efficiently finds the child corresponding to the
* specified ts.Node. It is assumed that AstDeclaration.isSupportedSyntaxKind() would return true for
* that node type, and that the node is an immediate child of the provided AstDeclaration.
*/
// NOTE: This could be a method of AstSymbol if it had a backpointer to its AstSymbolTable.
public getChildAstDeclarationByNode(node: ts.Node, parentAstDeclaration: AstDeclaration): AstDeclaration {
if (!parentAstDeclaration.astSymbol.analyzed) {
throw new Error('getChildDeclarationByNode() cannot be used for an AstSymbol that was not analyzed');
}
const childAstDeclaration: AstDeclaration | undefined = this._astDeclarationsByDeclaration.get(node);
if (!childAstDeclaration) {
throw new Error('Child declaration not found for the specified node');
}
if (childAstDeclaration.parent !== parentAstDeclaration) {
throw new InternalError('The found child is not attached to the parent AstDeclaration');
}
return childAstDeclaration;
}
/**
* For a given ts.Identifier that is part of an AstSymbol that we analyzed, return the AstEntity that
* it refers to. Returns undefined if it doesn't refer to anything interesting.
*
* @remarks
* Throws an Error if the ts.Identifier is not part of node tree that was analyzed.
*/
public tryGetEntityForNode(identifier: ts.Identifier | ts.ImportTypeNode): AstEntity | undefined {
if (!this._entitiesByNode.has(identifier)) {
throw new InternalError('tryGetEntityForIdentifier() called for an identifier that was not analyzed');
}
return this._entitiesByNode.get(identifier);
}
/**
* Builds an AstSymbol.localName for a given ts.Symbol. In the current implementation, the localName is
* a TypeScript-like expression that may be a string literal or ECMAScript symbol expression.
*
* ```ts
* class X {
* // localName="identifier"
* public identifier: number = 1;
* // localName="\"identifier\""
* public "quoted string!": number = 2;
* // localName="[MyNamespace.MySymbol]"
* public [MyNamespace.MySymbol]: number = 3;
* }
* ```
*/
public static getLocalNameForSymbol(symbol: ts.Symbol): string {
// TypeScript binds well-known ECMAScript symbols like "[Symbol.iterator]" as "__@iterator".
// Decode it back into "[Symbol.iterator]".
const wellKnownSymbolName: string | undefined = TypeScriptHelpers.tryDecodeWellKnownSymbolName(symbol.escapedName);
if (wellKnownSymbolName) {
return wellKnownSymbolName;
}
const isUniqueSymbol: boolean = TypeScriptHelpers.isUniqueSymbolName(symbol.escapedName);
// We will try to obtain the name from a declaration; otherwise we'll fall back to the symbol name.
let unquotedName: string = symbol.name;
for (const declaration of symbol.declarations ?? []) {
// Handle cases such as "export default class X { }" where the symbol name is "default"
// but the local name is "X".
const localSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetLocalSymbol(declaration);
if (localSymbol) {
unquotedName = localSymbol.name;
}
// If it is a non-well-known symbol, then return the late-bound name. For example, "X.Y.z" in this example:
//
// namespace X {
// export namespace Y {
// export const z: unique symbol = Symbol("z");
// }
// }
//
// class C {
// public [X.Y.z](): void { }
// }
//
if (isUniqueSymbol) {
const declarationName: ts.DeclarationName | undefined = ts.getNameOfDeclaration(declaration);
if (declarationName && ts.isComputedPropertyName(declarationName)) {
const lateBoundName: string | undefined = TypeScriptHelpers.tryGetLateBoundName(declarationName);
if (lateBoundName) {
// Here the string may contain an expression such as "[X.Y.z]". Names starting with "[" are always
// expressions. If a string literal contains those characters, the code below will JSON.stringify() it
// to avoid a collision.
return lateBoundName;
}
}
}
}
// Otherwise that name may come from a quoted string or pseudonym like `__constructor`.
// If the string is not a safe identifier, then we must add quotes.
// Note that if it was quoted but did not need to be quoted, here we will remove the quotes.
if (!SyntaxHelpers.isSafeUnquotedMemberIdentifier(unquotedName)) {
// For API Extractor's purposes, a canonical form is more appropriate than trying to reflect whatever
// appeared in the source code. The code is not even guaranteed to be consistent, for example:
//
// class X {
// public "f1"(x: string): void;
// public f1(x: boolean): void;
// public 'f1'(x: string | boolean): void { }
// }
return JSON.stringify(unquotedName);
}
return unquotedName;
}
private _analyzeAstNamespaceImport(astNamespaceImport: AstNamespaceImport): void {
if (astNamespaceImport.analyzed) {
return;
}
// mark before actual analyzing, to handle module cyclic reexport
astNamespaceImport.analyzed = true;
const exportedLocalEntities: Map<string, AstEntity> = this.fetchAstModuleExportInfo(
astNamespaceImport.astModule,
).exportedLocalEntities;
for (const exportedEntity of exportedLocalEntities.values()) {
this.analyze(exportedEntity);
}
}
private _analyzeAstSymbol(astSymbol: AstSymbol): void {
if (astSymbol.analyzed) {
return;
}
if (astSymbol.nominalAnalysis) {
// We don't analyze nominal symbols
astSymbol._notifyAnalyzed();
return;
}
// Start at the root of the tree
const rootAstSymbol: AstSymbol = astSymbol.rootAstSymbol;
// Calculate the full child tree for each definition
for (const astDeclaration of rootAstSymbol.astDeclarations) {
this._analyzeChildTree(astDeclaration.declaration, astDeclaration);
}
rootAstSymbol._notifyAnalyzed();
if (!astSymbol.isExternal) {
// If this symbol is non-external (i.e. it belongs to the working package), then we also analyze any
// referencedAstSymbols that are non-external. For example, this ensures that forgotten exports
// get analyzed.
rootAstSymbol.forEachDeclarationRecursive((astDeclaration: AstDeclaration) => {
for (const referencedAstEntity of astDeclaration.referencedAstEntities) {
// Walk up to the root of the tree, looking for any imports along the way
if (referencedAstEntity instanceof AstSymbol && !referencedAstEntity.isExternal) {
this._analyzeAstSymbol(referencedAstEntity);
}
if (referencedAstEntity instanceof AstNamespaceImport && !referencedAstEntity.astModule.isExternal) {
this._analyzeAstNamespaceImport(referencedAstEntity);
}
}
});
}
}
/**
* Used by analyze to recursively analyze the entire child tree.
*/
private _analyzeChildTree(node: ts.Node, governingAstDeclaration: AstDeclaration): void {
switch (node.kind) {
case ts.SyntaxKind.JSDocComment: // Skip JSDoc comments - TS considers @param tags TypeReference nodes
return;
// Is this a reference to another AstSymbol?
case ts.SyntaxKind.TypeReference: // general type references
case ts.SyntaxKind.ExpressionWithTypeArguments: // special case for e.g. the "extends" keyword
case ts.SyntaxKind.ComputedPropertyName: // used for EcmaScript "symbols", e.g. "[toPrimitive]".
case ts.SyntaxKind.TypeQuery: // represents for "typeof X" as a type
{
// Sometimes the type reference will involve multiple identifiers, e.g. "a.b.C".
// In this case, we only need to worry about importing the first identifier,
// so do a depth-first search for it:
const identifierNode: ts.Identifier | undefined = TypeScriptHelpers.findFirstChildNode(
node,
ts.SyntaxKind.Identifier,
);
if (identifierNode) {
let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(identifierNode);
if (!referencedAstEntity) {
const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(identifierNode);
if (!symbol) {
throw new Error('Symbol not found for identifier: ' + identifierNode.getText());
}
// Normally we expect getSymbolAtLocation() to take us to a declaration within the same source
// file, or else to an explicit "import" statement within the same source file. But in certain
// situations (e.g. a global variable) the symbol will refer to a declaration in some other
// source file. We'll call that case a "displaced symbol".
//
// For more info, see this discussion:
// https://github.com/microsoft/rushstack/issues/1765#issuecomment-595559849
let displacedSymbol = true;
for (const declaration of symbol.declarations ?? []) {
if (declaration.getSourceFile() === identifierNode.getSourceFile()) {
displacedSymbol = false;
break;
}
}
if (displacedSymbol) {
if (this._globalVariableAnalyzer.hasGlobalName(identifierNode.text)) {
// If the displaced symbol is a global variable, then API Extractor simply ignores it.
// Ambient declarations typically describe the runtime environment (provided by an API consumer),
// so we don't bother analyzing them as an API contract. (There are probably some packages
// that include interesting global variables in their API, but API Extractor doesn't support
// that yet; it would be a feature request.)
if (this._messageRouter.showDiagnostics && !this._alreadyWarnedGlobalNames.has(identifierNode.text)) {
this._alreadyWarnedGlobalNames.add(identifierNode.text);
this._messageRouter.logDiagnostic(
`Ignoring reference to global variable "${identifierNode.text}"` +
` in ` +
SourceFileLocationFormatter.formatDeclaration(identifierNode),
);
}
} else {
// If you encounter this, please report a bug with a repro. We're interested to know
// how it can occur.
throw new InternalError(`Unable to follow symbol for "${identifierNode.text}"`);
}
} else {
referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity(
symbol,
governingAstDeclaration.astSymbol.isExternal,
);
this._entitiesByNode.set(identifierNode, referencedAstEntity);
}
}
if (referencedAstEntity) {
governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity);
}
}
}
break;
// Is this the identifier for the governingAstDeclaration?
case ts.SyntaxKind.Identifier:
{
const identifierNode: ts.Identifier = node as ts.Identifier;
if (!this._entitiesByNode.has(identifierNode)) {
const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(identifierNode);
let referencedAstEntity: AstEntity | undefined;
if (symbol === governingAstDeclaration.astSymbol.followedSymbol) {
referencedAstEntity = this._fetchEntityForNode(identifierNode, governingAstDeclaration);
}
this._entitiesByNode.set(identifierNode, referencedAstEntity);
}
}
break;
case ts.SyntaxKind.ImportType:
{
const importTypeNode: ts.ImportTypeNode = node as ts.ImportTypeNode;
let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(importTypeNode);
if (!this._entitiesByNode.has(importTypeNode)) {
referencedAstEntity = this._fetchEntityForNode(importTypeNode, governingAstDeclaration);
if (!referencedAstEntity) {
// This should never happen
throw new Error('Failed to fetch entity for import() type node: ' + importTypeNode.getText());
}
this._entitiesByNode.set(importTypeNode, referencedAstEntity);
}
if (referencedAstEntity) {
governingAstDeclaration._notifyReferencedAstEntity(referencedAstEntity);
}
}
break;
default:
break;
}
// Is this node declaring a new AstSymbol?
const newGoverningAstDeclaration: AstDeclaration | undefined = this._fetchAstDeclaration(
node,
governingAstDeclaration.astSymbol.isExternal,
);
for (const childNode of node.getChildren()) {
this._analyzeChildTree(childNode, newGoverningAstDeclaration ?? governingAstDeclaration);
}
}
private _fetchEntityForNode(
node: ts.Identifier | ts.ImportTypeNode,
governingAstDeclaration: AstDeclaration,
): AstEntity | undefined {
let referencedAstEntity: AstEntity | undefined = this._entitiesByNode.get(node);
if (!referencedAstEntity) {
if (node.kind === ts.SyntaxKind.ImportType) {
referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntityFromImportTypeNode(
node,
governingAstDeclaration.astSymbol.isExternal,
);
} else {
const symbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(node);
if (!symbol) {
throw new Error('Symbol not found for identifier: ' + node.getText());
}
referencedAstEntity = this._exportAnalyzer.fetchReferencedAstEntity(
symbol,
governingAstDeclaration.astSymbol.isExternal,
);
}
this._entitiesByNode.set(node, referencedAstEntity);
}
return referencedAstEntity;
}
private _fetchAstDeclaration(node: ts.Node, isExternal: boolean): AstDeclaration | undefined {
if (!AstDeclaration.isSupportedSyntaxKind(node.kind)) {
return undefined;
}
const symbol: ts.Symbol | undefined = TypeScriptHelpers.getSymbolForDeclaration(
node as ts.Declaration,
this._typeChecker,
);
if (!symbol) {
throw new InternalError('Unable to find symbol for node');
}
const astSymbol: AstSymbol | undefined = this._fetchAstSymbol({
followedSymbol: symbol,
isExternal,
includeNominalAnalysis: true,
addIfMissing: true,
});
if (!astSymbol) {
return undefined;
}
const astDeclaration: AstDeclaration | undefined = this._astDeclarationsByDeclaration.get(node);
if (!astDeclaration) {
throw new InternalError('Unable to find constructed AstDeclaration');
}
return astDeclaration;
}
private _fetchAstSymbol(options: IFetchAstSymbolOptions): AstSymbol | undefined {
const followedSymbol: ts.Symbol = options.followedSymbol;
// Filter out symbols representing constructs that we don't care about
const arbitraryDeclaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(followedSymbol);
if (!arbitraryDeclaration) {
return undefined;
}
if (
followedSymbol.flags & (ts.SymbolFlags.TypeParameter | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Transient) &&
!TypeScriptInternals.isLateBoundSymbol(followedSymbol)
) {
return undefined;
}
// API Extractor doesn't analyze ambient declarations at all
if (
TypeScriptHelpers.isAmbient(followedSymbol, this._typeChecker) && // We make a special exemption for ambient declarations that appear in a source file containing
// an "export=" declaration that allows them to be imported as non-ambient.
!this._exportAnalyzer.isImportableAmbientSourceFile(arbitraryDeclaration.getSourceFile())
) {
return undefined;
}
// Make sure followedSymbol isn't an alias for something else
if (TypeScriptHelpers.isFollowableAlias(followedSymbol, this._typeChecker)) {
// We expect the caller to have already followed any aliases
throw new InternalError('AstSymbolTable._fetchAstSymbol() cannot be called with a symbol alias');
}
let astSymbol: AstSymbol | undefined = this._astSymbolsBySymbol.get(followedSymbol);
if (!astSymbol) {
// None of the above lookups worked, so create a new entry...
let nominalAnalysis = false;
if (options.isExternal) {
// If the file is from an external package that does not support AEDoc, normally we ignore it completely.
// But in some cases (e.g. checking star exports of an external package) we need an AstSymbol to
// represent it, but we don't need to analyze its sibling/children.
const followedSymbolSourceFileName: string = arbitraryDeclaration.getSourceFile().fileName;
if (!this._packageMetadataManager.isAedocSupportedFor(followedSymbolSourceFileName)) {
nominalAnalysis = true;
if (!options.includeNominalAnalysis) {
return undefined;
}
}
}
let parentAstSymbol: AstSymbol | undefined;
if (!nominalAnalysis) {
for (const declaration of followedSymbol.declarations ?? []) {
if (!AstDeclaration.isSupportedSyntaxKind(declaration.kind)) {
throw new InternalError(
`The "${followedSymbol.name}" symbol has a` +
` ts.SyntaxKind.${ts.SyntaxKind[declaration.kind]} declaration which is not (yet?)` +
` supported by API Extractor`,
);
}
}
// We always fetch the entire chain of parents for each declaration.
// (Children/siblings are only analyzed on demand.)
// Key assumptions behind this squirrely logic:
//
// IF a given symbol has two declarations D1 and D2; AND
// If D1 has a parent P1, then
// - D2 will also have a parent P2; AND
// - P1 and P2's symbol will be the same
// - but P1 and P2 may be different (e.g. merged namespaces containing merged interfaces)
// Is there a parent AstSymbol? First we check to see if there is a parent declaration:
if (arbitraryDeclaration) {
const arbitraryParentDeclaration: ts.Node | undefined =
this._tryFindFirstAstDeclarationParent(arbitraryDeclaration);
if (arbitraryParentDeclaration) {
const parentSymbol: ts.Symbol = TypeScriptHelpers.getSymbolForDeclaration(
arbitraryParentDeclaration as ts.Declaration,
this._typeChecker,
);
parentAstSymbol = this._fetchAstSymbol({
followedSymbol: parentSymbol,
isExternal: options.isExternal,
includeNominalAnalysis: false,
addIfMissing: true,
});
if (!parentAstSymbol) {
throw new InternalError('Unable to construct a parent AstSymbol for ' + followedSymbol.name);
}
}
}
}
const localName: string | undefined = options.localName ?? AstSymbolTable.getLocalNameForSymbol(followedSymbol);
astSymbol = new AstSymbol({
followedSymbol,
localName,
isExternal: options.isExternal,
nominalAnalysis,
parentAstSymbol,
rootAstSymbol: parentAstSymbol ? parentAstSymbol.rootAstSymbol : undefined,
});
this._astSymbolsBySymbol.set(followedSymbol, astSymbol);
// Okay, now while creating the declarations we will wire them up to the
// their corresponding parent declarations
for (const declaration of followedSymbol.declarations ?? []) {
let parentAstDeclaration: AstDeclaration | undefined;
if (parentAstSymbol) {
const parentDeclaration: ts.Node | undefined = this._tryFindFirstAstDeclarationParent(declaration);
if (!parentDeclaration) {
throw new InternalError('Missing parent declaration');
}
parentAstDeclaration = this._astDeclarationsByDeclaration.get(parentDeclaration);
if (!parentAstDeclaration) {
throw new InternalError('Missing parent AstDeclaration');
}
}
const astDeclaration: AstDeclaration = new AstDeclaration({
declaration,
astSymbol,
parent: parentAstDeclaration,
});
this._astDeclarationsByDeclaration.set(declaration, astDeclaration);
}
}
if (options.isExternal !== astSymbol.isExternal) {
throw new InternalError(
`Cannot assign isExternal=${options.isExternal} for` +
` the symbol ${astSymbol.localName} because it was previously registered` +
` with isExternal=${astSymbol.isExternal}`,
);
}
return astSymbol;
}
/**
* Returns the first parent satisfying isAstDeclaration(), or undefined if none is found.
*/
private _tryFindFirstAstDeclarationParent(node: ts.Node): ts.Node | undefined {
let currentNode: ts.Node | undefined = node.parent;
while (currentNode) {
if (AstDeclaration.isSupportedSyntaxKind(currentNode.kind)) {
return currentNode;
}
currentNode = currentNode.parent;
}
return undefined;
}
}

View File

@@ -0,0 +1,946 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import { InternalError } from '@rushstack/node-core-library';
import * as ts from 'typescript';
import type { AstEntity } from './AstEntity.js';
import { AstImport, type IAstImportOptions, AstImportKind } from './AstImport.js';
import { AstModule, AstModuleExportInfo } from './AstModule.js';
import { AstNamespaceImport } from './AstNamespaceImport.js';
import { AstSymbol } from './AstSymbol.js';
import type { IFetchAstSymbolOptions } from './AstSymbolTable.js';
import { SourceFileLocationFormatter } from './SourceFileLocationFormatter.js';
import { SyntaxHelpers } from './SyntaxHelpers.js';
import { TypeScriptHelpers } from './TypeScriptHelpers.js';
import { TypeScriptInternals } from './TypeScriptInternals.js';
/**
* Exposes the minimal APIs from AstSymbolTable that are needed by ExportAnalyzer.
*
* In particular, we want ExportAnalyzer to be able to call AstSymbolTable._fetchAstSymbol() even though it
* is a very private API that should not be exposed to any other components.
*/
export interface IAstSymbolTable {
analyze(astEntity: AstEntity): void;
fetchAstSymbol(options: IFetchAstSymbolOptions): AstSymbol | undefined;
}
/**
* Used with ExportAnalyzer.fetchAstModuleBySourceFile() to provide contextual information about how the source file
* was imported.
*/
interface IAstModuleReference {
/**
* For example, if we are following a statement like `import { X } from 'some-package'`, this will be the
* string `"some-package"`.
*/
moduleSpecifier: string;
/**
* For example, if we are following a statement like `import { X } from 'some-package'`, this will be the
* symbol for `X`.
*/
moduleSpecifierSymbol: ts.Symbol;
}
/**
* The ExportAnalyzer is an internal part of AstSymbolTable that has been moved out into its own source file
* because it is a complex and mostly self-contained algorithm.
*
* Its job is to build up AstModule objects by crawling import statements to discover where declarations come from.
* This is conceptually the same as the compiler's own TypeChecker.getExportsOfModule(), except that when
* ExportAnalyzer encounters a declaration that was imported from an external package, it remembers how it was imported
* (i.e. the AstImport object). Today the compiler API does not expose this information, which is crucial for
* generating .d.ts rollups.
*/
export class ExportAnalyzer {
private readonly _program: ts.Program;
private readonly _typeChecker: ts.TypeChecker;
private readonly _bundledPackageNames: ReadonlySet<string>;
private readonly _astSymbolTable: IAstSymbolTable;
private readonly _astModulesByModuleSymbol: Map<ts.Symbol, AstModule> = new Map<ts.Symbol, AstModule>();
// Used with isImportableAmbientSourceFile()
private readonly _importableAmbientSourceFiles: Set<ts.SourceFile> = new Set<ts.SourceFile>();
private readonly _astImportsByKey: Map<string, AstImport> = new Map<string, AstImport>();
private readonly _astNamespaceImportByModule: Map<AstModule, AstNamespaceImport> = new Map();
public constructor(
program: ts.Program,
typeChecker: ts.TypeChecker,
bundledPackageNames: ReadonlySet<string>,
astSymbolTable: IAstSymbolTable,
) {
this._program = program;
this._typeChecker = typeChecker;
this._bundledPackageNames = bundledPackageNames;
this._astSymbolTable = astSymbolTable;
}
/**
* For a given source file, this analyzes all of its exports and produces an AstModule object.
*
* @param sourceFile - the sourceFile
* @param moduleReference - contextual information about the import statement that took us to this source file.
* or `undefined` if this source file is the initial entry point
* @param isExternal - whether the given `moduleReference` is external.
*/
public fetchAstModuleFromSourceFile(
sourceFile: ts.SourceFile,
moduleReference: IAstModuleReference | undefined,
isExternal: boolean,
): AstModule {
const moduleSymbol: ts.Symbol = this._getModuleSymbolFromSourceFile(sourceFile, moduleReference);
// Don't traverse into a module that we already processed before:
// The compiler allows m1 to have "export * from 'm2'" and "export * from 'm3'",
// even if m2 and m3 both have "export * from 'm4'".
let astModule: AstModule | undefined = this._astModulesByModuleSymbol.get(moduleSymbol);
if (!astModule) {
// (If moduleReference === undefined, then this is the entry point of the local project being analyzed.)
const externalModulePath: string | undefined =
moduleReference !== undefined && isExternal ? moduleReference.moduleSpecifier : undefined;
astModule = new AstModule({ sourceFile, moduleSymbol, externalModulePath });
this._astModulesByModuleSymbol.set(moduleSymbol, astModule);
if (astModule.isExternal) {
// It's an external package, so do the special simplified analysis that doesn't crawl into referenced modules
for (const exportedSymbol of this._typeChecker.getExportsOfModule(moduleSymbol)) {
if (externalModulePath === undefined) {
throw new InternalError('Failed assertion: externalModulePath=undefined but astModule.isExternal=true');
}
const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(exportedSymbol, this._typeChecker);
// Ignore virtual symbols that don't have any declarations
const arbitraryDeclaration: ts.Declaration | undefined = TypeScriptHelpers.tryGetADeclaration(followedSymbol);
if (arbitraryDeclaration) {
const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({
followedSymbol,
isExternal: astModule.isExternal,
includeNominalAnalysis: true,
addIfMissing: true,
});
if (!astSymbol) {
throw new Error(
`Unsupported export ${JSON.stringify(exportedSymbol.name)}:\n` +
SourceFileLocationFormatter.formatDeclaration(arbitraryDeclaration),
);
}
astModule.cachedExportedEntities.set(exportedSymbol.name, astSymbol);
}
}
} else if (moduleSymbol.exports) {
// The module is part of the local project, so do the full analysis
// The "export * from 'module-name';" declarations are all attached to a single virtual symbol
// whose name is InternalSymbolName.ExportStar
const exportStarSymbol: ts.Symbol | undefined = moduleSymbol.exports.get(ts.InternalSymbolName.ExportStar);
if (exportStarSymbol) {
for (const exportStarDeclaration of exportStarSymbol.getDeclarations() ?? []) {
if (ts.isExportDeclaration(exportStarDeclaration)) {
const starExportedModule: AstModule | undefined = this._fetchSpecifierAstModule(
exportStarDeclaration,
exportStarSymbol,
);
if (starExportedModule !== undefined) {
astModule.starExportedModules.add(starExportedModule);
}
} else {
// Ignore ExportDeclaration nodes that don't match the expected pattern
// Should we report a warning?
}
}
}
}
}
return astModule;
}
/**
* Retrieves the symbol for the module corresponding to the ts.SourceFile that is being imported/exported.
*
* @remarks
* The `module` keyword can be used to declare multiple TypeScript modules inside a single source file.
* (This is a deprecated construct and mainly used for typings such as `@types/node`.) In this situation,
* `moduleReference` helps us to fish out the correct module symbol.
*/
private _getModuleSymbolFromSourceFile(
sourceFile: ts.SourceFile,
moduleReference: IAstModuleReference | undefined,
): ts.Symbol {
const moduleSymbol: ts.Symbol | undefined = TypeScriptInternals.tryGetSymbolForDeclaration(
sourceFile,
this._typeChecker,
);
if (moduleSymbol !== undefined) {
// This is the normal case. The SourceFile acts is a module and has a symbol.
return moduleSymbol;
}
if (
moduleReference !== undefined && // But there is also an elaborate case where the source file contains one or more "module" declarations,
// and our moduleReference took us to one of those.
(moduleReference.moduleSpecifierSymbol.flags & ts.SymbolFlags.Alias) !== 0
) {
// Follow the import/export declaration to one hop the exported item inside the target module
let followedSymbol: ts.Symbol | undefined = TypeScriptInternals.getImmediateAliasedSymbol(
moduleReference.moduleSpecifierSymbol,
this._typeChecker,
);
if (followedSymbol === undefined) {
// This is a workaround for a compiler bug where getImmediateAliasedSymbol() sometimes returns undefined
followedSymbol = this._typeChecker.getAliasedSymbol(moduleReference.moduleSpecifierSymbol);
}
if (followedSymbol !== undefined && followedSymbol !== moduleReference.moduleSpecifierSymbol) {
// The parent of the exported symbol will be the module that we're importing from
const parent: ts.Symbol | undefined = TypeScriptInternals.getSymbolParent(followedSymbol);
if (
parent !== undefined && // Make sure the thing we found is a module
(parent.flags & ts.SymbolFlags.ValueModule) !== 0
) {
// Record that that this is an ambient module that can also be imported from
this._importableAmbientSourceFiles.add(sourceFile);
return parent;
}
}
}
throw new InternalError('Unable to determine module for: ' + sourceFile.fileName);
}
/**
* Implementation of {@link AstSymbolTable.fetchAstModuleExportInfo}.
*/
public fetchAstModuleExportInfo(entryPointAstModule: AstModule): AstModuleExportInfo {
if (entryPointAstModule.isExternal) {
throw new Error('fetchAstModuleExportInfo() is not supported for external modules');
}
if (entryPointAstModule.astModuleExportInfo === undefined) {
const astModuleExportInfo: AstModuleExportInfo = new AstModuleExportInfo();
this._collectAllExportsRecursive(astModuleExportInfo, entryPointAstModule, new Set<AstModule>());
entryPointAstModule.astModuleExportInfo = astModuleExportInfo;
}
return entryPointAstModule.astModuleExportInfo;
}
/**
* Returns true if the module specifier refers to an external package. Ignores packages listed in the
* "bundledPackages" setting from the api-extractor.json config file.
*/
private _isExternalModulePath(
importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode,
moduleSpecifier: string,
): boolean {
const specifier: ts.Expression | ts.TypeNode | undefined = ts.isImportTypeNode(importOrExportDeclaration)
? importOrExportDeclaration.argument
: importOrExportDeclaration.moduleSpecifier;
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
specifier && ts.isStringLiteralLike(specifier)
? TypeScriptInternals.getModeForUsageLocation(importOrExportDeclaration.getSourceFile(), specifier)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
importOrExportDeclaration.getSourceFile(),
moduleSpecifier,
mode,
);
if (resolvedModule === undefined) {
// The TS compiler API `getResolvedModule` cannot resolve ambient modules. Thus, to match API Extractor's
// previous behavior, simply treat all ambient modules as external. This bug is tracked by
// https://github.com/microsoft/rushstack/issues/3335.
return true;
}
// Either something like `jquery` or `@microsoft/api-extractor`.
const packageName: string | undefined = resolvedModule.packageId?.name;
if (packageName !== undefined && this._bundledPackageNames.has(packageName)) {
return false;
}
if (resolvedModule.isExternalLibraryImport === undefined) {
// This presumably means the compiler couldn't figure out whether the module was external, but we're not
// sure how this can happen.
throw new InternalError(
`Cannot determine whether the module ${JSON.stringify(moduleSpecifier)} is external\n` +
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration),
);
}
return resolvedModule.isExternalLibraryImport;
}
/**
* Returns true if when we analyzed sourceFile, we found that it contains an "export=" statement that allows
* it to behave /either/ as an ambient module /or/ as a regular importable module. In this case,
* `AstSymbolTable._fetchAstSymbol()` will analyze its symbols even though `TypeScriptHelpers.isAmbient()`
* returns true.
*/
public isImportableAmbientSourceFile(sourceFile: ts.SourceFile): boolean {
return this._importableAmbientSourceFiles.has(sourceFile);
}
private _collectAllExportsRecursive(
astModuleExportInfo: AstModuleExportInfo,
astModule: AstModule,
visitedAstModules: Set<AstModule>,
): void {
if (visitedAstModules.has(astModule)) {
return;
}
visitedAstModules.add(astModule);
if (astModule.isExternal) {
astModuleExportInfo.starExportedExternalModules.add(astModule);
} else {
// Fetch each of the explicit exports for this module
if (astModule.moduleSymbol.exports) {
for (const [exportName, exportSymbol] of astModule.moduleSymbol.exports.entries()) {
switch (exportName) {
case ts.InternalSymbolName.ExportStar:
case ts.InternalSymbolName.ExportEquals:
break;
default:
// Don't collect the "export default" symbol unless this is the entry point module
if (
(exportName !== ts.InternalSymbolName.Default || visitedAstModules.size === 1) &&
!astModuleExportInfo.exportedLocalEntities.has(exportSymbol.name)
) {
const astEntity: AstEntity = this._getExportOfAstModule(exportSymbol.name, astModule);
if (astEntity instanceof AstSymbol && !astEntity.isExternal) {
this._astSymbolTable.analyze(astEntity);
}
if (astEntity instanceof AstNamespaceImport && !astEntity.astModule.isExternal) {
this._astSymbolTable.analyze(astEntity);
}
astModuleExportInfo.exportedLocalEntities.set(exportSymbol.name, astEntity);
}
break;
}
}
}
for (const starExportedModule of astModule.starExportedModules) {
this._collectAllExportsRecursive(astModuleExportInfo, starExportedModule, visitedAstModules);
}
}
}
/**
* For a given symbol (which was encountered in the specified sourceFile), this fetches the AstEntity that it
* refers to. For example, if a particular interface describes the return value of a function, this API can help
* us determine a TSDoc declaration reference for that symbol (if the symbol is exported).
*/
public fetchReferencedAstEntity(symbol: ts.Symbol, referringModuleIsExternal: boolean): AstEntity | undefined {
if ((symbol.flags & ts.SymbolFlags.FunctionScopedVariable) !== 0) {
// If a symbol refers back to part of its own definition, don't follow that rabbit hole
// Example:
//
// function f(x: number): typeof x {
// return 123;
// }
return undefined;
}
let current: ts.Symbol = symbol;
if (referringModuleIsExternal) {
current = TypeScriptHelpers.followAliases(symbol, this._typeChecker);
} else {
for (;;) {
// Is this symbol an import/export that we need to follow to find the real declaration?
for (const declaration of current.declarations ?? []) {
let matchedAstEntity: AstEntity | undefined;
matchedAstEntity = this._tryMatchExportDeclaration(declaration, current);
if (matchedAstEntity !== undefined) {
return matchedAstEntity;
}
matchedAstEntity = this._tryMatchImportDeclaration(declaration, current);
if (matchedAstEntity !== undefined) {
return matchedAstEntity;
}
}
if (!(current.flags & ts.SymbolFlags.Alias)) {
break;
}
const currentAlias: ts.Symbol = TypeScriptInternals.getImmediateAliasedSymbol(current, this._typeChecker);
// Stop if we reach the end of the chain
if (!currentAlias || currentAlias === current) {
break;
}
current = currentAlias;
}
}
// Otherwise, assume it is a normal declaration
const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({
followedSymbol: current,
isExternal: referringModuleIsExternal,
includeNominalAnalysis: false,
addIfMissing: true,
});
return astSymbol;
}
public fetchReferencedAstEntityFromImportTypeNode(
node: ts.ImportTypeNode,
referringModuleIsExternal: boolean,
): AstEntity | undefined {
const externalModulePath: string | undefined = this._tryGetExternalModulePath(node);
if (externalModulePath) {
let exportName: string;
if (node.qualifier) {
// Example input:
// import('api-extractor-lib1-test').Lib1GenericType<number>
//
// Extracted qualifier:
// Lib1GenericType
exportName = node.qualifier.getText().trim();
} else {
// Example input:
// import('api-extractor-lib1-test')
//
// Extracted qualifier:
// apiExtractorLib1Test
exportName = SyntaxHelpers.makeCamelCaseIdentifier(externalModulePath);
}
return this._fetchAstImport(undefined, {
importKind: AstImportKind.ImportType,
exportName,
modulePath: externalModulePath,
isTypeOnly: false,
});
}
// Internal reference: AstSymbol
const rightMostToken: ts.Identifier | ts.ImportTypeNode = node.qualifier
? node.qualifier.kind === ts.SyntaxKind.QualifiedName
? node.qualifier.right
: node.qualifier
: node;
// There is no symbol property in a ImportTypeNode, obtain the associated export symbol
const exportSymbol: ts.Symbol | undefined = this._typeChecker.getSymbolAtLocation(rightMostToken);
if (!exportSymbol) {
throw new InternalError(
`Symbol not found for identifier: ${node.getText()}\n` + SourceFileLocationFormatter.formatDeclaration(node),
);
}
let followedSymbol: ts.Symbol = exportSymbol;
for (;;) {
const referencedAstEntity: AstEntity | undefined = this.fetchReferencedAstEntity(
followedSymbol,
referringModuleIsExternal,
);
if (referencedAstEntity) {
return referencedAstEntity;
}
const followedSymbolNode: ts.ImportTypeNode | ts.Node | undefined =
followedSymbol.declarations && (followedSymbol.declarations[0] as ts.Node | undefined);
if (followedSymbolNode && followedSymbolNode.kind === ts.SyntaxKind.ImportType) {
return this.fetchReferencedAstEntityFromImportTypeNode(
followedSymbolNode as ts.ImportTypeNode,
referringModuleIsExternal,
);
}
if (!(followedSymbol.flags & ts.SymbolFlags.Alias)) {
break;
}
const currentAlias: ts.Symbol = this._typeChecker.getAliasedSymbol(followedSymbol);
if (!currentAlias || currentAlias === followedSymbol) {
break;
}
followedSymbol = currentAlias;
}
const astSymbol: AstSymbol | undefined = this._astSymbolTable.fetchAstSymbol({
followedSymbol,
isExternal: referringModuleIsExternal,
includeNominalAnalysis: false,
addIfMissing: true,
});
return astSymbol;
}
private _tryMatchExportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined {
const exportDeclaration: ts.ExportDeclaration | undefined = TypeScriptHelpers.findFirstParent<ts.ExportDeclaration>(
declaration,
ts.SyntaxKind.ExportDeclaration,
);
if (exportDeclaration) {
let exportName: string | undefined;
if (declaration.kind === ts.SyntaxKind.ExportSpecifier) {
// EXAMPLE:
// "export { A } from './file-a';"
//
// ExportDeclaration:
// ExportKeyword: pre=[export] sep=[ ]
// NamedExports:
// FirstPunctuation: pre=[{] sep=[ ]
// SyntaxList:
// ExportSpecifier: <------------- declaration
// Identifier: pre=[A] sep=[ ]
// CloseBraceToken: pre=[}] sep=[ ]
// FromKeyword: pre=[from] sep=[ ]
// StringLiteral: pre=['./file-a']
// SemicolonToken: pre=[;]
// Example: " ExportName as RenamedName"
const exportSpecifier: ts.ExportSpecifier = declaration as ts.ExportSpecifier;
exportName = (exportSpecifier.propertyName ?? exportSpecifier.name).getText().trim();
} else if (declaration.kind === ts.SyntaxKind.NamespaceExport) {
// EXAMPLE:
// "export * as theLib from 'the-lib';"
//
// ExportDeclaration:
// ExportKeyword: pre=[export] sep=[ ]
// NamespaceExport:
// AsteriskToken: pre=[*] sep=[ ]
// AsKeyword: pre=[as] sep=[ ]
// Identifier: pre=[theLib] sep=[ ]
// FromKeyword: pre=[from] sep=[ ]
// StringLiteral: pre=['the-lib']
// SemicolonToken: pre=[;]
// Issue tracking this feature: https://github.com/microsoft/rushstack/issues/2780
throw new Error(
`The "export * as ___" syntax is not supported yet; as a workaround,` +
` use "import * as ___" with a separate "export { ___ }" declaration\n` +
SourceFileLocationFormatter.formatDeclaration(declaration),
);
} else {
throw new InternalError(
`Unimplemented export declaration kind: ${declaration.getText()}\n` +
SourceFileLocationFormatter.formatDeclaration(declaration),
);
}
// Ignore "export { A }" without a module specifier
if (exportDeclaration.moduleSpecifier) {
const externalModulePath: string | undefined = this._tryGetExternalModulePath(exportDeclaration);
if (externalModulePath !== undefined) {
return this._fetchAstImport(declarationSymbol, {
importKind: AstImportKind.NamedImport,
modulePath: externalModulePath,
exportName,
isTypeOnly: false,
});
}
return this._getExportOfSpecifierAstModule(exportName, exportDeclaration, declarationSymbol);
}
}
return undefined;
}
private _tryMatchImportDeclaration(declaration: ts.Declaration, declarationSymbol: ts.Symbol): AstEntity | undefined {
const importDeclaration: ts.ImportDeclaration | undefined = TypeScriptHelpers.findFirstParent<ts.ImportDeclaration>(
declaration,
ts.SyntaxKind.ImportDeclaration,
);
if (importDeclaration) {
const externalModulePath: string | undefined = this._tryGetExternalModulePath(importDeclaration);
if (declaration.kind === ts.SyntaxKind.NamespaceImport) {
// EXAMPLE:
// "import * as theLib from 'the-lib';"
//
// ImportDeclaration:
// ImportKeyword: pre=[import] sep=[ ]
// ImportClause:
// NamespaceImport: <------------- declaration
// AsteriskToken: pre=[*] sep=[ ]
// AsKeyword: pre=[as] sep=[ ]
// Identifier: pre=[theLib] sep=[ ]
// FromKeyword: pre=[from] sep=[ ]
// StringLiteral: pre=['the-lib']
// SemicolonToken: pre=[;]
if (externalModulePath === undefined) {
const astModule: AstModule = this._fetchSpecifierAstModule(importDeclaration, declarationSymbol);
let namespaceImport: AstNamespaceImport | undefined = this._astNamespaceImportByModule.get(astModule);
if (namespaceImport === undefined) {
namespaceImport = new AstNamespaceImport({
namespaceName: declarationSymbol.name,
astModule,
declaration,
symbol: declarationSymbol,
});
this._astNamespaceImportByModule.set(astModule, namespaceImport);
}
return namespaceImport;
}
// Here importSymbol=undefined because {@inheritDoc} and such are not going to work correctly for
// a package or source file.
return this._fetchAstImport(undefined, {
importKind: AstImportKind.StarImport,
exportName: declarationSymbol.name,
modulePath: externalModulePath,
isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration),
});
}
if (declaration.kind === ts.SyntaxKind.ImportSpecifier) {
// EXAMPLE:
// "import { A, B } from 'the-lib';"
//
// ImportDeclaration:
// ImportKeyword: pre=[import] sep=[ ]
// ImportClause:
// NamedImports:
// FirstPunctuation: pre=[{] sep=[ ]
// SyntaxList:
// ImportSpecifier: <------------- declaration
// Identifier: pre=[A]
// CommaToken: pre=[,] sep=[ ]
// ImportSpecifier:
// Identifier: pre=[B] sep=[ ]
// CloseBraceToken: pre=[}] sep=[ ]
// FromKeyword: pre=[from] sep=[ ]
// StringLiteral: pre=['the-lib']
// SemicolonToken: pre=[;]
// Example: " ExportName as RenamedName"
const importSpecifier: ts.ImportSpecifier = declaration as ts.ImportSpecifier;
const exportName: string = (importSpecifier.propertyName ?? importSpecifier.name).getText().trim();
if (externalModulePath !== undefined) {
return this._fetchAstImport(declarationSymbol, {
importKind: AstImportKind.NamedImport,
modulePath: externalModulePath,
exportName,
isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration),
});
}
return this._getExportOfSpecifierAstModule(exportName, importDeclaration, declarationSymbol);
} else if (declaration.kind === ts.SyntaxKind.ImportClause) {
// EXAMPLE:
// "import A, { B } from './A';"
//
// ImportDeclaration:
// ImportKeyword: pre=[import] sep=[ ]
// ImportClause: <------------- declaration (referring to A)
// Identifier: pre=[A]
// CommaToken: pre=[,] sep=[ ]
// NamedImports:
// FirstPunctuation: pre=[{] sep=[ ]
// SyntaxList:
// ImportSpecifier:
// Identifier: pre=[B] sep=[ ]
// CloseBraceToken: pre=[}] sep=[ ]
// FromKeyword: pre=[from] sep=[ ]
// StringLiteral: pre=['./A']
// SemicolonToken: pre=[;]
const importClause: ts.ImportClause = declaration as ts.ImportClause;
const exportName: string = importClause.name
? importClause.name.getText().trim()
: ts.InternalSymbolName.Default;
if (externalModulePath !== undefined) {
return this._fetchAstImport(declarationSymbol, {
importKind: AstImportKind.DefaultImport,
modulePath: externalModulePath,
exportName,
isTypeOnly: ExportAnalyzer._getIsTypeOnly(importDeclaration),
});
}
return this._getExportOfSpecifierAstModule(ts.InternalSymbolName.Default, importDeclaration, declarationSymbol);
} else {
throw new InternalError(
`Unimplemented import declaration kind: ${declaration.getText()}\n` +
SourceFileLocationFormatter.formatDeclaration(declaration),
);
}
}
if (
ts.isImportEqualsDeclaration(declaration) && // EXAMPLE:
// import myLib = require('my-lib');
//
// ImportEqualsDeclaration:
// ImportKeyword: pre=[import] sep=[ ]
// Identifier: pre=[myLib] sep=[ ]
// FirstAssignment: pre=[=] sep=[ ]
// ExternalModuleReference:
// RequireKeyword: pre=[require]
// OpenParenToken: pre=[(]
// StringLiteral: pre=['my-lib']
// CloseParenToken: pre=[)]
// SemicolonToken: pre=[;]
ts.isExternalModuleReference(declaration.moduleReference) &&
ts.isStringLiteralLike(declaration.moduleReference.expression)
) {
const variableName: string = TypeScriptInternals.getTextOfIdentifierOrLiteral(declaration.name);
const externalModuleName: string = TypeScriptInternals.getTextOfIdentifierOrLiteral(
declaration.moduleReference.expression,
);
return this._fetchAstImport(declarationSymbol, {
importKind: AstImportKind.EqualsImport,
modulePath: externalModuleName,
exportName: variableName,
isTypeOnly: false,
});
}
return undefined;
}
private static _getIsTypeOnly(importDeclaration: ts.ImportDeclaration): boolean {
if (importDeclaration.importClause) {
return Boolean(importDeclaration.importClause.isTypeOnly);
}
return false;
}
private _getExportOfSpecifierAstModule(
exportName: string,
importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration,
exportSymbol: ts.Symbol,
): AstEntity {
const specifierAstModule: AstModule = this._fetchSpecifierAstModule(importOrExportDeclaration, exportSymbol);
const astEntity: AstEntity = this._getExportOfAstModule(exportName, specifierAstModule);
return astEntity;
}
private _getExportOfAstModule(exportName: string, astModule: AstModule): AstEntity {
const visitedAstModules: Set<AstModule> = new Set<AstModule>();
const astEntity: AstEntity | undefined = this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules);
if (astEntity === undefined) {
throw new InternalError(
`Unable to analyze the export ${JSON.stringify(exportName)} in\n` + astModule.sourceFile.fileName,
);
}
return astEntity;
}
/**
* Implementation of {@link AstSymbolTable.tryGetExportOfAstModule}.
*/
public tryGetExportOfAstModule(exportName: string, astModule: AstModule): AstEntity | undefined {
const visitedAstModules: Set<AstModule> = new Set<AstModule>();
return this._tryGetExportOfAstModule(exportName, astModule, visitedAstModules);
}
private _tryGetExportOfAstModule(
exportName: string,
astModule: AstModule,
visitedAstModules: Set<AstModule>,
): AstEntity | undefined {
if (visitedAstModules.has(astModule)) {
return undefined;
}
visitedAstModules.add(astModule);
let astEntity: AstEntity | undefined = astModule.cachedExportedEntities.get(exportName);
if (astEntity !== undefined) {
return astEntity;
}
// Try the explicit exports
const escapedExportName: ts.__String = ts.escapeLeadingUnderscores(exportName);
if (astModule.moduleSymbol.exports) {
const exportSymbol: ts.Symbol | undefined = astModule.moduleSymbol.exports.get(escapedExportName);
if (exportSymbol) {
astEntity = this.fetchReferencedAstEntity(exportSymbol, astModule.isExternal);
if (astEntity !== undefined) {
astModule.cachedExportedEntities.set(exportName, astEntity); // cache for next time
return astEntity;
}
}
}
// Try each of the star imports
for (const starExportedModule of astModule.starExportedModules) {
astEntity = this._tryGetExportOfAstModule(exportName, starExportedModule, visitedAstModules);
if (astEntity !== undefined) {
if (starExportedModule.externalModulePath !== undefined) {
// This entity was obtained from an external module, so return an AstImport instead
const astSymbol: AstSymbol = astEntity as AstSymbol;
return this._fetchAstImport(astSymbol.followedSymbol, {
importKind: AstImportKind.NamedImport,
modulePath: starExportedModule.externalModulePath,
exportName,
isTypeOnly: false,
});
}
return astEntity;
}
}
return undefined;
}
private _tryGetExternalModulePath(
importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode,
): string | undefined {
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
if (this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier)) {
return moduleSpecifier;
}
return undefined;
}
/**
* Given an ImportDeclaration of the form `export { X } from "___";`, this interprets the module specifier (`"___"`)
* and fetches the corresponding AstModule object.
*/
private _fetchSpecifierAstModule(
importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration,
exportSymbol: ts.Symbol,
): AstModule {
const moduleSpecifier: string = this._getModuleSpecifier(importOrExportDeclaration);
const mode: ts.ModuleKind.CommonJS | ts.ModuleKind.ESNext | undefined =
importOrExportDeclaration.moduleSpecifier && ts.isStringLiteralLike(importOrExportDeclaration.moduleSpecifier)
? TypeScriptInternals.getModeForUsageLocation(
importOrExportDeclaration.getSourceFile(),
importOrExportDeclaration.moduleSpecifier,
)
: undefined;
const resolvedModule: ts.ResolvedModuleFull | undefined = TypeScriptInternals.getResolvedModule(
importOrExportDeclaration.getSourceFile(),
moduleSpecifier,
mode,
);
if (resolvedModule === undefined) {
// Encountered in https://github.com/microsoft/rushstack/issues/1914.
//
// It's also possible for this to occur with ambient modules. However, in practice this doesn't happen
// as API Extractor treats all ambient modules as external per the logic in `_isExternalModulePath`, and
// thus this code path is never reached for ambient modules.
throw new InternalError(
`getResolvedModule() could not resolve module name ${JSON.stringify(moduleSpecifier)}\n` +
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration),
);
}
// Map the filename back to the corresponding SourceFile. This circuitous approach is needed because
// we have no way to access the compiler's internal resolveExternalModuleName() function
const moduleSourceFile: ts.SourceFile | undefined = this._program.getSourceFile(resolvedModule.resolvedFileName);
if (!moduleSourceFile) {
// This should not happen, since getResolvedModule() specifically looks up names that the compiler
// found in export declarations for this source file
throw new InternalError(
`getSourceFile() failed to locate ${JSON.stringify(resolvedModule.resolvedFileName)}\n` +
SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration),
);
}
const isExternal: boolean = this._isExternalModulePath(importOrExportDeclaration, moduleSpecifier);
const moduleReference: IAstModuleReference = {
moduleSpecifier,
moduleSpecifierSymbol: exportSymbol,
};
const specifierAstModule: AstModule = this.fetchAstModuleFromSourceFile(
moduleSourceFile,
moduleReference,
isExternal,
);
return specifierAstModule;
}
private _fetchAstImport(importSymbol: ts.Symbol | undefined, options: IAstImportOptions): AstImport {
const key: string = AstImport.getKey(options);
let astImport: AstImport | undefined = this._astImportsByKey.get(key);
if (astImport) {
// If we encounter at least one import that does not use the type-only form,
// then the .d.ts rollup will NOT use "import type".
if (!options.isTypeOnly) {
astImport.isTypeOnlyEverywhere = false;
}
} else {
astImport = new AstImport(options);
this._astImportsByKey.set(key, astImport);
if (importSymbol) {
const followedSymbol: ts.Symbol = TypeScriptHelpers.followAliases(importSymbol, this._typeChecker);
astImport.astSymbol = this._astSymbolTable.fetchAstSymbol({
followedSymbol,
isExternal: true,
includeNominalAnalysis: false,
addIfMissing: true,
});
}
}
return astImport;
}
private _getModuleSpecifier(
importOrExportDeclaration: ts.ExportDeclaration | ts.ImportDeclaration | ts.ImportTypeNode,
): string {
// The name of the module, which could be like "./SomeLocalFile' or like 'external-package/entry/point'
const moduleSpecifier: string | undefined = TypeScriptHelpers.getModuleSpecifier(importOrExportDeclaration);
if (!moduleSpecifier) {
throw new InternalError(
'Unable to parse module specifier\n' + SourceFileLocationFormatter.formatDeclaration(importOrExportDeclaration),
);
}
return moduleSpecifier;
}
}

View File

@@ -0,0 +1,201 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import {
type PackageJsonLookup,
FileSystem,
JsonFile,
type NewlineKind,
type INodePackageJson,
type JsonObject,
} from '@rushstack/node-core-library';
import { ConsoleMessageId } from '../api/ConsoleMessageId.js';
import { Extractor } from '../api/Extractor.js';
import type { MessageRouter } from '../collector/MessageRouter.js';
/**
* Represents analyzed information for a package.json file.
* This object is constructed and returned by PackageMetadataManager.
*/
export class PackageMetadata {
/**
* The absolute path to the package.json file being analyzed.
*/
public readonly packageJsonPath: string;
/**
* The parsed contents of package.json. Note that PackageJsonLookup
* only includes essential fields.
*/
public readonly packageJson: INodePackageJson;
/**
* If true, then the package's documentation comments can be assumed
* to contain API Extractor compatible TSDoc tags.
*/
public readonly aedocSupported: boolean;
public constructor(packageJsonPath: string, packageJson: INodePackageJson, aedocSupported: boolean) {
this.packageJsonPath = packageJsonPath;
this.packageJson = packageJson;
this.aedocSupported = aedocSupported;
}
}
/**
* This class maintains a cache of analyzed information obtained from package.json
* files. It is built on top of the PackageJsonLookup class.
*
* @remarks
*
* IMPORTANT: Don't use PackageMetadataManager to analyze source files from the current project:
* 1. Files such as tsdoc-metadata.json may not have been built yet, and thus may contain incorrect information.
* 2. The current project is not guaranteed to have a package.json file at all. For example, API Extractor can
* be invoked on a bare .d.ts file.
*
* Use ts.program.isSourceFileFromExternalLibrary() to test source files before passing the to PackageMetadataManager.
*/
export class PackageMetadataManager {
public static tsdocMetadataFilename: string = 'tsdoc-metadata.json';
private readonly _packageJsonLookup: PackageJsonLookup;
private readonly _messageRouter: MessageRouter;
private readonly _packageMetadataByPackageJsonPath: Map<string, PackageMetadata> = new Map<string, PackageMetadata>();
public constructor(packageJsonLookup: PackageJsonLookup, messageRouter: MessageRouter) {
this._packageJsonLookup = packageJsonLookup;
this._messageRouter = messageRouter;
}
// This feature is still being standardized: https://github.com/microsoft/tsdoc/issues/7
// In the future we will use the @microsoft/tsdoc library to read this file.
private static _resolveTsdocMetadataPathFromPackageJson(
packageFolder: string,
packageJson: INodePackageJson,
): string {
const tsdocMetadataFilename: string = PackageMetadataManager.tsdocMetadataFilename;
let tsdocMetadataRelativePath: string;
if (packageJson.tsdocMetadata) {
// 1. If package.json contains a field such as "tsdocMetadata": "./path1/path2/tsdoc-metadata.json",
// then that takes precedence. This convention will be rarely needed, since the other rules below generally
// produce a good result.
tsdocMetadataRelativePath = packageJson.tsdocMetadata;
} else if (packageJson.typings) {
// 2. If package.json contains a field such as "typings": "./path1/path2/index.d.ts", then we look
// for the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.typings), tsdocMetadataFilename);
} else if (packageJson.main) {
// 3. If package.json contains a field such as "main": "./path1/path2/index.js", then we look for
// the file under "./path1/path2/tsdoc-metadata.json"
tsdocMetadataRelativePath = path.join(path.dirname(packageJson.main), tsdocMetadataFilename);
} else {
// 4. If none of the above rules apply, then by default we look for the file under "./tsdoc-metadata.json"
// since the default entry point is "./index.js"
tsdocMetadataRelativePath = tsdocMetadataFilename;
}
// Always resolve relative to the package folder.
const tsdocMetadataPath: string = path.resolve(packageFolder, tsdocMetadataRelativePath);
return tsdocMetadataPath;
}
/**
* @param packageFolder - The package folder
* @param packageJson - The package JSON
* @param tsdocMetadataPath - An explicit path that can be configured in api-extractor.json.
* If this parameter is not an empty string, it overrides the normal path calculation.
* @returns the absolute path to the TSDoc metadata file
*/
public static resolveTsdocMetadataPath(
packageFolder: string,
packageJson: INodePackageJson,
tsdocMetadataPath?: string,
): string {
if (tsdocMetadataPath) {
return path.resolve(packageFolder, tsdocMetadataPath);
}
return PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(packageFolder, packageJson);
}
/**
* Writes the TSDoc metadata file to the specified output file.
*/
public static writeTsdocMetadataFile(tsdocMetadataPath: string, newlineKind: NewlineKind): void {
const fileObject: JsonObject = {
tsdocVersion: '0.12',
toolPackages: [
{
packageName: '@microsoft/api-extractor',
packageVersion: Extractor.version,
},
],
};
const fileContent: string =
'// This file is read by tools that parse documentation comments conforming to the TSDoc standard.\n' +
'// It should be published with your NPM package. It should not be tracked by Git.\n' +
JsonFile.stringify(fileObject);
FileSystem.writeFile(tsdocMetadataPath, fileContent, {
convertLineEndings: newlineKind,
ensureFolderExists: true,
});
}
/**
* Finds the package.json in a parent folder of the specified source file, and
* returns a PackageMetadata object. If no package.json was found, then undefined
* is returned. The results are cached.
*/
public tryFetchPackageMetadata(sourceFilePath: string): PackageMetadata | undefined {
const packageJsonFilePath: string | undefined =
this._packageJsonLookup.tryGetPackageJsonFilePathFor(sourceFilePath);
if (!packageJsonFilePath) {
return undefined;
}
let packageMetadata: PackageMetadata | undefined = this._packageMetadataByPackageJsonPath.get(packageJsonFilePath);
if (!packageMetadata) {
const packageJson: INodePackageJson = this._packageJsonLookup.loadNodePackageJson(packageJsonFilePath);
const packageJsonFolder: string = path.dirname(packageJsonFilePath);
let aedocSupported = false;
const tsdocMetadataPath: string = PackageMetadataManager._resolveTsdocMetadataPathFromPackageJson(
packageJsonFolder,
packageJson,
);
if (FileSystem.exists(tsdocMetadataPath)) {
this._messageRouter.logVerbose(ConsoleMessageId.FoundTSDocMetadata, 'Found metadata in ' + tsdocMetadataPath);
// If the file exists at all, assume it was written by API Extractor
aedocSupported = true;
}
packageMetadata = new PackageMetadata(packageJsonFilePath, packageJson, aedocSupported);
this._packageMetadataByPackageJsonPath.set(packageJsonFilePath, packageMetadata);
}
return packageMetadata;
}
/**
* Returns true if the source file is part of a package whose .d.ts files support AEDoc annotations.
*/
public isAedocSupportedFor(sourceFilePath: string): boolean {
const packageMetadata: PackageMetadata | undefined = this.tryFetchPackageMetadata(sourceFilePath);
if (!packageMetadata) {
return false;
}
return packageMetadata.aedocSupported;
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as path from 'node:path';
import { Path, Text } from '@rushstack/node-core-library';
import type * as ts from 'typescript';
export interface ISourceFileLocationFormatOptions {
sourceFileColumn?: number | undefined;
sourceFileLine?: number | undefined;
workingPackageFolderPath?: string | undefined;
}
export class SourceFileLocationFormatter {
/**
* Returns a string such as this, based on the context information in the provided node:
* "[C:\\Folder\\File.ts#123]"
*/
public static formatDeclaration(node: ts.Node, workingPackageFolderPath?: string): string {
const sourceFile: ts.SourceFile = node.getSourceFile();
const lineAndCharacter: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(node.getStart());
return SourceFileLocationFormatter.formatPath(sourceFile.fileName, {
sourceFileLine: lineAndCharacter.line + 1,
sourceFileColumn: lineAndCharacter.character + 1,
workingPackageFolderPath,
});
}
public static formatPath(sourceFilePath: string, options?: ISourceFileLocationFormatOptions): string {
const ioptions = options ?? {};
let result = '';
// Make the path relative to the workingPackageFolderPath
let scrubbedPath: string = sourceFilePath;
if (
ioptions.workingPackageFolderPath && // If it's under the working folder, make it a relative path
Path.isUnderOrEqual(sourceFilePath, ioptions.workingPackageFolderPath)
) {
scrubbedPath = path.relative(ioptions.workingPackageFolderPath, sourceFilePath);
}
// Convert it to a Unix-style path
scrubbedPath = Text.replaceAll(scrubbedPath, '\\', '/');
result += scrubbedPath;
if (ioptions.sourceFileLine) {
result += `:${ioptions.sourceFileLine}`;
if (ioptions.sourceFileColumn) {
result += `:${ioptions.sourceFileColumn}`;
}
}
return result;
}
}

Some files were not shown because too many files have changed in this diff Show More